前言
不过是一个软工的秃头学生的临死挣扎罢了
续:这篇博客是在考前写的,后面内容没来得及写上来就考试了,如果过了就当无事发生,如果挂了我尽量在补考之前写完。。
再续:考的还行,后面应该就不会更新了,嘻嘻
第一章:概述
知识点
编译
在Unix系统上,从源文件到目标文件的转化是由 编译器驱动程序 完成的。
系统硬件组成
系统之间的网络通信
操作系统的抽象表示
操作系统的几个基本抽象概念
进程
是操作系统对正在运行的程序的一种抽象。
线程
是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
- 一个进程由多个
线程
组成,每个线程都运行在进程的上下文中,共享同样的代码和数据。
并发运行
是指一个进程的指令和另一个进程的指令交错执行,多个进程同时活动。一个CPU通过上下文切换
实现并发执行多个程序。
虚拟内存
是主存和I/O设备
的抽象,它为每个进程提供一种独立占用内存的假象。
- 每个进程看到的内存都是一致的,称为
虚拟地址空间
。
文件
就是字节序列,每个I/O设备,包括磁盘、键盘、显示设备,甚至网络都可看为文件。
Admahl定律
Amdahl 定律(也叫阿姆达尔定律)的主要思想是:当我们对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。
若系统执行某应用程序需要时间 T 1 T_1 T1 。假设系统某部分所需执行时间与该时间的比例为 α \alpha α,而该部分性能提升比例为 k k k。即该部分初始所需时间为 α T 1 \alpha T_1 αT1,现在所需时间为 ( α T 1 ) / k (\alpha T_1)/k (αT1)/k,因此总的执行时间应为: T 2 = ( 1 − α ) T 1 + ( α T 1 ) / k = T 1 [ ( 1 − α ) + α / k ] T_2=(1-\alpha)T_1+(\alpha T_1)/k=T_1[(1-\alpha)+\alpha /k] T2=(1−α)T1+(αT1)/k=T1[(1−α)+α/k]。
由此可以计算加速比 S = T 1 / T 2 S=T_1/T_2 S=T1/T2为 S = 1 ( 1 − α ) − α / k S=\frac {1} {(1-\alpha)-\alpha/k} S=(1−α)−α/k1
练习题
例1.1
题面:
假设你是个卡车司机,要将土豆从爱达荷州的Boise运送到明尼苏达州的Minneapolis,全程2500公里。在限速范围内,你估计平均速度为100公里/小时,整个行程需要25个小时。
A.你听到新闻说蒙大拿州刚刚取消了限速,这使得行程中有1500公里卡车的速度可以为150公里/小时。那么这对整个行程的加速比是多少?
B.你可以在www. fasttrucks.com网站上为自己的卡车买个新的涡轮增压器。网站现货供应各种型号,不过速度越快,价格越高。如果想要让整个行程的加速比为1.67X,那么你必须以多快的速度通过蒙大拿州?
解答:
第二章:信息的表示和处理
知识点
进制转换
字数据的大小
字节顺序
大端/小端法分别以最高/最低有效字节
在最前面(地址较小)的方式存储数据。
以0x1234567为例
移位运算
逻辑右移
左端补0,算数右移
左端补符号位。
如有符号数-8,补码表示为1111 1000,算数右移1位(等价于除2),得到1111 1100,即-4。
位级运算
- 保持最低字节不变,其他字节均清零
&运算实现:比如保持 0x123456AD最低字节不变,其他字节清零,并且具有可移植性,应该如下编写0x123456AD&0xFF
- 最低字节置一,其他字节不变
|运算实现:比如 0x123456AD最低字节置一,其他字节不变,并且具有可移植性,应该如下编写0x123456AD|0xFF
- 保持高位字节不变,低位字节置一
| ~运算实现:比如 0x123456AD高位字节不变,低位字节置一,并且具有可移植性,应该如下编写 0x123456AD|(~0xFF )
- 只用 & | 实现 XOR 运算
由逻辑代数中异或得 A^B=A&(~B) + B&(~A)
A^B=(A&(~B) ) | (B&(~A))
整数表示
编码方式
整数数据类型
编码
无符号数编码
反码编码
正数
反码与原码相同,负数
反码是对其原码除符号位逐位取反
补码编码
正数
补码与原码相同,负数
补码为其补码加1
有符号数和无符号数之间的转换
强制类型转换的结果保持位置不变,只是改变了这些位值的解释方法。
扩展数字的位表示
无符号数扩充位使用零扩展,补码数扩充位使用符号扩展。
值得注意的是,从一个数据大小到另一个数据大小的转换,以及无符号和有符号数字之间的转换的的相对顺序能够影响一个程序的行为。
如当把 short 转换成 unsigned 时,我们要先改变大小,再完成从有符号到无符号的转变。即 (unsigned) sx等价于 (unsigned)(int) sx。
截断数字
无符号数截断
时,丢弃截断位;
补码数截断
时,丢弃截断位并将结果进行 U 2 T k U2T_k U2Tk转换。
整数运算
无符号加法
- 算术溢出:是指完整的整数结果不能放到数据类型的字长限制中去。
- 溢出检测:
- 无符号数求反:
补码加法
两个数的 ω \omega ω 位补码之和与无符号之和有完全相同的位级表示。实际上,大多数计算机使用同样的机器指令来执行无符号或有符号加法。
-
执行位级补码非的第一种方法是对每一位求补,再对结果加1
。在C语言中,我们可以说对于任意整数值x,计算表达式-x和~x+1得到的结果完全一样。
下面是一些示例,字长为4:
-
计算一个数x的补码非的第二种方法建立在把位向量分为两部分的基础上。假设k是最右边1的位置
,因而x的位级表示形如 [ x [x [x ω − 1 \omega-1 ω−1 , x ,x ,x ω − 2 \omega-2 ω−2 , ⋅ ⋅ ⋅ , x ,···,x ,⋅⋅⋅,x k + 1 k+1 k+1 , 1 , 0 , ⋅ ⋅ ⋅ , 0 ] ,1,0,···,0] ,1,0,⋅⋅⋅,0]。此时对位k左边的所有位取反即可。
无符号乘法
补码乘法
无符号和补码的乘法的位级等价性
- 证明:
乘以常数
- 乘以2 ⇒ \Rightarrow ⇒左移
- 其他常数 ⇒ \Rightarrow ⇒转换成2
除以2的幂
舍入
-
向下舍入: 对于任意实数a,定义⌊a⌋为唯一整数a’,使得 a ′ ≤ a < a ′ + 1 a′≤aa′≤a<a′+1。
如 ⌊ 3.14 ⌋ = 3 ⌊ 3.14 ⌋ = 3 , ⌊ − 3.14 ⌋ = − 4 ⌊ − 3.14 ⌋ = − 4 ⌊3.14⌋=3⌊3.14⌋=3,⌊−3.14⌋=−4⌊−3.14⌋=−4 ⌊3.14⌋=3⌊3.14⌋=3,⌊−3.14⌋=−4⌊−3.14⌋=−4
-
向上舍入: 对于任意实数a,定义⌈a⌉为唯一整数a’,使得 a ′ − 1 ≤ a < a a′−1≤aa′−1≤a<a
如 ⌈ 3.14 ⌉ = 4 ⌈ 3.14 ⌉ = 4 , ⌈ − 3.14 ⌉ = − 3 ⌈ − 3.14 ⌉ = − 3 ⌈3.14⌉=4⌈3.14⌉=4,⌈−3.14⌉=−3⌈−3.14⌉=−3 ⌈3.14⌉=4⌈3.14⌉=4,⌈−3.14⌉=−3⌈−3.14⌉=−3
-
向上舍入与向下舍入的关系:
⌈ x / y ⌉ = ⌊ ( x + y − 1 ) / y ⌋ ⌈ x / y ⌉ = ⌊ ( x + y − 1 ) / y ⌋ ⌈x/y⌉=⌊(x+y−1)/y⌋⌈x/y⌉=⌊(x+y−1)/y⌋ ⌈x/y⌉=⌊(x+y−1)/y⌋⌈x/y⌉=⌊(x+y−1)/y⌋
**证明:**令 x = q y + r x = q y + r x=qy+rx=qy+r x=qy+rx=qy+r,其中 0 ≤ r < y 0≤r0≤r<y,则 ⌊ ( x + y − 1 ) / y ⌋ = q + ⌊ ( r + y − 1 ) / y ⌋ ⌊ ( x + y − 1 ) / y ⌋ = q + ⌊ ( r + y − 1 ) / y ⌋ ⌊(x+y−1)/y⌋=q+⌊(r+y−1)/y⌋⌊(x+y−1)/y⌋=q+⌊(r+y−1)/y⌋ ⌊(x+y−1)/y⌋=q+⌊(r+y−1)/y⌋⌊(x+y−1)/y⌋=q+⌊(r+y−1)/y⌋。当 r = 0 r=0 r=0时, ⌈ x / y ⌉ = q ⌈ x / y ⌉ = q ⌈x/y⌉=q⌈x/y⌉=q ⌈x/y⌉=q⌈x/y⌉=q;当 r > 1 r>1 r>1时, ⌈ x / y ⌉ = q + 1 ⌈ x / y ⌉ = q + 1 ⌈x/y⌉=q+1⌈x/y⌉=q+1 ⌈x/y⌉=q+1⌈x/y⌉=q+1。因此,实现了向上舍入。
无符号除法
补码除法(向下舍入)
补码除法(向上舍入)
浮点数
二进制小数
- ∑ k = − j i b k × 2 k \sum_{k=-j}^{i} {b_k\times2^k} ∑k=−jibk×2k
IEEE浮点表示
V = ( − 1 ) s × M × 2 E V=(-1)^s\times M\times 2^E V=(−1)s×M×2E
符号s
决定正负数;尾数M
是二进制小数,范围是 [ 1.0 , 2.0 ) [1.0,2.0) [1.0,2.0);阶码E
用于对浮点数加权。
- 对于单精度32位浮点数,符号s占1位、阶码占k=8位、尾码占n=23位。
8位阶码字段 e x p = e 7 e 6 ⋅ ⋅ ⋅ e 0 exp=e_7e_6···e_0 exp=e7e6⋅⋅⋅e0编码阶码 E E E,23位小数字段 f r a c = f frac=f frac=f22 ⋅ ⋅ ⋅ f 0 ···f_0 ⋅⋅⋅f0编码尾数M。
规格化
- 条件: e x p ≠ 000...00 exp\neq000...00 exp=000...00并且 e x p ≠ 111...11 exp\neq111...11 exp=111...11
- 计算公式:
{ E = e x p − B i a s B i a s = 2 ( k − 1 ) − 1 M = ∣ 1. f r a c ∣ \left\{ \begin{aligned} E & = & exp-Bias \\ Bias & = & 2^{(k-1)}-1\\ M& = &|1.frac| \end{aligned} \right. ⎩⎪⎨⎪⎧EBiasM===exp−Bias2(k−1)−1∣1.frac∣
- Bias为偏置值,k则表示exp的位数,对单精度来说,k=8,则bias=127,对双精度来说,k=11,则bias=1023。
- f r a c frac frac 表示小数点之后的二进制位数,在规格化情况下规定小数点左侧的隐含位为1。
- 举例
非规格化
- 条件: e x p = 000...00 exp=000...00 exp=000...00
- 计算公式:
{ E = 1 − B i a s M = ∣ 0. f r a c ∣ \left\{ \begin{aligned} E & = & 1-Bias \\ M& = &|0.frac| \end{aligned} \right. { EM==1−Bias∣0.frac∣
- 非规格数用来表示0以及靠近0的数。
- 把符号位S值1,其余所有位均置0后,得到了-0.0; 同理,把所有位均置0,则得到 +0.0。
特殊值
- 条件: e x p = 111...11 exp=111...11 exp=111...11
- 含义
{ 尾 数 域 全 0 时 , 无 穷 大 尾 数 域 非 0 时 , N a N ( N o t a N u m b e r ) \begin{cases} 尾数域全0时,无穷大\\ 尾数域非0时,NaN(Not a Number) \end{cases} { 尾数域全0时,无穷大尾数域非0时,NaN(NotaNumber)
- 非规格化数分布在0附近,浮点数并非均匀分布,约靠近原点处约稠密。
示例
整数1234512345转为单精度浮点数,二进制数为 11000000111001 11 0000 0011 1001 11000000111001,小数表示为 1.1000000111001 × 2 13 1.1000000111001×2^{13} 1.1000000111001×213
,因此存储为:
- 符号域(1位),【0】;
- 尾码域(23位)丢弃开头的1,并在后面补10个0,【100 0000 1110 0100 0000 0000】;
- 阶码域(8位)为13加上偏置127,【1000 1100】;
舍入
对于不能精确的表示的数,我们采取一种系统的方法,找到“最接近”
的匹配值,它可以用期望的浮点形式表现出来,这就是舍入。
舍入一共有四种方式,分别是向偶数舍入、向零舍入、向上舍入以及向下舍入。
- 向偶数舍入,是将数字向上或向下舍入,使得结果的最低有效数字是偶数
- 向零舍入是将数字向靠近零的值舍入
- 向上舍入是将数字向比它大的方向靠近,存在 x ≤ x ′ x\leq x' x≤x′
- 向下舍入则是将数字向比它小的方向靠近,存在 ∣ x ∣ ≥ ∣ x ′ ∣ |x|\geq |x'| ∣x∣≥∣x′∣
- 通常情况下我们采取的舍入规则是在
原来的值是舍入值的中间值
时,采取向偶数舍入,在二进制中,偶数我们认为是末尾为0的数。而倘若不是这种情况的话,则一般会有选择性的使用向上和向下舍入,但总是会向最接近的值舍入
。其实这正是IEEE采取的默认的舍入方式,因为这种舍入方式总是企图向最近的值的舍入。
浮点运算
浮点乘法
( − 1 ) s 1 M 1 2 E 1 × ( − 1 ) s 2 M 2 2 E 2 = ( − 1 ) s M 2 E (-1)^{s_1} M_1 2^{E_1} \times (-1)^{s_2} M_2 2^{E_2}=(-1)^s M2^E (−1)s1M12E1×(−1)s2M22E2=(−1)sM2E
其中
{ s = s 1 ⋀ s 2 M = M 1 × M 2 E = E 1 + E 2 \left\{ \begin{aligned} s & = & s1\bigwedge s2 \\ M & = & M_1\times M_2\\ E& = &E_1+E_2 \end{aligned} \right. ⎩⎪⎪⎨⎪⎪⎧sME===s1⋀s2M1×M2E1+E2
- 阶码相加,尾数相乘
(除法则阶码相减,尾数相除)
- 规格化处理:若 M ≥ 2 M\geq 2 M≥2,则右移,增大 E E E
- 将 M M M进行舍入以适应 f r a c frac frac的精度
- 若E超出范围,则溢出
- 举例
浮点加法
- 对阶
① 所谓对阶是指将两个进行运算的浮点数的阶码对齐的操作。对于浮点数来说,两浮点数进行加减,首先看两数的阶码是否相同,即小数点位置是否对齐。
② 对阶的具体方法是:首先求出两浮点数阶码的差,即 ⊿ E = E x − E y ⊿E=E_x-E_y ⊿E=Ex−Ey,将小阶码加上 ⊿ E ⊿E ⊿E,使之与大阶码相等
,同时将小阶码对应的浮点数的尾数右移
相应位数,以保证该浮点数的值不变。
③ 对阶的原则是小阶对大阶
- 尾数运算
进行完成对阶后的尾数相加减。
- 结果规格化
(1) 对于IEEE标准的浮点数来说,就是尾数必须是1.M的形式。
(2) 规格化操作包括左规和右规两种情况:
① 左规操作:将尾数左移,同时阶码减值,直至尾数成为1.M的形式。
② 右规操作:将尾数右移1位,同时阶码增1,便成为规格化的形式了。
- 舍入处理
- 溢出判断
- 举例
C语言中的浮点数
练习题
第三章:程序的机器级表示
这一章开始渐渐接触到汇编代码,而且这一章的实验也挺好玩的
知识点
机器级代码
-
机器语言是直接面向处理器(Processor:CPU)的程序设计语言,但是每一种这样的微处理器(CPU)由于硬件设计和内部结构的不同,所以每一种微处理器都有自己的机器指令集,也就是机器语言。
-
计算机系统使用了多种不同的抽象,利用更简单的抽象模型来隐藏实现的细节。对于机器级编程来说,有两种抽象特别重要:
①、第一种是将机器级程序的格式和行为定义为指令集体系结构(Instruction set architecture ,ISA),它定义了处理器状态、指令的格式,以及每条指令对状态的影响。大多数 ISA,包括 Intel IA32 和 x86-64,将程序的行为描述成好像每条指令是按顺序执行的,即一条指令结束后,下一条指令开始。处理器的硬件远比描述的精细复杂,它们并发的执行许多指令,但是可以采取措施保证整体行为与 ISA 指定的顺序执行完全一致。
②、第二种是机器程序使用的存储器地址是虚拟地址,提供的存储器模型看上去是一个非常大的字节数组。存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来。
-
在整个编译过程中,编译器会完成大部分工作,将把用 C 语言提供的相对比较抽象的执行模型表示的程序转化成处理器执行的基本指令,也就是汇编语言,汇编语言在被汇编器转化成机器语言,然后计算机去执行。
-
在汇编语言中,如下的几个处理器状态是可见的:
①程序计数器(在 IA32 中通常称为 PC,用 %eip 表示):指示将要执行的下一条指令在存储器中的地址。
②整数寄存器文件:包含8个命名的位置,可以存储一些地址或者整数的数据。有的用来记录某些重要的程序状态,有的则用来保存临时数据。
③条件码寄存器:保存最近执行的算数或逻辑指令的状态信息,它们用来实现控制或数据流中的条件变化,比如用来实现 if 和 while 语句。
④浮点寄存器:存储浮点数。
-
注意:C 语言提供的模型可以在存储器中声明和分配各种数据类型的对象。但是实际上机器代码则只是简单的将存储器看成是一个很大的、按字节寻址的数组。
-
汇编代码不区分有符号或者无符号整数,不区分各种类型的指针。甚至不区分指针和整数。
数据类型
由于计算机是由16位体系结构扩展为32位体系结构的,Intel 用术语 “字”(word) 表示16位数据类型,因此 32 位表示 “双字”(double words),64 位数称为“四字”(quad words)
访问信息
整数寄存器
- 上述八个寄存器主要功能如下:
① %eax,可存放一般数据,而且可作为累加器使用;
② %ebx,可存放一般数据,而且可用来存放数据的指针(偏移地址);
③ %ecx,可存放一般数据,而且可用来做计数器,常常将循环次数用它来存放;
④ %edx,可存放一般数据,而且可用来存放乘法运算产生的部分积,或用来存放输入输出的端口地址(指针);
⑤ %esi,可存放一般数据,还可用于串操作中,存放源地址
,对一串数据访问;
⑥ %edi,可存放一般数据,还可用于串操作中,存放目的地址,对一串数据访问;
⑦ %esp,用于寻址一个称为堆栈
的存储区,通过它来访问堆栈数据;
⑧ %ebp,可存放一般数据,用来存放访问堆栈段的一个数据区,作为基地址
;
- 在大多数情况下,%eax、%ecx、%edx、%ebx、%esi、%edi等6个寄存器可以看做通用寄存器,对它们的使用没有限制;%esp、%ebp两个寄存器保存着指向程序栈中重要位置的指针,只有根据栈管理的标准惯例才能修改这两个寄存器中的值。
- 这8个寄存器都可以作为16位(字)或32位(双字)来访问。字节操作指令可以独立的读或者写%eax、%ecx、%edx、%ebx等4个寄存器的2个低位字节,因为%ax、%cx、%dx、%bx这4个16位寄存器又可分别分成ah,al ;bh,bl;ch,cl;dh,dl的8位寄存器。
- 生成1字节和2字节数字的指令会保持剩下的字节不变。
- 生成4字节数字的指令会把高位4个字节设置为0。
操作数指示符
- 立即数(immediate):书写方式是符号后跟一个标准 C C C表示的整数,比如 52 52 52, 0 x 1 F 0x1F 0x1F等等。
任何能放进一个32位的字里面的数值都可以做立即数
。
- 寄存器(register):它表示某个寄存器的内容,可以是8个32位寄存器中的一个(比如%eax),也可以是8个16位寄存器中的一个(比如%ax),还可以是8个单字节寄存器寄存器(比如%al)。上图是用 E a E_a Ea来表示任意寄存器a,用引用 R [ E a ] R[E_a] R[Ea]来表示它的值。
- 存储器(memory):它会根据计算出来的地址(通常称为有效地址)来访问某个存储器位置。我们将存储器看成一个很大的字节数组,用符号 M b [ A d d r ] M_b[Addr] Mb[Addr] 表示对存储在存储器中从地址 Addr 开始的 b 个字节值的引用。上图省略了下方的 b。
- 最后一行存储器语法 Imm(Eb,Ei,s),表示的是最常用的形式,分为四个部分,
一、Imm 是立即偏移数
二、Eb 是基址寄存器
三、Ei 是变址寄存器
四、s 是比例因子,必须是 1、2、4或8
然后有效地址计算公式为: Imm + R[Eb]+R[Ei]*s。比如对于2(%esp,%eax,4)这个操作数来讲,它代表的是内存地址为2+%esp+4 × \times ×%eax的存储器区域的值。
数据传送指令
- MOV类,把数据从源位置赋值到目标位置。源操作数为存储于寄存器或内存中的立即数,目的操作数为存储器或内存地址。
- 传送指令的两个操作数不能都执行存储器位置。
- 将一个值从一个存储器位置复制到另一个存储器位置需要两条指令:
①、第一条指令将源值加载到寄存器中
②、第二条指令将该寄存器值写入到目的位置。
- MOVS指令格式将较小的源数据复制到一个较大的数据位置。
高位用符号位扩展
,即目的位置的所有高位用源值的最高位数值进行填充。
- MOVZ 指令将较小的源数据复制到一个较大的数据位置。
高位用0扩展
,即目的位置的所有高位用0进行填充。
- 示例
压入和弹出栈数据
算术和逻辑操作
加载有效地址
- leal 指令也称为加载有效地址(load effective address)指令,它实际上是 movl 指令的变形。它的指令形式是从存储器读数据到寄存器,但实际上它根本没有引用存储器。 而是将有效地址写入到目的操作数,类似于 C 语言的取地址操作符“&”。
leal 立即数,寄存器 |
这类指令就是将立即数装载至寄存器,比如 leal 0x01,0x01,%eax 的效果是等价的 |
leal 地址,寄存器 |
leal指令的作用是将地址加载到寄存器,对于leal S,D而言,就是实现了 &S –> D 的功能 |
leal S, D |
结果是&S -> D |
movl S,D |
结果是S -> D |
- 此外,它还可以简单的描述普通的算术操作,比如假如寄存器 %edx 的值为 x,那么指令 leal 7 (%edx,%edx,4),%eax。 这表示的意思是设置寄存器 %eax 的值为 7+x+4x=5x+7。这里的leal指令根本与有效地址无关,但是需要注意的是目的操作数必须是寄存器。
一元操作
- 这四个指令都是一元操作,
即它们都只有一个操作数,即是源也是目的
。这个操作数可以是寄存器,也可以是存储器。
- 比如: incl (%esp) 会使栈顶的 4 字节元素加 1。可以联想到 C 语言的自增(++)或者自减(–)
二元操作
- 它们都是二元操作,其中第二个操作数即是源又是目的,我们可以联想到 C 语言的 x + = y x += y x+=y。
- 第一个操作数可以是立即数、寄存器或存储器,第二个操作数可以是寄存器或存储器位置。不过和 movl 指令一样,两个操作数不能同时是存储器位置。
- 第一个操作数是移位量,SAL 和 SHL 都是左移指令,效果是一样的,移动几位,右边补上几位0;右移指令不同,算术右移 SAR 是补上符号位,即右边的第一位;逻辑右移 SHR 是补上 0 。
- 移位的目的操作数可以是一个寄存器或是一个存储器位置。
特殊的算术操作
- 需要注意的是,存储结果的寄存器固定死了,是一对寄存器%edx(高32位)和%eax(低32位)组成的 64 位的四字。
-示例
- 计算两个64位值的全128位乘积
2.计算两个64位有符号数的商和余数
3. 计算两个64位无符号数的商和余数
控制
条件码
设置条件码
- 通常情况下,条件码寄存器的值无法主动被改变,它们大多时候是被动改变
- 几乎所有的算术与逻辑指令都会改变条件码寄存器的值,不过改变的前提是触发了条件码寄存器的条件
- leal 指令作为地址计算的时候,不改变任何条件码
①、CMP 指令,指令形式 CMP S2,S1。然后会根据 S1-S2 的差来设置条件码。除了只设置条件码而不更新目标寄存器外,CMP 指令和 SUB 指令的行为是一样的。比如两个操作数相等,那么之差为0,那么就会将零标志设置为 1;其他的标志也可以用来确定两个数的大小关系。
②、TEST 指令,和 AND 指令一样,除了TEST指令只设置条件码而不改变目的寄存器的值。
访问条件码
- 上图所说的同义名,比如说 s e t g setg setg(表示“设置大于”)和 s e t n l e setnle setnle(表示“不小于等于”)指的就是同一条机器指令,编译器和反编译器会随意决定使用哪个名字。
- set指令中的目的操作数,只能是前面所讲的8个单字节的寄存器或者是存储一个字节的存储器位置。
- 示例
条件分支
跳转指令
- 正常情况下,指令会按照他们出现的顺序一条一条地执行。而**跳转指令(jump)**会导致执行切换到程序中一个全新的位置,可以理解为方法或者函数的调用。
- 在汇编代码中,这些跳转的目的地通常用一个标号(label)指明。比如如下代码:
movl $0,%eax
jmpl .L1
movl (%eax),%edx
.L1:
popl %edx
指令 jmpl .L1 会导致程序跳过 movl 指令,从 popl 开始执行。
- 在产生目标代码文件时,汇编器会确定所有带标号指令的地址,并将跳转目标(目的指令的地址)编码为跳转指令的一部分。
- 如下图所示,jump 指令有三种跳转方式:
①直接跳转:跳转目标是作为指令的一部分编码的,比如上面的直接给一个标号作为跳转目标
②间接跳转:跳转目标是从寄存器或者存储器位置中读出的,比如 jmp * %eax 表示用寄存器 %eax 中的值作为跳转目标;再比如 jmp*(%eax) 以 %eax 中的值作为读地址,从存储器中读取跳转目标。
③其他条件跳转:根据条件码的某个组合,或者跳转,或者继续执行代码序列中的下一条指令。
用条件控制来实现条件分支
条件传送指令
- 条件传送指令相当于一个 i f / e l s e if/else if/else的赋值判断,一般情况下,条件传送指令的性能高于 i f / e l s e if/else if/else的赋值判断。
- 但是因为条件传送指令将对两个表达式都求值,因此如果两个表达式计算量很大时,那么条件传送指令的性能就可能不如 i f / e l s e if/else if/else的分支判断。
用条件传送来实现条件分支
- 示例
循环
do-while循环
- 举例
while循环
跳转到中间
guarded-do
- 先设置一个
门卫
,符合条件则进入循环
- 举例
for循环
- 转换成【初始化】+【while循环体】,while循环有两种转换方法
- 例题
switch语句
使用一个数组作为跳转表,着重了解跳转表的创建和使用
过程
运行时栈
- 机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。而为
单个过程分配的那部分栈称为帧栈(stack frame)
。
- 帧栈可以认为是程序栈的一段,它有两个端点,一个标识着起始地址,一个标识着结束地址,而这两个地址,则分别存储在固定的寄存器当中,即起始地址存在%ebp寄存器当中,结束地址存在%esp寄存器当中。也就是说寄存器 %ebp 为帧指针,寄存器 %esp 为栈指针。
- 当程序执行时,栈指针可以移动,因此大多数信息的访问都是相对于帧指针的。
- 每一个栈帧都建立在调用者的下方(也就是地址递减的方向),即栈朝低地址方向增长
- 因此如果我们将栈指针减去一定的值,就相当于给栈帧分配了一定空间的内存。这个理解起来很简单,因为在栈指针向下移动以后(也就是变小了),帧指针和栈指针中间的区域会变长,这就是给栈帧分配了更多的内存。
栈操作
pushq Src
入栈操作
- 从Src获取操作数
- 栈指针%rsp 减8
- 在 %rsp 给出的地址写入操作
popq Dest
出栈操作
- 读取 %rsp 给出的地址的值。
- 栈指针%rsp 加8
- 将值存储在 Dest(必须是寄存器)
转移控制
- call指令:call 指令有一个目标,即指明被调用过程起始的指令地址。直接调用的目标可以是一个标号,间接调用的目标是 * 后面跟一个操作符。
它一共做两件事,第一件是将返回地址(也就是call指令执行时PC的值,即call指令的下一条指令的地址
)压入栈顶(push),第二件是将程序跳转到当前调用的方法的起始地址
。第一件事是为了为过程的返回做准备,而第二件事则是真正的指令跳转。
- ret指令:它同样也是做两件事,第一件是将栈顶的返回地址弹出到PC(pop),第二件事则是按照PC此时指示的指令地址继续执行程序。这两件事其实也可以认为是一件事,因为第二件事是系统自己保证的,系统总是按照PC的指令地址执行程序。
- 示例
数据传送
- 前六个参数放置的寄存器
- 返回值放置在%rax中
- 不同方式的数据传递示例
栈上的局部存储
- 示例
寄存器中的局部存储空间
- 调用者保护
调用方在调用之前在其帧中保存临时值
- 被调用者保护
- 在使用之前,被调用方将临时值保存在其帧中
- 被调用方在返回调用方之前还原它们
- 在 IA32 中,寄存器%eax,%edx和%ecx被划分为调用者保存寄存器。当过程 P 调用 Q 时,Q可以覆盖这些寄存器,而不会破坏 P 所需的数据。
- 寄存器%ebx,%esi和%edi被划分为被调用者保存寄存器。这里 Q 必须在覆盖这些寄存器的值之前,先把他们保存到栈中,并在返回前恢复它们,因为 P(或某个更高层次的过程)可能会在今后的计算中需要这些值。
- 示例
递归
递归调用一个函数本身与调用其它函数是一样的。栈规则提供了一种机制,每次函数调用都有它自己的私有状态信息(保存的返回值、栈指针和被调用者保存寄存器的值)存储。如果需要,它还可以提供局部变量的存储。分配和释放的栈规则很自然的就与函数调用——返回的顺序匹配。
数组的分配与访问
数组的基本原则
我们知道数组是某种基本数据类型数据的集合,对于数据类型 T 和整型常数 N,数组的声明如下:
T A[N]
上面的 A 称为数组名称。它有两个效果:
①、它在存储器中分配一个 L*N 字节的连续区域,这里 L 是数据类型 T 的大小(单位为字节)
②、A 作为指向数组开头的指针,如果分配的连续区域的起始地址为 x a x_a xa,那么这个指针的值就是 x a x_a xa
即当我们用 A[i] 去读取数组元素的时候,其实我们访问的是 x a + i ∗ s i z e o f ( T ) x_a+i*sizeof(T) xa+i∗sizeof(T)。 s i z e o f ( T ) sizeof(T) sizeof(T)是获得数据类型T的占用内存大小,以字节为单位,比如如果T为int,那么 s i z e o f ( i n t ) sizeof(int) sizeof(int)就是4。因为数组的下标是从0开始的,当 i等于0时,我们访问的地址就是 x a x_a xa
- 在IA32中,存储器引用指令可以用来简化数组访问。比如对于上面的 int a[10],我们想访问 a[i],这时候 a 的地址存放在寄存器 %edx 中,而 i 存放在寄存器 %ecx 中。然后指令计算如下:
movl (%edx,%ecx,4), %eax
这会执行地址计算 x a + 4 i x_a+4i xa+4i,读取这个存储器位置的值,并把结果存放在寄存器%eax中。
指针运算
数组的嵌套
定长数组和变长数组
要理解定长和变长数组,我们必须搞清楚一个概念,就是说这个“定”和“变”是针对什么来说的。在这里我们说,这两个字是针对编译器来说的,也就是说,如果在编译时数组的长度确定,我们就称为定长数组,反之则称为变长数组
。
异质的数据结构
struct
所有的组成部分在存储器中连续存放,指向结构的指针指向结构的第一个字节。
联合
允许以多种类型来引用一个对象,总大小等于它最大字段的大小,而指向一个联合的指针,引用的是数据结构的起始位置。
数据对齐
x86-64系统对齐要求为:对于任何需要K字节的标量数据类型的起始地址必须是K的倍数。汇编.align 8
要求后面的数据起始位置是8的倍数。结构体的对齐除了要满足每个字段的对齐要求,还需要考虑整体的结构满足怎样的对齐要求。
练习题
第六章:存储器层次结构
知识点
存储技术
随机访问存储器
由于SRAM的双稳态特性,只要有电,它就会永远地保持它的值。
SRAM对干扰不敏感。
- DRAM:DRAM将每个位存储为对一个电容的充电。和SRAM不同,DRAM单元对干扰非常敏感。 存储器系统必须周期性地通过读出,然后重写来刷新存储器的每一位。
- 以下是SRAM与DRAM的比较:
- 增强的DRAM
快页模式DRAM,扩展数据输出DRAM,同步DRAM,双倍数据速率同步DRAM,视频RAM等等。
- 非易失性存储器ROM(Read Only Memory)
如果断电,DRAM和SRAM会丢失它们的信息,从这个意义上说,他们是易失的。非易失性存储器即使在断电后仍然保存着他们的信息
- 可编程ROM(PROM):只可以被编程一次。
- 可擦写可编程ROM(EPROM):EPROM和电子可擦除PROM都可以被多次擦除和编程。
- 闪存(flash memory):提供相对于传统磁盘的一种更快速更强进和更低消耗的选择。
磁盘的构造
磁盘是由盘片构成的,每个盘面有两面或者称为表面,表面覆盖有磁性材料,盘片中央有一个可旋转的主轴,使得盘片以固定旋转速率旋转磁盘通常包含一个或者多个这样的盘片并封装在一个密封容器中。