作者:姚开健
原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
1,计算机工作及汇编基础知识
现代计算机采用冯诺依曼体系结构,即存储程序计算机。计算机将代码指令储存在内存中,然后CPU将一条一条指令从内存中读取并执行,得出结果。在32位,并采用采用AT&T汇编的计算机里,eip是即ip寄存器,储存下一条指令的地址,程序员不能修改,esp是栈顶指针,ebp是栈底指针。当调用函数时,即call f,当前eip压栈(pushl %eip),并将eip修改为函数地址;当进入函数时,即enter,当前栈底指针压栈,即pushl %ebp,并修改栈底指针与栈顶指针一致,即movl %esp, %ebp;当函数执行完时,需要执行leave(当函数中调用了其他函数才需要leave,即movl %ebp, %esp ;popl %ebp)和ret(popl %eip)。更多汇编语言知识请参考:Linux 汇编语言开发指南http://www.ibm.com/developerworks/cn/linux/l-assembly/
2,简单C程序的汇编代码工作过程分析:
C代码如下:
int g(int x) { return x + 12; } int f(int x) { return g(x); } int main(void) { return f(12) + 12; }
gcc -S -o main.s main.c -m32
pushl %ebp movl %esp, %ebp
subl $4, %esp movl $12, (%esp)
subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp)
进入函数g又是enter:
pushl %ebp movl %esp, %ebp
接着执行
movl 8(%ebp), %eax addl $12, %eax
即把ebp上移2次,取出12送到eax寄存器,然后eax与12相加得24;
接着
popl %ebp
当前栈里的ebp为1984,popl %ebp之后ebp就指向1984,esp上移一次
接着执行
ret
即
leave
leave为 movl %ebp, %esp ; popl %ebp,即把当前栈顶指针修改为当前栈底指针即1984,并把栈底指针出栈,出栈后栈底指针为1996,栈底指针为1988:
接着执行
ret
即popl %eip。当前eip为23,第23行,
接着执行
addl $12, %eax
把局部变量12与当前eax寄存器里的值24相加得到36
接着执行
leave ret
总结
通过这个简单的C程序的汇编代码工作过程分析,我们知道计算机总是一条一条指令地执行,下一条指令地址保存在eip寄存器中(32位计算机才是eip),当进行函数调用时,1、函数参数入栈,然后2、call,即保存当前未执行的下一条指令地址即eip入栈,计算机自动修改eip为被调用函数地址,然后才进入被调用函数中执行,3、进入被调用函数需要把调用函数的栈底指针入栈保存以便返回时可以回退到原来执行的位置,并把被调用函数的栈底指针与栈顶指针保持一致,开始被调用函数的执行。函数执行时,可以通过指令获取在栈中的函数参数值。函数执行完毕时,leave和ret指令即把栈底指针出栈,恢复到调用函数原来的位置,再出栈eip,继续在断点处的执行。
但也有一些问题尚未明白:
1,main中,为什么不直接pushl 12,而是subl $4, %esp;movl $12, (%esp);
2,f中为什么要把12送入eax里?即movl 8(%ebp), %eax;为什么不直接把8(%ebp)送到(%esp)中,而是还有经过eax周转一下?函数调用时的参数机制是怎样的?