最近在研究二进制,研究到函数调用部分,将自己理解的原理做个记录。
首先需要了解系统栈的工作原理,栈可以理解成一种先进后出的数据结构,这就不用多说了。
在操作系统中,系统栈也起到用来维护函数调用、参数传递等关系的一个作用。嗯,这是我的理解。 在高级语言编程中,函数调用的底层原理是对用户屏蔽的,所以不用过多的纠结于底层的实现。而对于 汇编研究者来说,了解这个原理就很重要了。 首先可以想象一下,汇编语言在内存中是以指令的形式存在的,这些指令是按照顺序存储和执行的,高级语言中 编写的循环、调用,到了底层都会变成一些最基本的判断和跳转,如何在线性的结构上完成“非线性”的过程调度, 理解了这些,就理解了汇编。这里先抛出高级语言的一个例子:
/*20160701*/ #includeint funcA( int arg ){ arg += 1; return arg; } int main(){ int a; a = funcA(1); printf("%d", a); }
在这个程序中,main函数调用了函数funcA,funcA对传入的数据进行+1然后返回。
这个程序在编译之后,main函数变成这样:(gdb) disassemble main Dump of assembler code for function main: 0x0000000000400516 <+0>: push rbp 0x0000000000400517 <+1>: mov rbp,rsp 0x000000000040051a <+4>: sub rsp,0x10 0x000000000040051e <+8>: mov edi,0x1 0x0000000000400523 <+13>:call 0x4005060x0000000000400528 <+18>:mov DWORD PTR [rbp-0x4],eax 0x000000000040052b <+21>:mov eax,DWORD PTR [rbp-0x4] 0x000000000040052e <+24>:mov esi,eax 0x0000000000400530 <+26>:mov edi,0x4005d4 0x0000000000400535 <+31>:mov eax,0x0 0x000000000040053a <+36>:call 0x4003e0 0x000000000040053f <+41>:leave 0x0000000000400540 <+42>:ret End of assembler dump.
其中rbp是调用main函数的函数的栈桢的底部,这么说有点绕,简单的来说,main函数调用了funcA,那funcA中首先要做的一件事情就是把调用它的main函数栈桢的底部保存,所以在main函数被操作系统装载执行之后,main要做的首先是把调用它的函数的栈桢的底部保存,不然怎么返回呢?
第二个步骤把rsp的值传递给rbp,这是替换当前栈桢的底部,因为调用了funcA,所以要为funcA创建独立的栈桢,于是抬高栈底,怎么抬高呢,把栈顶传给指向栈桢底部的指针就可以了。 下一步是抬高栈顶,这是为funcA创建栈桢空间。 接着将参数传递给edi,因为这里只有一个参数,所以不涉及到参数顺序的问题,关于这个问题,可以去了解一下 调用了funcA,再来观察一下funcA的内部机制:(gdb) disassemble funcA Dump of assembler code for function funcA: 0x0000000000400506 <+0>: push rbp 0x0000000000400507 <+1>: mov rbp,rsp 0x000000000040050a <+4>: mov DWORD PTR [rbp-0x4],edi 0x000000000040050d <+7>: add DWORD PTR [rbp-0x4],0x1 0x0000000000400511 <+11>: mov eax,DWORD PTR [rbp-0x4] 0x0000000000400514 <+14>: pop rbp 0x0000000000400515 <+15>: ret End of assembler dump.
同样的,在funcA中,首先保存上一个函数,即main函数栈桢的栈底,然后将rsp的值赋给rbp,抬高栈桢底部。
接着从edi中取得参数,并放入位于自身栈桢空间中,rbp之后的双字单元内。 然后执行操作,将其自增。 执行完成之后,将返回值保存在eax中,等待返回。 弹出上一个函数的栈桢的底部,重新回到main函数的空间。PS:
直到目前为止,这个程序反编译出来的结果和书上说的原理还是有一些出入的,还有下面几个问题: 0x01 书上说的是,传递参数,会将参数按照一定顺序压栈,而不是像本程序中这样使用edi 0x02 在main函数调用funcA函数之后,将栈顶指针esp抬高了,但是在funcA函数执行完成需要返回到main函数的时候,只恢复了ebp指针,并没有恢复esp指针,这是为什么?希望接下来可以搞懂上面的两个问题。
本文中用到的相关代码:/*20160701*/#includeint funcA( int arg ){ arg += 1; return arg;}int main(){ int a; a = funcA(1); printf("%d", a);}