认识目标文件的内容

一、目标文件基本阐述

  1. 目标文件:编译器编译源代码后但未进行链接的中间文件(Linux下为.o文件)

  2. 结构特点:分段(主要为代码段和数据段)

  3. 分段的好处

  • 可以分别设置不同属性,数据虚存区域设置为可读写,指令虚存区域设置为只读

  • 符合现代CPU的缓存体系(数据缓存和指令缓存分离)

  • 节省内存,系统中运行多个该程序副本时,只需保留一份该程序的指令部分或只读数据(图标、图片、文本资源等)

  1. 学习的目的:认识底层具体工作细节,提高自己的修养境界

二、深入细节的示例分析

源码与基本段信息

  1. 源码
/*
 *  SimpleSection.c
 *  Linux: gcc -c SimpleSection.c
 *  Windows: cl SimpleSection.c /c /Za
 */

int printf( const char* format, ...);

int global_init_var = 84;
int global_uninit_var;

void func1( int i )
{
        printf("%d\n", i);
}

int main(void)
{
        static int static_var = 85;
        static int static_var2;

        int a = 1;
        int b;

        func1(static_var + static_var2 + a + b);

        return a;
}
  1. 基本段信息
# 平台: Ubuntu16.04
# 参数: -h 打印基本的段信息
$ objdump -h SimpleSection.o  

SimpleSection.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000055  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  00000098  2**2  # 存放的是已经初始化的全局静态变量和局部静态变量
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000004  0000000000000000  0000000000000000  000000a0  2**2  # 存放的是未初始化的全局变量和局部静态变量
                  ALLOC
  3 .rodata       00000004  0000000000000000  0000000000000000  000000a0  2**0  # 存放只读数据,一般是程序里边的只读变量(const修饰的变量)和字符串常量
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      00000036  0000000000000000  0000000000000000  000000a4  2**0  # 存放的是编译器的版本信息
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000da  2**0
                  CONTENTS, READONLY
  6 .eh_frame     00000058  0000000000000000  0000000000000000  000000e0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

# 简单说明: 
# Size为段的长度,File Off为段所在的位置,也就是偏移
# 属性: CONTENTS表示该段在文件中存在(.bss段里没有)

$ size SimpleSection.o
text	   data	    bss	    dec	    hex	 filename
177	    8	     4	    189	    bd	SimpleSection.o
  1. 当前框架图

  2. 代码段解析

# 平台: Ubuntu16.04  
# 参数: -s: 以十六进制打印所有的段的内容 
#        -d: 将所有包含指令的段反汇编
$ objdump -s -d SimpleSection.o 

SimpleSection.o:     file format elf64-x86-64

Contents of section .text:  # .text的数据内容。总共0x55字节,字节的内容是指令机器码
 0000 554889e5 4883ec10 897dfc8b 45fc89c6  UH..H....}..E...
 0010 bf000000 00b80000 0000e800 00000090  ................
 0020 c9c35548 89e54883 ec10c745 f8010000  ..UH..H....E....
 0030 008b1500 0000008b 05000000 0001c28b  ................
 0040 45f801c2 8b45fc01 d089c7e8 00000000  E....E..........
 0050 8b45f8c9 c3                          .E...       
    
Contents of section .data:  # 对应程序中 global_init_var的值为84, static_var为85
 0000 54000000 55000000                    T...U...        

Contents of section .rodata: # printf的字符串常量“%d\n”对应ASCII的十六进制
 0000 25640a00                             %d..      
      
Contents of section .comment:
 0000 00474343 3a202855 62756e74 7520352e  .GCC: (Ubuntu 5.
 0010 342e302d 36756275 6e747531 7e31362e  4.0-6ubuntu1~16.
 0020 30342e31 32292035 2e342e30 20323031  04.12) 5.4.0 201
 0030 36303630 3900                        60609.       
   
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 1c000000 1c000000  ................
 0020 00000000 22000000 00410e10 8602430d  ...."....A....C.
 0030 065d0c07 08000000 1c000000 3c000000  .]..........<...
 0040 00000000 33000000 00410e10 8602430d  ....3....A....C.
 0050 066e0c07 08000000                    .n......        

Disassembly of section .text:

0000000000000000 <func1>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	48 83 ec 10          	sub    $0x10,%rsp
   8:	89 7d fc             	mov    %edi,-0x4(%rbp)
   b:	8b 45 fc             	mov    -0x4(%rbp),%eax
   e:	89 c6                	mov    %eax,%esi
  10:	bf 00 00 00 00       	mov    $0x0,%edi
  15:	b8 00 00 00 00       	mov    $0x0,%eax
  1a:	e8 00 00 00 00       	callq  1f <func1+0x1f>
  1f:	90                   	nop
  20:	c9                   	leaveq 
  21:	c3                   	retq   

0000000000000022 <main>:
  22:	55                   	push   %rbp
  23:	48 89 e5             	mov    %rsp,%rbp
  26:	48 83 ec 10          	sub    $0x10,%rsp
  2a:	c7 45 f8 01 00 00 00 	movl   $0x1,-0x8(%rbp)
  31:	8b 15 00 00 00 00    	mov    0x0(%rip),%edx        # 37 <main+0x15>
  37:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax        # 3d <main+0x1b>
  3d:	01 c2                	add    %eax,%edx
  3f:	8b 45 f8             	mov    -0x8(%rbp),%eax
  42:	01 c2                	add    %eax,%edx
  44:	8b 45 fc             	mov    -0x4(%rbp),%eax
  47:	01 d0                	add    %edx,%eax
  49:	89 c7                	mov    %eax,%edi
  4b:	e8 00 00 00 00       	callq  50 <main+0x2e>
  50:	8b 45 f8             	mov    -0x8(%rbp),%eax
  53:	c9                   	leaveq 
  54:	c3                   	retq
  1. VMA与LMA的关系(转载)

https://www.crifan.com/detailed_lma_load_memory_address_and_vma_virtual_memory_address/

ELF文件结构描述

ELF文件头

  1. 目标:查看ELF文件头,可以找到段表所在的位置,以此得知整个目标文件的段结构
$ readelf -h SimpleSection.o 
# ELF标记0x7F, 'E'(0x45)、'L'(0x4c)、'F'(0x46)
# 文件类型: 0x0:无效文件 0x1:32为ELF文件  0x2:64位ELF文件
# 字节序: 0x0: 无效格式  0x1:小端格式  0x2:大端格式
# ELF主版本号: 0x1,ELF标准自1.2版本以后再也没有更新
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00  # 对应e_ident成员
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1072 (bytes into file)  # 段表的位置0x430
  Flags:                             0x0
  Size of this header:               64 (bytes)  # ELF文件头的大小为64字节
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)  # 段表描述符的大小,也就是段表结构体Elf64_Shdr的大小
  Number of section headers:         13  # 段表描述符的数量
  Section header string table index: 10  #表示.shstrtab所在的段在段表中的下标为10
  1. 相关的数据结构
#数据结构与上边的每一项是完全相符合的
#32位的elf结构体为Elf32_Ehdr,64位的elf结构体为Elf64_Ehdr,成员完全一样,只是有些成员大小不一样
$ cat /usr/include/elf.h  
...
typedef struct
{
  unsigned char e_ident[EI_NIDENT];     /* Magic number and other info */
  Elf64_Half    e_type;                 /* Object file type */
  Elf64_Half    e_machine;              /* Architecture */
  Elf64_Word    e_version;              /* Object file version */
  Elf64_Addr    e_entry;                /* Entry point virtual address */
  Elf64_Off     e_phoff;                /* Program header table file offset */
  Elf64_Off     e_shoff;                /* Section header table file offset */
  Elf64_Word    e_flags;                /* Processor-specific flags */
  Elf64_Half    e_ehsize;               /* ELF header size in bytes */
  Elf64_Half    e_phentsize;            /* Program header table entry size */
  Elf64_Half    e_phnum;                /* Program header table entry count */
  Elf64_Half    e_shentsize;            /* Section header table entry size */
  Elf64_Half    e_shnum;                /* Section header table entry count */
  Elf64_Half    e_shstrndx;             /* Section header string table index */
} Elf64_Ehdr;
...

段表(Section Table)

  1. 段表是目标文件中最重要的部分,记录每个段的地址、长度和属性等信息
# objdump -h只是将关键段显示出来,省略其他辅助性的段,用readelf查看elf文件的段才是真正的段表结构
# 段表结构由ELF文件头的e_shoff决定

$ readelf -S SimpleSection.o 
There are 13 section headers, starting at offset 0x430:  # 表明段表的位置位0x430

Section Headers: # 在这个程序中,段表就是有13个元素的数组,但第一个是无效的,也就是有12个有效的段,每个段是Elf64_Shdr结构体
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000055  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000320
       0000000000000078  0000000000000018   I      11     1     8   # .rela.text的info值为1代表重定向text的下标,11代表所使用的符号表在段表的下标
  [ 3] .data             PROGBITS         0000000000000000  00000098
       0000000000000008  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  000000a0
       0000000000000004  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  000000a0
       0000000000000004  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  000000a4
       0000000000000036  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000da
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  000000e0
       0000000000000058  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  00000398
       0000000000000030  0000000000000018   I      11     8     8
  [10] .shstrtab         STRTAB           0000000000000000  000003c8
       0000000000000061  0000000000000000           0     0     1
  [11] .symtab           SYMTAB           0000000000000000  00000138
       0000000000000180  0000000000000018          12    11     8
  [12] .strtab           STRTAB           0000000000000000  000002b8
       0000000000000066  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

# 说明:
1. Type: PROGBITS(程序段,代码段和数据段都是这种类型)、RELA(重定位表,针对那些对绝对地址引用的位置的记录)、NOBITS(该段在文件中没内容,如.bss)、STRTAB(字符串表)等

2. Flags: ALLOC(表示在进程空间中需要分配空间)

3. Link: 段的类型与链接相关的(如重定位表、符号表等)才有意义,当类型为REL时,Link表示所使用的相应符号表在段表的下标,info表示所作用的段在段表的下标
  1. 相关数据结构
#32位的段表结构为Elf32_Shdr,64位的elf结构体为Elf64_Shdr,成员完全一样,只是类型大小不一样
$ cat /usr/include/elf.h  
...
typedef struct  # 与上边的一一对应上
{
  Elf64_Word    sh_name;                /* Section name (string tbl index) */ # 段名,位于.shstrtab字符串表
  Elf64_Word    sh_type;                /* Section type */
  Elf64_Xword   sh_flags;               /* Section flags */
  Elf64_Addr    sh_addr;                /* Section virtual addr at execution */
  Elf64_Off     sh_offset;              /* Section file offset */
  Elf64_Xword   sh_size;                /* Section size in bytes */
  Elf64_Word    sh_link;                /* Link to another section */
  Elf64_Word    sh_info;                /* Additional section information */
  Elf64_Xword   sh_addralign;           /* Section alignment */
  Elf64_Xword   sh_entsize;             /* Entry size if section holds table */
} Elf64_Shdr;
...
  1. 根据段表可以清楚知道整体的结构

字符串表(.strtab、.shstrtab)

  1. 由于ELF中的字符串的长度往往是不定的,所以采用偏移的方式来引用字符串(字符串helloworld的偏移为1,world的偏移为6,以\0来衡量)

  2. 常见的段名

  • 字符串表:.strtab,保存普通字符串,比如符号的名字

  • 段表字符串表(Section Header String Table):.shstrtab,保存段表中用到的字符串,比如段名

  1. ELF文件头中有表明段表字符串表在段表中的位置(成员e_shstrndx),因此只要分析ELF文件头,就可以得到段表和段表字符串表的位置

ELF符号表(.symtab)

  1. 符号的概念:
  • 函数和变量统称为符号(Symbol),函数名或变量名就是符号名

  • 每个目标文件都有一个相应的符号表(Stmbol Table),这个表里边记录了目标文件中所要用到的所有符号,每一个定义的符号有个对应的值,也就是符号值,对于变量或者函数来说,符号值就是它们的地址

  1. 查看符号表
# nm命令可以查看目标文件的符号信息
$ nm SimpleSection.o 
0000000000000000 T func1
0000000000000000 D global_init_var
0000000000000004 C global_uninit_var
0000000000000022 T main
                 U printf
0000000000000004 d static_var.1840
0000000000000000 b static_var2.1841

#说明:
1. t,T   该符号位于代码段(text section)
2. d,D   该符号位于初始化数据段(data section)
3. C     该符号为common。common symbol是未初始化的数据。该符号没有包含在一个普通section中,只有在链接过程中才进行分配。符号的值表示该符号需要的字节数
4. U     该符号在当前文件中是未定义的,即该符号定义在别的文件中
5. b,B   该符号的值出现在非初始化数据段(BSS)中

# -s打印sym,即符号表
$ readelf -s SimpleSection.o 

Symbol table '.symtab' contains 16 entries:  # 16个元素的符号表数组
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS SimpleSection.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1     # 这些没有名字的代表下标为Ndx的段的段名,比如这里为1,即.text段的段名,可以通过objdump -t来查看
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 static_var.1840  # 变量名称变了,由于符号修饰
     7: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 static_var2.1841
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
    11: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var  # 已初始化,.data的下标为3 (不能看objdump -h的输出信息,只是列出主要的段,索引不对)
    12: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM global_uninit_var # 未初始化,.bss的下标为4
    13: 0000000000000000    34 FUNC    GLOBAL DEFAULT    1 func1  # func1函数定义在SimpleSection.c中,是代码段,.text段的下标为1,所以Ndx为1(readelf -a 或 objdump -x)
    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf # 在SimpleSection中被引用,但没有定义
    15: 0000000000000022    51 FUNC    GLOBAL DEFAULT    1 main   # 与func1函数一样
# 说明
1. Bind: LOCAL: 局部符号(外部不可见),GLOBAL: 全局符号(外部可见),WEAK: 弱引用
2. Type: NOTYPE: 未知类型   SECTION: 该符号表示一个段(必须是LOCAL) OBJECT: 数据对象(变量、数组等) FUNC: 函数或其他可执行代码  FILE: 源文件名(必须是LOCAL,并且Ndx为ABS)
3. Ndx: 符号所在的段, ABS: 符号包含一个绝对的值(文件名)  COMMON: “COMMON块”类型,一般来说是未初始化的全局符号定义  UNDEF: 未定义
  1. 相关数据结构
#32位的段表结构为Elf32_Sym,64位的elf结构体为Elf64_Sym,成员完全一样,只是类型大小不一样
$ cat /usr/include/elf.h  
...
typedef struct
{
  Elf32_Word    st_name;                /* Symbol name (string tbl index) */
  Elf32_Addr    st_value;               /* Symbol value */
  Elf32_Word    st_size;                /* Symbol size */
  unsigned char st_info;                /* Symbol type and binding */ # 低4位为符号类型,高4位为符号绑定信息
  unsigned char st_other;               /* Symbol visibility */
  Elf32_Section st_shndx;               /* Section index */
} Elf32_Sym;

typedef struct
{
  Elf64_Word    st_name;                /* Symbol name (string tbl index) */
  unsigned char st_info;                /* Symbol type and binding */
  unsigned char st_other;               /* Symbol visibility */
  Elf64_Section st_shndx;               /* Section index */
  Elf64_Addr    st_value;               /* Symbol value */
  Elf64_Xword   st_size;                /* Symbol size */
} Elf64_Sym;

三、结论

  1. 目标文件是以段的结构组织起来的,通过ELF文件头可以知道基本信息和重要的段的具体位置,比如字符串表和段表位置

  2. 段表通常是一个段,记录了目标文件所有段的具体位置和大小,可以清楚知道整个段结构布局

posted @ 2021-10-30 15:05  回忆浅唱  阅读(115)  评论(0编辑  收藏  举报