程序的鏈接(二):ELF目標文件

在學習鏈接的具體過程前,有必要好好了解一下ELF目標文件。

ELF的目標文件分為三類:

  • 可重定位目標文件(.o)
  • 其代碼和數(shù)據(jù)可和其他可重定位文件合并為可執(zhí)行文件
  • 每個 .o 文件由對應(yīng)的 .c 文件生成
  • 每個 .o 文件的代碼和數(shù)據(jù)地址都是從0開始的偏移
  • 可執(zhí)行目標文件(默認為a.out)
  • 包含的代碼和數(shù)據(jù)可以被直接復制到內(nèi)存并執(zhí)行
  • 代碼和數(shù)據(jù)的地址是虛擬地址空間中的地址
  • 共享的目標文件(.so 共享庫)
  • 特殊的可重定位目標文件,能在裝載到內(nèi)存或運行時自動被鏈接,稱為共享庫文件

可通過objdump命令對比可重定位目標文件和可執(zhí)行目標文件的不同:

image.png

可以看到,確實可重定位目標文件中的地址是從0開始的,而可執(zhí)行目標文件中的地址是虛擬地址空間中的地址。


接下來介紹ELF文件的兩種視圖:

  • 鏈接視圖:可重定位文件(Relocatable object files)
  • 執(zhí)行視圖:可執(zhí)行目標文件(Executable object files)


    image.png

鏈接視圖 —— 可重定位目標文件

來看一個簡單的C代碼及其所生成的可重定位目標文件的關(guān)系圖:


image.png

如上圖,編譯后的代碼部分放到 .text節(jié),已初始化的全局變量和已初始化的靜態(tài)變量會放到 .data節(jié),未初始化的全局變量和未初始化的靜態(tài)變量會放到 .bss節(jié)。
實際上,為了進行鏈接,可重定位目標文件還需要許多其他信息,如符號表、重定位信息等。這些后面會陸續(xù)介紹。

在這里要特別說明一下 .bss節(jié)。該節(jié)在可重定位目標文件中并不占用空間,只是在節(jié)頭表相應(yīng)的表項中說明要為 .bss節(jié)預留多大的空間。

可重定位目標文件中包含有很多的節(jié),格式如下圖:


image.png

其中:

  • ELF頭

包括16字節(jié)的標識信息、文件類型(.o,exec,.so)、機器類型(如Intel 80386)、節(jié)頭表的偏移、節(jié)頭表的表項大小及表項個數(shù)。

  • .text節(jié)

編譯后的代碼部分。

  • .rodata節(jié)

只讀數(shù)據(jù),如 printf用到的格式串、switch跳轉(zhuǎn)表等。

  • .data節(jié)

已初始化的全局變量和靜態(tài)變量。

  • .bss節(jié)

未初始化全局變量和靜態(tài)變量,僅是占位符,不占據(jù)任何磁盤空間。區(qū)分初始化和非初始化是為了空間效率。

  • .symtab節(jié)

存放函數(shù)和全局變量(符號表)的信息,它不包括局部變量。

  • .rel.text節(jié)

.text節(jié)的重定位信息,用于重新修改代碼段的指令中的地址信息。

  • .rel.data節(jié)

.data節(jié)的重定位信息,用于對被模塊使用或定義的全局變量進行重定位的信息。

  • .debug節(jié)

調(diào)試用的符號表(gcc -g)

  • .strtab節(jié)

包含 .symtab節(jié)和 .debug節(jié)中的符號及節(jié)名

  • 節(jié)頭表(Section header table)

包含每個節(jié)的節(jié)名在.strtab節(jié)中的偏移、節(jié)的偏移和節(jié)的大小.

下邊分別舉例講一下ELF頭節(jié)頭表

-------------------------------------- ELF頭 (ELF Header)------------------------------
ELF頭位于ELF文件的開始,其包含了文件結(jié)構(gòu)的說明信息。其結(jié)構(gòu)體定義如下:

#define EI_NIDENT 16

typedef struct {
    unsigned char e_ident[EI_NIDENT];              
    uint16_t      e_type;
    uint16_t      e_machine;
    uint32_t      e_version;
    ElfN_Addr    e_entry;
    ElfN_Off      e_phoff;
    ElfN_Off      e_shoff;
    uint32_t      e_flags;
    uint16_t      e_ehsize;
    uint16_t      e_phentsize;
    uint16_t      e_phnum;
    uint16_t      e_shentsize;
    uint16_t      e_shnum;
    uint16_t      e_shstrndx;
} ElfN_Ehdr;

用readelf -h 查看ELF頭。下表是ELF頭中各個成員的含義與readelf輸出結(jié)果的對照表:

成員 readelf輸出結(jié)果及含義
e_ident Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
e_type Type: REL(Relocatable file)
ELF文件類型
e_machine Machine: Intel 80386
ELF文件的CPU平臺屬性,相關(guān)常量以EM_開頭
e_version Version: 0x1
ELF版本號。一般為常數(shù)1
e_entry Entry point address: 0x0
入口地址,規(guī)定ELF程序的入口虛擬地址,操作系統(tǒng)在加載完該程序后從這個地址開始執(zhí)行進程的指令??芍囟ㄎ荒繕宋募话銢]有入口地址,則這個值為0
e_phoff Start of program header: 0 (bytes into file)
程序頭表在文件中的偏移,可重定位目標文件不存在程序頭表,故該值為0
e_shoff Start of section header: 280(bytes into file)
節(jié)頭表在文件中的偏移
e_flags Flags: 0x0
ELF標志位,用來標識一些ELF文件平臺相關(guān)的屬性,相關(guān)常量的格式一般為EF_machine_flag,machine為平臺,flag為標志
e_ehsize Size of this header: 52(bytes)
即ELF文件頭本身的大小
e_phentsize Size of program headers: 0(bytes)
程序頭表表項的大小
e_phnum Number of program headers: 0
程序頭表表項的數(shù)目
e_shentsize Size of section headers: 40(bytes)
節(jié)表表項的大小
e_shnum Number of section headers: 11
節(jié)表表項的數(shù)目
e_shstrndx Section header string table index: 8
節(jié)表字符串表在節(jié)頭表中的下標

-------------------------------------- 節(jié)頭表(Section Header Table) ----------------------
除了ELF頭之外,節(jié)頭表是ELF可重定位目標文件中最重要的部分內(nèi)容。
它描述了每個節(jié)的節(jié)名、在文件中的偏移、大小、訪問屬性、對齊方式等。其32位結(jié)構(gòu)定義如下(每項占40字節(jié)):

typedef struct {
    Elf32_Word sh_name;   //節(jié)名字符串在.strtab中的偏移
    Elf32_Word sh_type;   //節(jié)類型:無效/代碼或數(shù)據(jù)/符號/字符串/…
    Elf32_Word sh_flags;  //節(jié)標志:該節(jié)在虛擬空間中的訪問屬性
    Elf32_Addr sh_addr;   //虛擬地址:若可被加載,則對應(yīng)虛擬地址
    Elf32_Off sh_offset;  //在文件中的偏移地址,對.bss節(jié)而言則無意義
    Elf32_Word sh_size;   //節(jié)在文件中所占的長度
    Elf32_Word sh_link;   //sh_link和sh_info用于與鏈接相關(guān)的節(jié)(如
    Elf32_Word sh_info;   //     .rel.text節(jié)、.rel.data節(jié)、.symtab節(jié)等)   
    Elf32_Word sh_addralign; //節(jié)的對齊要求
    Elf32_Word sh_entsize;   //節(jié)中每個表項的長度,0表示無固定長度表項
} Elf32_Shdr;

使用 readelf -S命令查看節(jié)頭表,示例如下:


image.png

image.png

A(alloc)標志表示該節(jié)將進程空間中必須要分配空間。可以看到,.text、.data、.bss、.rodata節(jié)都有這個標志。

執(zhí)行視圖 —— 可執(zhí)行目標文件

再來看ELF的執(zhí)行視圖,也就是可執(zhí)行目標文件的格式:


image.png

它與可重定位目標文件稍有不同:

  1. ELF文件頭中的字段e_entry給出了執(zhí)行程序時第一條指令的地址,而在可重定位目標文件中,此字段為0;程序頭表的偏移e_poff和大小e_phentsiz和程序頭表項的個數(shù)e_phnum不為0;
  2. 多了一個程序頭表,也稱為段頭表(segment header table),是一個結(jié)構(gòu)體數(shù)組;
  3. 多了一個 .init節(jié),用于定義 _init函數(shù),該函數(shù)用于在可執(zhí)行目標文件開始執(zhí)行時的初始化工作。
  4. 少了兩個 .rel節(jié)(.rel.text和.rel.data),因為可執(zhí)行目標文件已經(jīng)在鏈接的過程中完成了重定位,已無須重定位。

使用readelf -h 來看可執(zhí)行目標文件的ELF頭,示例如下


image.png

程序頭表描述的可執(zhí)行文件中的節(jié)(section)與虛擬地址空間中的存儲段(segment)之間的映射關(guān)系。

程序頭表的結(jié)構(gòu)定義如下:

typedef struct {
    Elf32_Word p_type;      //段類型
    Elf32_Off p_offset;        //該段在文件中的起始偏移
    Elf32_Addr p_vaddr;     //該段的虛擬地址
    Elf32_Addr p_paddr;     //該段的物理地址
    Elf32_Word p_filesz;     //該段在文件中的大小
    Elf32_Word p_memsz;  //該段在內(nèi)存中的大小
    Elf32_Word p_flags;     //段的標志位,表示訪問權(quán)限(Read|Write|Exec)
    Elf32_Word p_align;     //段在內(nèi)存中的對齊要求
} Elf32_Phdr;

下圖是用readelf -l 查看到的某可執(zhí)行目標文件的程序頭表:


image.png

Type為Load表示可裝入內(nèi)存的段。
第一個可裝入段:第0x00000~0x004d3字節(jié)(包括ELF頭、程序頭表、.init節(jié)、.text節(jié)和.rodata節(jié)),映射到虛擬地址0x8048000開始長度為0x4d4字節(jié)的區(qū)域,按0x1000=4KB對齊,具有只讀/執(zhí)行權(quán)限(Flg=RE),是只讀代碼段。
第二個可裝入段:第0x000f0c~開始長度為0x108字節(jié)的 .data節(jié),映射到虛擬地址0x8049f0c開始長度為0x110字節(jié)的存儲區(qū)域,在0x110=272B存儲區(qū)中,前0x108=264B用 .data節(jié)內(nèi)容初始化,后面272-264=8B對應(yīng).bss節(jié),初始化為0,按0x1000=64K對齊,具有可讀可寫權(quán)限(Flg=RW),是可讀寫數(shù)據(jù)段。

映射到虛擬地址空間中如圖:


image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容