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

时间:2021-01-27 03:39:46

原文地址:http://blog.csdn.net/yayong/article/details/236653

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+0×10:      movl    $2,-8(%ebp)        ; 初始化局部变量j的值为2
    main+0×17:      movl    $3,-4(%ebp)        ; 给局部变量i赋值为3
    main+0×1e:      leal    -4(%ebp),%eax      ; 将局部变量i的地址装入到EAX寄存器中
    main+0×21:      incl    (%eax)             ; i++
    main+0×23:      movl    -8(%ebp),%eax      ; 将j的值装入EAX
    main+0×26:      addl    -4(%ebp),%eax      ; i+j并将结果存入EAX,作为返回值
    main+0×29:      leave                    ; 撤销Stack Frame 
    
main+0×2a:      ret                      ; main函数返回
    > 
    > main+0×10:b         ; 在
地址 main+0×10处设置断点
    > main+0×1e:b         ; 在
main+0×1e设置断点
    > main+0×29:b         ; main+0×1e设置断点
    > main+0×2a:b         ; main+0×1e设置断点
        
    下面的mdb的4个命令在一行输入,中间用分号间隔开,命令的含义在注释中给出:
    > :r;<esp,10/nap;<ebp=X;<eax=X    运行程序(:r 命令)
    mdb: stop at main+0×10               ; 以ESP寄存器为起始地址,指定格式输出16字节的栈内容(<esp,10/nap 命令)
    mdb: target stopped at:                ; 在最后输出EBP和EAX寄存器的值(<ebp=X 命令 和<eax=X 命令)
    main+0×10:      movl    $2,-8(%ebp)    ; 程序运行后在main +0×10处指令执行前中断,此时栈分配后还未初始化
    0×8047db0:      
    0×8047db0:      0xddbebca0             ; 这是变量j,4字节,未初始化,此处为栈顶,ESP的值就是0×8047db0   
    0×8047db4:      0xddbe137f             ; 这是变量i, 4字节,未初始化
    0×8047db8:      0×8047dd8              ; 这是_start的SFP(_start的EBP),4字节由main 的SFP指向它
    0×8047dbc:      _start+0×5d            ; 这是_start调用main之前压栈的下条指令地址,main返回后将恢复给EIP
    0×8047dc0:      1               
    0×8047dc4:      0×8047de4       
    0×8047dc8:      0×8047dec       
    0×8047dcc:      _start+0×35     
    0×8047dd0:      _fini           
    0×8047dd4:      ld.so.1`atexit_fini
    0×8047dd8:      0                      ; _start的SFP指向的内容为0,证明_start是程序的入口
    0×8047ddc:      0               
    0×8047de0:      1               
    0×8047de4:      0×8047eb4       
    0×8047de8:      0               
    0×8047dec:      0×8047eba       
                    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+0×1e
    mdb: target stopped at:
    main+0×1e:      leal    -4(%ebp),%eax  ; 程序运行到断点main+0×1e处停止,此时局部变量i,j赋值已完成
    0×8047db0:      
    0×8047db0:      2                      ; 这是变量j,4字节,值为2,此处为栈顶,ESP的值就是0×8047db0
    0×8047db4:      3                      ; 这是变量i,4字节,值为3 
    0×8047db8:      0×8047dd8              ; 这是_start的SFP,4字节
    0×8047dbc:      _start+0×5d            ; 这是返回_start后的EIP
    0×8047dc0:      1               
    0×8047dc4:      0×8047de4       
    0×8047dc8:      0×8047dec       
    0×8047dcc:      _start+0×35     
    0×8047dd0:      _fini           
    0×8047dd4:      ld.so.1`atexit_fini
    0×8047dd8:      0               
    0×8047ddc:      0               
    0×8047de0:      1               
    0×8047de4:      0×8047eb4       
    0×8047de8:      0               
    0×8047dec:      0×8047eba       
                    8047db8              ; 这是main当前EBP寄存器的值,即main的SFP
                    0                  ; EAX的值,当前为0
    > :c;<esp,10/nap;<ebp=X;<eax=X
    ; 继续运行程序,打印16字节栈和EBP,EAX内容
    mdb: stop at main+0×29
    mdb: target stopped at:
    main+0×29:      leave                  ; 运行到断点main+0×29处停止,计算已经完成,即将撤销Stack Frame
    0×8047db0:      
    0×8047db0:      2                      ; 这是变量j,4字节,值为2此处为栈顶,ESP的值就是0×8047db0       
    0×8047db4:      4                      ; 这是i++以后的变量i,4字节,值为3
    0×8047db8:      0×8047dd8              ; 这是_start的SFP,4字节
    0×8047dbc:      _start+0×5d            ; 这是返回_start后的EIP
    0×8047dc0:      1               
    0×8047dc4:      0×8047de4       
    0×8047dc8:      0×8047dec       
    0×8047dcc:      _start+0×35     
    0×8047dd0:      _fini           
    0×8047dd4:      ld.so.1`atexit_fini
    0×8047dd8:      0               
    0×8047ddc:      0               
    0×8047de0:      1               
    0×8047de4:      0×8047eb4       
    0×8047de8:      0               
    0×8047dec:      0×8047eba       
                    8047db8              ; 这是main当前EBP寄存器的值,即main的SFP        
                    6                  ; EAX的值,即函数的返回值,当前为6               
    > :c;<esp,10/nap;<ebp=X;<eax=X
    ; 继续运行程序,打印16字节栈和EBP,EAX内容
    mdb: stop at main+0×2a
    mdb: target stopped at:
    main+0×2a:      ret                  ; 运行到断点main+0×2a处停止,Stack Frame已被撤销,main即将返回
    0×8047dbc:      
    0×8047dbc:      _start+0×5d            ; Stack Frame已经被撤销,栈顶是返回_start后的EIP,main的栈已被释放
    0×8047dc0:      1               
    0×8047dc4:      0×8047de4       
    0×8047dc8:      0×8047dec       
    0×8047dcc:      _start+0×35     
    0×8047dd0:      _fini           
    0×8047dd4:      ld.so.1`atexit_fini
    0×8047dd8:      0               
    0×8047ddc:      0               
    0×8047de0:      1               
    0×8047de4:      0×8047eb4       
    0×8047de8:      0               
    0×8047dec:      0×8047eba       
    0×8047df0:      0×8047ed6       
    0×8047df4:      0×8047edd       
    0×8047df8:      0×8047ee4       
                    8047dd8            ; _start的SFP,之前存储在地址0×8047db8,main的Stack Frame撤销时恢复                            6                 ; EAX的值,即函数的返回值,当前为6               
    > :s;<esp,10/nap;<ebp=X;<eax=X
   ; 单步执行下条指令(:s 命令),打印16字节栈和EBP,EAX内容
    mdb: target stopped at:
    _start+0×5d:    addl    $0xc,%esp     ; 此时main已经返回,_start+0×5d曾经存储在地址0×8047dbc
    0×8047dc0:      
    0×8047dc0:      1                      main已经返回_start +0×5d已经被弹出
    0×8047dc4:      0×8047de4       
    0×8047dc8:      0×8047dec       
    0×8047dcc:      _start+0×35     
    0×8047dd0:      _fini           
    0×8047dd4:      ld.so.1`atexit_fini
    0×8047dd8:      0                      ; _start的SFP指向的内容为0,证明_start是程序的入口               
    0×8047ddc:      0               
    0×8047de0:      1               
    0×8047de4:      0×8047eb4       
    0×8047de8:      0               
    0×8047dec:      0×8047eba       
    0×8047df0:      0×8047ed6       
    0×8047df4:      0×8047edd       
    0×8047df8:      0×8047ee4       
    0×8047dfc:      0×8047ef3       
                    8047dd8            ; _start的SFP,之前存储在地址0×8047db8,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   $0×18,%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+0×10:          movl    $2,-8(%ebp)          ; j=2
    main+0×17:          movl    $4,-0xc(%ebp)        ; k=4
    main+0×1e:          movl    $3,-4(%ebp)          ; i=3
    main+0×25:          leal    -4(%ebp),%eax        ; 将i的地址装入到EAX
    main+0×28:          incl    (%eax)               ; i++
    main+0×2a:          movl    -8(%ebp),%eax        ; 将j的值装入到 EAX
    main+0×2d:          movl    -4(%ebp),%edx        ; 将i的值装入到 EDX
    main+0×30:          addl    %eax,%edx            ; j+i,结果存入EDX
    main+0×32:          leal    -0xc(%ebp),%eax      ; 将k的地址装入到EAX
    main+0×35:          addl    %edx,(%eax)          ; i+j+k,结果存入地址ebp-0xc即k中
    main+0×37:          movl    -0xc(%ebp),%eax      ; 将k的值装入EAX,作为返回值
    main+0×3a:          leave                        ; 撤销Stack Frame
    main+0×3b:          ret                          ; main函数返回
    > 
  

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

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

    如果,各定义一个50字节和100字节的字符数组,在这种情况下,实际分配多少栈空间呢?答案是0×8+0×40+0×70,即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+0×11:          subl    %eax,%esp
    main+0×13:          movl    $0,%eax
    main+0×18:          leave
    main+0×19:          ret
    > 0xb8=D                              ; 16进制换算10进制
                    184             
    > 0×40+0×70+0×8=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)。