本文将围绕如下问题进行详细讨论,使用的编译环境为VS2013
- 局部变量是如何创建的?
- 局部变量的值未初始化,为什么局部变量是随机值?
- 函数是如何传参的?传参的顺序是咋么样的?
- 形参和实参是什么关系?
- 函数是如何调用的?
- 函数调用结束后是如何返回的?
EAX EBX ECX EDX
EBP-栈底指针寄存器 ESP-栈顶指针寄存器
每次函数调用,都要在内存的栈区开辟空间,为该函数所开辟的内存空间称为函数栈帧;
main()函数的函数栈帧是由ESP与EBP进行维护,正在调用哪一个函数,ESP与EBP则去维护调用的函数的函数栈帧;
VS2013启动调试, 打开调用堆栈,不难得知
main()函数是由__tmainCRTStartup()函数所调用;main()函数调用之后的返回值存放于mainret中;
__tmain()CRTStartup()函数是由mainCRTStartup()函数所调用;
前文说到当我们调用某个函数,就为该函数开辟函数栈帧,当我们调用main()函数时,main()函数内部调用了Add函数,接下来将从汇编的角度去探索未知的细节
# include
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
//main函数内部调用了Add()函数
c = Add(a, b);
printf("%d\n", c);
return 0;
}
调用main()函数之前,内存已经为__tmainCRTStartup()分配好函数栈帧,同时ESP与EBP维护该函数的函数栈帧,如图
栈是一种具有特殊的访问方式的存储空间,其特殊性性在于使用方式为后进先出;
栈的两个基本操作:
- 入栈 就是 将一个新的元素放到栈顶
- 出栈 就是 从栈顶取出一个元素
注:栈操作都是以字为单位进行操作,一个字即2个字节;
指令示例:
- push 寄存器 ;将寄存器中的数据入栈
- pop 寄存器 ;出栈,用一个寄存器接收出栈的数据
push与pop指令的执行步骤
- push指令的执行步骤
1. sp=sp-2 2. 向SS:SP指向的字单元中送入数据;
其中ss存放栈段的段地址,而sp存放栈顶的偏移地址
- pop指令的执行步骤
1.从SS:SP指向的字单元中读取数据 2.SP=SP+2;
注意:任何时刻,SS:SP指向栈顶元素
//上述代码所对应的汇编代码
int main()
{
009D1410 push ebp
009D1411 mov ebp,esp
009D1413 sub esp,0E4h
009D1419 push ebx
009D141A push esi
009D141B push edi
009D141C lea edi,[ebp+FFFFFF1Ch]
009D1422 mov ecx,39h
009D1427 mov eax,0CCCCCCCCh
009D142C rep stos dword ptr es:[edi]
int a = 10;
009D142E mov dword ptr [ebp-8],0Ah
int b = 20;
009D1435 mov dword ptr [ebp-14h],14h
int c = 0;
009D143C mov dword ptr [ebp-20h],0
c = Add(a, b);
009D1443 mov eax,dword ptr [ebp-14h]
009D1446 push eax
009D1447 mov ecx,dword ptr [ebp-8]
009D144A push ecx
009D144B call 009D10E1
009D1450 add esp,8
009D1453 mov dword ptr [ebp-20h],eax
printf("%d\n", c);
009D1456 mov esi,esp
009D1458 mov eax,dword ptr [ebp-20h]
009D145B push eax
009D145C push 9D5858h
009D1461 call dword ptr ds:[009D9114h]
009D1467 add esp,8
009D146A cmp esi,esp
009D146C call 009D113B
return 0;
009D1471 xor eax,eax
}
009D1473 pop edi
009D1474 pop esi
009D1475 pop ebx
009D1476 add esp,0E4h
009D147C cmp ebp,esp
009D147E call 009D113B
009D1483 mov esp,ebp
009D1485 pop ebp
009D1486 ret
009D1410 push ebp
//将寄存器ebp中的数据送入栈顶,由于任意时刻,esp都要指向栈顶,则sp自动递减2
mov ebp,esp 含义为将esp中的值送入到ebp中
009D1411 mov ebp,esp
sub esp,0E4h 该指令的含义为寄存器esp中的数据减去0E4h,结果存放于esp当中;
009D1413 sub esp,0E4h
009D1419 push ebx
//该条指令含义为将ebx压入栈顶
009D141A push esi
//该条指令含义为将esi压入栈顶,其中esi为源变址寄存器
009D141B push edi
//该条指令含义为将edi压入栈顶,其中edi为目标变址寄存器
009D141C lea edi,[ebp+FFFFFF1Ch]
//lea - load effective address取偏移地址
//lea指令,将源操作数即存储单元的有效地址传送到目的操作数
//左边为目的操作数,操作结果保存于此,操作对象为通用寄存器
//右边为源操作数,源操作数只能为存储单元
//该指令含义为将偏移地址为ebp-0E4h送入寄存器EDI中
009D1422 mov ecx,39h
//该指令的含义为将39h送入到寄存器ecx之中
009D1427 mov eax,0CCCCCCCCh
//该条指令的含义为将0cccccccch送入到寄存器eax之中
009D142C rep stos dword ptr es:[edi]
//stos-store string串存储指令,将eax中的数据送入到默认目的地址 ES:[EDI]
// rep指令,重复前缀指令,每执行一次,ecx减1,直至ecx减至0为止
//该指令的含义为将eax中的值0cccccccch送入到es:[edi]之中,每次内存填充的的数据大小为双字,循环执行39h次
由edi开始,依次由低地址向高地址填充;
由于该机器为小端存储模式,通过监视窗口 内存窗口 观察
当局部变量未初始化时,由于cpu为该函数所开辟好的函数栈帧均为ccccccc,所以局部变量未初始化,当编译器不同时,其值有多种可能结果,所以其值为随机值;
int b = 20;
009D1435 mov dword ptr [ebp-14h],14h
//20所对应的16进制数字为14h,该指令的功能为将14h放于ebp-14h的地址处
int c = 0;
009D143C mov dword ptr [ebp-20h],0
//将0置于ebp-20h的地址处
局部变量是如何创建的?
局部变量创建的基本思路是首先为正在调用的函数开辟函数栈帧,在函数栈帧中找到部分空间,将其值存储在函数栈帧之中;
c = Add(a, b);
009D1443 mov eax,dword ptr [ebp-14h]
//将地址为ebp-14h中的值即局部变量b的值送入到寄存器eax之中保存,即eax=20
009D1446 push eax
//将eax中的中压入栈顶
009D1447 mov ecx,dword ptr [ebp-8]
//将地址为ebp-8 处的值送入寄存器ecx之中保存
009D144A push ecx
//将ecx压入栈顶
上述俩步过程的本质是为函数调用利用寄存器进行传参;
当我们进入Add函数内部,由于正在调用Add函数,则如下汇编代码将为Add函数开辟函数栈帧;
009D13C0 push ebp
//该汇编指令的含义是将来自main函数的ebp压入栈顶,同时由于esp时刻指向栈顶,所以esp自动递减
009D13C1 mov ebp,esp
//将寄存器esp中的值送入ebp
009D13C3 sub esp,0CCh
//该条汇编指令的含义为esp=esp-0cch
009D13C9 push ebx
009D13CA push esi
009D13CB push edi
009D13CC lea edi,[ebp+FFFFFF34h]
//该指令含义为将偏移地址为ebp-0cch送入寄存器EDI中
009D13D2 mov ecx,33h
//该指令的含义是将33h送入寄存器ecx中保存
009D13D7 mov eax,0CCCCCCCCh
//该指令的含义是将0cccccccch送入寄存器eax之中
009D13DC rep stos dword ptr es:[edi]
//stos-store string串存储指令,将eax中的数据送入到默认目的地址 ES:[EDI]
// rep指令,重复前缀指令,每执行一次,ecx减1,直至ecx减至0为止
//该指令的含义为将eax中的值0cccccccch送入到es:[edi],从edi开始向高地址填充,每次内存填充的的数据大小为双字,循环执行39h次
int z = 0;
009D13DE mov dword ptr [ebp-8],0
//形参x,y 并没有创建,形参来自于哪里?
z = x + y;
009D13E5 mov eax,dword ptr [ebp+8]
//该汇编指令的含义为将地址为ebp+8处的值送入到eax之中,即ecx中的数字10
//eax=10
009D13E8 add eax,dword ptr [ebp+0Ch]
//将地址为ebp+0ch处的值取出并且加到eax之中,该汇编指令的含义为eax=eax+20
//eax=30
009D13EB mov dword ptr [ebp-8],eax
//将eax中的值即累加结果送入到ebp-8的位置处
//而ebp-8的位置刚好创建了临时变量z,此时ebp-8的位置处存放30
函数调用时,形参并不会主动创建,当我们调用Add函数之前,就将相关参数存放到寄存器中,以压栈的形式保留在栈中,而且参数传递从右向左传递,压栈时先压入最右边的参数,其次一直向左,直到最左边;
形参根本不会在函数内部创建,而是回头找到在Add函数调用之前由于压栈操作保留再栈中的参数;
return z;
009D13EE mov eax,dword ptr [ebp-8]
//函数调用的结果存放于ebp-8的地址处,该汇编指令的含义是将ebp-8位置处的值送入到eax之中
//z既然是局部变量,出作用域销毁,如何将返回值带回去?
//函数调用的结果存放于寄存器中,寄存器中的值并不会销毁
// pop 寄存器 出栈,用寄存器接收出栈的数据
009D13F1 pop edi
//该条汇编指令的含义为用edi接收出栈的数据,同时esp自动递增
009D13F2 pop esi
//该条汇编指令的含义为用esi接收出栈的数据,同时esp自动递增
009D13F3 pop ebx
//该条汇编指令的含义为用ebx接收出栈的数据,同时esp自动递增
//回收Add函数所开辟好的函数栈帧
009D13F4 mov esp,ebp
//该汇编指令的含义是将ebp赋给esp
009D13F6 pop ebp
//此时当前栈顶为来自main函数的ebp,进行出栈操作,将main函数的ebp的值赋给ebp,ebp重新指向main函数的函数栈底
//当前栈顶元素为call指令下一条指令的地址,Add函数调用结束后,回到main函数的函数栈帧,应该从函数调用结束后的下一条指令开始执行
009D13F7 ret
//该汇编指令的含义为从当前栈顶弹出main函数函数栈帧的下一条指令的地址
//跳到call指令的下一条指令
//ret指令为转移指令,实现近转移,修改IP中的内容,程序是从cs:ip处执行
int Add(int x, int y)
{
009D13C0 push ebp
009D13C1 mov ebp,esp
009D13C3 sub esp,0CCh
009D13C9 push ebx
009D13CA push esi
009D13CB push edi
009D13CC lea edi,[ebp+FFFFFF34h]
009D13D2 mov ecx,33h
009D13D7 mov eax,0CCCCCCCCh
009D13DC rep stos dword ptr es:[edi]
int z = 0;
009D13DE mov dword ptr [ebp-8],0
z = x + y;
009D13E5 mov eax,dword ptr [ebp+8]
009D13E8 add eax,dword ptr [ebp+0Ch]
009D13EB mov dword ptr [ebp-8],eax
return z;
009D13EE mov eax,dword ptr [ebp-8]
}
009D13F1 pop edi
009D13F2 pop esi
009D13F3 pop ebx
009D13F4 mov esp,ebp
009D13F6 pop ebp
009D13F7 ret
接下来,回到main函数的函数栈帧
009D1450 add esp,8
//esp=esp+8,该条指令用于销毁形参
009D1453 mov dword ptr [ebp-20h],eax
//将寄存器eax中Add函数调用结束之后的返回值送到main函数函数栈帧中c的位置
//而且c正好是main函数中接受返回值的局部变量
接下里,只是main函数函数栈帧的销毁,不再一一赘述,汇编代码如下
printf("%d\n", c);
009D1456 mov esi,esp
009D1458 mov eax,dword ptr [ebp-20h]
009D145B push eax
009D145C push 9D5858h
009D1461 call dword ptr ds:[009D9114h]
009D1467 add esp,8
009D146A cmp esi,esp
009D146C call 009D113B
return 0;
009D1471 xor eax,eax
- 局部变量是如何创建的?
首先为函数分配好函数栈帧空间,初始化一部分空间之后,然后在函数栈帧中为局部变量分配空间;
- 局部变量的值未初始化,为什么局部变量是随机值?
随机值取决于不同的编译器,编译器不同,初始化的值不相同;
- 函数是如何传参的?传参的顺序是咋么样的?
函数在调用之前,通过压栈操作将参数从右向左依次压栈,参数保留在栈空间之中,函数调用时,通过指针的偏移量找到形参;
- 形参和实参是什么关系?
形参和实参只是值相同,但是空间上是独立的,所以形参是实参的一份临时拷贝,改变形参不影响实参;
- 函数是如何调用的?
详细过程见上文函数调用细节展示
- 函数调用结束后是如何返回的?
函数调用之前,就将call指令的下一条指令的地址压入栈中,并且将主函数的ebp压入栈中,函数调用结束之后,弹出ebp,就能找到上一个函数调用的ebp,找到esp,回到原来函数的栈帧空间,返回值通过寄存器的方式带回来的;