非常感谢网易云课堂能提供免费的学习资源。这篇博客为孟宁老师教授的Linux内核分析的作业。
AT&T汇编的语法和学习的x86汇编语法差别很大,最明显的就是源操作数和目的操作数的位置是相反的。
在实验楼的~/Code
目录下,touch main.c
,用vim编辑代码如下:
int func1(int x){
return x + 100;
}
int func0(int x){
return func1(x) + 110;
}
int main(void){
func0(1) + 200;
return 0;
}
使用gcc -S -o main.S main.c
转换成汇编代码,使用vim main.S
查看文件内容。
在每个函数调用的时候,都会构建相同的栈结构。
1. 执行call,将返回地址压入栈中。
2. 保存栈基址寄存器到栈顶。
3. 将栈顶指针寄存器内容保存到栈基址寄存器中。
如果函数中定义了局部变量,那么栈顶指针寄存器会减去相应的值(本函数内所有局部变量大小,并以寄存器大小对齐,以寄存器大小整数倍向上取整)。
在函数执行完毕之后,会销毁进入本函数时所建立的栈结构:
1. 用栈基址寄存器的内容恢复栈顶指针寄存器。(释放局部变量占用的空间。恢复栈顶指针寄存器后,栈顶指针寄存器正好指向保存的栈基址寄存器的内容)。
2. 在栈中弹出保存的栈基址寄存器到栈基址寄存器中。(将栈基址寄存器恢复到调用之前的状态,同时也释放了栈中,参数占用的空间)。
3. 执行ret指令,返回到call指令的下一条指令处继续执行。
总结:
1. 在整个过程中,堆栈是以机器字长为单位对其的。例如,使用64位编码,使用的是pushq
,popq
,栈中是以8字节对齐的。而在32位平台上使用的是pushl
,popl
,栈中是以4字节对齐的。
2. 在函数的栈结构构造完成之后,栈基址寄存器指向原栈基址寄存器的内容,加上一个机器字长,保存的是返回地址,加上两个机器字长是第一个参数。第一个参数之后以此类推。
【局部变量A】 <= 【SP】
【局部变量B】
【保存的BP】
【返回地址】
【第一个参数】
【第二个参数】
【……】
- C调用是从最右侧参数开始入栈,PASCAL是从最左侧参数开始入栈。所以C能相对简单地实现参数个数不确定的函数。还有其他的几种调用协议:
__stdcall
,__fastcall
,__thiscall
等等。有时为了加快调用(声明或者编译器优化),会用寄存器传参的方式。 - 一般情况下,如果数据长度小于等于机器字长,那么,调用时会统一以机器字长压入栈中。如果压入的是结构体等长于机器字长的数据,则从数据末尾开始压入栈中,以保持结构体各个域便宜位置在栈中同样适用。返回同理。
文添艺
原创作品转载请注明出处。
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-10000290