张建帮 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
最近开始学习孟宁老师在网易云课堂上开设的《Linux内核分析》课程,虽然才只上了一堂课,但是感觉收获颇丰,特撰文以记。
第一次课以C语言为例讲述了具体的函数调用过程,虽然只有短短的不到30行代码,但却包含了函数调用的精髓,让我对整个计算机系统有了更深入的理解。
仿照孟宁老师的代码,我的代码如下:
int g(int x, int y)
{
return x + y;
}
int f(int x)
{
return g(x, 1);
}
int main(void)
{
return f(8) + 1;
}
唯一的区别就在于,函数g由以前的1个参数变为了2个参数,目的是为了观察参数传递中的入栈顺序。事实证明,在参数传递过程中,是逆序压栈的,即最后一个参数第一个压栈,倒数第二个参数第二个压栈…第一个参数最后压栈。
使用
gcc –S –o main.s main.c -m32
命令将代码编译成汇编码后,再将所有以“.”开头的行去掉,并添加上我的个人注解,最后得到的代码如下:
g:
pushl %ebp ;保存函数f的栈底
movl %esp, %ebp ;设置当前函数g的栈底
;开始运算,运算的结果存放在寄存器%eax中
movl 12(%ebp), %eax
movl 8(%ebp), %edx
addl %edx, %eax
popl %ebp ;恢复函数f的栈底
ret ;相当于popl %eip,即开始执行f中的leave指令
f:
pushl %ebp ;保存函数main的栈底
movl %esp, %ebp ;设置当前函数f的栈底
;由于要调用函数g,因此要先为参数分配空间
;先将最后一个参数入栈,注意!!是最后
;一个参数值先入栈,然后是倒数第二个,
;然后是倒数第三个...一直到第一个
subl $8, %esp
movl $1, 4(%esp)
movl 8(%ebp), %eax
movl %eax, (%esp)
;参数准备好之后,开始调用函数g
call g ;相当于push %eip,即将下一条指令
;leave的地址入栈
leave ;执行此条指令之前,esp指向本函数中的
;最后一个参数,此指令相当于顺序执行下面
;2条指令:movl %ebp,%esp + popl %ebp
ret ;相当于popl %eip,开始执行main中的 addl指令
main:
pushl %ebp ;保存main函数调用者的栈底
movl %esp, %ebp ;设置main函数的栈底
;由于要调用函数f,因此要先为参数分配空间
;先将最后一个参数入栈,注意!!是最后
;一个参数值先入栈,然后是倒数第二个,
;然后是倒数第三个...一直到第一个
subl $4, %esp
movl $8, (%esp)
call f ;相当于push %eip,即将下一条指令
;addl的地址入栈
addl $1, %eax ;f函数的运算结果保存在%eax中,然后
;将其加1,保存在%eax中,得到main函数的
;的运算结果
leave ;执行此条指令之前,esp指向本函数中的
;最后一个参数,此指令相当于顺序执行下面
;2条指令:movl %ebp,%esp + popl %ebp
ret ;相当于popl %eip,开始执行main调用者的下一条指令
看不懂吗,没关系!如果是基本的汇编指令不懂,可以单击本文标题下面的链接,由网易云课堂的孟宁老师为你倾情奉上详解;如果是逻辑关系过于繁琐,那也没关系,可以先看看下面这幅图片:
这幅图片给出了基本的函数栈帧的结构,每个函数的栈帧都是由图中这样的结构组成:
- 调用该函数的函数的栈底地址,函数栈的开始,该函数的栈底指向这块内存
- 该函数的局部变量
- 该函数调用其他函数时需要传递的参数,如上图所示,为逆序入栈
- 该函数调用完其他函数后,下一条要执行的指令的地址,即被调用函数的返回地址
接下来就进入被调用函数的函数栈帧了,其结构与上面相同。而该函数在调用完成后,会释放掉它的栈帧空间,至于具体的分配与释放的细节,都在上面汇编代码的注解中,在孟宁老师的网易云课堂中《linux内核分析》也有详细的讲解。计算机中的这些函数调用来调用去,内存中的栈帧精准不停分配地释放,最后便达到了正确运行的目的。
明白了这整个结构后,再来看上面的汇编代码,相信大家应该没什么问题了。(友情提示:读汇编代码时最好是从main开始读,笔者一开始是其他地方开始读的,结果是丈二和尚摸不着头脑…)