csapp_BombLab(待完成
3 程序的机器级表示
编译器基于编程语言的规则、操作系统的惯例、目标机器的指令集生成机器代码。
汇编代码是机器代码的一种形式,它是机器代码的文本表示。
高级代码可移植性好,而汇编代码与特定机器密切相关。
能够阅读汇编代码:
精通细节很重要,是理解更深和更基本概念的先决条件。要认真研究示例、完成练习。
32位机器可以使用约 4GB 的随机访问存储器,64位机器可以使用 256TB(2^48) 的内存空间(这里说的是主存)。
3.2 程序编码
汇编器产生的目标代码是机器代码的一种形式,它包含二进制形式表示的所有指令,但还没有填入全局值的地址。
3.2.1 机器级代码
影响机器级程序的两种抽象:
- 指令集架构:定义了处理器状态、指令的格式、指令对状态的影响。
- 虚拟地址:机器代码将内存看成一个按字节寻址的数组。
对机器代码可见的处理器状态:
- 程序计数器
- 整数寄存器文件:保存临时数据或重要的程序状态
- 条件码寄存器:保存最近执行的算术或逻辑指令的状态信息。
- 一组向量寄存器:保存一个或多个整数或浮点数值
C 语言中的数组和结构,在机器代码中用一组连续的字节来表示。
汇编代码不区分有符号数和无符号数,不区分指针的不同类型,不区分指针和整数。
一条机器指令只执行一个非常基本的操作。
3.2.2 代码示例
反汇编
使用反汇编器可以根据机器代码产生汇编代码。如:48 89 d3 → mov %rdx,%rbx
机器代码与反汇编表示的特性:
- x86-64 的指令长度范围为 1~15 字节。常用指令和操作数少的指令所需字节少。
- 从十六进制字节值到汇编指令,格式为:某个数字唯一地对应某个汇编指令,比如 mov 指令以 48 开头。
- 指令结尾的 ‘q’ 是大小指示符,大多数情况下可以省略。
从源程序转换来的可执行目标文件中,除了程序过程的代码,还包含启动和终止程序的代码,与操作系统交互的代码。
链接器的任务之一就是为函数调用找到匹配的函数的可执行代码的位置。
3.2.3 关于格式的注解
在汇编代码中,以 ‘.’ (点) 开头的行是指导汇编器和链接器工作的伪指令。
3.3 数据格式
字节:byte,8位;字:word,16位;双字:double words,32位;四字:quad words,64位。
对应的指令后缀:movb, movw, movl, movq。
这里说的都是整数,浮点数使用一组完全不同的指令和寄存器。
3.4 访问信息
一个 64 位 CPU 中包含一组 16 个存储 64 位值的通用目的寄存器,用来存储整数和指针。
16 个寄存器标号为 raxrbp,r8r15
16 个寄存器的低位部分都可以作为字节、字、双字、四字来单独访问。分别表示为 al, ax, eax, rax。
低位操作的规则:
- 将寄存器作为目标位置时,生成字节和字的指令会保持剩下的字节不变
- 生成双字的指令会把高位四字节置为 0.
16个寄存器的作用
- rax:返回值
- rsp:栈指针
- rdi, rsi, rdx, rcx, r8, r9:第 1 到第 6 个参数
- rbx, rbp, r12~r15:被调用者保存
- r10, r11:调用者保存
3.4.1 操作数指示符
指令的操作数有三种类型:立即数,寄存器,内存引用
最常用的寻址方式:Imm(rb, ri, s):Imm + rb + ri*s
s 为比例因子,只能是 1,2,4,8 中的某一个
3.4.2 数据传送指令
mov类
mov 只会更新目的操作数指定的寄存器字节或内存位置。
mov 类是最简单的数据传送指令,mov 类有 5 种:
- movb, movw, movl:传送字节、字、双字
- movq:传送四字。如果源操作数是立即数,只能是双字,然后符号扩展到四字(假的四字)
- movabsq:传送绝对的四字。只能以立即数作为源操作数,以寄存器为目的。可以传送任意 64 位立即数。
movq 用来传送寄存器和内存引用中的四字,movabsq 用来传送四字的立即数
mov 类的源操作数和目的操作数不能同时为内存,即不能将值从内存复制到内存。
mov 指令中寄存器的大小必须与 mov 的后缀字符大小匹配。
movb $-17, %al
movz类
movz 系列和 movs 系列可以把较小的源值复制到较大的目的,目的都是寄存器。
movz 将目的寄存器剩余字节做零扩展,movs 做符号扩展
movz类:movzbw, movzbl, movzbq, movzwl, movzwq(movzbw 即从字节复制到字,其他类似)
movs类:movsbw, movsbl, movsbq, movswl, movswq, movslq, cltq
- cltq:没有操作数,将 eax 符号扩展到 rax,等价于 movslq %eax,%rax
3.4.3 数据传送示例
局部变量通常保存在寄存器中。
函数返回指令 ret 返回的值为寄存器 rax 中的值
强制类型转换是通过 mov 指令实现的。
当指针存在寄存器中时,a = *p 的汇编指令为: mov (rdi), rax
3.4.4 压入和弹出栈数据
栈向下增长,栈顶的地址是栈中元素地址中最低的。栈指针 rsp 保存栈顶元素的地址。
出入栈指令:
- pushq rax:压栈,栈指针减 8 并将 rax 中的值写入新的栈顶地址,等价于:subq $8, (rsp) ; movq rax,(rsp)。
- popq rax:出栈,栈指针加 8 并将出栈的值写入 rax 中,等价于:movq (rsp),rax ; add $8,(rasp)
使用 mov 指令和标准的内存寻址方法可以访问栈内的任意位置,而非仅限于栈顶。
3.5 算术和逻辑操作
x86-64 的每个指令类都有对应四种不同大小数据的指令
算术和逻辑操作共有四组:
加载有效地址
- leaq S, D:将 S 的地址保存到 D 中,D 必须是寄存器
一元操作
- inc D: D+1
- dec D: D-1
- neg D:取负
- not D:取补
二元操作(加减乘,与或异或,没有除法)
- add s, d: d=d+s
- sub s, d: d=d-s
- imul s, d: d=d*s 乘
- xor s, d: d=d^s 异或
- or s, d: d=d|s 或
- and s,d: d=d&s 与
移位
- sal k,d: d=d<
- shl k,d: d=d<
- sar k,d: d=d<
- shr k,d: d=d<
3.5.1 加载有效地址
leaq 实际上是 movq 指令的变形。操作是从内存读数据地址到寄存器。
leaq 在实际应用中常常不用来取地址,而用来计算加法和有限形式的乘法
leaq 9(rdi, rsi, 4), rax;//x in rdi,y in rsi。此操作实际上等于将 x+4*y+9 的结果存入 rax
3.5.2 一元和二元操作
一元操作中的操作数既是源又是目的。
二元操作中的第二个操作数既是源又是目的。
因为不能从内存到内存,因此当第二个操作数是内存地址时,要先从内存读出值,执行操作后再把结果写回去。
注意 sub s,d 是 d-s 而不是 s-d
3.5.3 移位操作
移位操作的移位量可以是一个立即数或放在单字节寄存器 cl 中。
当移位量大于目的数的长度时,只取移位量低字节中的值(小于目的数长度)来作为真实的移位量。
3.5.4 特殊的算术操作
两个 64 位数的乘积需要 128 位来表示,x86-64指令集可以有限的支持对 128 位数的操作,包括乘法和除法。
128 位数需要两个寄存器来存储,移动时也需要两个 movq 指令来移动。
这种情况对于有符号数和无符号数采用了不同的指令。
3.6 控制
条件语句、循环语句、分支语句都要求有条件的执行。
机器代码提供两种低级机制来实现有条件的行为:
- 测试数据值,然后根据测试的结果来改变控制流或数据流
- 使用 jump 指令进行跳转
3.6.1 条件码
条件码寄存器都是单个位的,是不同于整数寄存器的另一组寄存器。
条件码描述了最近的算术或逻辑操作的属性,可以通过检测这些寄存器来执行条件分支指令。
常用条件码:
- CF:进位标志。最近的操作使最高位产生了进位。可以用来检查无符号数的溢出
- ZF:零标志。最近的操作的结果为 0
- SF:符号标志。最近的操作的结果为负数。
- OF:溢出标志。最近的操作导致了补码溢出
除了 leaq 指令外,其余的所有算术和逻辑指令都会根据运算结果设置条件码。
此外还有两类特殊的指令,他们只设置条件码不更新目的寄存器:
- cmp s1, s2:除了不更新目的寄存器外与 sub 指令的行为相同
- test s1, s2:除了不更新目的寄存器外与 and 指令的行为相同
3.6.2 访问条件码
条件码一般不直接读取,常用的使用方法有 3 种:
- 根据条件码的某种组合,使用 set 指令类将一个字节设置为 0 或 1。
- 条件跳转到程序的某个其他部分
- 有条件地传送数据
set 指令类
set 指令的目的操作数是低位单字节寄存器元素或一个字节的内存位置。set 会将该字节设置为 0 或 1
set 指令类的后缀指明了所考虑的条件码的组合,如 setl (set less) 表示“小于时设置”

注意到上图中,set 指令对于大于、小于的比较分为了有符号和无符号两类。
大多数时候,机器代码对无符号和有符号两种情况使用一样的指令。
使用不同指令来处理无符号和有符号操作的情况:
- 不同的条件码组合:
- 不同版本的右移:sar 和 shr
- 不同的乘法和除法指令
汇编语言中数据本身不区分有符号和无符号,通过不同的指令来区分有符号操作和无符号操作。
注意在汇编代码中,8字节的操作数可能是 long,long long 或 指针
3.6.3 跳转指令
跳转指令的目的地由一个标号指明
jmp .L1 ;//跳转到 .L1 。在实际的跳转指令中,.L1 会直接编码为跳转目标的地址。 movq (rax),rdx .L1: popq rdx
jmp 可以是直接跳转,即操作数为标号。也可以间接跳转,即操作数是寄存器或内存引用,这种情况下跳转到寄存器中存储的地址处。
跳转指令分为有条件跳转和无条件跳转,只有 jmp 是无条件跳转。有条件跳转都只能是直接跳转。
有条件跳转类似 set 指令系列,根据条件码寄存器的值来判断是否进行跳转。

3.6.4 跳转指令的编码
跳转指令的机器编码(就是纯粹数字表示的机器语言)有几种方式,其中两种如下:
- PC 相对跳转:使用目标地址与跳转指令之后下一条指令的地址之间的差来编码。可以用 1、2 或 4 个字节来编码。
- 绝对地址编码:使用目标的绝对地址。用 4 个字节直接指出。
汇编器和链接器会自己选择适当的编码方式
3.6.5 用条件控制来实现条件分支
汇编代码层面的条件控制类似于 c 语言的 goto 语句。
汇编语言使用条件码和条件跳转来起到和 c 语言中 if 相似的作用
‘C 语言’ if( x<y ) { i++ } else { i– } ‘汇编’ cmpq rsi,rdi jge .L2 incl rax; .L2: decl rax;