概述
要清楚程序怎么执行,首先有两个基础的过程需要了解,一个是函数怎么被调用,另一个是函数怎么返回,本文将通过一个简单的例子对这两个过程进行简单讲解。
以下是一段简单的c代码。
首先我们利用如下命令对程序进行编译
gcc –S –o main.s main.c -m32
对于命令中选项的解释
-S
以下是man gcc 中和-S有关的说明
-S Stop after the stage of compilation proper; do not assemble. The
output is in the form of an assembler code file for each non-assembler input file specified.
-c
-S
-E If any of these options is used, then the linker is not run, and
object file names should not be used as arguments.
简而言之,-S选项要求gcc将代码编译生成汇编代码就停止,并不进行汇编代码转机器码的过程(assemble),连接器(linker)也不工作。
-o
output, 输出的意思。用户自定义输出文件的文件名。-m32
以下是man gcc 中和-m32有关的说明The -m32 option sets "int", "long", and pointer types to 32 bits, and generates code that runs on any i386 system.
加入这个选项使得gcc生成运行在32位系统的代码
上面命令输出main.s这个文件,这个文件是c代码对应的汇编代码。下面是main.s文件内容,里面的编译辅助信息已经全部删除。
下面我们手动模拟这段代码的执行过程,以研究函数调用和返回的过程。
假设这是代码运行前ebp,esp,eip,eax的状态,以及栈的内容。
1.进入到main函数后,先将ebp寄存器内容压栈,然后将esp内容送到ebp,这时ebp和esp同时指向地址为6的内存。内存6保存着main函数执行前ebp的内容。
2.然后执行两条指令,subl $4, %esp movl $2, (%esp), 这两条指令的运行结果如下图,相当于push $2。对照c代码,main函数里面只有一个地方出现了数字2,也就是调用f函数的时候,2作为参数传递给f函数。而在这里,做完将2压栈操作的后一条就是call f,就是对f函数的调用,所以我们可以猜测,函数调用时,函数的参数是通过压栈的方式实现的,这个猜测我们可以通过f函数中对g函数的调用证实是正确的。
3.call指令做具体做的事情是将当前的eip压栈,然后将eip值设置为被调用的代码地址。在这里,被调用的f函数行数是9,所以eip变为9;在执行call f时,eip指向的是call f 下一条指令,也就是23,所以被压栈的eip的值是23。值得注意的是,在这里代码的地址我用了行号代替,这只是为了方便说明。实际上eip里面存放的应该是内存地址,每次执行指令有eip的自增并不是增加1,而是根据指令长度而变化,以保证eip总是指向下一条指令的地址。
4.保存main函数的ebp,并开辟自己的运行栈
5.栈定往下跑一格
6..把之前main压栈的2取出来放到eax寄存器中,证明当时main对2的压栈操作是在传参数
7.把eax的内容放到栈顶。这三条语句的执行效果就是 push 8(%ebp),对照相应的c代码,f函数直接
8.调用g函数,操作和main函数调用f函数过程一样。
9.准备执行g函数
10.保存f函数的ebp,设置自己的栈
11.取出f函数之前压栈的参数2
12.和1相加,结果放在eax
13.栈顶元素存放到ebp,对应的栈顶增加4
14.ret指令作用是 pop eip,这个eip是执行call指令时被压栈的,他指向call的下一条指令。在这里,他指向f函数里面的leave
15.leave指令作用为将esp设置为ebp,然后pop ebp。在这条指令执行前,ebp存放的是f函数的栈底,通过9,10 行汇编代码我们看到,f函数的ebp指向的内存单元存放的是main的ebp,所以leave指令做的事情其实是将ebp设置成main的ebp。那么为什么g函数返回前没有leave指令呢?这时因为g函数返回前esp就是ebp,所以不需要将esp设置成ebp的过程,可以直接pop ebp。
16.运行ret,操作同g函数的返回
17.这里对应了c代码中 return f(2)+4; 注意到在g函数中g函数将运行结果直接放到eax后就不再操作eax,f函数对eax也没有操作,所以这部分eax加4前eax内容就是f函数返回的内容,加4后,eax内容作为main的返回
18.同样是leave
19.同样是ret
20. 我们重新看一下这段代码运行前的寄存器和堆栈内容
至此,本文的两个问题有答案了
一,函数调用是个什么过程
答:函数调用前,函数参数由调用者通过压栈的方式存放;调用时,eip被压栈保存;被调用函数执行第一步是将当前的ebp保存,并将当前栈顶作为自己的设置为自己的栈底。
二,函数返回是个什么过程
答:函数返回时将ebp恢复为保存在栈中的ebp,返回结果保存在eax中给调用者使用,eip恢复到调用前保存的eip,使得程序从调用前的环境继续执行。
邱聪 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
最后
以上就是懵懂宝贝为你收集整理的程序怎么跑?(1)--linux 课程学习笔记的全部内容,希望文章能够帮你解决程序怎么跑?(1)--linux 课程学习笔记所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复