孟宁《Linux内核分析》第一周实验
作者:Zou Le
原创作品转载请注明出处。
课程信息:
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
---------------------------实验正文---------------------------
本实验在实验楼64位LIinux虚拟机下进行。
C代码如下:
int increment5(int x) { return x + 5; } int solve(int x) { return increment5(x) - 2; } int main(void) { return solve(2017); }
C程序的执行逻辑为:以main函数为入口,调用solve函数,字面值2017作为solve函数的参数,在solve函数中执行调用increment5函数,increment5函数接受2017,并加上5,返回solve函数再减去2,最后回到main函数返回该值。
通过同目录下-S和-m32参数得到的汇编代码main.s,将汇编代码中以’.’开头的行删除后得到的代码如下:
increment5: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax addl $5, %eax popl %ebp ret solve: pushl %ebp movl %esp, %ebp pushl 8(%ebp) call increment5 addl $4, %esp subl $2, %eax leave ret main: pushl %ebp movl %esp, %ebp pushl $2017 call solve addl $4, %esp leave ret
实验截图如下:
由教材CSAPP第三章可知,内存为单个过程(函数)分配的那部分栈称为栈帧。最顶端的栈帧由两个指针界定,分别为%ebp指向栈底,%esp指向栈顶,每次push命令都会让%esp减去4(内存中栈向下增长),然后在新的地址写入值。pop则先读出当前%esp位置的值,再将栈减去4。
----------------程序运行时内存栈的变化描述---------------
首先给出带行号的代码作为基准。
本汇编代码执行的过程为:
可以观察到每次调用函数,首先执行的命令为
pushl %ebp
movl %esp, %ebp
若内存中main函数入口时esp和ebp都指向第0格。则这两行代码所做的事情就是将第0格的信息保存到第1格,然后将栈的原点ebp挪到第1格,以第1格作为本过程的栈底。
第20行,所做的事情为将字面值2017写入第2格,此时ebp指向第1格,esp指向第2格。
第21行,call命令将eip=22的命令写入第3格,然后令eip=9。
第9行和第10行,进入solve函数,先创建栈帧:将main函数的栈底信息(栈底=1)保存到第4格,然后以第4格作为本过程的栈底。注意到,此时8(%ebp)指向的是第2格,即solve函数的参数。对于单参数的函数,往往进入新函数后,其参数所在的位置就是8(%ebp),%ebp本身保存了上一个(要返回的函数)的栈底信息,再往前一格保存了eip指针要返回的位置。
第11行,esp再次增加四,在第5格写入字面值2017。
第12行,call命令将eip=13保存到第6格,然后令eip=2。
第2行和第3行,创建栈帧,将slove函数的栈底信息(栈底=4)保存到第7格,然后以第7格作为本函数的栈底。8(%ebp)随即指向第5格的值,即2017。
第4行,将第5格的值2017写入eax寄存器中。
第5行,将eax中的值增加5。
第6行,将第7格(栈底=4)的信息赋值给ebp,即又让ebp指向第4格,同时esp回退1,指向第6格。
第7行,ret的语意为 popl %eip,即此时esp指向第6格,内容为eip指向13。该命令修改eip指向第13行,并使得esp回退到第5格。
第13行,将esp再回退1格,指向第4格。(此时栈顶和栈低都是第4格)。注意到,每次call命令后都会跟一个addl $4, %esp。在本例子中,均为回退掉为call函数所准备的单个函数参数。
第14行,%eax中减去2。
第15行,leave指令。leave指令的语意为先movl %ebp, %esp,然后popl %ebp。本行中%ebp保存的信息为main函数的栈底(第1格),合起来的作用为让%ebp重新指向main函数的栈底1,并且esp指向第3格(eip=22)。
第16行,ret。此行令eip重新回到main函数,程序的第22行,同时esp回退到第2格。
第22行,将esp回退一格,回到第1格。
第23行,令栈底等于0,栈顶也等于0。
第24行程序结束,%eax中保存最后的结果。
具体内存信息和ebp、esp变化如下所示:
由上述过程可知,相比于高级语言如C语言,汇编语言有很大一部分繁琐的底层的函数调用,传值的信息。如我们所说的函数调用栈,是通过保存上级函数继续执行的地址和上级函数的原来的栈地址后,创建新的栈底来完成的。
通过本次实验,我对32位汇编中常出现的8(%ebp)和leave+ret有了“感觉”,感受到了C语言相对于汇编所做的抽象工作的重要性,也对程序的机器级表示有了初步的认识。