一段C语言和汇编的对应分析,揭示函数调用的本质html
最近网易云课堂开放了一节叫Linux内核分析的课程。一直对操做系统和计算机本质很感兴趣,因而进去看了下,才第一堂课,老师就要求学生写一篇关于课时1的博客做为做业。对于这种新颖的做业形式,笔者至关惊讶。好吧,做为任务,仍是完成一下吧,恰好须要消化一下。本文将会按照要求,将一段C语言代码编译成汇编,并给予分析和本身的思考。函数
首先对会涉及到的一些CPU寄存器和汇编的基础知识罗列一下:测试
●16位、32位、64位的CPU寄存器名称有所不一样,好比指令地址寄存器ip,在16位中叫ip,32位中叫eip,64位叫rip优化
●32位的汇编指令一般以l结尾,好比movl至关于mov的含义操作系统
●ebp : 堆栈基地址 寄存器,这个寄存器保存的是当前执行绪的栈底地址htm
●esp : 堆栈栈顶 寄存器,这个寄存器保存的是当前执行绪的栈顶地址ip
●eip : 指令地址 寄存器,这个寄存器保存的是指令所在的地址,CPU会不断的根据eip所指向的指令去内存取指令并执行,并自行累加取下一条指令逐条执行。eip没法直接赋值,call、ret、jmp等指令能够起到修改eip的做用内存
●%用于直接寻址寄存器,$用于表示当即数。movl $8, %eax表示把当即数8存到eax中编译器
●()用于内存间接寻址,好比movl $10, (%esp)表示将当即数10保存到esp所指向的内存地址中博客
●8(%ebp)表示先找到 ebp所指向的地址值+8后获得的地址
●栈地址值是向下增加的,即栈顶从高地址向低地址移动
准备工做
准备一段C代码:
int g(int x)
{
return x+5;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(10)+1;
}
使用实验楼环境
编译成汇编代码
使用以下命令编译上面的c代码
gcc -S -o main.s main.c -m32
去掉不重要的部分后,获得:
汇编代码结果为:
g:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $5, %eax
popl %ebp
ret
f:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
call g
leave
ret
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $10, (%esp)
call f
addl $1, %eax
leave
ret
分析
具体的逐步分析,这里就省了,老师课上讲的很详细了,这里主要是要进行思考和概括。
首先,咱们看到3个C函数对应生成了3个部分的汇编代码,分别用函数名做为标号隔开了
int g(int x) -> g:
int f(int x) -> f:
int main(void) -> main:
咱们知道程序是从main函数开始执行的,那么当程序被加载并运行时,上面的汇编代码会被加载到内存的某一个区域。并且,CPU中的不少寄存器都会初始化,固然其中最重要的是eip,由于eip是指向下一条将要执行的命令所在的内存地址,因此此时的eip应该指向main标号下的pushl %ebp:
main:
eip -> pushl %ebp
程序开始执行…
咱们捆绑着看,首先先看这两条:
pushl %ebp
movl %esp, %ebp
再观察一下整个代码,有没有发现不只仅是main函数,函数f和g的开头也是这两个指令。分析一下,不可贵出,这两条指令是指将当前栈基地址压栈后,从新将基地址定位到栈顶,这个含义实际上是保存好当前的基地址,从新开始一个新的栈。因为函数能够调函数,这里的当前基地址,其实是上一个函数的栈基地址。例如,在f函数中的这两句指令,实际上保存的是main函数的栈基地址。
接着来分析两句:
subl $4, %esp
movl $10, (%esp)
对照C代码不难发现,这是参数进栈,将当即数10,保存到栈顶(esp所指向的内存地址是栈顶)。而在f函数中也能够发现相似的语句:
subl $4, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
因此,咱们能够得出结论是,在调用函数前须要把参数逐个压栈,而压栈的顺序根据笔者的测试是从右向左的。
接着调用call指令,跳转到f函数,咱们知道call指令等同于下面的伪代码:
pushl %eip+1
movl %eip f
即把call指令的后一条指令进栈后,将eip赋值为目标函数的第一个指令地址。这样作显而易见:当所调用的函数结束后,须要返回当前函数继续执行,因此必需要保存下一条指令,不然回来的时候就找不到了。
来到f函数,首先是保存main函数的栈基地址,而后须要调用g函数,因而须要参数先进栈:
subl $4, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
这里重点思考一下,f函数是如何得到main函数传递过来的参数的,咱们看到
movl 8(%ebp), %eax
movl 8(%ebp), %eax
为何参数是从8(%ebp)中得到的呢?咱们知道8(%ebp)表示的是以ebp为基准向栈底回溯8个字节获得,为何是8个字节呢?
回想一下,在main函数中完成了参数进栈后作了两件事情:
1.因为call f指令的做用,call f下一条指令的地址被压栈了,这占用率4个字节
2.进入f函数后,当即将main函数的栈基地址进栈了,并且将ebp靠向了栈顶esp,这又占用了4个字节
因而经过8(%ebp)能够找到前一个函数的第一个整型参数的值。
一张图告诉你怎么回事:
看过了进入函数,调用函数的过程,再看一下函数是如何退出的。观察main和f不难发现,退出函数使用的是以下指令
leave
ret
leave指令至关于以下指令:
movl %ebp, %esp
popl %ebp
●第一条语句是将esp重置到ebp,能够理解为清空当前函数所使用的栈
●第二条语句是将栈顶值赋值给ebp,并弹出,栈顶值是什么呢?经过上面的分析不难发现,此时的栈顶值其实是前一个函数的栈基地址,因此第二条语句的意思就是把ebp恢复到前一个函数的栈基地址
接着ret就是至关于,恢复指令指向:
popl %eip
为何g函数没有leave呢?由于g函数内部没有任何的变量声明和函数调用栈一直都是空的,因此编译器优化了指令
总结
最后,经过这个例子,总结一下函数调用的过程:
进入函数:
当前栈基地址压栈(当前栈基地址其实是前一个函数的栈基地址)
调用其余函数:
1.参数从右到左进栈
2.下一条指令地址进栈
退出函数:
1.栈顶esp归位,回到本函数的ebp
2.基地址回退到上一个函数的基地址
3.eip退回到上一个函数即将要执行的那条语句的地址上
来自:P_Chou Tech Space,做者:周平连接:http://www.pchou.info/c-cpp/2015/03/03/c-and-asm.html