- 第一章 CPU 简介
- 第二章 Hello,world!
- 第三章 函数开始和结束
- 第四章 栈
- Chapter 5 printf() 与参数处理
- Chapter 6 scanf()
- CHAPER7 访问传递参数
- Chapter 8 一个或者多个字的返回值
- Chapter 9 指针
- Chapter 10 条件跳转
- 第 11 章 选择结构 switch()/case/default
- 第 12 章 循环结构
- 第 13 章 strlen()
- Chapter 14 Division by 9
- chapter 15 用 FPU 工作
- Chapter 16 数组
- Chapter 17 位域
- 第 18 章 结构体
- 19 章 联合体
- 第二十章 函数指针
- 第 21 章 在 32 位环境中的 64 位值
- 第二十二章 SIMD
- 23 章 64 位化
- 24 章 使用 x64 下的 SIMD 来处理浮点数
- 25 章 温度转换
- 26 章 C99 的限制
- 27 章 内联函数
- 第 28 章 得到不正确反汇编结果
- 第 29 章 花指令
- 第 30 章 16 位 Windows
- 第 31 章 类
- 三十二 ostream
- 34.2.2 MSVC
- 34.2.3 C++ 11 std::forward_list
- 34.3 std::vector
- 34.4 std::map and std::set
11.1 一些例子
#!bash
void f (int a)
{
switch (a)
{
case 0: printf ("zero
"); break;
case 1: printf ("one
"); break;
case 2: printf ("two
"); break;
default: printf ("something unknown
"); break;
};
};
11.1.1 X86
反汇编结果如下(MSVC 2010):
清单 11.1: MSVC 2010
#!bash
tv64 = -4 ; size = 4
_a$ = 8 ; size = 4
_f PROC
push ebp
mov ebp, esp
push ecx
mov eax, DWORD PTR _a$[ebp]
mov DWORD PTR tv64[ebp], eax
cmp DWORD PTR tv64[ebp], 0
je SHORT $LN4@f
cmp DWORD PTR tv64[ebp], 1
je SHORT $LN3@f
cmp DWORD PTR tv64[ebp], 2
je SHORT $LN2@f
jmp SHORT $LN1@f
$LN4@f:
push OFFSET $SG739 ; ’zero’, 0aH, 00H
call _printf
add esp, 4
jmp SHORT $LN7@f
$LN3@f:
push OFFSET $SG741 ; ’one’, 0aH, 00H
call _printf
add esp, 4
jmp SHORT $LN7@f
$LN2@f:
push OFFSET $SG743 ; ’two’, 0aH, 00H
call _printf
add esp, 4
jmp SHORT $LN7@f
$LN1@f:
push OFFSET $SG745 ; ’something unknown’, 0aH, 00H
call _printf
add esp, 4
$LN7@f:
mov esp, ebp
pop ebp
ret 0
_f ENDP
输出函数的 switch 中有一些 case 选择分支,事实上,它是和下面这个形式等价的:
#!cpp
void f (int a)
{
if (a==0)
printf ("zero
");
else if (a==1)
printf ("one
");
else if (a==2)
printf ("two
");
else
printf ("something unknown
");
};
当 switch() 中有一些 case 分支时,我们可以看到此类代码,虽然不能确定,但是,事实上 switch() 在机器码级别上就是对 if() 的封装。这也就是说,switch() 其实只是对有一大堆类似条件判断的 if() 的一个语法糖。
在生成代码时,除了编译器把输入变量移动到一个临时本地变量 tv64 中之外,这块代码对我们来说并无新意。
如果是在 GCC 4.4.1 下编译同样的代码,我们得到的结果也几乎一样,即使你打开了最高优化(-O3)也是如此。
让我们在微软 VC 编译器中打开/Ox 优化选项: cl 1.c /Fa1.asm /Ox
清单 11.2: MSVC
#!bash
_a$ = 8 ; size = 4
_f PROC
mov eax, DWORD PTR _a$[esp-4]
sub eax, 0
je SHORT $LN4@f
sub eax, 1
je SHORT $LN3@f
sub eax, 1
je SHORT $LN2@f
mov DWORD PTR _a$[esp-4], OFFSET $SG791 ; ’something unknown’, 0aH, 00H
jmp _printf
$LN2@f:
mov DWORD PTR _a$[esp-4], OFFSET $SG789 ; ’two’, 0aH, 00H
jmp _printf
$LN3@f:
mov DWORD PTR _a$[esp-4], OFFSET $SG787 ; ’one’, 0aH, 00H
jmp _printf
$LN4@f:
mov DWORD PTR _a$[esp-4], OFFSET $SG785 ; ’zero’, 0aH, 00H
jmp _printf
_f ENDP
我们可以看到浏览器做了更多的难以阅读的优化(Dirty hacks)。
首先,变量的值会被放入 EAX,接着 EAX 减 0。听起来这很奇怪,但它之后是需要检查先前 EAX 寄存器的值是否为 0 的,如果是,那么程序会设置上零标志位 ZF(这也表示了减去 0 之后,结果依然是 0),第一个条件跳转语句 JE(Jump if Equal 或者同义词 JZ - Jump if Zero)会因此触发跳转。如果这个条件不满足,JE 没有跳转的话,输入值将减去 1,之后就和之前的一样了,如果哪一次值是 0,那么 JE 就会触发,从而跳转到对应的处理语句上。
(译注:SUB 操作会重置零标志位 ZF,但是 MOV 不会设置标志位,而 JE 将只有在 ZF 标志位设置之后才会跳转。如果需要基于 EAX 的值来做 JE 跳转的话,是需要用这个方法设置标志位的)。
并且,如果没有 JE 语句被触发,最终,printf() 函数将收到“something unknown”的参数。
其次:我们看到了一些不寻常的东西——字符串指针被放在了变量里,然后 printf() 并没有通过 CALL,而是通过 JMP 来调用的。 这个可以很简单的解释清楚,调用者把参数压栈,然后通过 CALL 调用函数。CALL 通过把返回地址压栈,然后做无条件跳转来跳到我们的函数地址。我们的函数在执行时,不管在任何时候都有以下的栈结构(因为它没有任何移动栈指针的语句):
· ESP —— 指向返回地址
· ESP+4 —— 指向变量 a (也即参数)
另一方面,当我们这儿调用 printf() 函数的时候,它也需要有与我们这个函数相同的栈结构,不同之处只在于 printf() 的第一个参数是指向一个字符串的。 这也就是你之前看到的我们的代码所做的事情。
我们的代码把第一个参数的地址替换了,然后跳转到 printf(),就像第一个没有调用我们的函数 f() 而是先调用了 printf() 一样。 printf() 把一串字符输出到 stdout 中,然后执行 RET 语句, 这一句会从栈上弹出返回地址,因此,此时控制流会返回到调用 f() 的函数上,而不是 f() 上。
这一切之所以能发生,是因为 printf() 在 f() 的末尾。在一些情况下,这有些类似于 longjmp() 函数。当然,这一切只是为了提高执行速度。
ARM 编译器也有类似的优化,请见 5.3.2 节“带有多个参数的 printf() 函数调用”。
11.1.2 ARM: 优化后的 Keil + ARM 模式
#!bash
.text:0000014C f1
.text:0000014C 00 00 50 E3 CMP R0, #0
.text:00000150 13 0E 8F 02 ADREQ R0, aZero ; "zero
"
.text:00000154 05 00 00 0A BEQ loc_170
.text:00000158 01 00 50 E3 CMP R0, #1
.text:0000015C 4B 0F 8F 02 ADREQ R0, aOne ; "one
"
.text:00000160 02 00 00 0A BEQ loc_170
.text:00000164 02 00 50 E3 CMP R0, #2
.text:00000168 4A 0F 8F 12 ADRNE R0, aSomethingUnkno ; "something unknown
"
.text:0000016C 4E 0F 8F 02 ADREQ R0, aTwo ; "two
"
.text:00000170
.text:00000170 loc_170 ; CODE XREF: f1+8
.text:00000170 ; f1+14
.text:00000170 78 18 00 EA B __2printf
我们再一次看看这个代码,我们不能确定的说这就是源代码里面的 switch() 或者说它是 if() 的封装。
但是,我们可以看到这里它也在试图预测指令(像是 ADREQ(相等)),这里它会在 R0=0 的情况下触发,并且字符串“zero”的地址将被加载到 R0 中。如果 R0=0,下一个指令 BEQ 将把控制流定向到 loc_170 处。顺带一说,机智的读者们可能会文,之前的 ADREQ 已经用其他值填充了 R0 寄存器了,那么 BEQ 会被正确触发吗?答案是“是”。因为 BEQ 检查的是 CMP 所设置的标记位,但是 ADREQ 根本没有修改标记位。
还有,在 ARM 中,一些指令还会加上-S 后缀,这表明指令将会根据结果设置标记位。如果没有-S 的话,表明标记位并不会被修改。比如,ADD(而不是 ADDS)将会把两个操作数相加,但是并不会涉及标记位。这类指令对使用 CMP 设置标记位之后使用标记位的指令,例如条件跳转来说非常有用。
其他指令对我们来说已经很熟悉了。这里只有一个调用指向 printf(),在末尾,我们已经知道了这个小技巧(见 5.3.2 节)。在末尾处有三个指向 printf()的地址。 还有,需要注意的是如果 a=2 但是 a 并不在它的选择分支给定的常数中时,“CMP R0, #2”指令在这个情况下就需要知道 a 是否等于 2。如果结果为假,ADRNE 将会读取字符串“something unknown ”到 R0 中,因为 a 在之前已经和 0、1 做过是否相等的判断了,这里我们可以假定 a 并不等于 0 或者 1。并且,如果 R0=2,a 指向的字符串“two ”将会被 ADREQ 载入 R0。
11.1.3 ARM: 优化后的 Keil + thumb 模式
#!bash
.text:000000D4 f1
.text:000000D4 10 B5 PUSH {R4,LR}
.text:000000D6 00 28 CMP R0, #0
.text:000000D8 05 D0 BEQ zero_case
.text:000000DA 01 28 CMP R0, #1
.text:000000DC 05 D0 BEQ one_case
.text:000000DE 02 28 CMP R0, #2
.text:000000E0 05 D0 BEQ two_case
.text:000000E2 91 A0 ADR R0, aSomethingUnkno ; "something unknown
"
.text:000000E4 04 E0 B default_case
.text:000000E6 ;
-------------------------------------------------------------------------
.text:000000E6 zero_case ; CODE XREF: f1+4
.text:000000E6 95 A0 ADR R0, aZero ; "zero
"
.text:000000E8 02 E0 B default_case
.text:000000EA ;
-------------------------------------------------------------------------
.text:000000EA one_case ; CODE XREF: f1+8
.text:000000EA 96 A0 ADR R0, aOne ; "one
"
.text:000000EC 00 E0 B default_case
.text:000000EE ;
-------------------------------------------------------------------------
.text:000000EE two_case ; CODE XREF: f1+C
.text:000000EE 97 A0 ADR R0, aTwo ; "two
"
.text:000000F0 default_case ; CODE XREF: f1+10
.text:000000F0 ; f1+14
.text:000000F0 06 F0 7E F8 BL __2printf
.text:000000F4 10 BD POP {R4,PC}
.text:000000F4 ; End of function f1
正如我之前提到的,在 thumb 模式下并没有什么功能来连接预测结果,所以这里的 thumb 代码有点像容易理解的 x86 CISC 代码。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论