天天看點

《程式員的自我修養——連結、裝在與庫》第三章《目标檔案裡有什麼》讀書筆記

一. 目标檔案的概念

  1. 編譯器編譯源代碼後生成的檔案叫做目标檔案。
  2. 從結構上講,它是編譯後的可執行檔案格式,隻是還沒有經過連結過程。

二. 目标檔案的格式

  1. PC平台上主流的可執行檔案格式(Executable)主要有
    • Windows下的PE(Portable Executable)
    • Linux的ELF(Executable)
    它們都是COFF(Common file format)格式的變種。
  2. ELF分類
    ELF檔案類型 說明 執行個體
    可重定位檔案(Relocateable File) 包含代碼和資料,可以連結成可執行檔案或共享目标檔案 Linux的 .o .a,Windows的.obj .lib
    可執行檔案(Executeable File) 包含可直接執行的程式,它的代表就是ELF可執行檔案 Linux的/bin/bash檔案,Windows的.exe
    共享目标檔案(Shared Object File) 包含代碼和資料。連結器可以使用它跟其他可重定位檔案或共享目标檔案連結,産生新的目标檔案。 動态連結器可以将幾個共享目标檔案與可執行檔案結合,作為程序映像的一部分來運作 Linux的.so,Windows的.dll
    核心轉存儲檔案(Core Dump File) 當程序意外終止時,系統可以将該程序的位址空間的内容及終止時的一些其他資訊轉存儲到核心轉存儲檔案 Linux下的core dump

三. 目标檔案是什麼樣的

内容
File Header 描述整個檔案的屬性,包括檔案是否可執行,是靜态連結還是動态連結以及入口位址(如果是位置相關的可執行檔案),目标硬體,目标作業系統等資訊,檔案頭還包括一個段表(Section Table)
.text(or .code) section 代碼段
.data section 已初始化的全局變量和靜态變量
.bss section 未初始化的全局變量和靜态變量

目标檔案可分為程式指令和程式資料,這樣做的好處有以下幾個方面:

  1. 程式被裝載後,資料和指令分别被映射到兩個虛拟區域,資料區可讀寫,指令區隻讀,這樣可以防止程式指令被改寫。
  2. 現代CPU的緩存一般都被設計成資料緩存和指令緩存分離,是以程式的指令和資料分離對提高CPU的緩存命中率有好處。
  3. 副本程序之間可以共享程式指令。

四. 通過一個.o來了解目标檔案的段

實驗環境為:
GNU ld version -el7   
gcc version   (Red Hat -) (GCC)  
CentOS Linux release  (Core)
           

源代碼如下:

int g_init_var = ;
int g_unint_var;

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

int main(void) {
    static int static_var = ;
    static int static_var2;
    int a = ;
    int b;
    func1(static_var2 + static_var + a + b);
    return a;
}
           

執行

$ gcc -c SimpleSection.c 
           

編譯結果為 SimpleSection.o。執行

$ objdump -h SimpleSection.o
           

檢視段基本資訊。

SimpleSection.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
   .text                 **
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
   .data                 **
                  CONTENTS, ALLOC, LOAD, DATA
   .bss                c  **
                  ALLOC
   .rodata             c  **
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
   .comment      d      a0  **
                  CONTENTS, READONLY
   .note.GNU-stack       cd  **
                  CONTENTS, READONLY
   .eh_frame           d0  **
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

           

每個段資訊的第二行中的CONTENTS、ALLOC等表示了該段的屬性,CONTENTS表示該段在ELF檔案中存在。

4.1. 代碼段

執行

$ objdump -s -d SimpleSection.o
           

看所有段資訊,以及反彙編結果。

SimpleSection.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
   .text                 **
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
   .data                 **
                  CONTENTS, ALLOC, LOAD, DATA
   .bss                c  **
                  ALLOC
   .rodata             c  **
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
   .comment      d      a0  **
                  CONTENTS, READONLY
   .note.GNU-stack       cd  **
                  CONTENTS, READONLY
   .eh_frame           d0  **
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
[[email protected] ~]$ objdump -s -d SimpleSection.o

SimpleSection.o:     file format elf64-x86-

Contents of section .text:
   ec10 dfc8b fc89c6  UH..H....}..E...
  bf000000 b80000  c9  ................
  c3554889 e54883ec c745fc   .UH..H....E.....
  b150000 b05  c28b45  ...............E
  fc01c28b f801d0 c7e800 b  ....E...........
  fcc9c3                             E...            
Contents of section .data:
                       T...U...        
Contents of section .rodata:
  a00                             %d..            
Contents of section .comment:
   a202847  e  .GCC: (GNU) .
         (Red 
   e d3429         Hat -).   
Contents of section .eh_frame:
    a5200   .........zR..x..
  b0c0708  c000000 c000000  ................
     d  ....!....A....C.
  c0c07  c000000 c000000  .\..........<...
     d  .......A....C.
  c07                     .n......        

Disassembly of section .text:

 <func1>:
   :                         push   %rbp
   :     e5                mov    %rsp,%rbp
   :     ec              sub    $,%rsp
   :    d fc                mov    %edi,-(%rbp)
   b:   b  fc                mov    -(%rbp),%eax
   e:    c6                   mov    %eax,%esi
  :   bf              mov    $,%edi
  :   b8              mov    $,%eax
  a:   e8              callq  f <func1+>
  f:   c9                      leaveq 
  :   c3                      retq   

 <main>:
  :                         push   %rbp
  :     e5                mov    %rsp,%rbp
  :     ec              sub    $,%rsp
  :   c7  fc        movl   $,-(%rbp)
  :   b            mov    (%rip),%edx        # 36 <main+0x15>
  :   b            mov    (%rip),%eax        # 3c <main+0x1b>
  c:    c2                   add    %eax,%edx
  e:   b  fc                mov    -(%rbp),%eax
  :    c2                   add    %eax,%edx
  :   b  f8                mov    -(%rbp),%eax
  :    d0                   add    %edx,%eax
  :    c7                   mov    %eax,%edi
  a:   e8              callq  f <main+>
  f:   b  fc                mov    -(%rbp),%eax
  :   c9                      leaveq 
  :   c3                      retq   
           
  • -s 将所有段的内容以十六進制的方式列印出來
  • -d 将所有指令反彙編
  • 列印結果的最左邊一列是偏移量,中間四列是十六進制内容,最右邊一列是段内容的ASCII碼

4.2 資料段和隻讀資料段

  1. 單獨設立.rodata段的好處:
    • 語義上支援了C/C++的const關鍵字
    • 作業系統在加載的時候可以将.rodata段映射成隻讀,對于這個段任何修改操作都會作為非法操作處理,保證了程式的安全性
  2. 有些編譯器會把常量字元串放到.data段

4.3 BSS段

有些編譯器會将未初始化的變量存放在目标檔案的.bss段,有些則不存放,隻是預留一個未定義的全局變符号

4.4 其他段

常用段名 說明
.rodata1 Read Only Data,跟.rodata一樣
.comment 存放編譯器版本資訊,比如字元串: GCC:(GNU)4.9
.debug 調試資訊
.dynamic 動态連結資訊
.hash 符号hash表
.line 調試時的行号表,即源代碼行号與編譯後指令對應表
.note 額外的編譯器資訊
.strtab String Table 字元串表,用于存儲ELF檔案中用到的各種字元串
.symtab Symbol Table 符号表
.shstrtab Section String Table 段名表
.plt .got 動态連結的跳轉表和全局入口表
.init .fini 程式初始化與終結代碼段

這些段名都是以 . 作為字首,表示這些名字是系統保留的,應用程式也可以使用一些非系統保留的名字作為段名。比如可以在ELF檔案中插入一個 music 段,裡面存放一些MP3音樂(可以用gobjcopy實作),但是應用程式自定義的段名不能使用 . 作為字首,否則容易跟系統保留段名沖突。

五. ELF檔案結構描述

ELF總體結構如下:

ELF Header
.text
.data
.bss
other secitons
Section header table
String Tables
Symbol Tables

5.1 檔案頭

執行

$ readelf -h SimpleSection.o
           

檢視檔案頭資訊。

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  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:           (bytes into file)
  Start of section headers:           (bytes into file)
  Flags:                             
  Size of this header:                (bytes)
  Size of program headers:            (bytes)
  Number of program headers:         
  Size of section headers:            (bytes)
  Number of section headers:         
  Section header string table index: 
           

ELF檔案頭的資料結構在 /usr/include/elf.h 中的 Elf64_Ehdr 定義。

typedef uint16_t Elf64_Half;
typedef uint32_t Elf64_Word;
typedef int32_t  Elf64_Sword;
typedef uint64_t Elf64_Xword;
typedef int64_t  Elf64_Sxword;
typedef uint64_t Elf64_Addr;
typedef uint64_t Elf64_Off;
typedef uint16_t Elf64_Section;
typedef Elf64_Half Elf64_Versym;

#define EI_NIDENT (16)

typedef struct
{
    /**
     Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
     Class:                             ELF64
     Data:                              2's complement, little endian
     Version:                           1 (current)
     OS/ABI:                            UNIX - System V
     ABI Version:                       0
     */
    unsigned char e_ident[EI_NIDENT];     /* Magic number and other info */

    /*Type:                              REL (Relocatable file)*/
    Elf64_Half    e_type;                 /* Object file type */

    /*Machine:                           Advanced Micro Devices X86-64*/
    Elf64_Half    e_machine;              /* Architecture */

    /*Version:                           0x1*/
    Elf64_Word    e_version;              /* Object file version */

    /*Entry point address:               0x0*/
    Elf64_Addr    e_entry;                /* Entry point virtual address */

    /*Start of program headers:          0 (bytes into file)*/
    Elf64_Off     e_phoff;                /* Program header table file offset */

    /*Start of section headers:          400 (bytes into file)*/
    Elf64_Off     e_shoff;                /* Section header table file offset */

    /*Flags:                             0x0*/
    Elf64_Word    e_flags;                /* Processor-specific flags */

    /*Size of this header:               64 (bytes)*/
    Elf64_Half    e_ehsize;               /* ELF header size in bytes */

    /*Size of program headers:           0 (bytes)*/
    Elf64_Half    e_phentsize;            /* Program header table entry size */

    /*Number of program headers:         0*/
    Elf64_Half    e_phnum;                /* Program header table entry count */

    /*Size of section headers:           64 (bytes*/
    Elf64_Half    e_shentsize;            /* Section header table entry size */

    /*Number of section headers:         13*/
    Elf64_Half    e_shnum;                /* Section header table entry count */

    /*Section header string table index: 10*/
    Elf64_Half    e_shstrndx;             /* Section header string table index */
} Elf64_Ehdr;
           
  1. 魔數(Magic number)

    7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00

    • 這16個位元組對應 Elf64_Ehdr 的 e_ident。
    • 最開始4個位元組是所有ELF都必須相同的辨別碼,7f對應ASCII字元裡的DEL控制符,45 4c 46對于ASCII字元是ELF。
    • 第五個位元組 01 表示是32位的,如果是 02 則表示是64位的。
    • 第六個位元組 01 是位元組序,00 表示無效格式,01 表示小端,02表示大端。
    • 第七個位元組 01 是ELF檔案的主版本号,一般是 01,因為ELF标準自1.2版本以後再也沒有更新過。
    • 後面九個位元組ELF标準沒有定義,一般填0。
  2. 檔案類型(Type)

    e_type表示ELF檔案類型,系統通過這個變量來判斷ELF檔案的類型,而不是擴充名。相關常量類型定義如下:

    /* Legal values for e_type (object file type).  */
    
    #define ET_NONE         0               /* No file type */
    
    
    #define ET_REL          1               /* Relocatable file */
    
    
    #define ET_EXEC         2               /* Executable file */
    
    
    #define ET_DYN          3               /* Shared object file */
    
    
    #define ET_CORE         4               /* Core file */
               
  3. 機器類型(Machine)
    /* Legal values for e_machine (architecture).  */
    
    #define EM_NONE          0              /* No machine */
    
    
    #define EM_M32           1              /* AT&T WE 32100 */
    
    
    #define EM_SPARC         2              /* SUN SPARC */
    
    
    #define EM_386           3              /* Intel 80386 */
    
    
    #define EM_68K           4              /* Motorola m68k family */
    
    
    #define EM_88K           5              /* Motorola m88k family */
    
    
    #define EM_860           7              /* Intel 80860 */
    
    
    #define EM_MIPS          8              /* MIPS R3000 big-endian */
    
    
    #define EM_S370          9              /* IBM System/370 */
    
    
    #define EM_MIPS_RS3_LE  10              /* MIPS R3000 little-endian */
    
    /*此省略若幹個CPU*/   
               

5.2 段表

段表描述了ELF的各個段的資訊,比如段名、段的長度、段的偏移、段的讀寫權限以及其他屬性。也就是說,ELF檔案的段結構就是由段表決定的。執行

$ readelf -S SimpleSection.o
           

檢視段表資訊。

There are  section headers, starting at offset :

Section Headers:
  [Nr] Name             Type      Address           Offset   Size              EntSize           Flags  Link  Info  Align
  [ ]                  NULL                                
  [ ] .text            PROGBITS         AX                 
  [ ] .rela.text       RELA        b0                       
  [ ] .data            PROGBITS         WA                 
  [ ] .bss             NOBITS      c     WA                 
  [ ] .rodata          PROGBITS    c      A                 
  [ ] .comment         PROGBITS    a0 d    MS                 
  [ ] .note.GNU-stack  PROGBITS    cd                        
  [ ] .eh_frame        PROGBITS    d0      A                 
  [ ] .rela.eh_frame   RELA                               
  [] .shstrtab        STRTAB                              
  [] .symtab          SYMTAB      d0                      
  [] .strtab          STRTAB       b                       
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)
           

ELF段表資料結構在 elf.h 中的 Elf64_Shdr 定義。Elf64_Shdr又被稱為段描述符(Section Descriptor)。

/* Section header.  */
typedef struct
{
  Elf64_Word    sh_name;        /* Section name (string tbl index) */
  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. 段名(sh_name)

    段名 對于編譯器、連結器是有意義的,對作業系統并沒有實際意義。作業系統通過sh_type和sh_flags來決定如何處理該段。

  2. 段類型(sh_type)
    /* Legal values for sh_type (section type).  */
    
    #define SHT_NULL          0             /* Section header table entry unused */
    
    
    #define SHT_PROGBITS      1             /* Program data */
    
    
    #define SHT_SYMTAB        2             /* Symbol table */
    
    
    #define SHT_STRTAB        3             /* String table */
    
    
    #define SHT_RELA          4             /* Relocation entries with addends */
    
    
    #define SHT_HASH          5             /* Symbol hash table */
    
    
    #define SHT_DYNAMIC       6             /* Dynamic linking information */
    
    
    #define SHT_NOTE          7             /* Notes */
    
    
    #define SHT_NOBITS        8             /* Program space with no data (bss) */
    
    
    #define SHT_REL           9             /* Relocation entries, no addends */
    
    
    #define SHT_SHLIB         10            /* Reserved */
    
    
    #define SHT_DYNSYM        11            /* Dynamic linker symbol table */
    
    
    #define SHT_INIT_ARRAY    14            /* Array of constructors */
    
    
    #define SHT_FINI_ARRAY    15            /* Array of destructors */
    
    
    #define SHT_PREINIT_ARRAY 16            /* Array of pre-constructors */
    
    
    #define SHT_GROUP         17            /* Section group */
    
    
    #define SHT_SYMTAB_SHNDX  18            /* Extended section indeces */
    
    
    #define SHT_NUM           19            /* Number of defined types.  */
    
    
    #define SHT_LOOS          0x60000000    /* Start OS-specific.  */
    
    
    #define SHT_GNU_ATTRIBUTES 0x6ffffff5   /* Object attributes.  */
    
    
    #define SHT_GNU_HASH      0x6ffffff6    /* GNU-style hash table.  */
    
    
    #define SHT_GNU_LIBLIST   0x6ffffff7    /* Prelink library list */
    
    
    #define SHT_CHECKSUM      0x6ffffff8    /* Checksum for DSO content.  */
    
    
    #define SHT_LOSUNW        0x6ffffffa    /* Sun-specific low bound.  */
    
    
    #define SHT_SUNW_move     0x6ffffffa
    
    
    #define SHT_SUNW_COMDAT   0x6ffffffb
    
    
    #define SHT_SUNW_syminfo  0x6ffffffc
    
    
    #define SHT_GNU_verdef    0x6ffffffd    /* Version definition section.  */
    
    
    #define SHT_GNU_verneed   0x6ffffffe    /* Version needs section.  */
    
    
    #define SHT_GNU_versym    0x6fffffff    /* Version symbol table.  */
    
    
    #define SHT_HISUNW        0x6fffffff    /* Sun-specific high bound.  */
    
    
    #define SHT_HIOS          0x6fffffff    /* End OS-specific type */
    
    
    #define SHT_LOPROC        0x70000000    /* Start of processor-specific */
    
    
    #define SHT_HIPROC        0x7fffffff    /* End of processor-specific */
    
    
    #define SHT_LOUSER        0x80000000    /* Start of application-specific */
    
    
    #define SHT_HIUSER        0x8fffffff    /* End of application-specific */
               
  3. 段标志位(sh_flages)

    段标志位 表示段在程序空間中的屬性。

    /* Legal values for sh_flags (section flags).  */
    
    #define SHF_WRITE            (1 << 0)   /* Writable */
    
    
    #define SHF_ALLOC            (1 << 1)   /* Occupies memory during execution */
    
    
    #define SHF_EXECINSTR        (1 << 2)   /* Executable */
    
    
    #define SHF_MERGE            (1 << 4)   /* Might be merged */
    
    
    #define SHF_STRINGS          (1 << 5)   /* Contains nul-terminated strings */
    
    
    #define SHF_INFO_LINK        (1 << 6)   /* `sh_info' contains SHT index */
    
    
    #define SHF_LINK_ORDER       (1 << 7)   /* Preserve order after combining */
    
    
    #define SHF_OS_NONCONFORMING (1 << 8)   /* Non-standard OS specific handling
    
                                               required */
    
    #define SHF_GROUP            (1 << 9)   /* Section is member of a group.  */
    
    
    #define SHF_TLS              (1 << 10)  /* Section hold thread-local data.  */
    
    
    #define SHF_MASKOS           0x0ff00000 /* OS-specific.  */
    
    
    #define SHF_MASKPROC         0xf0000000 /* Processor-specific */
    
    
    #define SHF_ORDERED          (1 << 30)  /* Special ordering requirement
    
                                               (Solaris).  */
    
    #define SHF_EXCLUDE          (1 << 31)  /* Section is excluded unless
    
                                           referenced or allocated (Solaris).*/
               
  4. 段連結資訊(sh_link, sh_info)

    如果段的類型是連結相關的(包括動态連結和靜态連結),比如重定位表、符号表等,那麼段連結資訊的含義如下表。否則段連結資訊無意義(值為0)。

    sh_type sh_link sh_info
    SHT_DYNAMIC 該段所使用的字元串在字元串表中的下标
    SHT_HASH 該段所使用的符号表在段表中的下标
    SHT_REL, SHT_RELA 該段使用的符号表在段表中的下标 該重定位表所作用的段在段表中的下标
    SHT_SYSTAB, SHT_DYNSYM 作業系統相關 作業系統相關
  5. 段位址對齊(sh_addralign)

    有些段是有位址對齊要求的,比如我們假設有個段剛開始的位置包含了一個double變量(8個位元組),因為Intel x86架構要求浮點數的存儲位址必須是本身位址的整數倍,那麼這個段的sh_addr就必須是8的整數倍。位址對齊的數量都是2的指數倍,sh_addralign即表示對齊數量中的指數。如果sh_addralign為0或1,則表示該段對位址對齊沒有要求。

    [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [12] .strtab STRTAB 0000000000000000 00000650 000000000000005b 0000000000000000 0 0 1 [ 2] .rela.text RELA 0000000000000000 000006b0 0000000000000078 0000000000000018 11 1 8

    從上表可以看到.shsstrtab的結束位址為 0x650 + 0x5b = 0x6ab,這與.rela.text開始位址并不一緻,這就是位址對齊造成的。
  6. 項的長度(sh_entsize)

    有些段包含了一些固定大小的項,比如符号表,sh_entsize就表示每個固定項的大小。

5.3 重定位表

[Nr] Name             Type      Address           Offset   Size              EntSize           Flags  Link  Info  Align
 [ 2] .rela.text       RELA      0000000000000000  000006b0 0000000000000078  0000000000000018          11     1     8
           

這個段的類型(sh_type)為SH_REL,是一個重定位表。連接配接器在處理ELF檔案時,必須對ELF檔案的代碼段和資料段中那些對絕對位址引用的位置進行重定位。這些定位的資訊都記錄在ELF檔案的重定位表裡,對于每個需要重定位的代碼段或資料段,都會有一個相應的重定位表。

[Nr] Name             Type      Address           Offset   Size              EntSize           Flags  Link  Info  Align [ 1] .text            PROGBITS  0000000000000000  00000040 0000000000000054  0000000000000000  AX       0     0     4
[11] .symtab          SYMTAB    0000000000000000  000004d0 0000000000000180  0000000000000018          12    11     8
           

.rela.text的Link(sh_link)表示符号表的下标,也就是是.symtab的下标11;它的Inf(sh_info)表示它作用于哪個段,顯然它作用于.text段,該段對應的下标是1。

5.4 字元串表

ELF檔案中用到了很多字元串,比如段名、變量名等。因為字元串長度往往是不定的,是以用固定的結構來表示它比較困難。是以ELF檔案把字元串集中起來放到了一個表,然後使用表偏移來引用字元串。常見的段表為.strtab(String Tab)和.shstrtab(Section Header String Table)。.strtab儲存普通的字元串,比如符号的名字;.shstrtab儲存段表中用到的字元串,比如段名(sh_name)。

在5.1 檔案頭中ELF檔案頭資訊中有一個e_shstrndx(Section header string table index)值為10,正好對應.shstrtab的下标。

六. 連結的接口——符号

在連結中,我們将函數和變量統稱為符号(Symbol),函數名活變量名就是符号名(Symbol Name)。符号分為以下幾類:

  1. 定義在本目标檔案的全局符号,可以被其他目标檔案引用。比如SimpleSection.o中的func1、main、g_init_var。
  2. 在目标檔案中引用,卻未定義的全局符号,一般叫做外部符号(External Symbol)。比如SimpleSection.o中的printf。
  3. 段名,這種符号往往由編譯器産生,它的值就是該段的起始位址。
  4. 局部符号,這類符号隻在編譯單元内部課件。比如SimpleSection.o中的static_var和static_var2。
  5. 行号資訊。

1 2 對其他目标檔案不可見的。3 4 5對其他目标檔案是不可見的。可以用gnm檢視符号。

$ nm SimpleSection.o
           
T func1
 D g_init_var
 C g_unint_var
 T main
                 U printf
 d static_var
 b static_var2
           

6.1 ELF符号表結構

ELF檔案中符号表往往是檔案中的一個段,段名叫.symtab。其結構對應elf.h中的Elf64_Sym。

/* Symbol table entry.  */
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. 符号類型和綁定資訊(st_info)
    • st_info的低4位表示符号類型,高4位表示綁定資訊
      /* How to extract and insert information held in the st_info field.  */
      
      #define ELF32_ST_BIND(val)          (((unsigned char) (val)) >> )
      
      
      #define ELF32_ST_TYPE(val)          ((val) & )
      
      
      #define ELF32_ST_INFO(bind, type)   (((bind) << ) + ((type) & 0xf))
      
      
      /* Both Elf32_Sym and Elf64_Sym use the same one-byte st_info field.  */
      
      #define ELF64_ST_BIND(val)          ELF32_ST_BIND (val)
      
      
      #define ELF64_ST_TYPE(val)          ELF32_ST_TYPE (val)
      
      
      #define ELF64_ST_INFO(bind, type)   ELF32_ST_INFO ((bind), (type))
                 
    • 符号綁定資訊
      /* Legal values for ST_BIND subfield of st_info (symbol binding).  */
      
      #define STB_LOCAL       0       /* Local symbol */
      
      
      #define STB_GLOBAL      1       /* Global symbol */
      
      
      #define STB_WEAK        2       /* Weak symbol */
      
      
      #define STB_NUM         3       /* Number of defined types.  */
      
      
      #define STB_LOOS        10      /* Start of OS-specific */
      
      
      #define STB_GNU_UNIQUE  10      /* Unique symbol.  */
      
      
      #define STB_HIOS        12      /* End of OS-specific */
      
      
      #define STB_LOPROC      13      /* Start of processor-specific */
      
      
      #define STB_HIPROC      15      /* End of processor-specific */
                 
    • 符号類型
      /* Legal values for ST_TYPE subfield of st_info (symbol type).  */
      
      #define STT_NOTYPE             /* Symbol type is unspecified */
      
      
      #define STT_OBJECT             /* Symbol is a data object *//*比如變量、數組等*/
      
      
      #define STT_FUNC               /* Symbol is a code object *//*函數或其他可執行代碼*/
      
      
      #define STT_SECTION            /* Symbol associated with a section *//*必須是STB_LOCAL*/
      
      
      #define STT_FILE               /* Symbol's name is file name */
      
                                      /*一般都是源檔案名,一點是STB_LOCAL,并且它的st_shndx一定是SHN_ABS*/
      
      #define STT_COMMON             /* Symbol is a common data object */
      
      
      #define STT_TLS                /* Symbol is thread-local data object*/
      
      
      #define STT_NUM                /* Number of defined types.  */
      
      
      #define STT_LOOS              /* Start of OS-specific */
      
      
      #define STT_GNU_IFUNC         /* Symbol is indirect code object */
      
      
      #define STT_HIOS              /* End of OS-specific */
      
      
      #define STT_LOPROC            /* Start of processor-specific */
      
      
      #define STT_HIPROC            /* End of processor-specific */
                 
  2. 符号所在段(st_stndx)

    如果符号定義在目标檔案中,則表示符号所在段的下标;如果不是定義在目标檔案中,或者對于有些特殊符号,sh_shndx的值有些特殊。

    /* Special section indices.  */
    
    #define SHN_UNDEF       0       /* Undefined section */
    
                                    /*符号在目标檔案中被引用到,但是定義在其他目标檔案中。*/
    
    #define SHN_LORESERVE   0xff00      /* Start of reserved indices */
    
    
    #define SHN_LOPROC      0xff00      /* Start of processor-specific */
    
    
    #define SHN_BEFORE      0xff00      /* Order section before all others(Solaris). */
    
    
    #define SHN_AFTER       0xff01      /* Order section after all others(Solaris). */
    
    
    #define SHN_HIPROC      0xff1f      /* End of processor-specific */
    
    
    #define SHN_LOOS        0xff20      /* Start of OS-specific */
    
    
    #define SHN_HIOS        0xff3f      /* End of OS-specific */
    
    
    #define SHN_ABS         0xfff1      /* Associated symbol is absolute */
    
                                        /*表示該符号包含了一個絕對的值*/
    
    #define SHN_COMMON      0xfff2      /* Associated symbol is common */
    
                                        /*表示該符号是一個 COMMON塊 類型的符号,
                                          一般來說,未初始化的全局符号就是這種類型的*/
    
    #define SHN_XINDEX      0xffff      /* Index is in extra table.  */
    
    
    #define SHN_HIRESERVE   0xffff      /* End of reserved indices */
               

    COMMON塊 現在的編譯器和連結器都支援一種叫COMMON塊(Common Block)的機制,這種叫法最早來源于Fortran,早期的Fortran沒有動态配置設定空間的機制,程式員必須事先聲明所需要使用的臨時空間的大小。Fortran把這種空間叫COMMON塊,當不同的目标檔案需要的COMMON塊大小不一緻時,以最大的那塊為準。

    現代連結機制在處理弱符号(未初始化的全局變量)的時候,采用的就是與COMMON塊一樣的機制。

  3. 符号可見性(st_other)
    /* Symbol visibility specification encoded in the st_other field.  */
    
    #define STV_DEFAULT     0       /* Default symbol visibility rules */
    
    
    #define STV_INTERNAL    1       /* Processor specific hidden class */
    
    
    #define STV_HIDDEN      2       /* Sym unavailable in other modules */
    
    
    #define STV_PROTECTED   3       /* Not preemptible, not exported */
               
    • STV_DEFAULT 用它定義的符号将被導出。換句話說,它聲明符号是到處可見的。
    • STV_INTERNAL 符号在目前可執行檔案或共享庫之外不可通路。
    • STV_HIDDEN 用它定義的符号将不被導出,并且不能從其他對象進行使用。這将不允許它們在庫中被導出,但是可以在源檔案之間共享。實際上,隐藏的符号将不會出現在動态符号表中,但是還被留在符号表中用于靜态連結。
    • STV_PROTECED 符号在目前可執行檔案或共享對象之外可見,但是不會被覆寫。換句話說,如果共享庫中的一個受保護符号被該共享庫中的另一個代碼引用,那麼此代碼将總是引用共享庫中的此符号,即便可執行檔案定義了相同名稱的符号。
  4. 符号值(st_value)

    符号值分類:

    • 如果符号有定義并且不是COMMON塊類型,則st_value表示該符号在段中的偏移。
    • 如果符号是COMMON塊類型的,則st_value表示該符号的對齊屬性。
    • 在可執行程式中,st_value表示符号的虛拟位址。
    執行
    $ readelf -s SimpleSection.o
               
    檢視符号表詳細資訊。
    Symbol table '.symtab' contains 16 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         :       NOTYPE  LOCAL  DEFAULT  UND 
         :       FILE    LOCAL  DEFAULT  ABS SimpleSection.c
         :       SECTION LOCAL  DEFAULT     
         :       SECTION LOCAL  DEFAULT     
         :       SECTION LOCAL  DEFAULT     
         :       SECTION LOCAL  DEFAULT     
         :       OBJECT  LOCAL  DEFAULT     static_var2
         :       OBJECT  LOCAL  DEFAULT     static_var
         :       SECTION LOCAL  DEFAULT     
         :       SECTION LOCAL  DEFAULT     
        :       SECTION LOCAL  DEFAULT     
        :       OBJECT  GLOBAL DEFAULT     g_init_var
        :       OBJECT  GLOBAL DEFAULT  COM g_unint_var
        :      FUNC    GLOBAL DEFAULT     func1
        :       NOTYPE  GLOBAL DEFAULT  UND printf
        :      FUNC    GLOBAL DEFAULT     main
               
    詳解如下:
    • func1和main都是定義在SimpleSection.o中的函數,func1所處的位置為.text.func1段,下标為4(見5.1段表中的段表詳細資訊),是以Ndx值為4,main的情況一樣。它們是函數,是以類型是STT_FUNC;它們是全局符号,是以綁定資訊是STB_GLOBAL;Size表示函數所占的位元組數;Value表示函數相對段起始位置的偏移量。
    • printf該符号在SimpleSection.o中被引用,但是沒有被定義,是以Ndx是SHN_UNDEF。
    • g_init_var是已經初始化的全局變量,在.data段,是以下标是2。
    • g_uninit_var是為初始化的全局變量,它是COMMON塊類型的符号,它本身并沒有存在于.bss段。
    • 對于那些STT_SETION類型的符号,它們具體在哪個段是由Ndx決定的,即下标為Ndx的段。
    • SimpleSection.c 表示目标檔案的源檔案名。

5.3 特殊符号

特殊符号:當我們使用ld來連結生成可執行檔案時,它會為我們定義很多特殊的符号,這些符号并沒有在你的程式中定義,但是你可以直接聲明并引用它。幾個很具代表性的符号如下。

  • __executable_start 程式的起始位址,不是入口位址,是程式最開始的地方。
  • __etext或_etext或etext 代碼段的結束位址。
  • _edata或edata 資料段結束位址。
  • _end或end 程式結束位址。

舉個例子:

系統配置為:
GNU ld version -el7   
gcc version   (Red Hat -) (GCC)  
CentOS Linux release  (Core)
           
#include <stdio.h>

extern char __executable_start[];
extern char etext[], _etext[], __etext[];
extern char edata[], _edata[], __edata[];
extern char end[], _end[];

int main() {
    printf("executable start %p\n", __executable_start);
    printf(".text section end %p %p %p\n", etext, _etext, __etext);
    printf(".data section end %p %p\n", edata, _edata);
    printf("executable end %p %p\n", end, _end);
    return 0;
}
           

執行

$ gcc SpecialSymbol.c -o SpecialSymbol
$ ./SpecialSymbol
executable start 
.text section end 0x40061d 0x40061d 0x40061d
.data section end 0x601034 0x601034
executable end 0x601038 0x601038
           

注:我們所使用的連結器沒有定義__edata。

5.4 符号修飾和函數名

在C語言發明時,已經存在了相當多的使用彙編編寫的庫和目标檔案。這就産生了一個問題,如果一個C程式要使用這些庫的話,C語言中不可以使用彙編庫中定義的函數和變量的名字作為符号名,否則會發跟彙編庫發送沖突。

為了防止類似的符号名沖突,UNIX下C語言就規定,C語言中所有的全局變量和函數經過編譯後都在相應的符号名前加上下劃線。而Fortran語言經過編譯後,所有的符号前後都加上下劃線。(現在已經不這麼玩了。)這個簡單的方法一定範圍内解決了符号沖突問題,但當程式很大時,還是可能會有符号沖突的問題。于是像C++這樣的後來設計的語言增加了命名空間的方法來解決多子產品的符号沖突問題。

  1. C++符号修飾
    int func(int);
    float func(float);
    
    class C {
        int func(int);
        class IC {
            int func(int);
        };
    };
    
    namespace M {
        int func(int);
        class C {
            int func(int);
        };
    }
               

    為了區分不同參數和不同名稱空間的同名函數,引入函數簽名的概念,函數簽名包含了一個函數的資訊,包括函數名、參數類型、所在的類和命名空間及其他資訊。

    編譯器和連結器在處理符号時,他們使用名稱修飾的方法,使得每個函數簽名對應一個修飾後的名稱(Decorated Name)。上面6個func函數簽名在GCC編譯器下,相對應的修飾後名稱如下:

    函數簽名 修飾後名稱(符号名)
    int func(int) _Z4funci
    float func(float) _Z4funcf
    int C::func(int) _ZN1C4funcEi
    int C::IC::func(int) _ZN1C2IC4funcEi
    int M::func(int) _ZN1M4funcEi
    int M::C::func(int) _ZN1M1C4funcEi
    GCC修飾規則:
    • 所有的符号都以_Z開頭
    • 對應嵌套的名字(符号外層有命名空間或類)後面緊跟N
    • 然後是各個空間、類的名字(如果有的話)和函數的名稱
    • 在參數清單之前加E
    • 最後是參數清單
    binutils提供了一個 c++filt(Mac下是gc++filt)工具來解析被修飾過的名稱。執行
    $c++filt _ZN1M4funcEii 
               
    輸出結果
    M::func(int, int)
               
    簽名和名稱修飾機制同樣适用全局變量和靜态變量,比如一個命名空間foo中的全局變量bar,它修飾後的名字是_ZN3foo3barE。可以看到,變量的類型并沒有被加入到修飾後的名稱中,是以不論bar是int還是float類型的,它修飾後的名稱都是一樣的。

5.5 extern “C”

C++為了與C相容,在符号管理上,C++有一個用來聲明或定義一個C的符号的 extern “C” {} 的用法。C++編譯器會将大括号内的代碼當作C代碼處理,這時C++的符号修飾機制将不再起作用。

5.6 弱符号與強符号

  1. 強符号與弱符号

    強符号 編譯器預設函數和已初始化的全局變量為強符号。

    弱符号 未初始化的全局變量為弱符号。

    連結器按如下規則處理多次定義的全局符号:

    • 不允許強符号被多次定義,如果有多個強符号定義,連結器會報符号重複定義錯誤。
    • 如果一個符号在A目标檔案中是強符号,在B目标檔案中是弱符号,那麼選擇強符号。
    • 如果一個符号在所有目标檔案中都是弱符号,那麼選擇占用空間最大的那個。
  2. 強引用和弱引用

    強引用 如果沒有找到該符号的定義,連結器就會報符号未定義錯誤。

    弱引用 如果沒有找到該符号的定義,連結器一般會預設其為0,或一個特殊的值。

    GCC可以使用 __attribute__((weakref)) 這個擴充關鍵字來聲明對一個外部函數的引用為弱引用。

這種弱符号和弱引用使得程式功能更加容易剪裁群組合。比如庫中定義的弱符号可以被使用者定義的強符号所覆寫,進而使得程式可以使用自定義的版本的庫函數。或者程式可以對某些擴充功能子產品的引用定義為弱引用,當我們将擴充子產品與程式連結在一起時,功能子產品正常使用;如果我們去掉了某些功能子產品,那麼程式也可以正常連結,隻是少了某些功能。

舉個例子:

#include <stdio.h>
#include <pthread.h>

int pthread_create(pthread_t*, const pthread_attr_t*, void* (*)(void*), void*) __attribute__((weak));

int main() {
    if (pthread_create) {
        printf("This is multi-thread version!\n");
    } else {
        printf("This is single-thread version!\n");
    }
}
           

執行

$ gcc pthread.c -o pt
$ ./pt
This is single-thread version!
           
$ gcc pthread.c -lpthread -o pt
$ ./pt
This is multi-thread version!
           

5.7 調試資訊

幾乎所有的現代編譯器都支援源代碼級别的調試。如果在GCC編譯時加上 -g 參數,編譯器就會在目标檔案裡加上很多調試資訊。

[Nr] Name              Type      Address           Offset    Size              EntSize          Flags  Link  Info  Align
[ ] .debug_info       PROGBITS    a0  c                       
[ ] .rela.debug_info  RELA        d38                        
[ ] .debug_abbrev     PROGBITS    cc  e                       
[ ] .debug_aranges    PROGBITS    a                         
[] .rela.debug_arang RELA        fc0                        
[] .debug_line       PROGBITS    a  a                       
[] .rela.debug_line  RELA        ff0                       
[] .debug_str        PROGBITS          MS                 
           

這些段中儲存的就是調試資訊。現在的ELF檔案采用一個叫DWARF(Debug With arbitrary Record Format)的标準調試資訊格式。

在Linux下,我們可以使用 strip 指令來去掉檔案中的調試資訊。

可以運作

驗證調試資訊是否去除。