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


    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大新变化
  • 相关阅读:
    JavaScript Date 对象
    javascript Array类型 方法大全
    Flexbox
    CSS 去除浏览器默认 轮廓外框
    多行文本溢出显示省略号(…) text-overflow: ellipsis
    最全CSS3选择器
    何时使用 Em 与 Rem
    前端笔试面试题
    oracle中 lob类型
    MySQL 5.5.62 安装方法(标准配置版)
  • 原文地址:https://www.cnblogs.com/ainima/p/6330879.html
Copyright © 2020-2023  润新知