最近在研究二进制,研究到函数调用部分,将自己理解的原理做个记录。

首先需要了解系统栈的工作原理,栈可以理解成一种先进后出的数据结构,这就不用多说了。
在操作系统中,系统栈也起到用来维护函数调用、参数传递等关系的一个作用。嗯,这是我的理解。
在高级语言编程中,函数调用的底层原理是对用户屏蔽的,所以不用过多的纠结于底层的实现。而对于
汇编研究者来说,了解这个原理就很重要了。
首先可以想象一下,汇编语言在内存中是以指令的形式存在的,这些指令是按照顺序存储和执行的,高级语言中
编写的循环、调用,到了底层都会变成一些最基本的判断和跳转,如何在线性的结构上完成“非线性”的过程调度,
理解了这些,就理解了汇编。

这里先抛出高级语言的一个例子:

/*20160701*/
    #include <stdio.h>
    
    int 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   0x400506 <funcA>
       0x0000000000400528 <+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 <printf@plt>
       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*/
#include <stdio.h>

int funcA( int arg ){
    arg += 1;
    return arg;
}

int main(){
    int a;
    a = funcA(1);
    printf("%d", a);
}