CSAPP 第三章 程序的机器级表示
3.2 程序编码
有两个文件p1.c
和p2.c
,我们用Unix
命令编译这些代码
1 | gcc -Og -o p p1.c p2.c |
gcc
:指GCC
C编译器
-Og
:告诉编译器使用生成符合原始C语言整体结构的机器代码的优化等级,使用-O1,-O2
会导致产出的机器代码变形,难以理解。
gcc
命令调用了一整套的程序,将源代码转化成可执行代码。
首先,C预处理器扩展源代码,插入所有用#include
命令指定的文件,并扩展所有用#define
声明指定的宏。
其次,编译器产出两个源文件的汇编代码,名字为p1.s
和p2.s
接下来汇编器会将汇编代码转化成二进制目标文件p1.o
和p2.o
main.c
1 |
|
mstore.c
1 | long mult2(long,long); |
编译生成汇编文件mstore.s
1 | gcc -Og -S mstore.c |
使用vim
打开
1 | .section __TEXT,__text,regular,pure_instructions |
其中以.
开头的行都是指导汇编器和链接器工作的伪指令,直接忽略
可以简化为:
1 | mulstore: |
pushq %rbx
:将寄存器rbx
的值压入程序栈进行保存
为什么程序一开始要保存
rbx
的内容我们先了解调用者保存寄存器和被调用者保存寄存器
假如函数A调用了函数B,所以函数A就称为调用者(
Caller
),函数B为被调用者(Callee
)由于调用了函数B,寄存器
rbx
在函数中被修改了,逻辑上寄存器rbx
的内容在调用函数B的前后应该保持一致,解决这个问题有两个策略,第一,调用者保存:函数A在调用函数B之前,提前保存寄存器
rbx
的内容,执行完函数B之后恢复寄存器rbx
原来存储的内容第二,被调用者保存:函数B在使用寄存器
rbx
之前,先保存寄存器rbx
的值,在函数B返回之前,先回复寄存器rbx
原来存储的内容
popq %rbx
:在函数返回之前,恢复寄存器rbx
的内容
第二行:movq %rdx, %rbx
:将寄存器rdx
的内容复制到寄存器rbx
根据寄存器用法的定义
1 | void mulstore(long x, long y, long *dest); |
mov
指令的后缀"q"
表示数据的大小
Size of Data Type in x86-64
C 声明 | Inter 数据类型 | 汇编代码后缀 | 大小(字节) |
---|---|---|---|
char | Byte | b | 1 |
short | Word | w | 2 |
int | Double word | l | 4 |
long | Quad word | q | 8 |
char * | Quad word | q | 8 |
float | Single precision(单精度) | s | 4 |
double | Double precision(双精度) | l | 9 |
call mult2
:对应函数调用,该函数的返回值会保存到寄存器rax
中
movq %rax, (%rbx)
:将寄存器rax
的值送到内存中,内存的地址就存放在寄存器rbx
中
ret
:就是函数返回
翻译为机器代码
mstore.o
1 | gcc -Og -c mstore.c |
由于该文件是二进制的无法直接查看,我们需要用到反汇编工具----objdump
1 | objdump -d mstore.o |
1 | mstore.o: file format mach-o 64-bit x86-64 |
3.3 数据格式
大多数指令有一个或多个操作数,指示出执行一个操作中要使用的源数据值,以及放置结果的目的位置。
各种不同的操作数的被分为三种类型:
- 立即数:在
ATT
格式的汇编代码中是$
后面跟一个用标准C表示法表示的整数 - 寄存器:
- 内存引用:\(M_b[addr]\)
内存引用:
Imm(\(r_b,r_i,s\)) ----> Imm + \(R[r_b] + R[r_i]*s\)
Imm ----> 立即数
\(r_b\)----> 基址寄存器
\(r_i\)---->变址寄存器
s ----> 比例因子【1,2,4,8】
编译器会根据数组类型来确定比例因子的数值
Mov
指令
MOV 源操作数 目的操作数
源操作数可以为:立即数,寄存器,内存
目的操作数可以为:寄存器,内存,但是不能为立即数
MOV指令的源操作数和目的操作数不能都是内存的地址
当需要从内存一个地方复制到另一个地方时,需要用两条mov指令
mov memory , register
mov register, memory
MOV指令的后缀需要与寄存器的大小匹配
3.4 栈与数据传送
long
:64 --> %rax
int
:32 --> %eax
short
:16 --> %ax低8位 --> %al
数据传送示例:
main.c
1 |
|
exchange.c
1 | long exchange(long *xp, long y){ |
编译exchange.c
为汇编代码
1 | gcc -Og -S exchange.c |
去除多余没用信息后
1 | exchange: |
%rdi
保存函数的第一个参数,%rsi
保存函数的第二个参数
所以可以这样理解
1 | exchange: |
栈
栈顶是栈中地址最低的。
入栈push:
先将地址减小,再入栈
1 | pushq %rax |
pushq
就等于上面两条指令
出栈pop:
先出栈,再把地址增加
1 | popq %rbx |
3.5 算术和逻辑操作
加载有效地址leaq
leaq
:它的指令形式是从内存读数据到寄存器
1 | leaq S, D |
例如:
1 | leaq 7(%rdx, %rdx, 4), %rax |
表示:
假设 %rdx
的值为x
有效地址 = 7 + %rdx + %rdx * 4
= 5x + 7
目的操作数必须是一个寄存器
leaq
指令还可以执行加法和有限形式的乘法
scale.c
1 | long scalse(long x, long y, long z){ |
编译后形成的汇编代码为:
1 | scale: |
理解:
1 | scale: |
因为比例因子只能为[1,2,4,8]
,所以需要将12拆分
一元和二元操作
一元操作数:即是源操作数又是目的操作数
二元操作数:
例如:
1 | ADD S, D |
第二个操作数(D
)即是源又是目的操作数,第一个操作数可以是立即数,寄存器或是内存位置
第二个操作数可以是寄存器或是内存位置,但不能是立即数
移位操作
左移:
SAL和SHL,两者的效果一样,都是将右边填上0
右移:
SAR执行算术运算(填上符号位)
SHR执行逻辑运算(填上0)
3.6 控制
3.6.1 条件码
最常用的条件码:
- CF:进位标志。最近的操作使最高位产生了进位,CF = 1。可用来检查无符号操作的溢出。
- ZF:零标志。最近的操作得出的结果位0,ZF = 1
- SF:符号标志。最近的操作得到的结果为负数,SF = 1
- OF:溢出标志。最近的操作导致一个补码溢出----正溢出或负溢出
执行XOR
时,ZF = 0, OF = 0
INC(+1),DEC(-1)
时,只会改变OF,ZF
,不会改变CF
CMP和TEST
指令也可以设置条件码
1 | cmpq %rax, %rdx |
cmp指令是根据两个操作数的差来设置条件码寄存器,并不会更新目的寄存器的值
test指令设置条件码寄存器,并不会更新目的寄存器的值
条件码的使用
setl
和setb
表示“小于时设置(set
less)"和"低于时设置(set below)"
comp.c
1 | int comp(long a, long b){ |
编译为汇编代码:
1 | gcc -Og -S comp.c |
1 | comp: |
1 | int comp(char a, char b){ |
汇编为:
1 | comp: |
a < b --> SF ^ OF
t = a - b
Case1: a < b, t < 0, SF = 1, SF ^ OF = 1
Case2: a > b, t > 0, SF = 0, SF ^ OF = 0
Case3: a < b, a = -2, b = 127
a = 1111 1110
b = 0111 1111
t = a - b
a = 1111 1110
(-b) = 1000 0001(按位取反,末位+1)
a + (-b) = 1 0111 1111
t = 127 > 0, SF = 0, OF = 1, SF ^ OF = 1
综上:
对于有符号数的比较,采用的是SF和ZF的组合
对于无符号数的比较,采用的是CF和ZF的组合
3.6.2 访问条件码
常用的使用方式:
- 可以根据条件码的某种组合,将一个字节设置为0或者1,2
- 可以条件跳转到程序的某个其他的部分
- 可以有条件地传送数据
3.6.3 跳转指令
jump.c
1 | long absDiff_se(long x, long y){ |
1 | absDiff_se: |
另一种实现方式,效率更高:
1 | long comvdiff_se(long x, long y){ |
1 | cmovdiff_se: |
为什么基于条件传送的代码会比基于跳转指令的代码效率高?
处理器会根据分支预测器来猜测每条跳转指令是否执行,
当发生错误预测时,会浪费大量的时间,导致程序性能严重下降(流水线相关)