- 第一章 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
10.1 x86
10.1.1 x86 + MSVC
f_signed() 函数:
Listing 10.1: 非优化 MSVC 2010
#!bash
_a$ = 8
_b$ = 12
_f_signed PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _a$[ebp]
cmp eax, DWORD PTR _b$[ebp]
jle SHORT $LN3@f_signed
push OFFSET $SG737 ; ’a>b’
call _printf
add esp, 4
$LN3@f_signed:
mov ecx, DWORD PTR _a$[ebp]
cmp ecx, DWORD PTR _b$[ebp]
jne SHORT $LN2@f_signed
push OFFSET $SG739 ; ’a==b’
call _printf
add esp, 4
$LN2@f_signed:
mov edx, DWORD PTR _a$[ebp]
cmp edx, DWORD PTR _b$[ebp]
jge SHORT $LN4@f_signed
push OFFSET $SG741 ; ’a<b’
call _printf
add esp, 4
$LN4@f_signed:
pop ebp
ret 0
_f_signed ENDP
第一个指令 JLE 意味如果小于等于则跳转。换句话说,第二个操作数大于或者等于第一个操作数,控制流将传递到指定地址或者标签。否则(第二个操作数小于第一个操作数)第一个 printf() 将被调用。第二个检测 JNE:如果不相等则跳转。如果两个操作数相等控制流则不变。第三个检测 JGE:大于等于跳转,当第一个操作数大于或者等于第二个操作数时跳转。如果三种情况都没有发生则无 printf() 被调用,事实上,如果没有特殊干预,这种情况几乎不会发生。
f_unsigned() 函数类似,只是 JBE 和 JAE 替代了 JLE 和 JGE,我们来看 f_unsigned() 函数
Listing 10.2: GCC
#!bash
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
_f_unsigned PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _a$[ebp]
cmp eax, DWORD PTR _b$[ebp]
jbe SHORT $LN3@f_unsigned
push OFFSET $SG2761 ; ’a>b’
call _printf
add esp, 4
$LN3@f_unsigned:
mov ecx, DWORD PTR _a$[ebp]
cmp ecx, DWORD PTR _b$[ebp]
jne SHORT $LN2@f_unsigned
push OFFSET $SG2763 ; ’a==b’
call _printf
add esp, 4
$LN2@f_unsigned:
mov edx, DWORD PTR _a$[ebp]
cmp edx, DWORD PTR _b$[ebp]
jae SHORT $LN4@f_unsigned
push OFFSET $SG2765 ; ’a<b’
call _printf
add esp, 4
$LN4@f_unsigned:
pop ebp
ret 0
_f_unsigned ENDP
几乎是相同的,不同的是:JBE-小于等于跳转和 JAE-大于等于跳转。这些指令(JA/JAE/JBE/JBE) 不同于 JG/JGE/JL/JLE,它们使用无符号值。
我们也可以看到有符号值的表示(35)。因此我们看 JG/JL 代替 JA/JBE 的用法或者相反,我们几乎可以确定变量的有符号或者无符号类型。
main() 函数没有什么新的内容:
Listing 10.3: main()
#!bash
_main PROC
push ebp
mov ebp, esp
push 2
push 1
call _f_signed
add esp, 8
push 2
push 1
call _f_unsigned
add esp, 8
xor eax, eax
pop ebp
ret 0
_main ENDP
10.1.2 x86 + MSVC + OllyDbg
我们在 OD 里允许例子来查看标志寄存器。我们从 f_unsigned() 函数开始。CMP 执行了三次,每次的参数都相同,所以标志位也相同。
第一次比较的结果:fig. 10.1.标志位:C=1, P=1, A=1, Z=0, S=1, T=0, D=0, O=0.标志位名称为 OD 对其的简称。
当 CF=1 or ZF=1 时 JBE 将被触发,此时将跳转。
接下来的条件跳转:fig. 10.2.当 ZF=0(zero flag)时 JNZ 则被触发
第三个条件跳转:fig. 10.3.我们可以发现 14 当 CF=0 (carry flag) 时,JNB 将被触发。在该例中条件不为真,所以第三个 printf() 将被执行。
Figure 10.1: OllyDbg: f_unsigned(): 第一个条件跳转
Figure 10.2: OllyDbg: f_unsigned(): 第二个条件跳转
Figure 10.3: OllyDbg: f_unsigned(): 第三个条件跳转
现在我们在 OD 中看 f_signed() 函数使用有符号值。
可以看到标志寄存器:C=1, P=1, A=1, Z=0, S=1, T=0, D=0, O=0.
第一种条件跳转 JLE 将被触发 fig. 10.4.我们可以发现 14 ,当 ZF=1 or SF≠OF。该例中 SF≠OF,所以跳转将被触发。
下一个条件跳转将被触发:如果 ZF=0 (zero flag): fig. 10.5.
第三个条件跳转将不会被触发,因为仅有 SF=OF,该例中不为真: fig. 10.6.
Figure 10.4: OllyDbg: f_signed(): 第一个条件跳转
Figure 10.5: OllyDbg: f_signed(): 第二个条件跳转
Figure 10.6: OllyDbg: f_signed(): 第三个条件跳转
10.1.3 x86 + MSVC + Hiew
我们可以修改这个可执行文件,使其无论输入的什么值 f_unsigned() 函数都会打印“a==b”。
在 Hiew 中查看:fig. 10.7.
我们要完成以下 3 个任务:
1\. 使第一个跳转一直被触发;
2\. 使第二个跳转从不被触发;
3\. 使第三个跳转一直被触发。
我们需要使代码流进入第二个 printf(),这样才一直打印“a==b”。
三个指令(或字节)应该被修改:
1\. 第一个跳转修改为 JMP,但跳转偏移值不变。
2\. 第二个跳转有时可能被触发,我们修改跳转偏移值为 0 后,无论何种情况,程序总是跳向下一条指令。跳转地址等于跳转偏移值加上下一条指令地址,当跳转偏移值为 0 时,跳转地址就为下一条指令地址,所以无论如何下一条指令总被执行。
3\. 第三个跳转我们也修改为 JMP,这样跳转总被触发。
修改后:fig. 10.8.
如果忘了这些跳转,printf() 可能会被多次调用,这种行为可能是我们不需要的。
Figure 10.7: Hiew: f_unsigned() 函数
Figure 10.8: Hiew:我们修改 f_unsigned() 函数
10.1.4 Non-optimizing GCC
GCC 4.4.1 非优化状态产生的代码几乎一样,只是用 puts() (2.3.3) 替代 printf()。
10.1.5 Optimizing GCC
细心的读者可能会问,为什么要多次执行 CMP,如果标志寄存器每次都相同呢?可能 MSVC 不会做这样的优化,但是 GCC 4.8.1 可以做这样的深度优化:
Listing 10.4: GCC 4.8.1 f_signed()
#!bash
f_signed:
mov eax, DWORD PTR [esp+8]
cmp DWORD PTR [esp+4], eax
jg .L6
je .L7
jge .L1
mov DWORD PTR [esp+4], OFFSET FLAT:.LC2 ; "a<b"
jmp puts
.L6:
mov DWORD PTR [esp+4], OFFSET FLAT:.LC0 ; "a>b"
jmp puts
.L1:
rep ret
.L7:
mov DWORD PTR [esp+4], OFFSET FLAT:.LC1 ; "a==b"
jmp puts
我们可以看到 JMP puts 替代了 CALL puts/RETN。稍后我们介绍这种情况 11.1.1.。
不用说,这种类型的 x86 代码是很少见的。MSVC2012 似乎不会这样做。其他情况下,汇编程序能意识到此类使用。如果你在其它地方看到此类代码,更可能是手工构造的。
f_unsigned() 函数代码:
Listing 10.5: GCC 4.8.1 f_unsigned()
#!bash
f_unsigned:
push esi
push ebx
sub esp, 20
mov esi, DWORD PTR [esp+32]
mov ebx, DWORD PTR [esp+36]
cmp esi, ebx
ja .L13
cmp esi, ebx ; instruction may be removed
je .L14
.L10:
jb .L15
add esp, 20
pop ebx
pop esi
ret
.L15:
mov DWORD PTR [esp+32], OFFSET FLAT:.LC2 ; "a<b"
add esp, 20
pop ebx
pop esi
jmp puts
.L13:
mov DWORD PTR [esp], OFFSET FLAT:.LC0 ; "a>b"
call puts
cmp esi, ebx
jne .L10
.L14:
mov DWORD PTR [esp+32], OFFSET FLAT:.LC1 ; "a==b"
add esp, 20
pop ebx
pop esi
jmp puts
因此,GCC 4.8.1 的优化算法并不总是完美的。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

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