X86汇编语言学习手记(2)

时间:2021-01-27 03:39:58
X86汇编语言学习手记(2)

作者: Badcoffee

Email: blog.oliver@gmail.com
2004年11月

原文出处: http://blog.csdn.net/yayong
版权所有: 转载时请务必以超链接形式标明文章原始出处、作者信息及本声明

这是作者在学习X86汇编过程中的学习笔记,难免有错误和疏漏之处,欢迎指正。作者将随时修改错误并将新的版本发布在自己的Blog站点上。严格说来,本篇文档更侧重于C语言和C编译器方面的知识,如果涉及到基本的汇编语言的内容,可以参考相关文档。
X86 汇编语言学习手记(1)在作者的Blog上发布以来,得到了很多网友的肯定和鼓励,并且还有热心网友指出了其中的错误,作者已经将文档中已发现的错误修正后更新在Blog上。

    上一篇文章通过分析一个最简的C程序,引出了以下概念:
        Stack Frame 栈框架 和 SFP 栈框架指针
        Stack aligned 栈对齐
       
Calling Convention  调用约定 和 ABI ( Application Binary Interface) 应用程序二进制接口
    本章中,将通过进一步的实验,来深入了解这些概念。如果还不了解这些概念,可以参考 X86汇编语言学习手记(1)
       
1. 局部变量的栈分配

    上篇文章已经分析过一个最简的C程序,
    下面我们分析一下C编译器如何处理局部变量的分配,为此先给出如下程序:

    #vi test2.c

    int main()
    {
        int i;
        int j=2;
        i=3;
        i=++i;
        return i+j;
    }

    编译该程序,产生二进制文件,并利用mdb来观察程序运行中的stack的状态:
    #gcc test2.c -o test2
    #mdb test2
    Loading modules: [ libc.so.1 ]
    > main::dis
   
main:           pushl   %ebp
   
main+1:         movl    %esp,%ebp          ; main至main+1,创建Stack Frame
    main+3:         subl    $8,%esp            ; 为局部变量i,j分配栈空间,并保证栈16字节对齐
    main+6:         andl    $0xf0,%esp
   
main+9:         movl    $0,%eax
   
main+0xe:       subl    %eax,%esp          ; main+6至main+0xe,再次保证栈16字节对齐
   
main+0x10:      movl    $2,-8(%ebp)        ; 初始化局部变量j的值为2
    main+0x17:      movl    $3,-4(%ebp)         ; 给局部变量i赋值为3
    main+0x1e:      leal    -4(%ebp),%eax      ; 将局部变量i的地址装入到EAX寄存器中
    main+0x21:      incl    (%eax)             ; i++
    main+0x23:      movl    -8(%ebp),%eax      ; 将j的值装入EAX
    main+0x26:      addl    -4(%ebp),%eax      ; i+j并将结果存入EAX,作为返回值
    main+0x29:      leave                    ; 撤销Stack Frame
   
main+0x2a:      ret                      ; main函数返回
    >
    > main+0x10:b         ; 在
地址 main+0x10处 设置断点
    > main+0x1e:b         ; 在
main+0x1e 设置断点
    > main+0x29:b         ; main+0x1e 设置断点
    > main+0x2a:b         ; main+0x1e 设置断点
       
    下面的mdb的4个命令在一行输入,中间用分号间隔开,命令的含义在注释中给出:
    > :r;<esp,10/nap;<ebp=X;<eax=X    ; 运行程序(:r 命令)
    mdb: stop at main+0x10                ; 以ESP寄存器为起始地址,指定格式输出16字节的栈内容( <esp,10/nap 命令)
    mdb: target stopped at:                ; 在最后输出EBP和EAX寄存器的值( <ebp=X 命令 和 <eax=X 命令 )
    main+0x10:      movl    $2,-8(%ebp)    ; 程序运行后在main +0x10处指令执行前中断,此时栈分配后还未初始化
    0x8047db0:     
    0x8047db0:      0xddbebca0             ; 这是变量j,4字节,未初始化,此处为栈顶, ESP的值就是0x8047db0  
    0x8047db4:      0xddbe137f             ; 这是 变量 i, 4字节,未初始化
    0x8047db8:      0x8047dd8              ; 这是_start的SFP(_start的EBP),4字节由main 的SFP指向它
    0x8047dbc:      _start+0x5d            ; 这是_start调用main之前压栈的下条指令地址,main返回后将恢复给EIP
    0x8047dc0:      1              
    0x8047dc4:      0x8047de4      
    0x8047dc8:      0x8047dec      
    0x8047dcc:      _start+0x35    
    0x8047dd0:      _fini          
    0x8047dd4:      ld.so.1`atexit_fini
    0x8047dd8:      0                      ; _start的SFP指向的内容为0,证明_start是程序的入口
    0x8047ddc:      0              
    0x8047de0:      1              
    0x8047de4:      0x8047eb4      
    0x8047de8:      0              
    0x8047dec:      0x8047eba      
                    8047db8              ; 这是main当前EBP寄存器的值,即main的SFP
                    0                   ; EAX的值,当前为0

    > :c;<esp,10/nap;<ebp=X;<eax=X    ; 继续运行程序(:c 命令),其余3命令同上,打印16字节栈和EBP,EAX内容

    mdb: stop at main+0x1e
    mdb: target stopped at:
    main+0x1e:      leal    -4(%ebp),%eax  ; 程序运行到断点main+0x1e处停止,此时局部变量i,j赋值已完成
    0x8047db0:     
    0x8047db0:      2                      ; 这是变量j,4字节,值为2, 此处为栈顶, ESP的值就是0x8047db0
    0x8047db4:      3                      ; 这是变量i,4字节,值为3
    0x8047db8:      0x8047dd8              ; 这是_start的SFP,4字节
    0x8047dbc:      _start+0x5d            ; 这是返回_start后的EIP
    0x8047dc0:      1              
    0x8047dc4:      0x8047de4      
    0x8047dc8:      0x8047dec      
    0x8047dcc:      _start+0x35    
    0x8047dd0:      _fini          
    0x8047dd4:      ld.so.1`atexit_fini
    0x8047dd8:      0              
    0x8047ddc:      0              
    0x8047de0:      1              
    0x8047de4:      0x8047eb4      
    0x8047de8:      0              
    0x8047dec:      0x8047eba      
                    8047db8              ; 这是main当前EBP寄存器的值,即main的SFP
                    0                   ; EAX的值,当前为0
    > :c;<esp,10/nap;<ebp=X;<eax=X
    ; 继续运行程序,打印16字节栈和EBP,EAX内容
    mdb: stop at main+0x29
    mdb: target stopped at:
    main+0x29:      leave                  ; 运行到断点main+0x29处停止,计算已经完成,即将撤销Stack Frame
    0x8047db0:     
    0x8047db0:      2                      ; 这是变量j,4字节,值为2此处为栈顶, ESP的值就是0x8047db0      
    0x8047db4:      4                      ; 这是i++以后的变量i,4字节,值为3
    0x8047db8:      0x8047dd8              ; 这是_start的SFP,4字节
    0x8047dbc:      _start+0x5d            ; 这是返回_start后的EIP
    0x8047dc0:      1              
    0x8047dc4:      0x8047de4      
    0x8047dc8:      0x8047dec      
    0x8047dcc:      _start+0x35    
    0x8047dd0:      _fini          
    0x8047dd4:      ld.so.1`atexit_fini
    0x8047dd8:      0              
    0x8047ddc:      0              
    0x8047de0:      1              
    0x8047de4:      0x8047eb4      
    0x8047de8:      0              
    0x8047dec:      0x8047eba      
                    8047db8              ; 这是main当前EBP寄存器的值,即main的SFP       
                    6                   ; EAX的值 ,即函数的返回值 ,当前为6               
    > :c;<esp,10/nap;<ebp=X;<eax=X
    ; 继续运行程序,打印16字节栈和EBP,EAX内容
    mdb: stop at main+0x2a
    mdb: target stopped at:
    main+0x2a:      ret                   ; 运行到断点main+0x2a处停止,Stack Frame已被撤销,main即将返回
    0x8047dbc:     
    0x8047dbc:      _start+0x5d            ; Stack Frame已经被撤销,栈顶是返回_start后的EIP,main的栈已被释放
    0x8047dc0:      1              
    0x8047dc4:      0x8047de4      
    0x8047dc8:      0x8047dec      
    0x8047dcc:      _start+0x35    
    0x8047dd0:      _fini          
    0x8047dd4:      ld.so.1`atexit_fini
    0x8047dd8:      0              
    0x8047ddc:      0              
    0x8047de0:      1              
    0x8047de4:      0x8047eb4      
    0x8047de8:      0              
    0x8047dec:      0x8047eba      
    0x8047df0:      0x8047ed6      
    0x8047df4:      0x8047edd      
    0x8047df8:      0x8047ee4      
                    8047dd8             ; _start的SFP,之前存储在 地址 0x8047db8 ,main的Stack Frame撤销时恢复                            6                  ; EAX的值 ,即函数的返回值 ,当前为6               
    > :s;<esp,10/nap;<ebp=X;<eax=X
   ; 单步执行下条指令(:s 命令),打印16字节栈和EBP,EAX内容
    mdb: target stopped at:
    _start+0x5d:    addl    $0xc,%esp      ; 此时main已经返回,_start+0x5d曾经存储在 地址 0x8047dbc
    0x8047dc0:     
    0x8047dc0:      1                      ; main已经返回 _start +0x5d已经被弹出
    0x8047dc4:      0x8047de4      
    0x8047dc8:      0x8047dec      
    0x8047dcc:      _start+0x35    
    0x8047dd0:      _fini          
    0x8047dd4:      ld.so.1`atexit_fini
    0x8047dd8:      0                      ; _start的SFP指向的内容为0,证明_start是程序的入口              
    0x8047ddc:      0              
    0x8047de0:      1              
    0x8047de4:      0x8047eb4      
    0x8047de8:      0              
    0x8047dec:      0x8047eba      
    0x8047df0:      0x8047ed6      
    0x8047df4:      0x8047edd      
    0x8047df8:      0x8047ee4      
    0x8047dfc:      0x8047ef3      
                    8047dd8            ; _start的SFP,之前存储在 地址 0x8047db8 ,main的Stack Frame撤销时恢复 
                    6                  ; EAX的值为6, 还是main函数的返回值                 
    >


    通过mdb对程序运行时的寄存器和栈的观察和分析,可以得出局部变量在栈中的访问和分配及释放方式:
        1.局部变量的分配,可以通过esp减去所需字节数
            subl    $8,%esp
        2.局部变量的释放,可以通过leave指令
            leave      
        3.局部变量的访问,可以通过ebp减去偏移量
            movl    -8(%ebp),%eax
            addl    -4(%ebp),%eax

    问题:当存在2个以上的局部变量时,如何进行栈对齐?
    在上篇文章中,提到 subl $8,%esp语句除了分配栈空间外,还有一个作用就是栈对齐。那么本例中,由于i和j正好是8字节,那么如果存在2个以上的局部变量时,如何同时满足空间分配和栈对齐呢?

2. 两个以上的局部变量的栈分配

    在之前的C程序中,增加局部变量定义k,程序如下:
    # vi test3.c

    int main()
    {
        int i, j=2, k=4;
        i=3;
        i=++i;
        k=i+j+k;
        return k;
    }

    编译该程序后,用mdb反汇编得出如下结果:
    # gcc test3.c -o test3   
    # mdb test3
    Loading modules: [ libc.so.1 ]
    > main::dis
    main:               pushl   %ebp
    main+1:             movl    %esp,%ebp            ; main至main+1,创建Stack Frame
    main+3:            subl   $0x18,%esp         ; 为局部变量i,j,k分配栈空间,并保证栈16字节对齐
    main+6:             andl    $0xf0,%esp
    main+9:             movl    $0,%eax
    main+0xe:           subl    %eax,%esp            ; main+6至main+0xe,再次保证栈16字节对齐
    main+0x10:          movl    $2,-8(%ebp)          ; j=2
    main+0x17:          movl    $4,-0xc(%ebp)        ; k=4
    main+0x1e:          movl    $3,-4(%ebp)          ; i=3
    main+0x25:          leal    -4(%ebp),%eax        ; 将i的地址装入到EAX
    main+0x28:          incl    (%eax)               ; i++
    main+0x2a:          movl    -8(%ebp),%eax        ; 将j的值装入到 EAX
    main+0x2d:          movl    -4(%ebp),%edx        ; 将i的值装入到 EDX
    main+0x30:          addl    %eax,%edx            ; j+i,结果存入EDX
    main+0x32:          leal    -0xc(%ebp),%eax      ; 将k的地址装入到EAX
    main+0x35:          addl    %edx,(%eax)          ; i+j+k,结果存入地址ebp-0xc即k中
    main+0x37:          movl    -0xc(%ebp),%eax      ; 将k的值装入EAX,作为返回值
    main+0x3a:          leave                        ; 撤销Stack Frame
    main+0x3b:          ret                          ; main函数返回
    >
 

    问题:为什么3个变量分配了0x18字节的栈空间?
    在2个变量的时候,分配栈空间的指令是: subl $8,%esp
    而在3个局部变量的时候,分配栈空间的指令是: subl $0x18,%esp
    3个整型变量只需要0xc字节,为何实际上分配了0x18字节呢?
    答案就是: 保持16字节栈对齐

    在 X86 汇编语言学习手记(1)里,已经说明过gcc默认的编译是要16字节栈对齐的, subl $8,%esp会使栈16字节对齐,而8字节空间只能满足2个局部变量,如果再分配4字节满足第3个局部变量的话,那栈地址就不再16字节对齐的,而同时满足空间需要而且保持16字节栈对齐的最接近的就是0x18。

    如果,各定义一个50字节和100字节的字符数组,在这种情况下,实际分配多少栈空间呢?答案是0x8+0x40+0x70,即184字节。
    下面动手验证一下:

    # vi test4.c
    int main()
    {
        char str1[50];
        char str2[100];
        return 0;
    }
    # mdb test4
    Loading modules: [ libc.so.1 ]
    > main::dis
    main:               pushl   %ebp
    main+1:             movl    %esp,%ebp
    main+3:            subl   $0xb8,%esp   ; 为两个字符数组分配栈空间,同时保证16字节对齐
    main+9:             andl    $0xf0,%esp
    main+0xc:           movl    $0,%eax
    main+0x11:          subl    %eax,%esp
    main+0x13:          movl    $0,%eax
    main+0x18:          leave
    main+0x19:          ret
    > 0xb8=D                              ; 16进制换算10进制
                    184            
    > 0x40+0x70+0x8=X                     ; 表达式计算,结果指定为16进制
                    b8             
    >

    问题:定义了多个局部变量时,栈分配顺序是怎样的?
    局部变量栈分配的顺序是按照变量声明先后的顺序,同一行声明的变量是按照从左到右的顺序入栈的,在test2.c中,变量声明如下:
        int i, j=2, k=4;
    而反汇编的结果中:

        movl    $2,-8(%ebp)          ; j=2
        movl    $4,-0xc(%ebp)        ; k=4
        movl    $3,-4(%ebp)          ; i=3
    其中不难看出,i,j,k的栈中的位置如下图:

+----------------------------+------> 高地址
| EIP (_start函数的返回地址) |
+----------------------------+
| EBP (_start函数的EBP) | <------ main函数的EBP指针(即SFP框架指针)
+----------------------------+
| i (EBP-4) |
+----------------------------+
| j (EBP-8) |
+----------------------------+
| k (EBP-0xc) |
+----------------------------+------> 低地址

图 2-1

3. 小结

    这次通过几个试验程序,进一步了解了局部变量在栈中的分配和释放以及位置,并再次回顾了上篇文章中涉及到的以下概念:
        SFP 栈框架指针
        Stack aligned 栈对齐
    并且,利用Solaris提供的mdb工具,直观的观察到了栈在程序运行中的动态变化,以及Stack Frame的创建和撤销,根据给出的图例的内容( 图 2-1图 1-1),可以更清晰的了解IA32架构中栈在内存中的布局(Stack Layer)。


相关文档:
    X86 汇编语言学习手记(1)
    Solaris 上的开发环境安装及设置
    Linux AT&T 汇编语言开发指南
    ELF动态解析符号过程(修订版)
    关注: Solaris 10的10大新变化