zoukankan      html  css  js  c++  java
  • X86汇编语言学习手记(2)

    原文地址: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)。

  • 相关阅读:
    初认识AngularJS
    (imcomplete) UVa 10127 Ones
    UVa 10061 How many zero's and how many digits?
    UVa 11728 Alternate Task
    UVa 11490 Just Another Problem
    UVa 10673 Play with Floor and Ceil
    JSON对象和字符串的收发(JS客户端用typeof()进行判断非常重要)
    HTML.ActionLink 和 Url.Action 的区别
    EASYUI TREE得到当前节点数据的GETDATA方法
    jqueery easyui tree把已选中的节点数据拼成json或者数组(非常重要)
  • 原文地址:https://www.cnblogs.com/wangkangluo1/p/2155714.html
Copyright © 2011-2022 走看看