認識目標文件的內容


一、目標文件基本闡述

  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. 段表通常是一個段,記錄了目標文件所有段的具體位置和大小,可以清楚知道整個段結構布局


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM