10.1 x86

10.1.1 x86 + MSVC

f_signed() 函数:

Listing 10.1: 非优化MSVC 2010

  1. _a$ = 8
  2. _b$ = 12
  3. _f_signed PROC
  4. push ebp
  5. mov ebp, esp
  6. mov eax, DWORD PTR _a$[ebp]
  7. cmp eax, DWORD PTR _b$[ebp]
  8. jle SHORT $LN3@f_signed
  9. push OFFSET $SG737 ; "a>b"
  10. call _printf
  11. add esp, 4
  12. $LN3@f_signed:
  13. mov ecx, DWORD PTR _a$[ebp]
  14. cmp ecx, DWORD PTR _b$[ebp]
  15. jne SHORT $LN2@f_signed
  16. push OFFSET $SG739 ; "a==b"
  17. call _printf
  18. add esp, 4
  19. $LN2@f_signed:
  20. mov edx, DWORD PTR _a$[ebp]
  21. cmp edx, DWORD PTR _b$[ebp]
  22. jge SHORT $LN4@f_signed
  23. push OFFSET $SG741 ; "a<b"
  24. call _printf
  25. add esp, 4
  26. $LN4@f_signed:
  27. pop ebp
  28. ret 0
  29. _f_signed ENDP

第一个指令JLE意味如果小于等于则跳转。换句话说,第二个操作数大于或者等于第一个操作数,控制流将传递到指定地址或者标签。否则(第二个操作数小于第一个操作数)第一个printf()将被调用。第二个检测JNE:如果不相等则跳转。如果两个操作数相等控制流则不变。第三个检测JGE:大于等于跳转,当第一个操作数大于或者等于第二个操作数时跳转。如果三种情况都没有发生则无printf()被调用,事实上,如果没有特殊干预,这种情况几乎不会发生。

f_unsigned()函数类似,只是JBE和JAE替代了JLE和JGE,我们来看f_unsigned()函数

Listing 10.2: GCC

  1. _a$ = 8 ; size = 4
  2. _b$ = 12 ; size = 4
  3. _f_unsigned PROC
  4. push ebp
  5. mov ebp, esp
  6. mov eax, DWORD PTR _a$[ebp]
  7. cmp eax, DWORD PTR _b$[ebp]
  8. jbe SHORT $LN3@f_unsigned
  9. push OFFSET $SG2761 ; "a>b"
  10. call _printf
  11. add esp, 4
  12. $LN3@f_unsigned:
  13. mov ecx, DWORD PTR _a$[ebp]
  14. cmp ecx, DWORD PTR _b$[ebp]
  15. jne SHORT $LN2@f_unsigned
  16. push OFFSET $SG2763 ; "a==b"
  17. call _printf
  18. add esp, 4
  19. $LN2@f_unsigned:
  20. mov edx, DWORD PTR _a$[ebp]
  21. cmp edx, DWORD PTR _b$[ebp]
  22. jae SHORT $LN4@f_unsigned
  23. push OFFSET $SG2765 ; "a<b"
  24. call _printf
  25. add esp, 4
  26. $LN4@f_unsigned:
  27. pop ebp
  28. ret 0
  29. _f_unsigned ENDP

几乎是相同的,不同的是:JBE-小于等于跳转和JAE-大于等于跳转。这些指令(JA/JAE/JBE/JBE)不同于JG/JGE/JL/JLE,它们使用无符号值。

我们也可以看到有符号值的表示(35)。因此我们看JG/JL代替JA/JBE的用法或者相反,我们几乎可以确定变量的有符号或者无符号类型。

main()函数没有什么新的内容:

Listing 10.3: main()

  1. _main PROC
  2. push ebp
  3. mov ebp, esp
  4. push 2
  5. push 1
  6. call _f_signed
  7. add esp, 8
  8. push 2
  9. push 1
  10. call _f_unsigned
  11. add esp, 8
  12. xor eax, eax
  13. pop ebp
  14. ret 0
  15. _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()将被执行。

10.1 x86 - 图1

Figure 10.1: OllyDbg: f_unsigned(): 第一个条件跳转

10.1 x86 - 图2

Figure 10.2: OllyDbg: f_unsigned(): 第二个条件跳转

10.1 x86 - 图3

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.

10.1 x86 - 图4

Figure 10.4: OllyDbg: f_signed(): 第一个条件跳转

10.1 x86 - 图5

Figure 10.5: OllyDbg: f_signed(): 第二个条件跳转

10.1 x86 - 图6

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()可能会被多次调用,这种行为可能是我们不需要的。

10.1 x86 - 图7

Figure 10.7: Hiew: f_unsigned() 函数

10.1 x86 - 图8

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()

  1. f_signed:
  2. mov eax, DWORD PTR [esp+8]
  3. cmp DWORD PTR [esp+4], eax
  4. jg .L6
  5. je .L7
  6. jge .L1
  7. mov DWORD PTR [esp+4], OFFSET FLAT:.LC2 ; "a<b"
  8. jmp puts
  9. .L6:
  10. mov DWORD PTR [esp+4], OFFSET FLAT:.LC0 ; "a>b"
  11. jmp puts
  12. .L1:
  13. rep ret
  14. .L7:
  15. mov DWORD PTR [esp+4], OFFSET FLAT:.LC1 ; "a==b"
  16. jmp puts

我们可以看到JMP puts替代了CALL puts/RETN。稍后我们介绍这种情况11.1.1.。

不用说,这种类型的x86代码是很少见的。MSVC2012似乎不会这样做。其他情况下,汇编程序能意识到此类使用。如果你在其它地方看到此类代码,更可能是手工构造的。

f_unsigned()函数代码:

Listing 10.5: GCC 4.8.1 f_unsigned()

  1. f_unsigned:
  2. push esi
  3. push ebx
  4. sub esp, 20
  5. mov esi, DWORD PTR [esp+32]
  6. mov ebx, DWORD PTR [esp+36]
  7. cmp esi, ebx
  8. ja .L13
  9. cmp esi, ebx ; instruction may be removed
  10. je .L14
  11. .L10:
  12. jb .L15
  13. add esp, 20
  14. pop ebx
  15. pop esi
  16. ret
  17. .L15:
  18. mov DWORD PTR [esp+32], OFFSET FLAT:.LC2 ; "a<b"
  19. add esp, 20
  20. pop ebx
  21. pop esi
  22. jmp puts
  23. .L13:
  24. mov DWORD PTR [esp], OFFSET FLAT:.LC0 ; "a>b"
  25. call puts
  26. cmp esi, ebx
  27. jne .L10
  28. .L14:
  29. mov DWORD PTR [esp+32], OFFSET FLAT:.LC1 ; "a==b"
  30. add esp, 20
  31. pop ebx
  32. pop esi
  33. jmp puts

因此,GCC 4.8.1的优化算法并不总是完美的。