0 前言
在初学编程语言时,一般都从C语言开始入手。 但看似语法简单,逻辑清晰易读的C代码如何在计算机上工作呢。我们进行如下实验来做一个初步分析。
1 C语言源码
-
int g(int x)
-
{
-
return x + 3;
-
}
-
-
int f(int x)
-
{
-
return g(x);
-
}
-
-
int main(void)
-
{
-
return f(8) + 1;
-
}
如上述代码所示,我们通过阅读代码,发现上述代码通过多层函数调用,经过计算得到了结果。但在计算机上是如何完成这次计算的呢?
2 汇编
3 分析
从汇编代码逐行分析可以得到,当调用栈最长的时候(即从main调用了f再调用了g时),栈区内容如下图所示。
我们根据上图,结合汇编代码,来进行分析。
1) enter 进入函数
pushl %ebp ;保存本函数调用者函数的堆栈顶 注:课件中的初始%ebp位置不合适 ,main前面依然有函数调用,所以不太可能跟%esp在此时重叠。
movl %esp %ebp ;更新%ebp,保存本函数的堆栈顶
2) 取出调用函数提供的参数 (以f函数为例)
subl $4 %esp
movl 8(%ebp), %eax ;x86结构通过堆栈而不是寄存器传参(? 编译优化会不会改变传参方式? ),本行获取调用者传递的参数。
movl %eax, (%esp) ;这里%eax寄存器进行了“中转”,看似所有传参都是会从stack中取到%eax中。
3) 构建临时变量 (以main函数为例)
subl $4, %esp
movl $8,(%esp) ;实际上相当于把临时变量 8压栈。
注:main函数写的比较简单,不能算构建临时变量,直接通过f(8)进行函数调用,并没有构建变量。 如果构建临时变量并传参的话,汇编了一下,情况比较复杂,超出了本题目范围,就不在本文描述了。
4) 进行计算(以g函数为例)
addl $3, %eax ;就是计算,没什么好说的,返回值写到%eax中。
5) return 退出函数
movl %ebp %esp
popl %ebp ;与enter相对,%esp还原到上一个函数的栈底, %ebp还原到上一个函数栈顶。
4 结论
通过上一节的分析,我们总结出,
程序的执行的过程就是函数调用与执行完毕的过程,伴随着堆栈的压入与弹出。
每个函数的执行大体分为几个步骤:
函数进入(保存上一个函数的%ebp, 更新 %ebp %esp ) --->
参数获取(通过%ebp寻找压栈的位置) --->
构建临时变量(一般用于计算结果保存,或调用其他函数) --->
函数计算 --->
计算结果返回
作者:胡川
原创作品转载请注明出处
《Linux内核分析》MOOC课程
阅读(461) | 评论(1) | 转发(0) |