我们在分析问题的时候经常会需要查看进程的栈和帧中的值,下面我们就用一个简单的例子来分析一下这个过程。
源代码:
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int main()
{
int a, b;
a = 3;
b = 4;
int ret = add(a, b);
std::cout << "Result:"<<ret<<"\t\n";
}
生成的汇编代码:
main 函数的汇编代码
10 004c1000 55 push ebp
10 004c1001 8bec mov ebp,esp
10 004c1003 83ec0c sub esp,0Ch
12 004c1006 c745f803000000 mov dword ptr [ebp-8],3
13 004c100d c745fc04000000 mov dword ptr [ebp-4],4
14 004c1014 8b45fc mov eax,dword ptr [ebp-4]
14 004c1017 50 push eax
14 004c1018 8b4df8 mov ecx,dword ptr [ebp-8]
14 004c101b 51 push ecx
14 004c101c e83f000000 call statckTest!add (004c1060)
14 004c1021 83c408 add esp,8
14 004c1024 8945f4 mov dword ptr [ebp-0Ch],eax
15 004c1027 6840314c00 push offset statckTest!GS_ExceptionPointers+0x8 (004c3140)
15 004c102c 8b55f4 mov edx,dword ptr [ebp-0Ch]
15 004c102f 52 push edx
15 004c1030 6844314c00 push offset statckTest!std::_Fake_alloc+0x1 (004c3144)
15 004c1035 a16c304c00 mov eax,dword ptr [statckTest!_imp_?coutstd (004c306c)]
15 004c103a 50 push eax
15 004c103b e8e0020000 call statckTest!std::operator<<<std::char_traits<char> > (004c1320)
15 004c1040 83c408 add esp,8
15 004c1043 8bc8 mov ecx,eax
15 004c1045 ff1540304c00 call dword ptr [statckTest!_imp_??6?$basic_ostreamDU?$char_traitsDstdstdQAEAAV01HZ (004c3040)]
15 004c104b 50 push eax
15 004c104c e8cf020000 call statckTest!std::operator<<<std::char_traits<char> > (004c1320)
15 004c1051 83c408 add esp,8
16 004c1054 33c0 xor eax,eax
16 004c1056 8be5 mov esp,ebp
16 004c1058 5d pop ebp
16 004c1059 c3 ret
这段汇编代码是一个函数的一部分,它执行了以下操作:
-
函数开始:
push ebp
和mov ebp,esp
:保存当前的栈指针(esp
)到基指针(ebp
),并设置ebp
为当前栈指针的值。这是函数调用的标准开头,用于设置函数的局部变量和参数的基础地址。
-
局部变量分配:
sub esp,0Ch
:从栈指针中减去12(0Ch
是十六进制的12),为局部变量分配空间。
-
初始化局部变量:
mov dword ptr [ebp-8],3
:将整数值3存储到ebp-8
指向的局部变量中。mov dword ptr [ebp-4],4
:将整数值4存储到ebp-4
指向的局部变量中。
-
函数调用准备:
mov eax,dword ptr [ebp-4]
和mov ecx,dword ptr [ebp-8]
:将两个局部变量的值分别加载到eax
和ecx
寄存器中。push eax
和push ecx
:将这两个值压入栈中,作为add
函数的参数。
-
函数调用:
call statckTest!add (004c1060)
:调用名为add
的函数,该函数可能位于004c1060
地址处。
-
处理函数返回值:
mov dword ptr [ebp-0Ch],eax
:将add
函数的返回值存储在ebp-0Ch
指向的局部变量中。
-
异常处理准备:
push offset statckTest!GS_ExceptionPointers+0x8
:将GS_ExceptionPointers+0x8
的地址压入栈中,可能是为了异常处理。push dword ptr [ebp-0Ch]
:将先前存储的add
函数的返回值压入栈中。push offset statckTest!std::_Fake_alloc+0x1
:将std::_Fake_alloc+0x1
的地址压入栈中,这可能与异常处理相关。
-
输出到控制台:
mov eax,dword ptr [statckTest!_imp_?coutstd (004c306c)]
:获取std::cout
的地址,并将其存储在eax
寄存器中。push eax
:将std::cout
的地址压入栈中。call statckTest!std::operator<<<std::char_traits<char> > (004c1320)
:调用std::operator<<
来输出一个值到std::cout
。- 同样的操作重复了一次,可能是为了输出两个不同的值。
-
清理栈:
add esp,8
:调整栈指针,清理之前压入栈的参数。
-
函数结束:
xor eax,eax
:将eax
寄存器清零,这通常是函数返回前的一个操作。mov esp,ebp
和pop ebp
:恢复栈指针和基指针。ret
:从函数返回。
add 函数的汇编代码
18 004c1060 55 push ebp
18 004c1061 8bec mov ebp,esp
19 004c1063 8b4508 mov eax,dword ptr [ebp+8]
19 004c1066 03450c add eax,dword ptr [ebp+0Ch]
20 004c1069 5d pop ebp
20 004c106a c3 ret
这段汇编代码是一个简单的函数,它执行以下操作:
-
push ebp
和mov ebp,esp
:保存当前的栈指针(esp
)到基指针(ebp
),并设置ebp
为当前栈指针的值。这是函数调用的标准开头,用于设置函数的局部变量和参数的基础地址。 -
mov eax,dword ptr [ebp+8]
:从ebp+8
的地址处取出一个双字(32位)值,并将其存储到eax
寄存器中。这通常意味着函数接收一个整数参数,该参数在栈上的位置是ebp+8
。 -
add eax,dword ptr [ebp+0Ch]
:从ebp+0Ch
的地址处取出另一个双字值,并将其加到eax
寄存器中的当前值上。这通常意味着函数接收另一个整数参数,该参数在栈上的位置是ebp+0Ch
。 -
pop ebp
:恢复基指针ebp
的原始值。 -
ret
:从函数返回。由于eax
寄存器现在包含两个参数的和,这通常意味着函数返回这两个参数的和作为结果。
函数调用过程的栈帧分布:
从main函数的汇编中我们可以看到调用函数add 的过程, 下面两句给局部变量在栈中分配内存
12 004c1006 c745f803000000 mov dword ptr [ebp-8],3
13 004c100d c745fc04000000 mov dword ptr [ebp-4],4
从中可以看到a 放在ebp-8 和b 放在ebp -4 这个地方。
eax=765712f0 ebx=00c9d000 ecx=00000000 edx=00000000 esi=011fa0c8 edi=011fea68
eip=004c1006 esp=00f6fd70 ebp=00f6fd7c iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
从中可以看到ebp=00f6fd7c,esp=00f6fd70,分配了12个字节的空间。
0:000> t
eax=00000004 ebx=00c9d000 ecx=00000000 edx=00000000 esi=011fa0c8 edi=011fea68
eip=004c1017 esp=00f6fd70 ebp=00f6fd7c iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
statckTest!main+0x17:
004c1017 50 push eax
0:000> t
eax=00000004 ebx=00c9d000 ecx=00000000 edx=00000000 esi=011fa0c8 edi=011fea68
eip=004c1018 esp=00f6fd6c ebp=00f6fd7c iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
statckTest!main+0x18:
004c1018 8b4df8 mov ecx,dword ptr [ebp-8] ss:002b:00f6fd74=00000003
0:000> t
eax=00000004 ebx=00c9d000 ecx=00000003 edx=00000000 esi=011fa0c8 edi=011fea68
eip=004c101b esp=00f6fd6c ebp=00f6fd7c iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
statckTest!main+0x1b:
004c101b 51 push ecx
0:000> t
eax=00000004 ebx=00c9d000 ecx=00000003 edx=00000000 esi=011fa0c8 edi=011fea68
eip=004c101c esp=00f6fd68 ebp=00f6fd7c iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
statckTest!main+0x1c:
004c101c e83f000000 call statckTest!add (004c1060)
0:000> dd 002b:00f6fd40
002b:00f6fd40 00f6fd60 7648abda 00000000 004c310c
002b:00f6fd50 00000000 004c1716 764a5406 00000001
002b:00f6fd60 00f6fd80 004c1726 00000003 00000004
002b:00f6fd70 00c9d000 00000003 00000004 00f6fdc4
002b:00f6fd80 004c1822 00000001 011fa0c8 011fea68
002b:00f6fd90 a7b8e129 004c18aa 004c18aa 00c9d000
002b:00f6fda0 00000000 00000000 00000000 00f6fd90
002b:00f6fdb0 00000000 00f6fe20 004c1fb5 a7022ab5
调用add 函数之前会把参数压入栈中
14 004c1014 8b45fc mov eax,dword ptr [ebp-4]
14 004c1017 50 push eax
14 004c1018 8b4df8 mov ecx,dword ptr [ebp-8]
14 004c101b 51 push ecx
14 004c101c e83f000000 call statckTest!add (004c1060)
004c101c e83f000000 call statckTest!add (004c1060)这条指令调用之前的main 函数堆栈
进入add 函数之后的内存布局
002b:00f6fd40 00f6fd60 7648abda 00000000 004c310c
002b:00f6fd50 00000000 004c1716 764a5406 00000001
002b:00f6fd60 00f6fd7c 004c1021 00000003 00000004
002b:00f6fd70 00c9d000 00000003 00000004 00f6fdc4
002b:00f6fd80 004c1822 00000001 011fa0c8 011fea68
002b:00f6fd90 a7b8e129 004c18aa 004c18aa 00c9d000
002b:00f6fda0 00000000 00000000 00000000 00f6fd90
002b:00f6fdb0 00000000 00f6fe20 004c1fb5 a7022ab5
函数的返回地址是 14 004c1021 83c408 add esp,8 正好是add 函数调用完成之后下一条汇编的地址。
14 004c1021 83c408 add esp,8
14 004c1024 8945f4 mov dword ptr [ebp-0Ch],eax
上面两条是内存回收和将add 函数的返回值存贮在002b:00f6fd70 这个地址上面。
002b:00f6fd40 00f6fd60 7648abda 00000000 004c310c
002b:00f6fd50 00000000 004c1716 764a5406 00000001
002b:00f6fd60 00f6fd7c 004c1021 00000003 00000004
002b:00f6fd70 00000007 00000003 00000004 00f6fdc4
002b:00f6fd80 004c1822 00000001 011fa0c8 011fea68
002b:00f6fd90 a7b8e129 004c18aa 004c18aa 00c9d000
002b:00f6fda0 00000000 00000000 00000000 00f6fd90
002b:00f6fdb0 00000000 00f6fe20 004c1fb5 a7022ab5
完成之后函数的ebp 和esp 恢复到之前的状态。
本文简单介绍了c 语言函数调用过程中的内存布局,在我们分析dump文件或者实时查看内存布局的时候可以使用本文的方法。
备注:如果生成的exe 无法添加函数断点请在工程配置中设置如下(vistual studio)