精通细节是理解更深和更基本概念的先决条件 "This is a subject where mastering the details is a prerequisite to understanding the deeper and more fundamental concepts."
Disassembly of section .text:
0000000000000000 <sum>:
0: 8d 04 37 lea (%rdi,%rsi,1),%eax
3: 01 05 00 00 00 00 add %eax,0x0(%rip) # 9 <sum+0x9>
9: c3 retq
指定-m32:
00000000 <sum>:
0: 8b 44 24 08 mov 0x8(%esp),%eax
4: 03 44 24 04 add 0x4(%esp),%eax
8: 01 05 00 00 00 00 add %eax,0x0
e: c3 ret
指令的操作数(operand)有三类,IA32 格式如下:
- 立即数 Immediate
- 例如
$−577$ 、$0x1F$
- 例如
- 寄存器 Register
- 例如
%eax
,%ax
- 例如
- 存储器 Memory
- 例如
4(%eax)
、9(%eax, edx)
、0xF9(%eax, edx, 4)
含寄存器就带括号的。还有绝对地址的写法,例如0x104
- 其中
0xF9(%eax, edx, 4)
中的 4 是变址的比例因子,必须是 1、2、4 或 8(这一点在乘法中常见)。通常引用中有两个寄存器适合于二维的数组或者表
- 例如
- 缺哪一个就假设他不存在公式中
- 比例
$s$ 只能是 1 2 4 8 - 如果 没有括号,表示 寄存器中存的值
如果 有括号,表示 寄存器中存的地址 中的值
这个过程中
$Imm$ 也是和括号内一起算地址的,如(P122)$260(%rcx, %rdx) \ \rightarrow 260+0x1+0x3 \ \rightarrow 0x108 \ \rightarrow 0x13$
- MOV 类
源操作数,目的操作数 源操作数->目的操作数 movb 字节 movb(%rdi, %rcx), %al 内存->寄存器 movw 字(2 字节) movw %bp, %sp 寄存器->寄存器 movl 双字 movl $0x4050, %eax 直接数->寄存器 movq 四字 movq %rax, -12(%rbp) 寄存器->内存 - 扩展 从小到大时需要扩展,否则高位可能为其他数据
- movzxx
- 零扩展,补零
- movsxx
- 符号扩展,补符号位
- xx 表示类型从左扩展到右
- movzxx
Disassembly of section .text:
00000000 <exchange>:
0: 8b 54 24 04 mov 0x4(%esp), %edx
4: 8b 02 mov (%edx), %eax
6: 8b 4c 24 08 mov 0x8(%esp), %ecx
a: 89 0a mov %ecx, (%edx)
c: c3 ret
理解:
- xp 存储在相对于寄存器 esp 偏移 4 的地方(0x4(%esp))
- y 存储在相对于寄存器 esp 偏移 8 的地方(0x8(%esp))
- 这里是 esp(和书中不同),说明了两个参数是存储在栈中,栈也是存储的一部分,只不过通过 esp 来控制访问的
mov 0x4(%esp), %edx
将 xp 的值加载到 edx 中mov (%edx), %eax
将 xp 对应的地址处的值加载到 eax 中(long x)mov 0x8(%esp), %ecx
将 y 的值加载到 ecx 中mov %ecx, (%edx)
将 y 的值存储到 xp 对应的存储地址处ret
返回,返回值在 eax 中,正是*xp 之前的值
- C 语言中的指针其实就是地址,引用指针就是将指针取到寄存器中,然后在存储器访问中使用这个寄存器
- 函数体中的局部变量 x 存在寄存器,而非存储器中
栈顶到栈底,地址增大,一般将栈顶放在下方
%rsp
保存栈顶元素地址
大地址 栈底
↑ ↑
小地址 栈顶
-
pushq S
压入-
R[%rsp] <- R[%rsp] - 8
M[R[%rsp]] <- S
- 等价于
subq $8, %rsp
movq %rbp, (%rsp)
-
-
popq D
弹出-
D <- M[R[%rsp]]
R[%rsp] <- R[%rsp] + 8
- 等价于
movq (%rsp), %rax
addq $8, %rsp
-
弹出时,不会清除弹出的值,而是之后的压入中覆盖掉
==注意非的写法,是 D = ~D
==
leaq Src, Dst
>Src
为地址表达式
- lea 没有引用内存,只是进行计算
例如,指令
leaq Imm(ra, rb, n) D
先计算了Imm + ra + n * rb
的结果 之后,没有去访问内存取这个结果内的值,而是直接将这个结果写入寄存器D
- 而 mov 是加载那个地址处的值到寄存器中
例如,指令
movq Imm(ra, rb, n) D
同样先计算了Imm + ra + n * rb
的这个结果 之后,在内存中寻找这个结果对应的值,写入寄存器D
例如
x 在 %rdi
, y 在 %rsi
,两个形参都是值
leaq (%rdi, %rdi, 4), %rsi
为 y <- 5 * x
移位指令 | 移位量 | 移位的数 |
---|---|---|
左/右 算术/逻辑 长度 | 立即数 (如 $4 ) 或 %cl |
寄存器 或 内存位置 |
移位量是单字节编码,移位量是立即数或者放在单字节寄存器 %cl
中,注意只能是这个寄存器
左 | 右 | |
---|---|---|
算术 | sal |
sar |
逻辑 | shl |
shr |
int arith(int x, int y, int z){
int t1 = x + y;
int t2 = z * 48;
int t3 = t1 & 0xFFFF;
int t4 = t2 * t3;
return t4;
}
Disassembly of section .text:
00000000 <arith>:
0: 8b 44 24 0c mov 0xc(%esp),%eax
4: 8d 04 40 lea (%eax,%eax,2),%eax
7: c1 e0 04 shl $0x4,%eax
a: 8b 54 24 08 mov 0x8(%esp),%edx
e: 03 54 24 04 add 0x4(%esp),%edx
12: 0f b7 d2 movzwl %dx,%edx
15: 0f af c2 imul %edx,%eax
18: c3 ret
分析:
- 可以看到参数 x,y,z 分别放在栈(
%esp
)的临近位置 - 先计算的是 z * 48 而不是按函数中给出的顺序,前 3 条指令意思是:
- z 存入
%eax
- 2 * z + z = 3z 存入
%eax
- 3z << 4 → 3z * 16 = 48z 存入
%eax
- z 存入
- 接下来的 2 条计算 x+y 存入
%edx
- 然后利用
movz
只保留低 2B 存入%edx
- 最后相乘,结果保存在
%eax
中,返回
机器代码提供两种低级机制来实现有条件的行为:测试数据值,然后根据测试的结果改变控制流或数据流
CF |
(unsigned) t < (unsigned) a |
进位 | 最高位产生了进位 |
ZF |
(t == 0) |
零 | 得到结果为 0 |
SF |
(t < 0) |
符号 | 得到结果为负数 |
OF |
(a < 0 == b < 0) && (t < 0 != a < 0) |
溢出 | 补码溢出 |
算术逻辑操作后会产生一些列条件码
此外,还有 CMP
和 TEST
指令,只设置条件码,不改变其他寄存器
==$S_2 - S_1$== | 比较 | |
测试 |
注意 CMP 的==顺序==,是 第二个 cmp b, a
比较的是 a - b
test
的实际意义:- 判断某一位是否为 1
test a, 100b
+jne
- 判断是否为空,0
test a, a
+jne
- 判断某一位是否为 1
- 通用
- 零标志(ZF)决定是否相等
- 符号标志(SF)决定正负
- 无符号
- 零标志(ZF)和进位标志(CF)
seta D $D \leftarrow \sim CF & ZF$ 大于 setae D $D \leftarrow \sim CF$ 大于等于 setb D $D \leftarrow CF$ 小于 setae D $D \leftarrow CF \mid ZF$ 小于等于
- 零标志(ZF)和进位标志(CF)
- 补码
- 符号和溢出异或 SF^OF 当作无符号里的进位(CF)
- SF^OF 检测
$a \lt b$ 是否为真 - 无符号里用 above,below,补码用 greater,less
- 直接跳转
- 目标作为指令的一部分
jmp .L1
其中.L1
是一个标号(label),写在汇编代码中je
jge
等条件跳转,只能是直接
- 间接跳转
- 目标从寄存器或内存读出
jmp *%rax
寄存器中的值作为目标jmp *(%rax)
寄存器中的值作为地址,从主存中取值
- 汇编代码中,跳转目标用符号标号书写
- 汇编器及后面的链接器,会产生跳转目标的适当编码
- 通常是 PC 相对的(PC-relative) 即将==目标地址==与==紧跟在跳转指令后的指令地址==之差作为偏移量,编码为 1、2 或 4 字节
- 也有绝对地址编码,4 个字节指定目标
-
当执行 PC-relative 寻址时,程序计数器的值是 跳转指令后面那条指令 的地址,而不是跳转指令本身的地址,因为处理器会首先更新 PC
-
如
40042f: 74 f4 je xxxxxx 400431: 5d pop *%rbp
xxxxxx = 400431 + f4 = 400425 注意 f4 是负数
-
t = test-expr;
if (!t)
goto false;
// 因为 then-statement 一般都有,所以条件判断 !t
then-statement
goto done; // return
false:
else-statement
done:
有 else if 时,要注意是否还有隐含的 else
Disassembly of section .text:
00000000 <absdiff>:
0: 53 push %ebx
1: 8b 4c 24 08 mov 0x8(%esp),%ecx
5: 8b 54 24 0c mov 0xc(%esp),%edx
9: 89 d3 mov %edx,%ebx
b: 29 cb sub %ecx,%ebx
d: 89 c8 mov %ecx,%eax
f: 29 d0 sub %edx,%eax
11: 39 d1 cmp %edx,%ecx
13: 0f 4c c3 cmovl %ebx,%eax
16: 5b pop %ebx
17: c3 ret
cmovl:利用前面比较得到的条件码,进行有条件的 mov,这里的条件是 less
cmov + 条件,如 cmovge,cmovs
- 先计算 then 和 else 两个分支的结果
- then 的结果作为返回值
- 条件判断,是否把 else 的结果赋值给 then 的结果
基于条件传送指令的代码比基于条件控制转移的代码性能好,控制流不依赖于数据,使得处理器更容易保持流水线是满的
absdiff_condition.c, cmovdiff.c
00000000 <absdiff>:
0: 53 push %ebx
1: 8b 4c 24 08 mov 0x8(%esp),%ecx
5: 8b 54 24 0c mov 0xc(%esp),%edx
9: 89 cb mov %ecx,%ebx
b: 29 d3 sub %edx,%ebx
d: 89 d0 mov %edx,%eax
f: 29 c8 sub %ecx,%eax
11: 39 d1 cmp %edx,%ecx
13: 0f 4f c3 cmovg %ebx,%eax
16: 5b pop %ebx
17: c3 ret
条件==跳转==会进行分支预测,若实际结果和预测不同,则需丢弃预测后的内容,重新填充流水线(即惩罚) 条件==传送==无需预测测试的结果,处理器只是从 source 中读值,检查条件码,然后要么更新目的寄存器,要么保持不变,所以就没有预测错误的惩罚
并非所有的条件表达式都可以用条件传送来编译。两个表达式中任何一个发生错误或副作用,都会导致非法行为
编译器必须权衡计算开销和由于分支预测错误导致的性能处罚之间的相对性能
00000000 <fact_do>:
0: 8b 54 24 04 mov 0x4(%esp),%edx
4: b8 01 00 00 00 mov $0x1,%eax
9: 0f af c2 imul %edx,%eax
c: 83 ea 01 sub $0x1,%edx
f: 83 fa 01 cmp $0x1,%edx
12: 7f f5 jg 9 <fact_do+0x9>
14: f3 c3 repz ret
循环控制变量 n 在 edx 中,result 在 eax 中,跳转使用的相对地址,至于 repz ret 的含义不太理解,说 AMD 的分支预测有问题如果只用一个 ret
-
跳转到中间
goto test; loop: body-statement test: t = test-expr; if (t) goto loop;
-
guarded-do
t = test-expr; if (!t) goto done; loop: body-statement t = test-expr; if (t) goto loop; done:
00000000 <fact_while>:
0: 8b 54 24 04 mov 0x4(%esp),%edx
4: 83 fa 01 cmp $0x1,%edx
7: 7e 12 jle 1b <fact_while+0x1b>
9: b8 01 00 00 00 mov $0x1,%eax
e: 0f af c2 imul %edx,%eax
11: 83 ea 01 sub $0x1,%edx
14: 83 fa 01 cmp $0x1,%edx
17: 75 f5 jne e <fact_while+0xe>
19: f3 c3 repz ret
1b: b8 01 00 00 00 mov $0x1,%eax
20: c3 ret
先判断条件,然后和 do-while 一样,在每个条件跳转指令之后都加上了 repz ret
00000000 <fact_for>:
0: 8b 4c 24 04 mov 0x4(%esp),%ecx
4: 83 f9 01 cmp $0x1,%ecx
7: 7e 16 jle 1f <fact_for+0x1f>
9: b8 01 00 00 00 mov $0x1,%eax
e: ba 02 00 00 00 mov $0x2,%edx
13: 0f af c2 imul %edx,%eax
16: 83 c2 01 add $0x1,%edx
19: 39 d1 cmp %edx,%ecx
1b: 7d f6 jge 13 <fact_for+0x13>
1d: f3 c3 repz ret
1f: b8 01 00 00 00 mov $0x1,%eax
24: c3 ret
没啥区别,除了控制变量递增,上述三种循环控制效率是一样的
使用跳转表的==优点==是执行 switch 语句的时间与 case 的数量无关。当 case 数据量比较多,并且值得取值范围较小时就会使用跳转表(jump table)
Disassembly of section .rodata:
00000000 <.rodata>:
0: 17 pop %ss
1: 00 00 add %al,(%eax)
3: 00 26 add %ah,(%esi)
5: 00 00 add %al,(%eax)
7: 00 1b add %bl,(%ebx)
9: 00 00 add %al,(%eax)
b: 00 1e add %bl,(%esi)
d: 00 00 add %al,(%eax)
f: 00 22 add %ah,(%edx)
11: 00 00 add %al,(%eax)
13: 00 26 add %ah,(%esi)
15: 00 00 add %al,(%eax)
17: 00 22 add %ah,(%edx)
19: 00 00 add %al,(%eax)
...
Disassembly of section .text:
00000000 <switch_eg>:
0: 8b 44 24 04 mov 0x4(%esp),%eax
4: 8b 4c 24 08 mov 0x8(%esp),%ecx
8: 8d 51 9c lea -0x64(%ecx),%edx
b: 83 fa 06 cmp $0x6,%edx
e: 77 16 ja 26 <switch_eg+0x26>
10: ff 24 95 00 00 00 00 jmp *0x0(,%edx,4)
17: 83 c0 0d add $0xd,%eax
1a: c3 ret
1b: 83 c0 0a add $0xa,%eax
1e: 83 c0 0b add $0xb,%eax
21: c3 ret
22: 0f af c0 imul %eax,%eax
25: c3 ret
26: b8 00 00 00 00 mov $0x0,%eax
2b: c3 ret
分析:没有生成像书中那么好看的 jump table,也没有那么明显,但是的确有,主要的思想是快速定位要执行的指令。 开始指令分析,n -> ecx , x -> eax, (n-100) -> edx , 100 就是 0X64, 然后比较如果 edx 超过 6,那么就执行 default(返回 0),否则执行间接 jump,间接地址是怎么计算的呢?4 * edx + 0, 立即数 0 说明跳转表就在该指令的后面,然后根据不同的情况定位到不同的指令块,比如说 case 102, edx=2,就会 jmp 到:
1b: 83 c0 0a add $0xa,%eax
1e: 83 c0 0b add $0xb,%eax
21: c3 ret
00000000 <switch_eg_impl>:
0: 8b 44 24 08 mov 0x8(%esp),%eax
4: 83 e8 64 sub $0x64,%eax
7: 83 f8 06 cmp $0x6,%eax
a: 77 07 ja 13 <switch_eg_impl+0x13>
c: ff 24 85 00 00 00 00 jmp *0x0(,%eax,4)
13: f3 c3 repz ret
15: b8 00 00 00 00 mov $0x0,%eax
1a: eb 16 jmp 32 <switch_eg_impl+0x32>
1c: 8b 44 24 04 mov 0x4(%esp),%eax
20: 8d 04 40 lea (%eax,%eax,2),%eax
23: 8b 54 24 04 mov 0x4(%esp),%edx
27: 8d 04 82 lea (%edx,%eax,4),%eax
2a: c3 ret
2b: 8b 44 24 04 mov 0x4(%esp),%eax
2f: 83 c0 0a add $0xa,%eax
32: 83 c0 0b add $0xb,%eax
35: c3 ret
36: 8b 44 24 04 mov 0x4(%esp),%eax
3a: 0f af c0 imul %eax,%eax
3d: c3 ret
- GCC 语法,&& 创建一个指向代码位置的指针
- GCC 支持 computed goto, 是对 C 的扩展,代码中的 go *jt[index]
为单个过程分配的那部分栈称为栈帧 stack frame
调用者 P 调用被调用者 Q,Q 的参数放在 P 的栈帧中,栈帧的最后放的是当前调用者 P 的返回地址,栈帧以保存 EBP 开始。
call Q call *Operand
call
将返回地址(紧跟 call 之后那条指令的地址)入栈,并跳转到被调用过程 Q
的起始处。ret 指令从栈中弹出地址,并跳转到该处
谁应该去保存 调用者保存,就是说允许被调用者修改,所以应当有调用者保存 被调用者保存,就是说被调用者得恢复这些值
rbx
,rbp
,r12~r15
划分为被调用者保存寄存器,调用前后的值不能改变- 被调用者如果要使用那些寄存器,就在使用前保存,使用完后恢复
- 一般是将他们的值先压入栈中,调用结束再弹出
注意通过链接后才看到 caller 的汇编代码,64bit 对应的汇编:
00000000004004f6 <swap_add>:
4004f6: 55 push %rbp # rbp 的值入栈,也就是暂存
4004f7: 48 89 e5 mov %rsp,%rbp
4004fa: 48 89 7d e8 mov %rdi,-0x18(%rbp)
4004fe: 48 89 75 e0 mov %rsi,-0x20(%rbp)
400502: 48 8b 45 e8 mov -0x18(%rbp),%rax
400506: 8b 00 mov (%rax),%eax
400508: 89 45 f8 mov %eax,-0x8(%rbp)
40050b: 48 8b 45 e0 mov -0x20(%rbp),%rax
40050f: 8b 00 mov (%rax),%eax
400511: 89 45 fc mov %eax,-0x4(%rbp)
400514: 48 8b 45 e8 mov -0x18(%rbp),%rax
400518: 8b 55 fc mov -0x4(%rbp),%edx
40051b: 89 10 mov %edx,(%rax)
40051d: 48 8b 45 e0 mov -0x20(%rbp),%rax
400521: 8b 55 f8 mov -0x8(%rbp),%edx
400524: 89 10 mov %edx,(%rax)
400526: 8b 55 f8 mov -0x8(%rbp),%edx
400529: 8b 45 fc mov -0x4(%rbp),%eax
40052c: 01 d0 add %edx,%eax
40052e: 5d pop %rbp # 弹出值存入 rbp,也就是恢复
40052f: c3 retq
0000000000400530 <caller>:
400530: 55 push %rbp
400531: 48 89 e5 mov %rsp,%rbp
400534: 48 83 ec 10 sub $0x10,%rsp # 分配栈空间
400538: c7 45 f0 7b 00 00 00 movl $0x7b,-0x10(%rbp)
40053f: c7 45 f4 c8 01 00 00 movl $0x1c8,-0xc(%rbp)
400546: 48 8d 55 f4 lea -0xc(%rbp),%rdx # arg 2
40054a: 48 8d 45 f0 lea -0x10(%rbp),%rax # arg 1
40054e: 48 89 d6 mov %rdx,%rsi # swap 参数 2
400551: 48 89 c7 mov %rax,%rdi # swap 参数 1
400554: e8 9d ff ff ff callq 4004f6 <swap_add>
400559: 89 45 f8 mov %eax,-0x8(%rbp)
40055c: 8b 55 f0 mov -0x10(%rbp),%edx
40055f: 8b 45 f4 mov -0xc(%rbp),%eax
400562: 29 c2 sub %eax,%edx
400564: 89 d0 mov %edx,%eax
400566: 89 45 fc mov %eax,-0x4(%rbp)
400569: 8b 45 f8 mov -0x8(%rbp),%eax
40056c: 0f af 45 fc imul -0x4(%rbp),%eax
400570: c9 leaveq
400571: c3 retq
把书上的分析理解,很重要,不要逃避
Disassembly of section .text:
00000000 <rfact>:
0: 53 push %ebx
1: 83 ec 08 sub $0x8,%esp
4: 8b 5c 24 10 mov 0x10(%esp),%ebx
8: b8 01 00 00 00 mov $0x1,%eax # res = 1
d: 83 fb 01 cmp $0x1,%ebx
10: 7e 12 jle 24 <rfact+0x24> # n < 1 ?
12: 83 ec 0c sub $0xc,%esp
15: 8d 43 ff lea -0x1(%ebx),%eax # n = n - 1
18: 50 push %eax
19: e8 fc ff ff ff call 1a <rfact+0x1a>
1e: 83 c4 10 add $0x10,%esp
21: 0f af c3 imul %ebx,%eax # res = res * n
24: 83 c4 08 add $0x8,%esp # done
27: 5b pop %ebx
28: c3 ret
上面对应的是没有经过链接的反汇编,可以看到主体在,但是没有 push %ebp
movl %esp,%ebp
等这些栈帧控制指令。
T A[N]
- 内存中分配
$\operatorname{sizeof}(L) \times N$ 的连续空间 - A 为指向数组开头的指针,即
0(%rdx)
,假设%rdx
是 A 的寄存器
取数方法
movl (%rdx, %rcx, 4), %eax
E + i * size
指针的运算,在 C 语言中只需要和常数运算,而在汇编中会有伸缩,如
T A[N];
T *ptr=A;
ptr++; // 汇编中,(ptr的寄存器)+1*sizeof(T)
T A[N][N]
多维数组中,A[i]
是一个指针,表示数组 A 第 i 行
令 Aptr = A[i]
,则C 语言中下一行为 Aptr + N
,汇编中对应地伸缩
struct node_s {
struct node_s *left; // 8
struct node_s *right; // 8
double data[2]; // 8*2
} // 32 in total
typedef enum { N_LEAF, N_INTERNAL} nodetype_t;
sturct node_t {
nodetype_t type; // 4
union {
struct {
struct node_t *left; // 8
struct node_t *right; // 8
} internal;
double data[2] // 8*2
}
} // 24 in total
- 对比
struct
的大小是所有字段对齐之后的大小之和union
的大小是所有字段大小的最大值
- 类型
- 指针是定长的数据(实际上是一个内存地址)
- 具有一个类型(指向数据的类型,即 T *)
- 指向的数据可以是不同长度的,取决于类型
- 运算符
- 用 & 运算符创建。这个运算符可以应用到任何 lvalue 类的 C 表达式上。
- 用 * 运算符间接引用。间接引用通过存储器引用实现,要么是存储到一个指定的地址,要么是从指定的地址读取
- 数组与指针
- 一个数组的名字可以像一个指针变量一样引用(但是不能修改)
- 数组引用与指针运算和间接引用有一样的效果
- 数组引用和指针运算都需要用对象大小对偏移量进行伸缩
- 类型转换
- 将指针从一种类型强制转换成另一种类型,只改变它的类型,而不改变它的值
- 强制类型转换的一个效果是改变指针运算的伸缩
例如,如果 p 是一个 char* 类型的指针,那么表达式
(int)p+7
计算为p+28
, 而(int)(p+7)
计算为p+7
- 函数指针
- 这提供了一个很强大的存储和向代码传递引用的功能,这些引用可以被程序的某个其他部分调用。