6.2 x86
6.2.1 MSVC
MVSC 2010编译后得到下面代码
CONST SEGMENT
$SG3831 DB ’Enter X:’, 0aH, 00H
$SG3832 DB ’%d’, 00H
35
6.2. X86 CHAPTER 6. SCANF()
$SG3833 DB ’You entered %d...’, 0aH, 00H
CONST ENDS
PUBLIC _main
EXTRN _scanf:PROC
EXTRN _printf:PROC
; Function compile flags: /Odtp
_TEXT SEGMENT
_x$ = -4 ; size = 4
_main PROC
push ebp
mov ebp, esp
push ecx
push OFFSET $SG3831 ; ’Enter X:’
call _printf
add esp, 4
lea eax, DWORD PTR _x$[ebp]
push eax
push OFFSET $SG3832 ; ’%d’
call _scanf
add esp, 8
mov ecx, DWORD PTR _x$[ebp]
push ecx
push OFFSET $SG3833 ; ’You entered %d...’
call _printf
add esp, 8
; return 0
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
X是局部变量。
C/C++标准告诉我们它只对函数内部可见,无法从外部访问。习惯上,局部变量放在栈中。也可能有其他方法,但在x86中是这样。
函数序言后下一条指令PUSH ECX目的并不是要存储ECX的状态(注意程序结尾没有与之相对的POP ECX)。
事实上这条指令仅仅是在栈中分配了4字节用于存储变量x。
变量x可以用宏 _x$ 来访问(等于-4),EBP寄存器指向当前栈帧。
在一个函数执行完之后,EBP将指向当前栈帧,就无法通过EBP+offset来访问局部变量和函数参数了。
也可以使用ESP寄存器,但由于它经常变化所以使用不方便。所以说在函数刚开始时,EBP的值保存了此时ESP的值。
下面是一个非常典型的32位栈帧结构
...
EBP-8 local variable #2, marked in IDA as var_8
EBP-4 local variable #1, marked in IDA as var_4
EBP saved value of EBP
EBP+4 return address
EBP+8 argument#1, marked in IDA as arg_0
EBP+0xC argument#2, marked in IDA as arg_4
EBP+0x10 argument#3, marked in IDA as arg_8
...
在我们的例子中,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.1 命令行输出
scanf()在这里执行。图6.3。scanf()在EAX中返回1,这意味着成功读入了一个值。现在我们关心的那个栈元素中的值是0x7B(123)。
接下来,这个值从栈中复制到ECX寄存器中,然后传递给printf()。图6.4
图6.2 OllyDbg:计算局部变量的地址
图6.3:OllyDbg:scanf()执行
图6.4:OllyDbg:准备把值传递给printf()
6.2.3 GCC
让我们在Linux GCC 4.4.1下编译这段代码
GCC把第一个调用的printf()替换成了puts(),原因在2.3.3节中讲过了。
和之前一样,参数都是用MOV指令放入栈中。