函数调用栈
我们常用函数,知道使用函数时会跳到函数定义的代码段去执行,然后执行完后再返回到调用函数去,但以下的一些问题却仍不清楚。

之前说了call其实包含了两步,分别是



这个调用过程的原理是什么
调用函数前要做什么事情
函数的参数是如何传递的
如何跳转到被调用函数
执行完被调函数后如何返回调用函数并且保证能接着运行
要知道这些,需要结合代码的反汇编来看。
写了一段简单的函数调用的代码
以下为main函数的反汇编
其中ebp为栈底指针,esp为栈顶指针。
可以看到我们所说的指令 比如int a=15;这个指令,它靠的是ebp栈底指针的偏移量来确定某处有个4个字节的地方存储15这个值的。
具体汇编就是
00EA2C2E mov dword ptr [a],0Fh
00EA2C2E是 mov dword ptr [a],0Fh 这个指令在代码段的地址,这个地址是虚拟地址空间地址。并非物理地址
0Fh就是15的十六进制
word代表2个字节,dword代表4个字节
ptr[a]就是a的地址处,其实这个的真正模样应该是ptr[ebp-4]
这句汇编的意思就是执行将0Fh这个值移动到ebp-4这个地址,占用4个字节,简单来讲就是给栈底上的4个字节后赋值15,也占4个字节。
后面
int b=10;
00EA2C35 mov dword ptr [b],0Ah
int result=0;
00EA2C3C mov dword ptr [result],0
就是分别压入了2个整型的0入栈
重点来了
到了函数调用这块了,我们看看反汇编
result=sum(a,b);
00EA2C43 mov eax,dword ptr [b]
00EA2C46 push eax
00EA2C47 mov ecx,dword ptr [a]
00EA2C4A push ecx
00EA2C4B call sum (0EA143Dh)
00EA2C50 add esp,8
00EA2C53 mov dword ptr [result],eax
其中call就表示跳到被调用函数指令地址。 然而其实call里面分为两步,我们稍后再说
重点是call的前4行汇编是做什么的?
eax ecx都指的是寄存器。
那么前四行意思就是给eax寄存器赋值为b的值,然后eax压栈,再给ecx寄存器赋值为a的值,然后ecx压栈。
这样看来就是先后把b和a的值压入栈顶。而我们可以发现b和a就是sum函数所需要的实参。
目前来看是这样的
之前说了call其实包含了两步,分别是
1.把调用方 使用调用函数这条指令的下一条指令的地址push压栈
2.跳到call指令里那个的指令地址,即被调函数的指令地址。
00EA2C4B call sum (0EA143Dh)
00EA2C50 add esp,8
00EA2C53 mov dword ptr [result],eax
代到这块的汇编就是将00EA2C50压栈,然后再跳转到0EA143Dh
我们看看0EA143Dh 是什么。
00EA143D jmp sum (0EA4450h)
括号里 0EA4450h就是我们刚看的sum定义的地方
int sum(int a,int b)
{
00EA4450 push ebp
00EA4451 mov ebp,esp
00EA4453 sub esp,0CCh
00EA4459 push ebx
00EA445A push esi
00EA445B push edi
00EA445C lea edi,[ebp-0CCh]
00EA4462 mov ecx,33h
00EA4467 mov eax,0CCCCCCCCh
00EA446C rep stos dword ptr es:[edi]
int r=0;
00EA446E mov
。。。。
。。。
。。
。
这样是不是就很明晰了
然而函数的一开始又有一大串汇编指令,main函数也有,刚才忽略没讲 ,现在来看,这些指令到底是做什么的?
.......
00EA4450 push ebp
00EA4451 mov ebp,esp
00EA4453 sub esp,0CCh
00EA4459 push ebx
00EA445A push esi
00EA445B push edi
00EA445C lea edi,[ebp-0CCh]
00EA4462 mov ecx,33h
00EA4467 mov eax,0CCCCCCCCh
00EA446C rep stos dword ptr es:[edi]
int r=0;
.......
首先ebp压入栈,这个ebp是main的栈底指针的值,然后esp的值给ebp,也就是让栈底指针指向栈顶esp指向的地方,简言这两步就是为了保存原先的main栈底地址,然后让栈底指针ebp移到最上方,这就变成了开辟了新的栈了,新栈就是被调用函数的栈。
00EA4453 sub esp,0CCh
让esp栈顶指针sub减等0cch,也就是让新栈开辟了0xcc字节的空间,即204个字节。
之后push了三个寄存器 ebx esi edi
然后lea edi,[ebp-0CCh] 这句指令意思为让edi指向ebp-0cch处的地址,也就是让edi寄存器存储了新栈顶指针的值。
之后又给ecx 存储了33h,eax存储了0cccccccch。
33h的十进制为51。是不是刚好51 *4=204,204是我们新栈开辟的大小。
所以说
00EA4462 mov ecx,33h
00EA4467 mov eax,0CCCCCCCCh
00EA446C rep stos dword ptr es:[edi]
这三行的意思,就是循环ecx次,edi从栈顶向栈底依次赋值为eax。
也就是循环51次,从栈顶向栈底赋值4个字节的数据0cccccccch,直到edi走向栈底了,把栈内的数据全部赋值了。这就是每个函数开始后,创建了栈,把栈内数据全部清理为0cccccccc,我们有时会遇到打印越界的数组出现 烫烫烫烫 其实一对 cc 对应的字符就是 烫。
栈开辟完了之后,进入函数运算
.....
int r=0;
00EA446E mov dword ptr [r],0
r=a+b;
00EA4475 mov eax,dword ptr [a]
00EA4478 add eax,dword ptr [b]
00EA447B mov dword ptr [r],eax
return r;
00EA447E mov eax,dword ptr [r]
}
00EA4481 pop edi
00EA4482 pop esi
00EA4483 pop ebx
00EA4484 mov esp,ebp
00EA4486 pop ebp
}
00EA4487 ret
.......
我们先看这部分
r=a+b;
00EA4475 mov eax,dword ptr [a]
00EA4478 add eax,dword ptr [b]
00EA447B mov dword ptr [r],eax
可以看到 这个r=a+b的过程是这样的。把实参a的值先赋值给eax寄存器,然后再让eax寄存器加等实参b的值。
也就是a+b的结果先一步计算好了存储在eax中,然后再把eax里的值赋值给栈中的r。
return r;
00EA447E mov eax,dword ptr [r]
返回r,可以看到是把r中的值给了寄存器,通过寄存器带回调用方函数的。
重点又来了
看看栈的销毁是怎么做的
00EA4481 pop edi
00EA4482 pop esi
00EA4483 pop ebx
00EA4484 mov esp,ebp
00EA4486 pop ebp
}
00EA4487 ret
首先3个寄存器出栈。
然后让esp的值变为ebp,也就是让栈顶指针指向栈底。然后ebp出栈,意思就把存储的main的原先的ebp的值出栈,并赋值给ebp。这样,ebp就重新指向main的栈底了。
然后ret指令就是让栈顶的值出栈,现在的栈顶就是存储那个下一条指令地址的值,出栈就可以跳回到调用方刚执行完函数的地方。就实现了回退并连接上次运行地方的功能。
然后转到主函数汇编
.......
00EA2C4B call sum (0EA143Dh)
00EA2C50 add esp,8
00EA2C53 mov dword ptr [result],eax
........
让esp加等8,意思就是把两个4个字节累积8个字节的栈帧舍弃。然后将eax里保存的return的结果赋值给result。
流程图如下,红色代表顺序
还有一个遗留问题是刚才的sum函数的参数只有两个四字节数据,因此用的是寄存器带的数据,可是寄存器非常有限的,如果我的实参是个结构体类型,大小远远大于四个字节呢,这是参数该如何带呢?
小于4个字节时用1个寄存器,大于4小于8时 用2个寄存器
如果大于8个字节那就不能用寄存器了,而是直接让栈顶指针esp减等参数的大小,然后类似开辟栈时,循环拷贝0ccccccc那样,用2个寄存器。一个记录调用方函数的那个实参的其实地址。一个记录拷贝循环次数。这样循环拷贝进行传参。
返回值也是通样,如果返回的值大于8个字节时,将在调用方函数的栈内开辟一块返回值临时量区域,然后把return的值循环拷贝回调用方。所以在新栈开辟的时候会多压入一个临时量的地址。