- DU
- 原创作品
- 《Linux内核分析》MOOC课程
试验截图
小小吐槽
汇编代码长度太长,不利于截图(主要是一页放不下整个代码);且在实验楼环境中,毕竟虚拟机中,即便是阅读代码也不如本机来得自如方便。所以,试验截图仅仅为了说明我是确确实实在实验楼中做过这个试验,具体汇编代码分析,会有更具体的非截图显示的代码。
具体截图
汇编代码工作中的堆栈变化分析
以下代码都是在实验楼环境下试验之后,下载到本地的代码。
main.c文件
与老师代码不同的是,我加入了一些局部变量,同时f函数使其传入两个参数。
int f(int x, int y){
int i = 1;
int j = 2;
int k = 3;
return i + j + k + x + y;
}
int g(int x){
return f(1, 2) + x;
}
int main(){
int i = 1;
return g(i) + 2;
}
去除干扰项后干净的main.o文件
先说下生成main.o的命令:
gcc –S –o main.s main.c -m32
-m32是为了在64位环境下生成生成32位的中间代码。
把前缀是.的无关项都去掉,就生成了下面所示的汇编代码了:
f:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl $1, -12(%ebp)
movl $2, -8(%ebp)
movl $3, -4(%ebp)
movl -8(%ebp), %eax
movl -12(%ebp), %edx
addl %eax, %edx
movl -4(%ebp), %eax
addl %eax, %edx
movl 8(%ebp), %eax
addl %eax, %edx
movl 12(%ebp), %eax
addl %edx, %eax
leave
ret
g:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
movl $2, 4(%esp)
movl $1, (%esp)
call f
movl 8(%ebp), %edx
addl %edx, %eax
leave
ret
main:
pushl %ebp
movl %esp, %ebp
subl $20, %esp
movl $1, -4(%ebp)
movl -4(%ebp), %eax
movl %eax, (%esp)
call g
addl $2, %eax
leave
ret
程序执行过程中栈变化图示
程序执行过程中栈变化的分析
大体的程序执行流程,通过上述栈的示意图能看得很清楚。
执行call指令后,CPU把返回地址压入栈,再转移到被调用函数执行,esp在执行过程中随时会用到,用esp来存取局部变量是不现实的,所以用ebp来作为局部变量指针。
pushl %ebp
movl %esp, %ebp
leave
两相配合,使得esp可以更安全、自由的使用,同时借助ebp保证了每次都能返回正确的地方。
以ebp为中间分界线,ebp本身指向原ebp值,ebp+4指向由call指令推入的返回地址,ebp+8及以上则为调用函数传入被调用函数的实参值;ebp以下即为局部变量。
ebp所存放的就是在刚进入被调用函数时候的esp的值,需要在堆栈中预留空间来存放局部变量,使用subl $预留空间, %esp。这个预留空间是如何确定的,这是目前面临的问题。
堆栈预留空间
通过一些小的试验来猜测该预留多大的栈空间。
测试代码
void a(char c){}
void b(){
char c = 1;
}
void c(){
int i = 1;
int j = 2;
int k = 3;
int n = 4;
char c = 1;
}
void d(){
a(1);
}
void e(){
char c = 1;
a(c);
}
所得汇编代码
只写出相关部分
a:
...
subl $4, %esp
movl 8(%ebp), %eax
movb %al, -4(%ebp)
...
b:
...
subl $16, %esp
movb $1, -1(%ebp)
...
c:
...
subl $32, %esp
movl $1, -16(%ebp)
movl $2, -12(%ebp)
movl $3, -8(%ebp)
movl $4, -4(%ebp)
movb $1, -17(%ebp)
...
d:
...
subl $4, %esp
movl $1, (%esp)
...
e:
...
subl $20, %esp
movb $1, -1(%ebp)
movsbl -1(%ebp), %eax
movl %eax, (%esp)
...
汇编代码分析
通过b的汇编代码可以看出:
尽管实际上movb $1, -1(%ebp),只用了1,系统却给预留出了16的栈空间;
通过c的汇编代码可以看出:
局部变量需要超过16的栈空间,即便只是超出1,也会额外分配16的栈空间;
从以上可得出结论,gcc每次分配局部变量栈空间是以16为单位。
通过e的汇编代码可以看出:
movl $1, (%esp),调用被调用函数的时候,实参是放在esp所指向的栈空间内的,即栈顶;如若有多个参数,则按照正增长的顺序从栈顶开始依次往下排。传入的实参,在汇编代码中可以很容易看出来,使用X(%esp)的即为实参。
同时还可以看出,立即数作为参数的时候,分配最小栈空间为4.
通过上述汇编代码的认知,再看e就清楚很多:
一个局部变量,栈空间为16;一个需要传入一个char的函数,参数个数为1,类型为char,需要4栈空间。一共需要20个。
同时,由于传参的栈空间可以重复利用,所以说参数所占用的栈空间大小是,一个函数内部所需要调用的所有函数的参数需要栈空间的最大值。这个可以通过更详尽的试验来验证,考虑到篇幅,就不再贴相关代码。
总结
通过一步步的分析,可以看出计算机的工作流程大致如下:
以冯诺依曼体系为基础,依据存储程序的模型,一步步取出指令,自动执行;
eip(32位机器为例)存放当前指令地址,通过eip来寻找指令并执行;
一般情况下机器指令执行是顺序执行的,call、jmp等指令可以改变eip值,实现跳转;
高级语言(比如C)在执行的时候会被翻译成更贴近机器的汇编,之后再是机器语言,交与机器执行;
栈很重要,通过跟踪栈的变化,能够了解到函数调用被调用函数时候自身数据如何保存的,数据是如何传递的,以及从被调用函数返回调用函数时,如何找到调用函数相关数据;
esp、ebp使得栈更具灵活性。
参考文献
《Windows环境下32位汇编语言程序设计 第二版》 p66