函数

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int plus(int x, int y) {
/*
如果直接返回 x + y
编译器会生成 XOR ECX,ECX,将ECX寄存器置0
*/
int result = x + y;
return result;
}

void main() {

__asm {
mov eax,eax
mov eax,eax
mov eax,eax
}

plus(1, 2);

return;
}

下断点的时候要在函数调用前,否则会直接跳过函数执行过程。

反汇编:

在执行CALL指令后首先会为函数分配一个堆栈空间(为的是EBP寻址)。具体过程是,先保存当前ESP的值到EBP,然后提升ESP,这里提升了0XCC个字节。由于函数执行过程中可能会用到寄存器,所以将EBX、ESI、EDI三个寄存器压入栈中。

LEA EDI,[EBP-0C]一直到REP STOS DWORD PTR ES:[EDI]这些指令是为了防止缓冲区溢出,具体过程是,从新保存的EBP一直往上0XC个字节为缓冲区,将EAX寄存器填充为0XCCCCCCCC,循环3次写入到缓冲区中

至于为什么缓冲区被分配为0XC个字节,因为我用的编译器做过优化,3个变量一共0XC字节,如果直接返回结果return x+y,编译器会生成这条指令:XOR EXC,EXC,将ECX寄存器置0,也就是不需要缓冲区。

然后通过EBP寻址开始执行函数命令,EBP+8是因为除了CALL指令压入栈的地址外,还有EBP本来的地址,将运算结果写入到EAX寄存器。

最后按反顺序依次还原堆栈空间,弹出三个寄存器的值,将ESP和EBP还原,函数返回后将ESP+8,调用结束。


堆栈图如下:


变量

全局变量

全局变量全部存放在静态存储区(默认值为0),在编译时就确定了地址和长度,在程序开始执行时给全局变量分配存储区,程序行完毕就释放。

局部变量

局部变量储存在堆栈中,只有在函数执行时才会随机分配地址,这个地址取决于函数调用时的堆栈指针寄存器的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 push        ebp  
mov ebp,esp
sub esp,0CCh
push ebx
push esi
push edi
lea edi,[ebp-0Ch]
mov ecx,3
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
mov ecx,3C035h
call 00031316
;6: int result;
;7: result = x + y;
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
;保存运算结果到局部变量
mov dword ptr [ebp-8],eax
;8: return result;
;将局部变量的值返回
mov eax,dword ptr [ebp-8]
;9: }

参数是在调用函数前被压栈的,局部变量储存在栈空间的缓冲区中,如上面的堆栈图一样,在ESP提升之后,将EBP往上的一段空间作为缓冲区,并填充CC

MOV DWORD PTR [EBP-8],EAX这句,将EAX寄存器的值写入到EBP-8,这个EBP-8就是上面局部变量result的地址(从EBP开始往上是缓冲区,用于储存局部变量)

在调用结束时ESP和EBP会恢复成以前的值,这也是为什么在C语言中局部变量不能直接使用的原因(值是未定义的)

返回值

大多数C/C++编译器会把返回值写入到EAX寄存器中

例如:

MOV EAX,DWORD PTR [EBP-8]

返回后使用返回值

1
2
3
4
5
push        2  
push 1
call 005513B1
add esp,8
mov dword ptr [ebp-8],eax

以上代码中使用的外平栈,在调用完毕后再将ESP+8,然后将EAX的值写入到局部变量中, [EBP-8]就是main函数局部变量的地址