首页 > 代码库 > [Linux内核分析第一周课程] 由C语言程序的汇编表示观察CPU寄存器与内存的互动

[Linux内核分析第一周课程] 由C语言程序的汇编表示观察CPU寄存器与内存的互动

孟宁《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语言相对于汇编所做的抽象工作的重要性,也对程序的机器级表示有了初步的认识。

[Linux内核分析第一周课程] 由C语言程序的汇编表示观察CPU寄存器与内存的互动