3.3.4函数栈帧的创建
通过上面的汇编代码,接下来一行一行地拆解汇编代码
009118B0 push ebp //把ebp寄存器中的值进行压栈 009118B1 mov ebp,esp //move指令会把esp的值存放到ebp中,此时就产生了mian函数的ebp 009118B3 sub esp,0E4h //sub的作用是使esp中的地址减去一个十六进制的数字0xE4,产生新的esp //此时esp是main函数栈帧的esp,与上面的ebp之间维护了一块栈空间, //这块空间就是为main函数开辟的栈空间,这块空间将存储main函数中的 //局部变量,以及调试信息等。 009118B9 push ebx //将寄存器ebx的值压栈,esp-4 009118BA push esi //将寄存器esi的值压栈,esp-4 009118BB push edi //将寄存器edi的值压栈,esp-4 //上面三条指令将三个寄存器的值保存在栈区,因为这三个寄存器在函数 //随后的执行过程中可能被修改,如此便避免其中的值被修改,也方便退 //出函数时恢复其中的值 //以下操作便是在初始化main函数的栈帧空间 //1.先把 ebp-0x24h 的地址,放到edi中 //2.把 9 放入ecx //3.把0CCCCCCCC放在 eax 中 //4.将从 ebp-0x24h 到 ebp 这一段内存的每个字节都初始化为0CCCCCCCC 009118BC lea edi,[ebp-24h] 009118BF mov ecx,9 009118C4 mov eax,0CCCCCCCCh 009118C9 rep stos dword ptr es:[edi]
画图展示如下
平常在写代码没有初始化变量直接打印会出现一连串的烫烫烫,其实打印的就是0xCCCCCCCC
例如
接着再分析main函数的核心代码
int a = 3; 00EE18D5 mov dword ptr [ebp-8],3 //将3存储到 ebp-8 的地址处,也就是变量a的地址 int b = 2; 00EE18DC mov dword ptr [ebp-14h],2 //将2存储到 ebp-14h 的地址处,也就是变量b的地址 int ret = 0; 00EE18E3 mov dword ptr [ebp-20h],0 //将0存储到 ebp-20h 的地址处,就是变量 ret 的地址 //以上汇编代码的本质就是变量 a,b,ret 的创建和初始化,就是局部变量 //的创建和初始化 //局部变量是在局部变量所在的函数的栈帧空间中所创建的
画图展示如下
Add函数的传参
//调用Add函数 ret = Add(a, b); //调用Add函数时的传参,是把参数压栈到栈帧空间 00EE18EA mov eax,dword ptr [ebp-14h] 寄存器中 //传递b,将 ebp-14h 地址处的2存放在 eax 00EE18ED push eax //将 eax 的值进行压栈,esp-4 00EE18EE mov ecx,dword ptr [ebp-8] //传递a,将 ebp-8 地址处的3存放在 ecx 寄存器中 00EE18F1 push ecx //将 ecx 的值进行压栈,esp-4 //跳转调用函数 00EE18F2 call 00EE13B6 00EE18F7 add esp,8 00EE18FA mov dword ptr [ebp-20h],eax
00EE18F2 call 00EE13B6 00EE18F7 add esp,8 00EE18FA mov dword ptr [ebp-20h],eax
call指令是要执行函数调用逻辑的,在执行 call指令之前会先把 call指令下一条指令的地址进行压栈操作,此操作是为了解决当函数调用结束后要回到 call指令的下一条指令的地址处,继续执行程序。
当跳转到Add函数时,就要开始观察Add函数的反汇编代码
int Add(int x, int y) { 00EE1830 push ebp //保存 main函数 栈帧的 ebp ,esp-4 00EE1831 mov ebp,esp //将main函数的 esp 赋值给新的 ebp,ebp就变成 Add 函数的 ebp 00EE1833 sub esp,0CCh //sub的作用是使esp中的地址减去一个十六进制的数字0CCh,产生新的esp //此时esp是Add函数栈帧的esp,与上面的ebp之间维护了一块栈空间, //这块空间就是为Add函数开辟的栈空间,这块空间将存储Add函数中的 //局部变量,以及调试信息等 00EE1839 push ebx //将 ebx 的值进行压栈,esp-4 00EE183A push esi //将 esi 的值进行压栈,esp-4 00EE183B push edi //将 edi 的值进行压栈,esp-4 00EE183C lea edi,[ebp-0Ch] 00EE183F mov ecx,3 00EE1844 mov eax,0CCCCCCCCh 00EE1849 rep stos dword ptr es:[edi] 00EE184B mov ecx,0EEC008h 00EE1850 call 00EE131B int z = 0; 00EE1855 mov dword ptr [ebp-8],0 //将0存放在 ebp-8 的地址处,其实就是 z 的地址处 z = x + y; 00EE185C mov eax,dword ptr [ebp+8] //将 ebp+8 的地址处的数字存储到 eax 中 00EE185F add eax,dword ptr [ebp+0Ch] //将 ebp+0xC地址处的数字加到 eax 中 00EE1862 mov dword ptr [ebp-8],eax //将 eax 的结果保存到 ebp-8 的地址处,也就是 z 的地址处 return z; 00EE1865 mov eax,dword ptr [ebp-8] //将 ebp-8 地址处的值存放在 eax 中,本质上就是将 z 的值存储到 //eax 中,通过 eax 带回计算的结果,作为函数的返回值 } 00EE1868 pop edi 00EE1869 pop esi 00EE186A pop ebx 00EE186B add esp,0CCh 00EE1871 cmp ebp,esp 00EE1873 call 00EE1244 00EE1878 mov esp,ebp 00EE187A pop ebp 00EE187B ret
代码执行到Add函数时,就需要开始创建Add函数的栈帧空间
在Add函数中创建栈帧的方法与在main函数相似。
创建Add函数栈帧的整体思路
1. 将mian函数的 ebp 压栈 2. 计算新的 ebp 和 esp 3. 将 ebx,esi,edi 寄存器的值保存 4. 计算求和,在计算的过程中,通过 ebp 的地址访问函数调用前压栈 进去的参数,也就是形参访问 5.将求出的和放在 eax 寄存器中带回
上图中的a'和b'其实是Add函数的形参x,y。图中就很好地说明函数在传参过程中,以及函数在进行传值调用时,形参就是实参的一份临时拷贝,对形参的修改不会影响实参。
3.3.5函数栈帧的销毁
当函数调用即将结束时,前面创建的函数栈帧也即将开始销毁
接下来,通过反汇编代码来具体了解是怎么销毁的
00EE1868 pop edi //在栈顶弹出一个值,存放到 edi 中,esp+4 00EE1869 pop esi //在栈顶弹出一个值,存放到 esi 中,esp+4 00EE186A pop ebx //在栈顶弹出一个值,存放到 ebx 中,esp+4 00EE186B add esp,0CCh 00EE1871 cmp ebp,esp //再次将Add函数的 ebp 的值赋值给 esp ,相当于回收了Add函数的 栈帧空间 00EE1873 call 00EE1244 00EE1878 mov esp,ebp 00EE187A pop ebp //弹出栈顶的值存放到 ebp,栈顶此时的值恰好是main函数的 ebp,esp+4 //此时恢复了main函数的栈帧维护,esp指向mian函数栈帧的栈顶, ebp指向了main函数栈帧的栈底 00EE187B ret //ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令 下一条指令的地址,此时 esp+4 ,接着就直接跳转到call指令下一条指令 的地址处,举行执行程序。
回到call指令的下一条指令的地址处
00EE18F7 add esp,8 00EE18FA mov dword ptr [ebp-20h],eax printf("%d\n", ret); 00EE18FD mov eax,dword ptr [ebp-20h] 00EE1900 push eax 00EE1901 push 0EE7B30h 00EE1906 call 00EE10D2 00EE190B add esp,8
调用完Add函数,回到函数时,继续执行后面的代码。
00EE18F7 add esp,8 //esp加上8,等价于跳过main函数中压栈的 a'和b' 00EE18FA mov dword ptr [ebp-20h],eax //将 eax 中的值,存放到 ebp-0x20h的地址处,也就是存储到main函数 //中的变量 ret 中,eax 中的值就是Add函数中计算的 x 和 y 的和,显而 //易见,本次函数的返回值是由 eax 寄存器带回来的,程序是在函数调用 //返回之后,在 eax 中读取返回值的。