天天看點

二進制檔案格式

一.libc.so

相信很多人都知道編譯一個c或者一個c++程式的時候都知道要經過預處理,編譯,彙編,連結這四步操作。大家都知道printf這個函數在stdio.h 這個頭檔案中,原來我在windows裡面的devc++看到了printf的定義,但是當我在linux裡面打開stdio.h的時候并沒有看到函數的定義,有的隻是函數的聲明,這大大激發了我對知識的渴望。後來我知道了printf這個函數是動态連結過來的。我們來看一下這段代碼。

1 #include <stdio.h>
  2          
  3 int main(void)
  4 {        
  5     printf("hello world");
  6     while (1)
  7         ;                                                                                                                                                                                
  8     return 0;
  9 }        
           

這段代碼用了個printf函數。我們編譯然後執行以下操作。

[email protected] ~ ./a.out&
[1] 3937
           
[email protected]  /proc/3937  cat  maps
56504cc3b000-56504cc3c000 r--p 00000000 103:09 1836512                   /home/lonelyeagle/a.out
56504cc3c000-56504cc3d000 r-xp 00001000 103:09 1836512                   /home/lonelyeagle/a.out
56504cc3d000-56504cc3e000 r--p 00002000 103:09 1836512                   /home/lonelyeagle/a.out
56504cc3e000-56504cc3f000 r--p 00002000 103:09 1836512                   /home/lonelyeagle/a.out
56504cc3f000-56504cc40000 rw-p 00003000 103:09 1836512                   /home/lonelyeagle/a.out
56504e481000-56504e4a2000 rw-p 00000000 00:00 0                          [heap]
7f5782648000-7f578264a000 rw-p 00000000 00:00 0 
7f578264a000-7f5782670000 r--p 00000000 103:08 1838455                   /usr/lib/libc-2.33.so
7f5782670000-7f57827bb000 r-xp 00026000 103:08 1838455                   /usr/lib/libc-2.33.so
7f57827bb000-7f5782807000 r--p 00171000 103:08 1838455                   /usr/lib/libc-2.33.so
7f5782807000-7f578280a000 r--p 001bc000 103:08 1838455                   /usr/lib/libc-2.33.so
7f578280a000-7f578280d000 rw-p 001bf000 103:08 1838455                   /usr/lib/libc-2.33.so
7f578280d000-7f5782818000 rw-p 00000000 00:00 0 
7f5782839000-7f578283a000 r--p 00000000 103:08 1838444                   /usr/lib/ld-2.33.so
7f578283a000-7f578285e000 r-xp 00001000 103:08 1838444                   /usr/lib/ld-2.33.so
7f578285e000-7f5782867000 r--p 00025000 103:08 1838444                   /usr/lib/ld-2.33.so
7f5782867000-7f5782869000 r--p 0002d000 103:08 1838444                   /usr/lib/ld-2.33.so
7f5782869000-7f578286b000 rw-p 0002f000 103:08 1838444                   /usr/lib/ld-2.33.so
7ffd24920000-7ffd24941000 rw-p 00000000 00:00 0                          [stack]
7ffd249c0000-7ffd249c4000 r--p 00000000 00:00 0                          [vvar]
7ffd249c4000-7ffd249c6000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]
           

這樣我們就可以看到整個程序的位址空間的檔案的映射。其中的 /usr/lib/libc-2.33.so 就是我們c程式在linux上運作時需要的動态連結庫。/usr/lib/ld-2.33.so就是linux下的動态連接配接器,是以printf這個函數從那裡來這個問題的答案已經顯而易見了。

二.elf檔案的格式

這裡不得不說的就是elf檔案的格式。可執行檔案,動态連結庫,靜态連結庫檔案都按照elf檔案格式存儲的,是以學習elf檔案格式對我們了解一個程式非常的有用。

我們先來看看下面這段代碼

1 int printf(const char* format, ... );
    2      
    3 int global_init_var = 84;
    4 int global_uninit_var;
    5      
    6 void funcl(int i)
    7 {    
    8     printf("%d\n", i);
    9 }    
   10      
   11 int main(void)
   12 {    
   13     static int static_var = 85;
   14     static int static_var2;
   15     int a = 1;
   16     int b;                                                                                                                    
   17     funcl(static_var + static_var2 + a + b);
   18     return 0;
   19 }    

           
gcc -c 測試.c    這個指令隻編譯不連結
           

我們把這個目标檔案的基本資訊列印出來

[email protected]  ~/程式員的自我修養  objdump -h 測試.o 

測試.o:     檔案格式 elf64-x86-64

節:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000005c  0000000000000000  0000000000000000  00000040  2**0    
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE      實際存在
  1 .data         00000008  0000000000000000  0000000000000000  0000009c  2**2
                  CONTENTS, ALLOC, LOAD, DATA                       實際存在
  2 .bss          00000008  0000000000000000  0000000000000000  000000a4  2**2
                  ALLOC
  3 .rodata       00000004  0000000000000000  0000000000000000  000000a4  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA            實際存在 
  4 .comment      00000013  0000000000000000  0000000000000000  000000a8  2**0
                  CONTENTS, READONLY                              實際存在
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000bb  2**0
                  CONTENTS, READONLY       但是size 為0 
  6 .note.gnu.property 00000030  0000000000000000  0000000000000000  000000c0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA   實際存在
  7 .eh_frame     00000058  0000000000000000  0000000000000000  000000f0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA   實際存在
           

不同的目标檔案可以擁有不同數量及不同類型的“段”

程式源代碼編譯後的機器指令被放在代碼段,代碼段常見的名字有“.code",".text",已初始化全局變量和局部靜态變量資料經常放在資料段,一般的名字都叫".data".未初始化的全局變量和局部靜态變量一般放在一個叫"bss"的段裡。

第2行中的contents ,alloc等表示段的屬性。contents表示該段在檔案中存在内容,.bss段沒有contents,表示他實際上在elf檔案中不存在内容。

我們對這個目标檔案反彙編

下面的是目标檔案中各個段的内容,有contents的就表示存在内容,是以隻顯示了下面幾個段的内容。

最左邊的是偏移量,中間的那些是内容,我們可以看到,一行分為4列,一列8個數字,也就是4個位元組,是以一行16個位元組。最右邊是内容用ascll碼翻譯出來的内容。

[email protected]  ~/程式員的自我修養  objdump -s -d 測試.o 

測試.o:     檔案格式 elf64-x86-64

Contents of section .text:
 0000 554889e5 4883ec10 897dfc8b 45fc89c6  UH..H....}..E...
 0010 488d0500 00000048 89c7b800 000000e8  H......H........
 0020 00000000 90c9c355 4889e548 83ec10c7  .......UH..H....
 0030 45f80100 00008b15 00000000 8b050000  E...............
 0040 000001c2 8b45f801 c28b45fc 01d089c7  .....E....E.....
 0050 e8000000 00b80000 0000c9c3           ............    
Contents of section .data:
 0000 54000000 55000000                    T...U...        
Contents of section .rodata:
 0000 25640a00                             %d..            
Contents of section .comment:
 0000 00474343 3a202847 4e552920 31312e31  .GCC: (GNU) 11.1
 0010 2e3000                               .0.             
Contents of section .note.gnu.property:
 0000 04000000 20000000 05000000 474e5500  .... .......GNU.
 0010 020001c0 04000000 00000000 00000000  ................
 0020 010001c0 04000000 01000000 00000000  ................
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 1c000000 1c000000  ................
 0020 00000000 27000000 00410e10 8602430d  ....'....A....C.
 0030 06620c07 08000000 1c000000 3c000000  .b..........<...
 0040 00000000 35000000 00410e10 8602430d  ....5....A....C.
 0050 06700c07 08000000                    .p......        
           

這個是反彙編的結果

Disassembly of section .text:

0000000000000000 <funcl>:
   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:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # 17 <funcl+0x17>
  17:   48 89 c7                mov    %rax,%rdi
  1a:   b8 00 00 00 00          mov    $0x0,%eax
  1f:   e8 00 00 00 00          call   24 <funcl+0x24>
  24:   90                      nop
  25:   c9                      leave  
  26:   c3                      ret    

0000000000000027 <main>:
  27:   55                      push   %rbp
  28:   48 89 e5                mov    %rsp,%rbp
  2b:   48 83 ec 10             sub    $0x10,%rsp
  2f:   c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)
  36:   8b 15 00 00 00 00       mov    0x0(%rip),%edx        # 3c <main+0x15>
  3c:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 42 <main+0x1b>
  42:   01 c2                   add    %eax,%edx
  44:   8b 45 f8                mov    -0x8(%rbp),%eax
  47:   01 c2                   add    %eax,%edx
  49:   8b 45 fc                mov    -0x4(%rbp),%eax
  4c:   01 d0                   add    %edx,%eax
  4e:   89 c7                   mov    %eax,%edi
  50:   e8 00 00 00 00          call   55 <main+0x2e>
  55:   b8 00 00 00 00          mov    $0x0,%eax
  5a:   c9                      leave  
  5b:   c3                      ret    
           

1.data段

.data段儲存的是那些已經初始化了的全局靜态變量和局部靜态變量。前面的代碼裡面一共有兩個這樣的變量,分别是global_init_varabal與static_var。這兩個變量每個都是4位元組,一共8個位元組,是以.data這個段的大小是8位元組

我們看到".data"段裡的前4個位元組,從低到高分别為 0x54,0x00,0x00,0x00,這個值剛好是global_init_varabal,即十進制的84。

2.rodata段

.rodata段存放的是隻讀資料,一般是程式裡面的隻讀變量(如const修飾的變量)和字元串常量。對這個段的任何修改操作都會作為非法操作處理,保證了程式的安全性。printf函數裡面用到了一個字元串常量,"%d\n",它是一種隻讀資料,是以他被放在了.rodata段。

3.bss段

.bss 段存放的是未初始化的全局變量和局部靜态變量。上述代碼中global_unint_var 和 static_var2 就是存放.bss段,更準确的說法是.bss 段為他們預留了空間。

4.檔案頭

[email protected]  ~/程式員的自我修養  readelf -h 測試.o 
ELF 頭:
  Magic:  7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  類别:                              ELF64
  資料:                              2 補碼,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  類型:                              REL (可重定位檔案)
  系統架構:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口點位址:              0x0
  程式頭起點:              0 (bytes into file)
  Start of section headers:          1064 (bytes into file)
  标志:             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         14
  Section header string table index: 13
           

檔案頭,它包含了描述整個檔案的基本屬性,比如elf檔案版本,目标機器型号,程式入口位址等。

magic的16個位元組用來辨別ELF檔案的平台屬性。

5.段表

[email protected]  ~/程式員的自我修養  readelf -S 測試.o
There are 14 section headers, starting at offset 0x428:

節頭:
  [号] 名稱              類型(type)             位址              偏移量
       大小              全體大小          旗标(flg)   連結(lk)   資訊(inf)   對齊(al)
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000005c  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000308
       0000000000000078  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000009c
       0000000000000008  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  000000a4
       0000000000000008  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  000000a4
       0000000000000004  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  000000a8
       0000000000000013  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000bb
       0000000000000000  0000000000000000           0     0     1
  [ 8] .note.gnu.pr[...] NOTE             0000000000000000  000000c0
       0000000000000030  0000000000000000   A       0     0     8
  [ 9] .eh_frame         PROGBITS         0000000000000000  000000f0
       0000000000000058  0000000000000000   A       0     0     8
  [10] .rela.eh_frame    RELA             0000000000000000  00000380
       0000000000000030  0000000000000018   I      11     9     8
  [11] .symtab           SYMTAB           0000000000000000  00000148
       0000000000000150  0000000000000018          12     8     8
  [12] .strtab           STRTAB           0000000000000000  00000298
       000000000000006f  0000000000000000           0     0     1
  [13] .shstrtab         STRTAB           0000000000000000  000003b0
       0000000000000074  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

           

段表就是儲存這些段的基本屬性的結構比如每個段的段名,段的長度,在檔案中的偏移,讀寫權力及段的其他屬性。elf檔案的段結構就是由段表決定的,編譯器,連結器,和裝載器都是依靠段表來定位和通路各個段的屬性的。

對于我們的代碼來說,段表就是有14個元素的數組,elf段表的這個數組的第一個元素是無效的段描述符,他的類型是null,除此之外每個段描述符都對應一個段。也就是說我們的代碼一共有13個段。另外,objdump -h 隻是把elf檔案中最關鍵的段顯示了出來,而省略了其他輔助性的段。

1.段的類型相關常量

常量 含義
NULL 無效段
PROGBITS 程式段,代碼段,資料段都是這種類型 1
SYMTAB 表示該段的内容為符号表 2
STRTAB 表示該段的内容為字元串表 3
RELA 重定位表,該段包含了重定位資訊,這個段跟靜态連結和動态連結有關 4
HASH 符号表的哈希表 5
DYNAMIC 動态連結資訊 6
NOTE 提示性資訊 7
NOBITS 表示該段在檔案中沒有内容,比如.BSS段 8
REL 包含了重定位資訊 9
SHLIB 保留 10
DNYSYM 動态連結的符号表 11

2. 段的标志位(也就是上面圖裡面的旗幟)

也可以參考圖裡面 Key to Flags 裡面的内容。

WRITE 表示該段在程序中可寫
ALLOC 表示這段在程序空間中需要配置設定空間,有些包含訓示或控制資訊的段不需要在程序空間中被配置設定空間,他們一般不會有這個标志,像代碼段,資料段和.bss段都會有這個标志位
EXECINSTR 表示該段在程序空間中可以被執行,一般指代碼段

3.段的連結資訊

如果段的類型是與連結相關的,比如重定位表,符号表等,那麼sh_link和sh_info這兩個成員包含的意義如下面的表所比示,對于其他的段,sh_link 和 sh_info 沒有意義

type link(lk) info(inf)
DYNAMIC 該段使用的字元串在段表中的下标
HASH 該段所使用的符号表在段表中的下标
REL 和 RELA 該段所使用的相應符号表在段表中的下标 該重定位表所作用的段在段表中的下标

6.重定位表

在測試.o中有一個.rel.text的段,他的類型為rela,也就是說他是一個重定位表,因為連結器在處理目标檔案時,需要對目标檔案中某些部件進行重定位,即代碼段和資料段中那些絕對位址的引用的位置,這些重定位資訊都記錄在elf檔案的重定位表裡面,對于每個需要重定位的代碼段或資料段,都會有一個相應的重定位表,比如.rel.text就是.text的重定位表,.text中有printf函數的調用。.data段中沒有對絕對位址的引用,它隻是包含了幾個常量。是以沒有.rel.data這個段。

一個重定位表,它的"link"表示符号表的下标,他的"info"表示他作用于那個段。比如,“.rel.text"作用于作用于".text"段,而".text"段的下标為"1",那麼".rel.text"的"info"為1,在上圖中,我們可以觀察到符号表的下标是11。不隻是.rel.text是這樣,.rela.eh_frame的sh_link也是11。

7.符号表

[email protected]  ~/程式員的自我修養  readelf  -s 測試.o
 
Symbol table '.symtab' contains 14 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS ��.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     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.1
     7: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 static_var2.0
     8: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 global_uninit_var
    10: 0000000000000000    39 FUNC    GLOBAL DEFAULT    1 funcl
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    13: 0000000000000027    53 FUNC    GLOBAL DEFAULT    1 main
           

1.符号綁定(Bind)

宏定義名 說明
local 局部符号,對于目标檔案的外表不可見
global 1 全局符号,外部可見
weak 2 弱引用

強符号與弱符号

我們經常在程式設計中碰到一種情況叫符号重複定義。多個目标檔案中含有相同名字全局符号的定義,那麼這些目标檔案連結的時候将會出現符号重複定義的錯誤。

對于c/c++來說,編譯器預設函數和初始化了的全局變量為強符号,未初始化的全局變量為弱符号。

注意:強符号和弱符号都是針對定義來說的,不是針對符号的引用。

1 extern int ext;
    2      
    3 int weak;
    4 int strong = 1;
    5 __attribute__((week)) week2 = 2;   //弱引用
    6      
    7 int main()
    8 {    
    9     return 0;       
           

上面這段程式,"week"和"week2"是弱符号,因為沒有初始化。"strong"和"main"是強符号。”ext"即非強符号也非弱符号,它是一個外部符号的引用。我們這裡隻是針對強符号和弱符号來說的。

三大規則

1.不允許強符号被多次定義(即不同的目标檔案中不能有同名的強符号);如果有多個強符号,則連結器報符号重複定義錯誤。

2.如果一個符号在某個目标檔案中是強符号,在其他檔案中都是弱符号,那麼選擇強符号。

3.如果一個符号在所有目标檔案中都是弱符号,那麼選擇其中占用空間最大的一個。

1 #include <stdio.h>
  2 int week;    // 弱引用
  3 int week = 10;    // 強引用
  4      
  5 int main(void)
  6 {    
  7     printf("%d\n",week);                                                                                                                                                                 
  8     return 0;
  9 }    
           
[email protected]  ~  gcc 測試.c
 l[email protected]  ~  ./a.out
10
           

弱引用和強引用

目前我們所看到的對外部目标檔案的符号引用在目标檔案被最終連結成可執行檔案時,他們需要被正确決議。如果沒有找到該符号的定義,連結器就會報符号為定義錯誤,這種被稱為強引用與之對應還有一種弱引用,在處理弱引用時,如果該符号有定義,則連結器将該符号的引用決議,如果該符号未被定義,則連結器對于該引用不報錯。 連結器處理強引用和弱引用的過程幾乎一樣,隻是對于未被定義的弱引用,連結器對于該引用不報錯。弱符号和弱引用主要使用者庫的連結過程。 這種弱符号和弱引用對于庫來說十分有用,比如庫中定義的弱符号可以被使用者定義的強符号所覆寫,進而使得程式可以使用自定義版本的庫函數,或者程式可以對某些擴充功能子產品的引用定義為弱引用,當我們将擴充子產品與程式連結在一起時,功能子產品就可以正常使用,如果我們去掉了某些功能子產品,那麼程式也可以正常連結。

3.符号類型(Type)

宏定義名 說明
NOTYPE 說明
OBJECT 1 該符号是一個資料對象,比如變量,數組
FUNC 2 該符号是一個函數或者其他可執行代碼
SECTION 3 該符号表示一個段
FILE 4 該符号表示檔案名

3.符号所在段(Ndx)

如果符号定義在本目标檔案中,那麼這個成員表示符号所在的段在段表中的下标。對于上圖Num為0 的那個符号,永遠都是一個為定義的符号。我們的printf函數因為沒有被定義,是以他的Ndx是UND,意思是沒有定義。

4.符号值

也就是value, 表示的是符号所對應的函數或者變量位于Ndx指定的段,偏移Value的位置。舉個例子,因為main和func1都定義在代碼裡面,是以他們所在的位置都為代碼段,是以Ndx的值為1,對應的段就是.text。這個printf函數,因為隻是帶代碼裡面被引用,沒有被定義,是以他的Ndx為und,也就是沒有被定義的意思。

三.總結

由于篇幅的原因,這裡隻說了主要的幾個段,其實還有很多的東西可以講,學習elf檔案格式對于我們程式設計可能沒有太直接的用處,但是對于提升我們對于一個程式的了解會有很大的提升。