X86汇编语言学习手记 -- 汇编和C协同

时间:2021-07-14 01:16:49

本文名字虽然是汇编语言学习手记, 但实际论述了业界真正的C编译器和链接器生成汇编语言的惯例, 这些知识点是熟悉汇编语言后使用汇编和C协同真正进行工作的基础.

在第三小节给出了一个ELF文件segment/section dump实例;

当然随着编译器的发展, ELF sections也有些变动, 如.rel.got=>.rel.dyn/.rel.plt; 在.got外,增加了.got.plt等. 当然不大部分还是可以做为概略描述参考的.

很久以前保存的专题文章, 因为工作需要温习ELF时, 查阅此文; 找到原文链接http://blog.csdn.net/yayong/article/category/42846, 转载在此.

 

X86汇编语言学习手记(1-3)

原文出处:http://blog.csdn.net/yayong
作者: Badcoffee
Email:
blog.oliver@gmail.com

版权所有:转载时请务必以超链接形式标明文章原始出处、作者信息及本声明
这是作者在学习X86汇编过程中的学习笔记,难免有错误和疏漏之处,欢迎指正。
作者将随时修改错误并将新的版本发布在自己的Blog站点上。
严格说来,本篇文档更侧重于C语言和C编译器方面的知识,如果涉及到具体汇编语言的内容,可以参考相关文档。


X86
汇编语言学习手记(1)
2004
10


1.
编译环境
   OS: Solaris 9 X86
   Compiler: gcc 3.3.2
   Linker: Solaris Link Editors 5.x
   Debug Tool: mdb
   Editor: vi

  
注:关于编译环境的安装和设置,可以参考文章:Solaris 上的开发环境安装及设置
       mdb
Solaris提供的kernel debug工具,这里用它做反汇编和汇编语言调试工具。
      
如果在Linux平台可以用gdb进行反汇编和调试。

2.
最简C代码分析
    为简化问题,来分析一下最简的c代码生成的汇编代码:
    # vi test1.c
     
    int main()
    {
        return 0;
    }  
   
   
编译该程序,产生二进制文件:
    # gcc test1.c -o test1
    # file test1  
    test1: ELF 32-bit LSB executable 80386 Version 1, dynamically linked, not stripped


    test1
是一个ELF格式32位小端(Little Endian)的可执行文件,动态链接并且符号表没有去除。
   
这正是Unix/Linux平台典型的可执行文件格式。
   
mdb反汇编可以观察生成的汇编代码:

    # mdb test1
    Loading modules: [ libc.so.1 ]
    > main::dis                       ;
反汇编main函数,mdb的命令一般格式为  <地址>::dis
    main:          pushl   %ebp       ; ebp
寄存器内容压栈,即保存main函数的上级调用函数的栈基地址
    main+1:        movl    %esp,%ebp  ; esp
值赋给ebp,设置main函数的栈基址
   
main+3:          subl    $8,%esp
    main+6:          andl    $0xf0,%esp
    main+9:          movl    $0,%eax
    main+0xe:        subl    %eax,%esp
    main+0x10:     movl    $0,%eax    ;
设置函数返回值0
    main+0x15:     leave;
ebp值赋给esppop先前栈内的上级函数栈的基地址给ebp,恢复原栈基址
    main+0x16:     ret                ; main
函数返回,回到上级调用
    >


   
注:这里得到的汇编语言语法格式与Intel的手册有很大不同,Unix/Linux采用AT&T汇编格式作为汇编语言的语法格式
   
如果想了解AT&T汇编可以参考文章:Linux AT&T 汇编语言开发指南

   
问题:谁调用了 main函数?
    
    
C语言的层面来看,main函数是一个程序的起始入口点,而实际上,ELF可执行文件的入口点并不是main而是_start
     mdb
也可以反汇编_start
      
    > _start::dis                       ;
_start的地址开始反汇编
    _start:              pushl   $0
    _start+2:            pushl   $0
    _start+4:            movl    %esp,%ebp
    _start+6:            pushl   %edx
    _start+7:            movl    $0x80504b0,%eax
    _start+0xc:          testl   %eax,%eax
    _start+0xe:          je      +0xf            <_start+0x1d>
    _start+0x10:         pushl   $0x80504b0
    _start+0x15:         call    -0x75           <atexit>
    _start+0x1a:         addl    $4,%esp
    _start+0x1d:         movl    $0x8060710,%eax
    _start+0x22:         testl   %eax,%eax
    _start+0x24:         je      +7              <_start+0x2b>
    _start+0x26:         call    -0x86           <atexit>
    _start+0x2b:         pushl   $0x80506cd
    _start+0x30:         call    -0x90           <atexit>
    _start+0x35:         movl    +8(%ebp),%eax
    _start+0x38:         leal    +0x10(%ebp,%eax,4),%edx
    _start+0x3c:         movl    %edx,0x8060804
    _start+0x42:         andl    $0xf0,%esp
    _start+0x45:         subl    $4,%esp
    _start+0x48:         pushl   %edx
    _start+0x49:         leal    +0xc(%ebp),%edx
    _start+0x4c:         pushl   %edx
    _start+0x4d:         pushl   %eax
    _start+0x4e:         call    +0x152          <_init>
    _start+0x53:         call    -0xa3           <__fpstart>
    _start+0x58:        call    +0xfb        <main>              ;
在这里调用了main函数
    _start+0x5d:         addl    $0xc,%esp
    _start+0x60:         pushl   %eax
    _start+0x61:         call    -0xa1           <exit>
    _start+0x66:         pushl   $0
    _start+0x68:         movl    $1,%eax
    _start+0x6d:         lcall   $7,$0
    _start+0x74:         hlt
    >

   
问题:为什么用EAX寄存器保存函数返回值?
   
实际上IA32并没有规定用哪个寄存器来保存返回值。但如果反汇编Solaris/Linux的二进制文件,就会发现,都用EAX保存函数返回值。
   
这不是偶然现象,是操作系统的ABI(Application Binary Interface)来决定的。
    Solaris/Linux
操作系统的ABI就是Sytem V ABI


   
概念SFP (Stack Frame Pointer)栈框架指针 

   
正确理解SFP必须了解:
        IA32
的栈的概念
        CPU
32位寄存器ESP/EBP的作用
        PUSH/POP
指令是如何影响栈的
        CALL/RET/LEAVE
等指令是如何影响栈的

   
如我们所知:
    1)IA32
的栈是用来存放临时数据,而且是LIFO,即后进先出的。栈的增长方向是从高地址向低地址增长,按字节为单位编址。
    2) EBP
是栈基址的指针,永远指向栈底(高地址),ESP是栈指针,永远指向栈顶(低地址)。
    3) PUSH
一个long型数据时,以字节为单位将数据压入栈,从高到低按字节依次将数据存入ESP-1ESP-2ESP-3ESP-4的地址单元。
    4) POP
一个long型数据,过程与PUSH相反,依次将ESP-4ESP-3ESP-2ESP-1从栈内弹出,放入一个32位寄存器。
    5) CALL
指令用来调用一个函数或过程,此时,下一条指令地址会被压入堆栈,以备返回时能恢复执行下条指令。
    6) RET
指令用来从一个函数或过程返回,之前CALL保存的下条指令地址会从栈内弹出到EIP寄存器中,程序转到CALL之前下条指令处执行
    7) ENTER
是建立当前函数的栈框架,即相当于以下两条指令:
        pushl   %ebp
        movl    %esp,%ebp

    8) LEAVE
是释放当前函数或者过程的栈框架,即相当于以下两条指令:
        movl ebp esp
        popl  ebp


   
如果反汇编一个函数,很多时候会在函数进入和返回处,发现有类似如下形式的汇编语句:
       
        pushl   %ebp            ; ebp寄存器内容压栈,即保存main函数的上级调用函数的栈基地址
        movl    %esp,%ebp       ; esp
值赋给ebp,设置 main函数的栈基址
        ...........             ;
以上两条指令相当于 enter 0,0

        ...........
        leave                   ;
ebp值赋给esppop先前栈内的上级函数栈的基地址给ebp,恢复原栈基址
        ret                     ; main
函数返回,回到上级调用

    这些语句就是用来创建和释放一个函数或者过程的栈框架的。
   
原来编译器会自动在函数入口和出口处插入创建和释放栈框架的语句。
   
函数被调用时:
    1) EIP/EBP
成为新函数栈的边界
   
函数被调用时,返回时的EIP首先被压入堆栈;创建栈框架时,上级函数栈的EBP被压入堆栈,与EIP一道行成新函数栈框架的边界
    2) EBP
成为栈框架指针SFP,用来指示新函数栈的边界
   
栈框架建立后,EBP指向的栈的内容就是上一级函数栈的EBP,可以想象,通过EBP就可以把层层调用函数的栈都回朔遍历一遍,调试器就是利用这个特性实现 backtrace功能的
    3) ESP
总是作为栈指针指向栈顶,用来分配栈空间
   
栈分配空间给函数局部变量时的语句通常就是给ESP减去一个常数值,例如,分配一个整型数据就是 ESP-4
    4)
函数的参数传递和局部变量访问可以通过SFPEBP来实现
    由于栈框架指针永远指向当前函数的栈基地址,参数和局部变量访问通常为如下形式:
        +8+xx(%ebp)         ;
函数入口参数的的访问
        -xx(%ebp)           ;函数局部变量访问
           
   
假如函数A调用函数B,函数B调用函数C,则函数栈框架及调用关系如下图所示:

       +-------------------------+----> 高地址
  
     | EIP (上级函数返回地址)   |
        +-------------------------+
 +-->   | EBP (上级函数的EBP)     | --+ <------当前函数AEBP (SFP框架指针)
 
|      +-------------------------+   +-->偏移量A
 
|      | Local Variables         |   |
 |      | ..........              | --+ <------ESP指向函数A新分配的局部变量,局部变量可以通过Aebp-偏移量A访问
 
| f    +-------------------------+
 | r    | Arg n(函数B的第n个参数)  |
 | a    +-------------------------+
 | m    | Arg .(函数B的第.个参数)  |
 | e    +-------------------------+
 |      | Arg 1(函数B的第1个参数)  |
 | o    +-------------------------+
 | f    | Arg 0(函数B的第0个参数)  | --+ <------ B函数的参数可以由Bebp+偏移量B访问
 
|      +-------------------------+   +--> 偏移量B
 
| A    | EIP (A函数的返回地址)    |   |
 |      +-------------------------+ --+
 +---   | EBP (A函数的EBP)        |<--+ <------ 当前函数BEBP (SFP框架指针)
  
     +-------------------------+   |
  
     | Local Variables         |   |
  
     | ..........              |   | <------ ESP指向函数B新分配的局部变量
  
     +-------------------------+   |
  
     | Arg n(函数C的第n个参数)  |   |
  
     +-------------------------+   |
  
     | Arg .(函数C的第.个参数)  |   |
  
     +-------------------------+   +--> frame of B
  
     | Arg 1(函数C的第1个参数)  |   |
  
     +-------------------------+   |
  
     | Arg 0(函数C的第0个参数)  |   |
  
     +-------------------------+   |
  
     | EIP (B函数的返回地址)    |   |
  
     +-------------------------+   |
 +-->
  | EBP (B函数的EBP)        | --+ <------ 当前函数CEBP (SFP框架指针)
 
|      +-------------------------+
 |      | Local Variables         |
 |      | ..........              | <------ ESP指向函数C新分配的局部变量
 
|      +-------------------------+----> 低地址
frame of C
       
               1-1

      
   
再分析test1反汇编结果中剩余部分语句的含义:
       
    # mdb test1
    Loading modules: [ libc.so.1 ]
    > main::dis                        ;
反汇编main函数
    main:          pushl   %ebp                           
    main+1:        movl    %esp,%ebp        ;
创建Stack Frame(栈框架)
    main+3:       subl    $8,%esp       ;通过ESP-8来分配8字节堆栈空间
    main+6:       andl    $0xf0,%esp    ;使栈地址16字节对齐
    main+9:       movl    $0,%eax       ;无意义
    main+0xe:     subl    %eax,%esp     ;无意义
    main+0x10:     movl    $0,%eax          ;
设置main函数返回值
    main+0x15:     leave                   ; 撤销Stack Frame(栈框架)
    main+0x16:     ret                      ; main
函数返回
    >

   
以下两句似乎是没有意义的,果真是这样吗?
        movl    $0,%eax
        subl     %eax,%esp
      
   
gccO2级优化来重新编译test1.c:
    # gcc -O2 test1.c -o test1
    # mdb test1
    > main::dis
    main:         pushl   %ebp
    main+1:       movl    %esp,%ebp
    main+3:       subl    $8,%esp
    main+6:       andl    $0xf0,%esp
    main+9:       xorl    %eax,%eax      ;
设置main返回值,使用xorl异或指令来使eax0
    main+0xb:     leave
    main+0xc:     ret

    >
   
新的反汇编结果比最初的结果要简洁一些,果然之前被认为无用的语句被优化掉了,进一步验证了之前的猜测。
   
提示:编译器产生的某些语句可能在程序实际语义上没有用处,可以用优化选项去掉这些语句。

    问题:为什么用xorl来设置eax的值?
    注意到优化后的代码中,eax返回值的设置由movl $0,%eax 变为 xorl %eax,%eax,这是因为IA32指令中,xorlmovl有更高的运行速度。

   
概念Stack aligned栈对齐
    那么,以下语句到底是和作用呢?
        subl    $8,%esp
       andl    $0xf0,%esp     ;
通过andl使低4位为0,保证栈地址16字节对齐
      
   
表面来看,这条语句最直接的后果是使ESP的地址后4位为0,即16字节对齐,那么为什么这么做呢?
   
原来,IA32系列CPU的一些指令分别在4816字节对齐时会有更快的运行速度,因此gcc编译器为提高生成代码在IA32上的运行速度,默认对产生的代码进行16字节对齐

        andl $0xf0,%esp的意义很明显,那么 subl $8,%esp 呢,是必须的吗?
   
这里假设在进入main函数之前,栈是16字节对齐的话,那么,进入main函数后,EIPEBP被压入堆栈后,栈地址最末4位二进制位必定是1000esp -8则恰好使后4位地址二进制位为0000。看来,这也是为保证栈16字节对齐的。

   
如果查一下gcc的手册,就会发现关于栈对齐的参数设置:
    -mpreferred-stack-boundary=n    ;
希望栈按照2n次的字节边界对齐, n的取值范围是2-12

    默认情况下,n是等于4的,也就是说,默认情况下,gcc16字节对齐,以适应IA32大多数指令的要求。

   
让我们利用-mpreferred-stack-boundary=2来去除栈对齐指令:
     
    # gcc -mpreferred-stack-boundary=2 test1.c -o test1
      
    > main::dis
    main:       pushl   %ebp
    main+1:     movl    %esp,%ebp
    main+3:     movl    $0,%eax
    main+8:     leave
    main+9:     ret
    >

   
可以看到,栈对齐指令没有了,因为,IA32的栈本身就是4字节对齐的,不需要用额外指令进行对齐。
   
那么,栈框架指针SFP是不是必须的呢?
    # gcc -mpreferred-stack-boundary=2 -fomit-frame-pointer test1.c -o test
    > main::dis
    main:       movl    $0,%eax
    main+5:     ret
    >

   
由此可知,-fomit-frame-pointer可以去除SFP
      
   
问题:去除SFP后有什么缺点呢?
      
    1)
增加调式难度
       
由于SFP在调试器backtrace的指令中被使用到,因此没有SFP该调试指令就无法使用。
    2)
降低汇编代码可读性
       
函数参数和局部变量的访问,在没有ebp的情况下,都只能通过+xx(esp)的方式访问,而很难区分两种方式,降低了程序的可读性。
      
   
问题:去除SFP有什么优点呢?
      
    1)
节省栈空间
    2)减少建立和撤销栈框架的指令后,简化了代码
    3)使ebp空闲出来,使之作为通用寄存器使用,增加通用寄存器的数量
    4)以上3点使得程序运行速度更快

    概念:Calling Convention 调用约定和 ABI (Application Binary Interface)应用程序二进制接口
        
        函数如何找到它的参数?
        函数如何返回结果?
        函数在哪里存放局部变量?
        那一个硬件寄存器是起始空间?
        那一个硬件寄存器必须预先保留?

    Calling Convention  调用约定对以上问题作出了规定。Calling Convention也是ABI的一部分。
    因此,遵守相同ABI规范的操作系统,使其相互间实现二进制代码的互操作成为了可能。
    例如:由于SolarisLinux都遵守System VABISolaris 10就提供了直接运行Linux二进制程序的功能。
   
详见文章:关注: Solaris 10的10大新变化
            
3.
小结
    本文通过最简的C程序,引入以下概念:
        SFP 栈框架指针
        Stack aligned 栈对齐
        Calling Convention  调用约定 ABI (Application Binary Interface)应用程序二进制接口
   
今后,将通过进一步的实验,来深入了解这些概念。通过掌握这些概念,使在汇编级调试程序产生的core dump、掌握C语言高级调试技巧成为了可能。

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

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设置断点
       
   
下面的mdb4个命令在一行输入,中间用分号间隔开,命令的含义在注释中给出:
    > :r;<esp,10/nap;<ebp=X;<eax=X    ;
运行程序(:r命令)
    mdb: stop at main+0x10;
ESP寄存器为起始地址,指定格式输出16字节的栈内容(<esp,10/nap命令)
    mdb: target stopped at:                ;
在最后输出EBPEAX寄存器的值(<ebp=X命令<eax=X命令)
    main+0x10:      movl    $2,-8(%ebp)   ;
main +0x10处指令执行前中断,此时栈分配后还未初始化
    0x8047db0:     
   
0x8047db0:      0xddbebca0;这是变量j4字节,未初始化,此处为栈顶,ESP的值就是0x8047db0 
    0x8047db4:      0xddbe137f             ;
这是变量i 4字节,未初始化
   
0x8047db8:      0x8047dd8         ;这是_startSFP(_startEBP),4字节mainSFP指向它
    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寄存器的值,即mainSFP
                   
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                     ; 这是变量j4字节,值为2,此处为栈顶,ESP的值就是0x8047db0
    0x8047db4:      3                      ;
这是变量i4字节,值为3
   
0x8047db8:      0x8047dd8             ; 这是_startSFP4字节
    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寄存器的值,即mainSFP
                   
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              ; 这是变量j4字节,值为2此处为栈顶,ESP的值就是0x8047db0      
    0x8047db4:      4                      ;
这是i++以后的变量i4字节,值为3
   
0x8047db8:      0x8047dd8             ; 这是_startSFP4字节
    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寄存器的值,即mainSFP       
                   
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后的EIPmain栈已释放
    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 ;_startSFP,之前存储在地址0x8047db8,mainStack 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                     ; _startSFP指向的内容为0,证明_start是程序的入口              
    0x8047ddc:      0              
    0x8047de0:      1              
    0x8047de4:      0x8047eb4      
    0x8047de8:      0              
    0x8047dec:      0x8047eba      
    0x8047df0:      0x8047ed6      
    0x8047df4:      0x8047edd      
    0x8047df8:      0x8047ee4      
    0x8047dfc:      0x8047ef3      
                   
8047dd8  ; _startSFP,之前存储在地址0x8047db8mainStack 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语句除了分配栈空间外,还有一个作用就是栈对齐。那么本例中,由于ij正好是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-0xck
    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汇编语言学习手记(3)
2004
12


   
X86汇编语言学习手记(1)(2)中,可以看到栈(Stack)作为进程执行过程中数据的临时存储区域,通常包含如下几类数据:
       
局部变量
       
函数调用的返回地址
       
函数调用的入口参数
        SFP
栈框架指针 (可以通过编译器优化选项去除)
   
本章中,将继续通过实验,了解全局变量和静态变量在进程中是如何存储和分配的。

注:不同的Calling Convention对入口参数的规定是有一定差别的,函数调用入口参数也有可能通过寄存器来传递。
   
例如IBMPower PCAMDOpteron,函数的入口参数全部或部分就是通过寄存器来传递的。

1.
全局变量和全局常量的实验

   
延续之前的方式,给出一个简单的C程序,其中声明的全局变量分为3种:
       
初始化过的全局变量
       
未初始化的全局变量      
       
全局常量

    #vi test5.c

    int i=1;
    int j=2;
    int k=3;
    int l,m;
    int n;
    const int o=7;
    const int p=8;
    const int q=9;
    int main()
    {
        l=4;
        m=5;
        n=6;

        return i+j+k+l+m+n+o+p+q;
    }

    # gcc test5.c -o test5
    # mdb test5
    Loading modules: [ libc.so.1 ]
    > main::dis
    main:              pushl   %ebp             ; main
main+1,创建Stack Frame
    main+1:            movl    %esp,%ebp
    main+3:            subl    $8,%esp
    main+6:            andl    $0xf0,%esp
    main+9:            movl    $0,%eax
    main+0xe:          subl    %eax,%esp ; main+(3-0xe),
为局部变量预留栈空间,并保证栈16字节对齐
    main+0x10:         movl    $4,0x8060948       ; l=4
    main+0x1a:         movl    $5,0x806094c       ; m=5
    main+0x24:         movl    $6,0x8060950       ; n=6
    main+0x2e:         movl    0x8060908,%eax
    main+0x33:         addl    0x8060904,%eax
    main+0x39:         addl    0x806090c,%eax
    main+0x3f:         addl    0x8060948,%eax
    main+0x45:         addl    0x806094c,%eax
    main+0x4b:         addl    0x8060950,%eax
    main+0x51:         addl    0x8050808,%eax
    main+0x57:         addl    0x805080c,%eax
    main+0x5d:         addl    0x8050810,%eax     ; main+0x2e
main+0x5di+j+k+l+m+n+o+p+q
    main+0x63:         leave                      ;
撤销Stack Frame
    main+0x64:         ret                        ; main
函数返回  

   
现在,让我们在全局变量初始化后的地方设置断点,观察一下这几个全局变量的值:
    > main+0x2e:b                                 ;
设置断点
    > :r                                          ;
运行程序
    mdb: stop at main+0x2e
    mdb: target stopped at:
    main+0x2e:      movl    0x8060908,%eax
    > 0x8060904,03/nap                            ;
察看全局变量 i,j,k的值
    test5`i:
    test5`i:       
    test5`i:        1              
    test5`j:        2              
    test5`k:        3              
    > 0x8060948,03/nap                            ;
察看全局变量l,m,n的值
    test5`l:
    test5`l:       
    test5`l:        4              
    test5`m:        5              
    test5`n:        6              
    > 0x8050808,03/nap                            ;
察看全局变量o,p,q的值
    o:
    o:             
    o:              7              
    p:              8              
    q:              9              
    >
   
   
概念:进程地址空间 Process Address Space

       +----------------------+ ----> 0xFFFFFFFF (4GB)
       |                                   |
       |   Kernel Space           |
       |                                   |
       +----------------------+ ----> _kernel_base (0xE0000000)
       |                                   |
       |   Other Library           |
       :                                   :
       :                                   :
       |                                   |
       +----------------------+
       |     data section            |
       |   Lib C Library           |
       |     text section             |
       :                                   :
       :                                   :
       +----------------------+
       |                                   |
       |                                   |
       :                                   :
       :       grow up                :
       :                                   :
       |    User Heap               |
       |                                   |
       +----------------------+
       |      bss                        |
       |                                   |
       |    User Data                |
       |                                   |
       +----------------------+     
       |                                   |
       |    User Text                |
       |                                   |
       |                                   |
       +----------------------+ ----> 0x08050000
       |                                   |
       |    User Stack              |
       |                                   |
       :     grow down             :
       :                                   :
       :                                   :
       |                                   |
       |                                   |
       +----------------------+ ----> 0

      
3-1 SolarisIA32上的进程地址空间

如图3-1所示,SolarisIA32上的进程地址空间和Linux是相似的,在用户进程的4GB地址空间内:

    Kernel
总是映射到用户地址空间的最高端,从宏定义_kernel_base0xFFFFFFFF的区域
   
用户进程所依赖的各个共享库紧接着Kernel映射在用户地址空间的高端
   
最后是用户进程地址空间在地址空间的低端
   
   
各共享库的代码段,存放着二进制可执行的机器指令,是由kernel把该库ELF文件的代码段map到虚存空间,属性是read/exec/share
   
各共享库的数据段,存放着程序执行所需的全局变量,是由kernelELF文件的数据段map到虚存空间,属性为read/write/private
   
用户代码段,存放着二进制形式的可执行的机器指令,是由kernelELF文件的代码段map到虚存空间,属性为read/exec
   
用户代码段之上是数据段,存放着程序执行所需的全局变量,是由kernelELF文件的数据段map到虚存空间,属性为 read/write/private
   
用户代码段之下是栈(stack),作为进程的临时数据区,是由kernel把匿名内存map到虚存空间,属性为read/write/exec
   
用户数据段之上是堆(heap),当且仅当malloc调用时存在,是由kernel把匿名内存map到虚存空间,属性为read/write/exec
   
   
注意StackHeap的区别和联系:
   
相同点:
       1.
都是来自于kernel分配的匿名内存,和磁盘上的ELF文件无关
       2.
属性均为read/write/exec
   
不同点:
       1.
栈的分配在C语言层面一般是通过声明局部变量,调用函数引起的;堆的分配则是通过显式的调用(malloc)引起的
       2.
栈的释放在C语言层面是对用户透明的,用户不需要关心,由C编译器产生的相应的指令代劳;堆则需显式的调用(free)来释放
       3.
栈空间的增长方向是从高地址到低地址;堆空间的增长方向是由低地址到高地址
       4.
栈存在于任何进程的地址空间;堆则在程序中没有调用malloc的情况下不存在

   
用户地址空间的布局随着CPUOS的不同,略有差异,以上都是基于X86 CPUSolaris OS上的情况的讨论。
   
   
使用pmap命令,可以观察到系统中的指定进程的地址空间分布情况,下面就是用pmap观察bash进程的一个例子:
   
    # pmap 1030
    1030:   -bash
    08045000      12K rw---    [ stack ]                        ; bash
的栈
    08050000     444K r-x--  /usr/bin/bash                      ; bash
文本段
    080CE000      72K rwx--  /usr/bin/bash                      ; bash
的数据段
    080E0000     156K rwx--    [ heap ]                         ; bash
的堆
    DD8C0000       8K r-x--  /usr/lib/locale/zh_CN.GB18030/methods_zh_CN.GB18030.so.2; so
的文本段
    DD8D1000       4K rwx--  /usr/lib/locale/zh_CN.GB18030/methods_zh_CN.GB18030.so.2 ; so
的数据段
    DD8E0000     324K r-x--  /usr/lib/locale/zh_CN.GB18030/zh_CN.GB18030.so.2
    DD940000       8K rwx--  /usr/lib/locale/zh_CN.GB18030/zh_CN.GB18030.so.2
    DD950000       4K rwx--    [ anon ]                         ;
匿名内存,由映射/dev/zero设备来创建的
    DD960000      12K r-x--  /usr/lib/libmp.so.2
    DD973000       4K rwx--  /usr/lib/libmp.so.2
    DD980000     628K r-x--  /usr/lib/libc.so.1
    DDA2D000      24K rwx--  /usr/lib/libc.so.1
    DDA33000       4K rwx--  /usr/lib/libc.so.1
    DDA50000       4K rwx--    [ anon ]
    DDA60000     548K r-x--  /usr/lib/libnsl.so.1
    DDAF9000      20K rwx--  /usr/lib/libnsl.so.1
    DDAFE000      32K rwx--  /usr/lib/libnsl.so.1
    DDB10000      44K r-x--  /usr/lib/libsocket.so.1
    DDB2B000       4K rwx--  /usr/lib/libsocket.so.1
    DDB30000     152K r-x--  /usr/lib/libcurses.so.1
    DDB66000      28K rwx--  /usr/lib/libcurses.so.1
    DDB6D000       8K rwx--  /usr/lib/libcurses.so.1
    DDB80000       4K r-x--  /usr/lib/libdl.so.1
    DDB90000     292K r-x--  /usr/lib/ld.so.1
    DDBE9000      16K rwx--  /usr/lib/ld.so.1
    DDBED000       8K rwx--  /usr/lib/ld.so.1
    total      2864K

   
问题:全局变量和全局常量在进程地址空间的位置?

   
显然,根据前面的叙述,全局变量在用户的数据段,那么全局常量呢,是数据段吗?
   
同样的,可以利用mdbtest5进程挂起,然后用pmap命令求证一下:  
    # mdb test5        
    Loading modules: [ libc.so.1 ]
    > ::sysbp _exit         ;
在系统调用_exit处设置断点
    > :r                    ;
运行程序
    mdb: stop on entry to _exit
    mdb: target stopped at:
    libc.so.1`exit+0x2b:    jae     +0x15           <libc.so.1`exit+0x40>
    >

   
此时,程序运行后在_exit处挂起,可以利用pmap在另一个终端内查看test5进程的地址空间了:
    # ps -ef | grep test5
        root  1387  1386  0 02:23:53 pts/1    0:00 test5
        root  1399  1390  0 02:25:03 pts/3    0:00 grep test5
        root  1386  1338  0 02:23:41 pts/1    0:00 mdb test5
    # pmap -F 1387       ;
pmap强制查看
    1387:   test5
    08044000      16K rwx--    [ stack ]         ; test5
stack
   
08050000       4K r-x--  /export/home/asm/L3/test5        ; test5的代码段,起始地址为0x08050000
   
08060000       4K rwx--  /export/home/asm/L3/test5        ; test5的数据段,起始地址为0x08060000
    DDAC0000     628K r-x--  /usr/lib/libc.so.1
    DDB6D000      24K rwx--  /usr/lib/libc.so.1
    DDB73000       4K rwx--  /usr/lib/libc.so.1
    DDB80000       4K r-x--  /usr/lib/libdl.so.1
    DDB90000     292K r-x--  /usr/lib/ld.so.1
    DDBE9000      16K rwx--  /usr/lib/ld.so.1
    DDBED000       8K rwx--  /usr/lib/ld.so.1
     total      1000K


   
可以看到,由于test5程序没有使用malloc来申请内存,所以没有heap的映射
   
前面用mdb观察过这些全局变量和常量的初始化值,它们的地址分别是:
   
全局变量i,j,k
        0x8060904
起始的12字节            
   
全局变量l,m,n
        0x8060948
起始的12字节
   
全局常量o,p,q      
        0x8050808
起始的12字节

  
   
显然,根据这些变量的地址,我们可以初步判断出这些变量属于哪个段:
   
由于test5数据段起始地址为0x08060000,我们得出结论:全局变量i,j,k,l,m,n属于数据段
   
test5代码段的起始地址为0x08050000,我们得出结论:全局常量o,p,q属于代码段

   
得出这个结论的确有点让人意外:全局常量竟然在代码段。
   
却又似乎在情理之中:数据段内存映射后的属性是r/w/x,而常量要求是只读属性,所以在代码段(r-x)就合情合理了。

   
问题:为什么这些全局变量地址不是连续的?
   
   
很容易注意到,全局变量i,j,kl,m,n以及全局常量o,p,q是连续声明的,但地址实际上并不连续,而是在3段连续12字节的地址上。
   
当然,全局常量属于代码段,所以地址和全局变量是分开的;那么,为什么全局变量也并非连续呢?
   
前面谈到数据段实际上是从ELF格式的二进制文件映射到进程的地址空间的,就通过分析ELF文件格式来寻找答案吧:

    # file test5
        test5:          ELF 32-bit LSB executable 80386 Version 1, dynamically linked, not stripped
    # elfdump test5
   
    ELF Header                       ; ELF
头信息共52(0x34)字节,具体意义可以参考ELF format的相关文档
    ei_magic:   { 0x7f, E, L, F }         ; ELF
的幻数
    ei_class:   ELFCLASS32          ei_data:      ELFDATA2LSB   ; 32
位的ELF文件,小端(LSB)编码
    e_machine:  EM_386              e_version:    EV_CURRENT    ; Intel 80386
版本1
    e_type:     ET_EXEC                                         ;
可执行文件
    e_flags:                     0
    e_entry:             0x8050600  e_ehsize:     52  e_shstrndx:   27   ;
程序入口点_start的地址0x8050600
    e_shoff:                0x1584  e_shentsize:  40  e_shnum:      29   ; Section header table
的大小是29*40
    e_phoff:                  0x34  e_phentsize:  32  e_phnum:       5

    Program Header[0]:                    ;
描述Program header table本身在内存中如何映射
        p_vaddr:     
0x8050034       p_flags:    [ PF_X  PF_R ]
        p_paddr:      0               p_type:     [ PT_PHDR ]
        p_filesz:     0xa0            p_memsz:    0xa0
        p_offset:     0x34            p_align:    0

    Program Header[1]:                    ;
描述程序装载器的路径名(.interp section)存放在文件的位置
        p_vaddr:      0               p_flags:    [ PF_R ]
        p_paddr:      0               p_type:     [ PT_INTERP ]
        p_filesz:     0x11            p_memsz:    0
        p_offset:     0xd4            p_align:    0

    Program Header[2]:                    ;
描述代码段在内存中如何映射,起始地址0x8050000,大小为 0x814
        p_vaddr:     
0x8050000       p_flags:    [ PF_X  PF_R ]
        p_paddr:      0               p_type:     [ PT_LOAD ]
        p_filesz:     0x814           p_memsz:    0x814
        p_offset:     0               p_align:    0x10000

    Program Header[3]:                    ;
描述数据段在内存中如何映射,起始地址0x8060814,大小为0x144
        p_vaddr:     
0x8060814       p_flags:    [ PF_X  PF_W  PF_R ]
        p_paddr:      0               p_type:     [ PT_LOAD ]
        p_filesz:     0x118           p_memsz:    0x144
        p_offset:     0x814           p_align:    0x10000

    Program Header[4]:                   
描述动态链接信息(.dynamic section)在内存中如何映射
        p_vaddr:     
0x8060848       p_flags:    [ PF_X  PF_W  PF_R ]
        p_paddr:      0               p_type:     [ PT_DYNAMIC ]
        p_filesz:     0xb8            p_memsz:    0
        p_offset:     0x848           p_align:    0

    Section Header[1]:  sh_name: .interp  ;
section保存了程序的解释程序(interpreter)的路径
        sh_addr:     
0x80500d4       sh_flags:   [ SHF_ALLOC ]
        sh_size:      0x11            sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0xd4            sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x1          

    Section Header[2]:  sh_name: .hash    ;
section保存着一个符号的哈希表
        sh_addr:     
0x80500e8       sh_flags:   [ SHF_ALLOC ]
        sh_size:      0x104           sh_type:    [ SHT_HASH ]
        sh_offset:    0xe8            sh_entsize: 0x4
        sh_link:      3               sh_info:    0
        sh_addralign: 0x4          

    Section Header[3]:  sh_name: .dynsym   ;
section保存着动态符号表 
        sh_addr:     
0x80501ec       sh_flags:   [ SHF_ALLOC ]
        sh_size:      0x200           sh_type:    [ SHT_DYNSYM ]
        sh_offset:    0x1ec           sh_entsize: 0x10
        sh_link:      4               sh_info:    1
        sh_addralign: 0x4          

    Section Header[4]:  sh_name: .dynstr   ;
section保存着动态连接时需要的字符串
        sh_addr:     
0x80503ec       sh_flags:   [ SHF_ALLOC  SHF_STRINGS ]
        sh_size:      0x11a           sh_type:    [ SHT_STRTAB ]
        sh_offset:    0x3ec           sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x1          

    Section Header[5]:  sh_name: .SUNW_version   ;
sectionSUN扩展的,保存版本信息
        sh_addr:     
0x8050508       sh_flags:   [ SHF_ALLOC ]
        sh_size:      0x20            sh_type:    [ SHT_SUNW_verneed ]
        sh_offset:    0x508           sh_entsize: 0
        sh_link:      4               sh_info:    1
        sh_addralign: 0x4          

    Section Header[6]:  sh_name: .rel.got   ;
section保存着.got section中部分符号的重定位信息
        sh_addr:     
0x8050528       sh_flags:   [ SHF_ALLOC  SHF_INFO_LINK ]
        sh_size:      0x18            sh_type:    [ SHT_REL ]
        sh_offset:    0x528           sh_entsize: 0x8
        sh_link:      3               sh_info:    14
        sh_addralign: 0x4          

    Section Header[7]:  sh_name: .rel.bss   ;
section保存着.bss section中部分符号的重定位信息
        sh_addr:     
0x8050540       sh_flags:   [ SHF_ALLOC  SHF_INFO_LINK ]
        sh_size:      0x8             sh_type:    [ SHT_REL ]
        sh_offset:    0x540           sh_entsize: 0x8
        sh_link:      3               sh_info:    22
        sh_addralign: 0x4          

    Section Header[8]:  sh_name: .rel.plt   ;
section保存着.plt section中部分符号的重定位信息
        sh_addr:     
0x8050548       sh_flags:   [ SHF_ALLOC  SHF_INFO_LINK ]
        sh_size:      0x38            sh_type:    [ SHT_REL ]
        sh_offset:    0x548           sh_entsize: 0x8
        sh_link:      3               sh_info:    9
        sh_addralign: 0x4          

    Section Header[9]:  sh_name: .plt   ;
section保存着过程连接表(Procedure Linkage Table
        sh_addr:     
0x8050580       sh_flags:   [ SHF_ALLOC  SHF_EXECINSTR ]
        sh_size:      0x80            sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0x580           sh_entsize: 0x10
        sh_link:      0               sh_info:    0
        sh_addralign: 0x4          

    Section Header[10]:  sh_name: .text   ;
section保存着程序的正文部分,即可执行指令
        sh_addr:     
0x8050600       sh_flags:   [ SHF_ALLOC  SHF_EXECINSTR ]
        sh_size:      0x1ec           sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0x600           sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x4          

    Section Header[11]:  sh_name: .init    ;
section保存着可执行指令,它构成了进程的初始化代码
        sh_addr:     
0x80507ec       sh_flags:   [ SHF_ALLOC  SHF_EXECINSTR ]
        sh_size:      0xd             sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0x7ec           sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x1          

    Section Header[12]:  sh_name: .fini    ;
section保存着可执行指令,它构成了进程的终止代码
        sh_addr:     
0x80507f9       sh_flags:   [ SHF_ALLOC  SHF_EXECINSTR ]
        sh_size:      0x8             sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0x7f9           sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x1          

    Section Header[13]:  sh_name: .rodata   ;
section保存着只读数据
        sh_addr:     
0x8050804       sh_flags:   [ SHF_ALLOC ]
        sh_size:      0x10            sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0x804           sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x4          

    Section Header[14]:  sh_name: .got   ;
section保存着全局的偏移量表
        sh_addr:     
0x8060814       sh_flags:   [ SHF_WRITE  SHF_ALLOC ]
        sh_size:      0x34            sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0x814           sh_entsize: 0x4
        sh_link:      0               sh_info:    0
        sh_addralign: 0x4          

    Section Header[15]:  sh_name: .dynamic   ;
section保存着动态连接的信息
        sh_addr:     
0x8060848       sh_flags:   [ SHF_WRITE  SHF_ALLOC ]
        sh_size:      0xb8            sh_type:    [ SHT_DYNAMIC ]
        sh_offset:    0x848           sh_entsize: 0x8
        sh_link:      4               sh_info:    0
        sh_addralign: 0x4          

    Section Header[16]:  sh_name: .data      ;
sections保存着初始化了的数据
        sh_addr:     
0x8060900       sh_flags:   [ SHF_WRITE  SHF_ALLOC ]
        sh_size:      0x10            sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0x900           sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x4          

    Section Header[17]:  sh_name: .ctors
        sh_addr:     
0x8060910       sh_flags:   [ SHF_WRITE  SHF_ALLOC ]
        sh_size:      0x8             sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0x910           sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x4          

    Section Header[18]:  sh_name: .dtors
        sh_addr:     
0x8060918       sh_flags:   [ SHF_WRITE  SHF_ALLOC ]
        sh_size:      0x8             sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0x918           sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x4          

    Section Header[19]:  sh_name: .eh_frame
        sh_addr:     
0x8060920       sh_flags:   [ SHF_WRITE  SHF_ALLOC ]
        sh_size:      0x4             sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0x920           sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x4          

    Section Header[20]:  sh_name: .jcr
        sh_addr:     
0x8060924       sh_flags:   [ SHF_WRITE  SHF_ALLOC ]
        sh_size:      0x4             sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0x924           sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x4          

    Section Header[21]:  sh_name: .data.rel.local
        sh_addr:     
0x8060928       sh_flags:   [ SHF_WRITE  SHF_ALLOC ]
        sh_size:      0x4             sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0x928           sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x4          

    Section Header[22]:  sh_name: .bss   ;
sectiopn保存着未初始化的数据
        sh_addr:     
0x806092c       sh_flags:   [ SHF_WRITE  SHF_ALLOC ]
        sh_size:      0x2c            sh_type:    [ SHT_NOBITS ]   ;
指示不占据ELF空间sh_size是内存大小
        sh_offset:    0x92c           sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x4          

    Section Header[23]:  sh_name: .symtab   ;
section保存着一个符号表
        sh_addr:      0               sh_flags:   0
        sh_size:      0x540           sh_type:    [ SHT_SYMTAB ]
        sh_offset:    0x92c           sh_entsize: 0x10
        sh_link:      24              sh_info:    53
        sh_addralign: 0x4          

    Section Header[24]:  sh_name: .strtab   ;
section保存着字符串表
        sh_addr:      0               sh_flags:   [ SHF_STRINGS ]
        sh_size:      0x20b           sh_type:    [ SHT_STRTAB ]
        sh_offset:    0xe6c           sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x1          

    Section Header[25]:  sh_name: .comment   ;
section保存着版本控制信息
        sh_addr:      0               sh_flags:   0
        sh_size:      0x24d           sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0x1077          sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x1          

    Section Header[26]:  sh_name: .stab.index
        sh_addr:      0               sh_flags:   0
        sh_size:      0x24            sh_type:    [ SHT_PROGBITS ]
        sh_offset:    0x12c4          sh_entsize: 0xc
        sh_link:      0               sh_info:    0
        sh_addralign: 0x4          

    Section Header[27]:  sh_name: .shstrtab   ;
section保存着section名称
        sh_addr:      0               sh_flags:   [ SHF_STRINGS ]
        sh_size:      0xdc            sh_type:    [ SHT_STRTAB ]
        sh_offset:    0x12e8          sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x1          

    Section Header[28]:  sh_name: .stab.indexstr
        sh_addr:      0               sh_flags:   0
        sh_size:      0x1c0           sh_type:    [ SHT_STRTAB ]
        sh_offset:    0x13c4          sh_entsize: 0
        sh_link:      0               sh_info:    0
        sh_addralign: 0x1          

    Interpreter:
        /usr/lib/ld.so.1

    Version Needed Section:  .SUNW_version
            file                        version
            libc.so.1                   SYSVABI_1.3         

    Symbol Table:  .dynsym                     ;
动态解析和链接所需的符号表
     index    value       size     type bind oth ver shndx       name
       [0]  0x00000000 0x00000000  NOTY LOCL  D    0 UNDEF      
       [1]  0x080507ec 0x0000000d  FUNC GLOB  D    0 .init       _init
       [2]  0x08050804 0x00000004  OBJT GLOB  D    0 .rodata     _lib_version
       [3]  0x08050580 0x00000000  OBJT GLOB  D    0 .plt        _PROCEDURE_LINKAGE_TABLE_
       [4]  0x08050600 0x00000075  FUNC GLOB  D    0 .text       _start
       [5]  0x08060900 0x00000000  OBJT GLOB  D    0 .data       __dso_handle
       [6]  0x08060848 0x00000000  OBJT GLOB  D    0 .dynamic    _DYNAMIC
       [7]  0x00000000 0x00000000  NOTY WEAK  D    0 UNDEF       __deregister_frame_info_bases
       [8]  0x08050814 0x00000000  OBJT GLOB  D    0 .rodata     _etext
       [9]  0x08060958 0x00000000  OBJT GLOB  D    0 .bss        _end
      [10]  0x08050590 0x00000000  FUNC WEAK  D    0 UNDEF       _cleanup
      [11]  0x08050675 0x00000001  FUNC WEAK  D    0 .text       _mcount
      [12]  0x08060904 0x00000004  OBJT GLOB  D    0 .data       i
      [13]  0x08060908 0x00000004  OBJT GLOB  D    0 .data       j
      [14]  0x0806090c 0x00000004  OBJT GLOB  D    0 .data       k

     
[15]  0x08060948 0x00000004  OBJT GLOB  D    0 .bss        l
      [16]  0x08060954 0x00000004  OBJT GLOB  D    0 .bss        _environ
      [17]  0x08060814 0x00000000  OBJT GLOB  D    0 .got        _GLOBAL_OFFSET_TABLE_
     
[18]  0x0806094c 0x00000004  OBJT GLOB  D    0 .bss        m
      [19]  0x0806092c 0x00000000  OBJT GLOB  D    0 .data.rel.l _edata
      [20]  0x08060954 0x00000004  OBJT WEAK  D    0 .bss        environ
      [21]  0x080507f9 0x00000008  FUNC GLOB  D    0 .fini       _fini
      [22]  0x080505a0 0x00000000  FUNC GLOB  D    0 UNDEF       atexit
     
[23]  0x08060950 0x00000004  OBJT GLOB  D    0 .bss        n
      [24]  0x08050808 0x00000004  OBJT GLOB  D    0 .rodata     o
      [25]  0x0805080c 0x00000004  OBJT GLOB  D    0 .rodata     p

      [26]  0x00000000 0x00000000  NOTY WEAK  D    0 UNDEF       _Jv_RegisterClasses
     
[27]  0x08050810 0x00000004  OBJT GLOB  D    0 .rodata     q
      [28]  0x080505b0 0x00000000  FUNC GLOB  D    0 UNDEF       __fpstart
      [29]  0x08050753 0x00000065  FUNC GLOB  D    0 .text       main
      [30]  0x00000000 0x00000000  NOTY WEAK  D    0 UNDEF       __register_frame_info_bases
      [31]  0x080505c0 0x00000000  FUNC GLOB  D    0 UNDEF       exit

    Symbol Table:  .symtab                     ;
程序链接所需的符号表
     index    value       size     type bind oth ver shndx       name
       [0]  0x00000000 0x00000000  NOTY LOCL  D    0 UNDEF      
       [1]  0x00000000 0x00000000  FILE LOCL  D    0 ABS         test5
       [2]  0x080500d4 0x00000000  SECT LOCL  D    0 .interp    
       [3]  0x080500e8 0x00000000  SECT LOCL  D    0 .hash      
       [4]  0x080501ec 0x00000000  SECT LOCL  D    0 .dynsym    
       [5]  0x080503ec 0x00000000  SECT LOCL  D    0 .dynstr    
       [6]  0x08050508 0x00000000  SECT LOCL  D    0 .SUNW_versi
       [7]  0x08050528 0x00000000  SECT LOCL  D    0 .rel.got   
       [8]  0x08050540 0x00000000  SECT LOCL  D    0 .rel.bss   
       [9]  0x08050548 0x00000000  SECT LOCL  D    0 .rel.plt   
      [10]  0x08050580 0x00000000  SECT LOCL  D    0 .plt       
      [11]  0x08050600 0x00000000  SECT LOCL  D    0 .text      
      [12]  0x080507ec 0x00000000  SECT LOCL  D    0 .init      
      [13]  0x080507f9 0x00000000  SECT LOCL  D    0 .fini      
      [14]  0x08050804 0x00000000  SECT LOCL  D    0 .rodata    
      [15]  0x08060814 0x00000000  SECT LOCL  D    0 .got       
      [16]  0x08060848 0x00000000  SECT LOCL  D    0 .dynamic   
      [17]  0x08060900 0x00000000  SECT LOCL  D    0 .data      
      [18]  0x08060910 0x00000000  SECT LOCL  D    0 .ctors     
      [19]  0x08060918 0x00000000  SECT LOCL  D    0 .dtors     
      [20]  0x08060920 0x00000000  SECT LOCL  D    0 .eh_frame  
      [21]  0x08060924 0x00000000  SECT LOCL  D    0 .jcr       
      [22]  0x08060928 0x00000000  SECT LOCL  D    0 .data.rel.l
      [23]  0x0806092c 0x00000000  SECT LOCL  D    0 .bss       
      [24]  0x00000000 0x00000000  SECT LOCL  D    0 .symtab    
      [25]  0x00000000 0x00000000  SECT LOCL  D    0 .strtab    
      [26]  0x00000000 0x00000000  SECT LOCL  D    0 .comment   
      [27]  0x00000000 0x00000000  SECT LOCL  D    0 .stab.index
      [28]  0x00000000 0x00000000  SECT LOCL  D    0 .shstrtab  
      [29]  0x00000000 0x00000000  SECT LOCL  D    0 .stab.index
      [30]  0x08050000 0x00000000  OBJT LOCL  D    0 .interp     _START_
      [31]  0x08060958 0x00000000  OBJT LOCL  D    0 .bss        _END_
      [32]  0x00000000 0x00000000  FILE LOCL  D    0 ABS         crt1.s
      [33]  0x00000000 0x00000000  FILE LOCL  D    0 ABS         crti.s
      [34]  0x00000000 0x00000000  FILE LOCL  D    0 ABS         values-Xa.c
      [35]  0x00000000 0x00000000  FILE LOCL  D    0 ABS         crtstuff.c
      [36]  0x08060910 0x00000000  OBJT LOCL  D    0 .ctors      __CTOR_LIST__
      [37]  0x08060918 0x00000000  OBJT LOCL  D    0 .dtors      __DTOR_LIST__
      [38]  0x08060920 0x00000000  OBJT LOCL  D    0 .eh_frame   __EH_FRAME_BEGIN__
      [39]  0x08060924 0x00000000  OBJT LOCL  D    0 .jcr        __JCR_LIST__
      [40]  0x08060928 0x00000000  OBJT LOCL  D    0 .data.rel.l p.0
      [41]  0x0806092c 0x00000001  OBJT LOCL  D    0 .bss        completed.1
      [42]  0x08050678 0x00000000  FUNC LOCL  D    0 .text       __do_global_dtors_aux
      [43]  0x08060930 0x00000018  OBJT LOCL  D    0 .bss        object.2
      [44]  0x080506e4 0x00000000  FUNC LOCL  D    0 .text       frame_dummy
      [45]  0x00000000 0x00000000  FILE LOCL  D    0 ABS         test5.c
      [46]  0x00000000 0x00000000  FILE LOCL  D    0 ABS         crtstuff.c
      [47]  0x08060914 0x00000000  OBJT LOCL  D    0 .ctors      __CTOR_END__
      [48]  0x0806091c 0x00000000  OBJT LOCL  D    0 .dtors      __DTOR_END__
      [49]  0x08060920 0x00000000  OBJT LOCL  D    0 .eh_frame   __FRAME_END__
      [50]  0x08060924 0x00000000  OBJT LOCL  D    0 .jcr        __JCR_END__
      [51]  0x080507b8 0x00000000  FUNC LOCL  D    0 .text       __do_global_ctors_aux
      [52]  0x00000000 0x00000000  FILE LOCL  D    0 ABS         crtn.o
      [53]  0x080507ec 0x0000000d  FUNC GLOB  D    0 .init       _init
      [54]  0x08050804 0x00000004  OBJT GLOB  D    0 .rodata     _lib_version
      [55]  0x08050580 0x00000000  OBJT GLOB  D    0 .plt        _PROCEDURE_LINKAGE_TABLE_
      [56]  0x08050600 0x00000075  FUNC GLOB  D    0 .text       _start
      [57]  0x08060900 0x00000000  OBJT GLOB  D    0 .data       __dso_handle
      [58]  0x08060848 0x00000000  OBJT GLOB  D    0 .dynamic    _DYNAMIC
      [59]  0x00000000 0x00000000  NOTY WEAK  D    0 UNDEF       __deregister_frame_info_bases
      [60]  0x08050814 0x00000000  OBJT GLOB  D    0 .rodata     _etext
      [61]  0x08060958 0x00000000  OBJT GLOB  D    0 .bss        _end
      [62]  0x08050590 0x00000000  FUNC WEAK  D    0 UNDEF       _cleanup
      [63]  0x08050675 0x00000001  FUNC WEAK  D    0 .text       _mcount
      [64]  0x08060904 0x00000004  OBJT GLOB  D    0 .data       i
      [65]  0x08060908 0x00000004  OBJT GLOB  D    0 .data       j
      [66]  0x0806090c 0x00000004  OBJT GLOB  D    0 .data       k

     
[67]  0x08060948 0x00000004  OBJT GLOB  D    0 .bss        l
      [68]  0x08060954 0x00000004  OBJT GLOB  D    0 .bss        _environ
      [69]  0x08060814 0x00000000  OBJT GLOB  D    0 .got        _GLOBAL_OFFSET_TABLE_
     
[70]  0x0806094c 0x00000004  OBJT GLOB  D    0 .bss        m
      [71]  0x0806092c 0x00000000  OBJT GLOB  D    0 .data.rel.l _edata
      [72]  0x08060954 0x00000004  OBJT WEAK  D    0 .bss        environ
      [73]  0x080507f9 0x00000008  FUNC GLOB  D    0 .fini       _fini
      [74]  0x080505a0 0x00000000  FUNC GLOB  D    0 UNDEF       atexit
     
[75]  0x08060950 0x00000004  OBJT GLOB  D    0 .bss        n
     
[76]  0x08050808 0x00000004  OBJT GLOB  D    0 .rodata     o
      [77]  0x0805080c 0x00000004  OBJT GLOB  D    0 .rodata     p

      [78]  0x00000000 0x00000000  NOTY WEAK  D    0 UNDEF       _Jv_RegisterClasses
     
[79]  0x08050810 0x00000004  OBJT GLOB  D    0 .rodata     q
      [80]  0x080505b0 0x00000000  FUNC GLOB  D    0 UNDEF       __fpstart
      [81]  0x08050753 0x00000065  FUNC GLOB  D    0 .text       main
      [82]  0x00000000 0x00000000  NOTY WEAK  D    0 UNDEF       __register_frame_info_bases
      [83]  0x080505c0 0x00000000  FUNC GLOB  D    0 UNDEF       exit

    Hash Section:  .hash 
    bucket    symndx    name
         0  [1]         _init
         1  [2]         _lib_version
            [3]         _PROCEDURE_LINKAGE_TABLE_
         2  [4]         _start
            [5]         __dso_handle
         4  [6]         _DYNAMIC
         9  [7]         __deregister_frame_info_bases
            [8]         _etext
        10  [9]         _end
        12  [10]        _cleanup
            [11]        _mcount
            [12]        i
        13  [13]        j
        14  [14]        k
        15  [15]        l
        16  [16]        _environ
            [17]        _GLOBAL_OFFSET_TABLE_
            [18]        m
            [19]        _edata
            [20]        environ
        17  [21]        _fini
            [22]        atexit
            [23]        n
        18  [24]        o
        19  [25]        p
        20  [26]        _Jv_RegisterClasses
            [27]        q
        25  [28]        __fpstart
        26  [29]        main
        29  [30]        __register_frame_info_bases
            [31]        exit

        13  buckets contain        0 symbols
        10  buckets contain        1 symbols
         5  buckets contain        2 symbols
         2  buckets contain        3 symbols
         1  buckets contain        5 symbols
        31  buckets               31 symbols (globals)

    Global Offset Table: 13 entries
     ndx     addr             
value           reloc                     addend            symbol
    [00000]  08060814  08060848 R_386_NONE         00000000
    [00001]  08060818  00000000 R_386_NONE         00000000
    [00002]  0806081c  00000000 R_386_NONE         00000000
    [00003]  08060820  08050596 R_386_JMP_SLOT     00000000 _cleanup
    [00004]  08060824 
080505a6 R_386_JMP_SLOT     00000000 atexit
    [00005]  08060828 
080505b6 R_386_JMP_SLOT     00000000 __fpstart
    [00006]  0806082c 
080505c6 R_386_JMP_SLOT     00000000 exit
    [00007]  08060830  00000000 R_386_GLOB_DAT     00000000 __deregister_frame_info_bases
    [00008]  08060834 
080505d6 R_386_JMP_SLOT     00000000 __deregister_frame_info_bases
    [00009]  08060838  00000000 R_386_GLOB_DAT     00000000 __register_frame_info_bases
    [00010]  0806083c  00000000 R_386_GLOB_DAT     00000000 _Jv_RegisterClasses
    [00011]  08060840 
080505e6 R_386_JMP_SLOT     00000000 _Jv_RegisterClasses
    [00012]  08060844 
080505f6 R_386_JMP_SLOT     00000000 __register_frame_info_bases

    Relocation: .rel.got
        type                       offset              section   with respect to
        R_386_GLOB_DAT             0x8060830             .rel.got       __deregister_frame_info_bases
        R_386_GLOB_DAT             0x8060838             .rel.got       __register_frame_info_bases
        R_386_GLOB_DAT             0x806083c             .rel.got       _Jv_RegisterClasses

    Relocation: .rel.bss
        type                       offset              section   with respect to
        R_386_COPY                 0x8060954             .rel.bss       _environ

    Relocation: .rel.plt
        type                       offset              section   with respect to
        R_386_JMP_SLOT             0x8060820             .rel.plt       _cleanup
        R_386_JMP_SLOT             0x8060824             .rel.plt       atexit
        R_386_JMP_SLOT             0x8060828             .rel.plt       __fpstart
        R_386_JMP_SLOT             0x806082c             .rel.plt       exit
        R_386_JMP_SLOT             0x8060834             .rel.plt       __deregister_frame_info_bases
        R_386_JMP_SLOT             0x8060840             .rel.plt       _Jv_RegisterClasses
        R_386_JMP_SLOT             0x8060844             .rel.plt       __register_frame_info_bases

    Dynamic Section:  .dynamic
     index  tag               value
       [0]  NEEDED           0x104             libc.so.1
       [1]  INIT             0x80507ec        
       [2]  FINI             0x80507f9        
       [3]  HASH             0x80500e8        
       [4]  STRTAB           0x80503ec        
       [5]  STRSZ            0x11a            
       [6]  SYMTAB           0x80501ec        
       [7]  SYMENT           0x10             
       [8]  CHECKSUM         0x6a10           
       [9]  VERNEED          0x8050508        
      [10]  VERNEEDNUM       0x1              
      [11]  PLTRELSZ         0x38             
      [12]  PLTREL           0x11             
      [13]  JMPREL           0x8050548        
      [14]  REL              0x8050528        
      [15]  RELSZ            0x58             
      [16]  RELENT           0x8              
      [17]  DEBUG            0                
      [18]  FEATURE_1        0x1               [ PARINIT ]
      [19]  FLAGS            0                 0
      [20]  FLAGS_1          0                 0
      [21]  PLTGOT           0x8060814

   
利用elfdump可以查看ELF文件格式的详细信息,可以在符号表.dynsym.symtab中找到程序中定义的全局变量和全局常量:
       i,j,k
.data section
       l,m,n
.bss section
       o,p,q
.rodata section
   
   
概念:ELF(Executable and Linking Format)可执行连接格式

    ELF
格式是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary InterfaceABI)而开发和发布的。
   
目前,ELF格式是Unix/Linux平台上应用最广泛的二进制工业标准之一

   
下图从不同视角给出了ELF文件的一般格式:

    Linking
视角                       Execution视角
    ============                      ==============
    ELF header                        ELF header
    Program header table (optional)   Program header table
    Section 1                         Segment 1
    ...                               Segment 2
    Section n                         ...
    Section header table              Section header table (optional)

   
3-2 ELF文件格式摘自 EXECUTABLE AND LINKABLE FORMAT (ELF)

   
可以根据test5 ELF文件的Program header tableSection header table中文件偏移量的信息描绘出test5的内容:

    entry name     
起始文件偏移+实际大小=下个entry起始偏移
    -------------------------------------------------------
    ELF header                  0x0+0x34=0x34
    Program header              0x34+0xa0=0xd4
    Section 1  .interp          0xd4+0x11=0xe5
    000                         0xe5+0x3=0xe8
    Section 2  .hash            0xe8+0x104=0x1ec
    Section 3  .dynsym          0x1ec+0x200=0x3ec
    Section 4  .dynstr          0x3ec+0x11a=0x506
    00                          0x506+0x2=0x508
    Section 5  .SUNW_version    0x508+0x20=0x528
    Section 6  .rel.got         0x528+0x18=0x540
    Section 7  .rel.bss         0x540+0x8=0x548  
    Section 8  .rel.plt         0x548+0x38=0x580
    Section 9  .plt             0x580+0x80=0x600    
    Section 10 .text            0x600+0x1ec=0x7ec             
    Section 11 .init            0x7ec+0xd=0x7f9  
    Section 12 .fini            0x7f9+0x8=0x801
    000                         0x801+0x3=0x804
    Section 13 .rodata          0x804+0x10=0x814
    Section 14 .got             0x814+0x34=0x848
    Section 15 .dynamic         0x848+0xb8=900
    Section 16 .data            0x900+0x10=0x910
    Section 17 .ctors           0x910+0x8=0x918
    Section 18 .dtors           0x918+0x8=0x920
    Section 19 .eh_frame        0x920+0x4=0x924
    Section 20 .jcr             0x924+0x4=0x928
    Section 21 .data.rel.local  0x928+0x4=0x92c
    Section 22 .bss          0x92c+0x0=0x958
    Section 23 .symtab          0x92c+0x540=0xe6c
    Section 24 .strtab          0xe6c+0x20b=1077
    Section 25 .comment         0x1077+0x24d=0x12c4
    Section 26 .stab.index      0x12c4+0x24=0x12e8
    Section 27 .shstrtab        0x12e8+0xdc=0x13c4
    Section 28 .stab.indexstr   0x13c4+0x1c0=0x1584
    Section header table        0x1584+0x488=0x1a0c ;29x40=1160=0x488
这是根据Elf header信息算得
    ------------------------------------------------------
   
3-3 test5ELF文件格式

    # ls -al test5
    -rwxr-xr-x    1 root     other        6668 2004-12-19 06:56 test5

   
可以看到test5的大小是0x1a0c字节,即6688字节。
   
可以看到,.bss section用于保存未初始化的全局变量,因此不占据ELF文件空间;
    ELF
文件装入时,会按照Section header table 22中的.bss的相关属性,为.bss映射相应大小的内存空间,并初始化为0

    ELF
文件的Program header table描述了如何将ELF装入内存:
        Program Header 2
描述了用户代码段的起始地址和大小: 0x8050000+0x814=0x8050814
        Program Header 3
描述了用户数据段的起始地址和大小: 0x8060814+0x144=0x8060958
   
   
问题:为何前面pmap得到的结果是数据段从0x8060000开始,而ELF文件的Program Header 3却是从0x8060814开始?
   
   
如果查一下ELF文件的格式规范的话,就能找到答案:

        Program Header[2]:
            p_vaddr:     
0x8050000       p_flags:    [ PF_X  PF_R ]
            p_paddr:      0               p_type:     [ PT_LOAD ]
            p_filesz:     0x814           p_memsz:    0x814
            p_offset:     0               p_align:    0x10000   ;
指定64K页对齐

        Program Header[3]:
            p_vaddr:     
0x8060814       p_flags:    [ PF_X  PF_W  PF_R ]
            p_paddr:      0               p_type:     [ PT_LOAD ]
            p_filesz:     0x118           p_memsz:    0x144
            p_offset:     0x814           p_align:    0x10000   ;
指定64K页对齐

    p_align
指定了映射代码段和数据段的时候,必须按照64K页对齐的方式,即起始映射地址必须以0000结尾。
   
代码段的起始地址正好满足该条件,如果数据段不考虑页对齐的话,应该紧跟代码段的下一个字节即0x8050814开始。
   
但是正因为64K页对齐的缘故,只能从最接近0x805081464K页对齐地址0x8060000开始。
   
而实际上,在对ELF进行内存映射时,是按页为单位进行映射的,test5的大小是0x1a0c,不足1页大小,代码段和数据段都是在第1页。
   
映射发生时,先映射这第1页到0x8050000的代码段,属性read/exec;再映射这1页到0x8060000的数据段,属性 read/write/exec
   
这样,实际上的数据段起始地址就是0x8060000+0x814=0x8060814

   
问题:为什么要页对齐 page align

   
首先,内存映射是以页为最小单位的,这是因为Solaris的内存管理是页式内存管理(目前大多数现代OS都是如此)
   
其次,代码段和数据段因为有不同的权限要求(代码段要求只读),因此必须进行2次映射;
   
最后,就是效率的要求;

   
尽管ELF文件的映射是solaris内核中seg_vn段驱动程序来完成的,但仍可以通过系统调用mmap(2)来学习基本的内存映射常识。
   
对于其它系统,如Linux情况也类似。
   
那为何在本例中是要求64K页对齐呢?答案是:这也是SolarisSparc上的页对齐要求

   
概念:ELF文件loading

   
根据Program header tablesection header table描绘出test5代码段及数据段的内部情况就很容易了:

    entry name         
起始地址+实际大小=下个entry起始地址
    =================User Text============================
0x8050000
    ELF header                  0x8050000+0x34=0x8050034
    Program header              0x8050034+0xa0=0x80500d4
    Section 1  .interp          0x80500d4+0x11=0x80500e5
    0                           0x80500e5+0x3=0x80500e8
      ; 3字节0填充
    Section 2  .hash            0x80500e8+0x104=0x80501ec
    Section 3  .dynsym          0x80501ec+0x200=0x80503ec
    Section 4  .dynstr          0x80503ec+0x11a=0x8050506
    0                           0x8050506+0x2=0x8050508
      ; 2字节0填充
    Section 5  .SUNW_version    0x8050508+0x20=0x8050528
    Section 6  .rel.got         0x8050528+0x18=0x8050540
    Section 7  .rel.bss         0x8050540+0x8=0x8050548  
    Section 8  .rel.plt         0x8050548+0x38=0x8050580
    Section 9  .plt             0x8050580+0x80=0x8050600    
    Section 10 .text            0x8050600+0x1ec=0x80507ec             
    Section 11 .init            0x80507ec+0xd=0x80507f9  
    Section 12 .fini            0x80507f9+0x8=0x8050801
    0                           0x8050801+0x3=0x8050804
      ; 3字节0填充
    Section 13 .rodata       0x8050804+0x10=0x8050814
   ;o,p,q在代码段的. rodata section
    -----------------------------------------------------
    Section 14 .got             0x8050814+0x34=0x8050848      ;
这是代码段的第一页也是最后一页,
    Section 15 .dynamic         0x8050848+0xb8=8050900  ;
因此数据段内容会追加到代码段最后一页末尾
    Section 16 .data            0x8050900+0x10=0x8050910
    Section 17 .ctors           0x8050910+0x8=0x8050918
    Section 18 .dtors           0x8050918+0x8=0x8050920
    Section 19 .eh_frame        0x8050920+0x4=0x8050924
    Section 20 .jcr             0x8050924+0x4=0x8050928
    Section 21 .data.rel.local  0x8050928+0x4=0x805092c
    Section 22 .bss             0x805092c+0x2c=0x8050958
    pending data                0x8050958+0x6a8=8051000         ;
页末是0x6a8字节填充
    ======================================================

                        no mapping

    =================User Data============================
0x8060000
    ELF header                  0x8060000+0x34=0x8060034     ;
这是数据段的最后一页也是第一页,     
    Program header              0x8060034+0xa0=0x80600d4   ;
因此代码段内容会追加到数据段第一页之前
    Section 1  .interp          0x80600d4+0x11=0x80600e5
    0                           0x80600e5+0x3=0x80600e8
      ; 3字节0填充
    Section 2  .hash            0x80600e8+0x104=0x80601ec
    Section 3  .dynsym          0x80601ec+0x200=0x80603ec
    Section 4  .dynstr          0x80603ec+0x11a=0x8060506
    0                           0x8060506+0x2=0x8060508
      ; 2字节0填充
    Section 5  .SUNW_version    0x8060508+0x20=0x8060528
    Section 6  .rel.got         0x8060528+0x18=0x8060540
    Section 7  .rel.bss         0x8060540+0x8=0x8060548  
    Section 8  .rel.plt         0x8060548+0x38=0x8060580
    Section 9  .plt             0x8060580+0x80=0x8060600    
    Section 10 .text            0x8060600+0x1ec=0x80607ec             
    Section 11 .init            0x80607ec+0xd=0x80607f9  
    Section 12 .fini            0x80607f9+0x8=0x8060801
    0                           0x8060801+0x3=0x8060804
      ; 3字节0填充
    Section 13 .rodata          0x8060804+0x10=0x8060814
    -----------------------------------------------------
    Section 14 .got             0x8060814+0x34=0x8060848
    Section 15 .dynamic         0x8060848+0xb8=8060900
    Section 16 .data         0x8060900+0x10=0x8060910
   ;i,j,k在数据段的.data section
    Section 17 .ctors           0x8060910+0x8=0x8060918
    Section 18 .dtors           0x8060918+0x8=0x8060920
    Section 19 .eh_frame        0x8060920+0x4=0x8060924
    Section 20 .jcr             0x8060924+0x4=0x8060928
    Section 21 .data.rel.local  0x8060928+0x4=0x806092c
    Section 22 .bss          0x806092c+0x2c=0x8060958
   ; l,m,n在数据段的.bss section
   
0                           0x8060958+0x6a8=8061000       ; 页末是0x6a8字节0填充
    =======================================================
   
3-4 test5的代码段和数据段内部结构

       
以下各section因为section header table中的sh_flags不包含SHF_ALLOC,因此不会被映射到内存:
        Section 23 .symtab
           
符号表,不映射到内存,strip命令可以去除
        Section 24 .strtab
           
字符串表,主要保存着和 .symtab的名字字符串以及其它字符串,不映射到内存,strip命令可去除
        Section 25 .comment
           
保存着该二进制程序相关的信息,格式内容决定于二进制程序本身,不映射到内存,strip命令保留该section
        Section 26 .stab.index
           
保存着该二进制程序相关的信息,格式内容决定于二进制程序本身,不映射到内存,strip命令可以去除
        Section 27 .shstrtab
           
字符串表,只保存section name,不映射到内存,strip命令保留该section
        Section 28 .stab.indexstr
           
字符串表,不映射到内存,strip命令可以去除

   
有了图 3-4,就能清楚的找到全局变量和全局常量的在数据段和代码段的精确位置,也就能回答前面提出的问题:
      
全局常量o,p,q属于代码段的.rodata section,这个section因为属于代码段而具有只读属性,用于保存只读数据
      
全局变量i,j,k属于数据段的.data section,用于保存有初值的全局变量,这个section同时在ELF文件和内存中占据空间
      
全局变量l,m,n属于代码段的.bss section,用于保存未初始化的全局变量,这个section占据内存空间而不占据ELF文件空间
   
由于分属于几个不同的section,地址空间必定不连续了
      
   
下面把ELF文件的装入归纳如下:

       *
第一个代码段页面包含了 ELF headerProgram header table以及其他信息
       *
最后的代码段页末尾追加一个数据段开始的拷贝
       *
第一个数据段页面前有一个代码段结束的拷贝
       *
最后的数据段页面也许会包含与正在运行的进程无关的文件信息

2. ELF
文件装载的验证

    ELF
文件本身的格式可以直接用工具观察二进制文件,下面的命令可以观察到.comment section的相关内容:
    bash-2.05# od -A x -c -j 0x1077 -N 0x24d test5
    0000000   G   N   U       C       c   r   t   1   .   s  /0   a   s   :
    0000010       F   o   r   t   e       D   e   v   e   l   o   p   e   r
    0000020       7       C   o   m   p   i   l   e   r       C   o   m   m
    0000030   o   n       7   .   0       I   A   3   2   -   i   t   e   a
    0000040   m       2   0   0   1   /   1   2   /   1   2  /0   G   N   U
    0000050       C       c   r   t   i   .   s  /0   a   s   :       F   o
    0000060   r   t   e       D   e   v   e   l   o   p   e   r       7   
    0000070   C   o   m   p   i   l   e   r       C   o   m   m   o   n   
    0000080   7   .   0       I   A   3   2   -   i   t   e   a   m       2
    0000090   0   0   1   /   1   2   /   1   2  /0  /0   @   (   #   )   S
    00000a0   u   n   O   S       5   .   9       G   e   n   e   r   i   c
    00000b0   _   1   1   2   2   3   4   -   0   3       N   o   v   e   m
    00000c0   b   e   r       2   0   0   2  /0   G   C   C   :       (   G
    00000d0   N   U   )       3   .   3   .   2  /0   a   s   :       F   o
    00000e0   r   t   e       D   e   v   e   l   o   p   e   r       7   
    00000f0   C   o   m   p   i   l   e   r       C   o   m   m   o   n   
    0000100   7   .   0       I   A   3   2   -   i   t   e   a   m       2
    0000110   0   0   1   /   1   2   /   1   2  /0   G   C   C   :       (
    0000120   G   N   U   )       3   .   3   .   2  /0   a   s   :       F
    0000130   o   r   t   e       D   e   v   e   l   o   p   e   r       7
    0000140       C   o   m   p   i   l   e   r       C   o   m   m   o   n
    0000150       7   .   0       I   A   3   2   -   i   t   e   a   m   
    0000160   2   0   0   1   /   1   2   /   1   2  /0   G   C   C   :   
    0000170   (   G   N   U   )       3   .   3   .   2  /0   a   s   :   
    0000180   F   o   r   t   e       D   e   v   e   l   o   p   e   r   
    0000190   7       C   o   m   p   i   l   e   r       C   o   m   m   o
    00001a0   n       7   .   0       I   A   3   2   -   i   t   e   a   m
    00001b0       2   0   0   1   /   1   2   /   1   2  /0   G   N   U   
    00001c0   C       c   r   t   n   .   o  /0   a   s   :       F   o   r
    00001d0   t   e       D   e   v   e   l   o   p   e   r       7       C
    00001e0   o   m   p   i   l   e   r       C   o   m   m   o   n       7
    00001f0   .   0       I   A   3   2   -   i   t   e   a   m       2   0
    0000200   0   1   /   1   2   /   1   2  /0   l   d   :       S   o   f
    0000210   t   w   a   r   e       G   e   n   e   r   a   t   i   o   n
    0000220       U   t   i   l   i   t   i   e   s       -       S   o   l
    0000230   a   r   i   s       L   i   n   k       E   d   i   t   o   r
    0000240   s   :       5   .   9   -   1   .   2   7   6  /0
    000024d

   
显然,.comment section的内容是编译器和链接器的版本信息。

   
观察ELF载入内存后的情况则需要该ELF程序的进程在系统中挂起,才能读到相关内容。
   
利用mdb可以查看装入内存中的test5的代码段和数据段值,下面就验证一下 3-4:

    # mdb test5
    Loading modules: [ libc.so.1 ]
    > main::dis
    main:                           pushl   %ebp
    main+1:                         movl    %esp,%ebp
    main+3:                         subl    $8,%esp
    main+6:                         andl    $0xf0,%esp
    main+9:                         movl    $0,%eax
    main+0xe:                       subl    %eax,%esp
    main+0x10:                      movl    $4,0x8060948
    main+0x1a:                      movl    $5,0x806094c
    main+0x24:                      movl    $6,0x8060950
    main+0x2e:                      movl    0x8060908,%eax
    main+0x33:                      addl    0x8060904,%eax
    main+0x39:                      addl    0x806090c,%eax
    main+0x3f:                      addl    0x8060948,%eax
    main+0x45:                      addl    0x806094c,%eax
    main+0x4b:                      addl    0x8060950,%eax
    main+0x51:                      addl    0x8050808,%eax
    main+0x57:                      addl    0x805080c,%eax
    main+0x5d:                      addl    0x8050810,%eax
    main+0x63:                      leave
    main+0x64:                      ret
    > main+0x2e:b            ;
设置断点
    > :r                     ;
运行
    mdb: stop at main+0x2e
    mdb: target stopped at:
    main+0x2e:      movl    0x8060908,%eax
    > 0x8050000,0x4/naB      ;
查看ELF header4字节
    0x8050000:     
    0x8050000:      7f     
    0x8050001:      45     
    0x8050002:      4c     
    0x8050003:      46  
    > 0x8050000,0x4/nac      ;
查看ELF header4字节
    0x8050000:     
    0x8050000:     
    0x8050001:      E
    0x8050002:      L
    0x8050003:      F
    > 0x80500d4,11/c         ;
查看.interp section
    0x80500d4:      /usr/lib/ld.so.1
    > 0x8050600::dis         ;
查看.text section的第一的过程,也是ELF的入口点
    _start:                         pushl   $0
    _start+2:                       pushl   $0
    _start+4:                       movl    %esp,%ebp
    _start+6:                       pushl   %edx
    _start+7:                       movl    $0x8050590,%eax
    _start+0xc:                     testl   %eax,%eax
    _start+0xe:                     je      +0xf            <_start+0x1d>
    _start+0x10:                    pushl   $0x8050590
    _start+0x15:                    call    -0x75           <PLT=libc.so.1`atexit>
    _start+0x1a:                    addl    $4,%esp
    _start+0x1d:                    movl    $0x8060848,%eax
    _start+0x22:                    testl   %eax,%eax
    _start+0x24:                    je      +7              <_start+0x2b>
    _start+0x26:                    call    -0x86           <PLT=libc.so.1`atexit>
    _start+0x2b:                    pushl   $0x80507f9
    _start+0x30:                    call    -0x90           <PLT=libc.so.1`atexit>
    _start+0x35:                    movl    +8(%ebp),%eax
    _start+0x38:                    leal    +0x10(%ebp,%eax,4),%edx
    _start+0x3c:                    movl    %edx,0x8060954
    _start+0x42:                    andl    $0xf0,%esp
    _start+0x45:                    subl    $4,%esp
    _start+0x48:                    pushl   %edx
    _start+0x49:                    leal    +0xc(%ebp),%edx
    _start+0x4c:                    pushl   %edx
    _start+0x4d:                    pushl   %eax
    _start+0x4e:                    call    +0x19e          <_init>
    _start+0x53:                    call    -0xa3           <PLT=libc.so.1`_fpstart>
    _start+0x58:                    call    +0xfb           <main>
    _start+0x5d:                    addl    $0xc,%esp
    _start+0x60:                    pushl   %eax
    _start+0x61:                    call    -0xa1           <PLT:exit>
    _start+0x66:                    pushl   $0
    _start+0x68:                    movl    $1,%eax
    _start+0x6d:                    lcall   $7,$0
    _start+0x74:                    hlt
    > 0x8050804,0x4/nap      ;
查看.rodata section,包含真正的o,p,q几个全局常量
    _lib_version:
    _lib_version:  
    _lib_version:   1              
    o:              7              
    p:              8              
    q:              9
    > 0x8050900,0x4/nap      ;
查看填充在代码段之后的.data section,这部分实际上是无效的数据
    0x8050900:     
    0x8050900:      0              
    0x8050904:      1        ;
全局变量i    
    0x8050908:      2        ;
全局变量j   
    0x805090c:      3        ;
全局变量k   
    > 0x8060000,0x4/naB      ;
查看填充到数据段之前的ELF header4字节,这部分实际是无效的
    0x8060000:     
    0x8060000:      7f     
    0x8060001:      45     
    0x8060002:      4c     
    0x8060003:      46
    > 0x8060000,0x4/nac      ;
查看填充到数据段之前的ELF header4字节,这部分实际是无效的
    0x8060000:     
    0x8060000:     
    0x8060001:      E
    0x8060002:      L
    0x8060003:      F
    > 0x8060804,0x4/nap      ;
查看填充到数据段之前的.rodata section,这部分实际是无效的
    0x8060804:     
    0x8060804:      1              
    0x8060808:      7        ;
全局常量o    
    0x806080c:      8        ;
全局常量p    
    0x8060810:      9        ;
全局常量q
    > 0x8060900,0x4/nap      ;
查看.date section,包含真正的i,j,k几个全局变量
    0x8060900:     
    0x8060900:      0              
    test5`i:        1              
    test5`j:        2              
    test5`k:        3              
    > 0x806092c,0xb/nap      ;
查看.bss section,包含真正的l,m,n几个全局变量
    test5`completed.1:
    test5`completed.1:             
    test5`completed.1:              0              
    test5`object.2: 0              
    test5`object.2+4:               0              
    test5`object.2+8:               0              
    test5`object.2+0xc:             0              
    test5`object.2+0x10:            0              
    test5`object.2+0x14:            0              
    test5`l:        4              
    test5`m:        5              
    test5`n:        6              
    test5`environ:  0x8047e00
    > 0x8060958,0x6a8/nab    ;
查看数据段末尾追加的0x6a8字节数据,全部为0
    0x8060958:     
    0x8060958:      0      
    0x8060959:      0      
    0x806095a:      0      
    .................
    .................
    0x8060ffe:      0      
    0x8060fff:      0
    > 0x8060fff,2/nab        ;
验证页边界数据是否映射
    0x8060fff:     
    0x8060fff:      0      
    mdb: failed to read data from target: no mapping for address
    0x8061000:     
    > 0x8050fff,2/nab        ;
验证页边界数据是否映射
    0x8050fff:     
    0x8050fff:      0151   
    mdb: failed to read data from target: no mapping for address
    0x8051000: 

3.
小结
   
本次实验再次分析和验证了全局变量和全局常量在进程地址空间的位置以及和ELF文件的关系,并涉及到以下几方面的概念:
       Process Address Space
进程地址空间
       ELF EXECUTABLE AND LINKABLE FORMAT
可执行链接格式
       Page align
页对齐
   
并且,利用Solaris提供的mdb,pmap,elfdump,od工具,直接观察到ELF文件的装载和格式