本文共 1541 字,大约阅读时间需要 5 分钟。
大多数人都知道,一个函数的调用会发生压栈,EIP的变化,返回时会出栈和还原EIP,但是当前环境下大多学习编程的人没有学习过汇编语言,所以我这里用小白文的方式让大家理解具体发生了什么。
说用小白文,就用小白文,但是还是需要说一些基础知识。这里不会写出所有寄存器,因为毕竟要用小白文。不然没接触过汇编的会被各个寄存器搞懵逼。
- EBP 栈的基址指针,也就是栈底
- ESP 指向栈顶
- EIP 指向CPU下一次运行的地址
每个方法有自己的栈,线程有自己的线程栈,进程有自己的进程栈,这里不说线程/进程内核切换栈压/出过程,只说一下普通的函数调用过程。
大家也都知道递归会出现栈溢出,就是因为栈的大小是有限的,你递归一次就要进行压栈,压到不够用了不就挂了?
关于栈的概念相信所有程序员都懂,不过这里要说的栈可能和你想象的不太一样,因为这个栈是栈顶移动,栈底不动,压栈栈顶做减法 ,大家试着把这个栈想象成古人写字一样,从右到左。每次新的函数调用就另起一行从头写。
举个栗子 下面的代码
void main(){ call(1);}void call(int v){ //todo something return;}
在调用call(1)的时候,分为下面几个步骤:
- 将1压入栈 (如果是变量就是将地址压入栈,esp-4 x86下指针地址就是一个4字节)
- call xxxx 记录当前eip的下一行地址(当前地址的下一行,不是call地址的下一行,所以又要做减法咯 esp-4)
- 然后当前 EIP 会指向call的函数地址(让CPU下一次执行从CALL函数的地址开始)
- 然后进入call开始执行
- 压入ebp(esp 又要-4 存放ebp,此时esp+4 的话就会找到返回的eip地址,+8就是那个1了),将esp的值赋给ebp(因为每个函数有自己的栈),现在做的就是将栈初始化,栈顶栈底在同一个地址。(这里是两步我合成一步写好理解)。
- 做了一些事情
- ret 从esp中取出之前保存的值,赋值给eip
- 程序跳转回刚才保存的地址(eip) 因为默认的__cdecl,这种方式是调用者自身平栈,所以你在这个进入call后的汇编代码中看不见负责清理压入的参数(1)的汇编代码。 正好说一下如果是__stdcall的情况下你可能会看见ret x 这个x就是被调用者(进入的这个call)负责平栈。
文字看着累,来看个图:
最简单的理解就是,在执行call以前,因为要跳转到call的地址,call地址和调用它的地址可能相隔很远,所以我必须先保存当前位置的下一行的地址,保存好了之后才会跳转过去执行,执行完了之后取出之前保存的值,让程序跳转回来,继续从刚才保存的地址开始走。上面的例子是个x86下的汇编指令,如果是x64程序指针地址就会是8字节大小,如果你是在看AT&T的汇编指令,需要区分一下mov %esp,%ebp,AT&T的汇编指令你可以理解为按顺序的阅读,更适合阅读,像这样:
事实上目前的编译器优化,会优化成和你想象的不太一样的情况,比如,memcpy一个"aaaa",它可能直接优化成*指针=aaaa的ascii码,因为aaaa刚好占4字节且是常量,他会直接优化成指针赋值.
很多没有接触过编译器或汇编的小伙伴,可能会认为,代码越短,效率越高,事实上并非如此,无论如何都会出现寄存器保存临时值,有时候长的代码和一行代码效率是相同的,所以代码通俗易懂才是硬道理,不要硬凑成一行,读起来令人找不到头,毕竟代码是给人看的。
转载地址:http://sxlsi.baihongyu.com/