6.2 x86

6.2.1 MSVC

MVSC 2010编译后得到下面代码

  1. CONST SEGMENT
  2. $SG3831 DB Enter X:’, 0aH, 00H
  3. $SG3832 DB ’%d’, 00H
  4. 35
  5. 6.2. X86 CHAPTER 6. SCANF()
  6. $SG3833 DB You entered %d...’, 0aH, 00H
  7. CONST ENDS
  8. PUBLIC _main
  9. EXTRN _scanf:PROC
  10. EXTRN _printf:PROC
  11. ; Function compile flags: /Odtp
  12. _TEXT SEGMENT
  13. _x$ = -4 ; size = 4
  14. _main PROC
  15. push ebp
  16. mov ebp, esp
  17. push ecx
  18. push OFFSET $SG3831 ; Enter X:’
  19. call _printf
  20. add esp, 4
  21. lea eax, DWORD PTR _x$[ebp]
  22. push eax
  23. push OFFSET $SG3832 ; ’%d
  24. call _scanf
  25. add esp, 8
  26. mov ecx, DWORD PTR _x$[ebp]
  27. push ecx
  28. push OFFSET $SG3833 ; You entered %d...’
  29. call _printf
  30. add esp, 8
  31. ; return 0
  32. xor eax, eax
  33. mov esp, ebp
  34. pop ebp
  35. ret 0
  36. _main ENDP
  37. _TEXT ENDS

X是局部变量。

C/C++标准告诉我们它只对函数内部可见,无法从外部访问。习惯上,局部变量放在栈中。也可能有其他方法,但在x86中是这样。

函数序言后下一条指令PUSH ECX目的并不是要存储ECX的状态(注意程序结尾没有与之相对的POP ECX)。

事实上这条指令仅仅是在栈中分配了4字节用于存储变量x。

变量x可以用宏 _x$ 来访问(等于-4),EBP寄存器指向当前栈帧。

在一个函数执行完之后,EBP将指向当前栈帧,就无法通过EBP+offset来访问局部变量和函数参数了。

也可以使用ESP寄存器,但由于它经常变化所以使用不方便。所以说在函数刚开始时,EBP的值保存了此时ESP的值。

下面是一个非常典型的32位栈帧结构

  1. ...
  2. EBP-8 local variable #2, marked in IDA as var_8
  3. EBP-4 local variable #1, marked in IDA as var_4
  4. EBP saved value of EBP
  5. EBP+4 return address
  6. EBP+8 argument#1, marked in IDA as arg_0
  7. EBP+0xC argument#2, marked in IDA as arg_4
  8. EBP+0x10 argument#3, marked in IDA as arg_8
  9. ...

在我们的例子中,scanf()有两个参数。

第一个参数是指向“%d”的字符串指针,第二个是变量x的地址。

首先,lea eax, DWORD PTR _x$[ebp]指令将变量x的地址放入EAX寄存器。LEA作用是“取有效地址”,然而之后的主要用途有所变化(b.6.2)。

可以说,LEA在这里只是把EBP的值与宏 _x$的值相乘,并存储在EAX寄存器中。

lea eax, [ebp-4]也是一样。

EBP的值减去4,结果放在EAX寄存器中。接着EAX寄存器的值被压入栈中,再调用printf()。

之后,printf()被调用。第一个参数是一个字符串指针:“You entered %d …”。

第二个参数是通过mov ecx, [ebp-4]使用的,这个指令把变量x的内容传给ECX而不是它的地址。

然后,ECX的值放入栈中,接着最后一次调用printf()。

6.2.2 MSVC+OllyDbg

让我们在OllyDbg中使用这个例子。首先载入程序,按F8直到进入我们的可执行文件而不是ntdll.dll。往下滚动屏幕找到main()。点击第一条指令(PUSH EBP),按F2,再按F9,触发main()开始处的断点。

让我们来跟随到准备变量x的地址的位置。图6.2

可以右击寄存器窗口的EAX,再点击“堆栈窗口中跟随”。这个地址会在堆栈窗口中显示。观察,这是局部栈中的一个变量。我在图中用红色箭头标出。这里是一些无用数据(0x77D478)。PUSH指令将会把这个栈元素的地址压入栈中。然后按F8直到scanf()函数执行完。在scanf()执行时,我们要在命令行窗口中输入,例如输入123。

6.2 x86 - 图1

图6.1 命令行输出

scanf()在这里执行。图6.3。scanf()在EAX中返回1,这意味着成功读入了一个值。现在我们关心的那个栈元素中的值是0x7B(123)。

接下来,这个值从栈中复制到ECX寄存器中,然后传递给printf()。图6.4

6.2 x86 - 图2

图6.2 OllyDbg:计算局部变量的地址

6.2 x86 - 图3

图6.3:OllyDbg:scanf()执行

6.2 x86 - 图4

图6.4:OllyDbg:准备把值传递给printf()

6.2.3 GCC

让我们在Linux GCC 4.4.1下编译这段代码

GCC把第一个调用的printf()替换成了puts(),原因在2.3.3节中讲过了。

和之前一样,参数都是用MOV指令放入栈中。