深入理解程序構(gòu)造(一)

真正了不起的程序員對自己的程序的每一個(gè)字節(jié)都了如指掌。

編碼

源代碼經(jīng)過編譯器編譯后產(chǎn)生的文件叫做目標(biāo)文件,多個(gè)目標(biāo)文件鏈接后可以產(chǎn)生可執(zhí)行文件,所以目標(biāo)文件除了有些符號和地址沒有通過鏈接來調(diào)整,其基本格式與可執(zhí)行文件相似。

目標(biāo)文件的格式##

目前流行的可執(zhí)行文件格式(Executable)主要就是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkble Format),都是COFF(Common File format)的變種, 在Linux中目標(biāo)文件就是常見的中間文件.o,對應(yīng)的在Windows中就是.obj文件。由于格式與可執(zhí)行文件相近,所以基本可以看做一種類型的文件。在Windows下統(tǒng)稱為PE-COFF文件格式,在Linux下,統(tǒng)稱為ELF文件。

除了可執(zhí)行文件,包括動(dòng)態(tài)鏈接庫(Windows下的.dll, Linux 下的.so)以及靜態(tài)鏈接庫(Windows 下的.lib, Linux下的.a)都是按照以上格式存儲的,在Windows下的格式都是PE-COFF,Linux下則按照ELF格式存儲。唯一不同的是Linux下的靜態(tài)鏈接庫(.a 文件),它基本上就是把許多目標(biāo)文件捆綁在一起打包,類似tar命令, 再加上一些索引。
ELF文件標(biāo)準(zhǔn)大概包含了以下四種文件類型:

  • 可重定位文件:主要包含代碼和數(shù)據(jù),可以被用來鏈接成可執(zhí)行文件或者共享目標(biāo)文件,靜態(tài)鏈接庫也歸類于這一類,包括Linux的.o文件,Windows的.obj文件
  • 可執(zhí)行文件:包含可以直接執(zhí)行的程序,比如Linux下的/bin/bash,Windows下的.exe
  • 共享目標(biāo)文件:主要包含代碼和數(shù)據(jù),第一種用途可以與其它文件鏈接生成可重定位或者共享目標(biāo)文件,再者直接鏈接到可執(zhí)行文件,作為進(jìn)程映象的一部分動(dòng)態(tài)執(zhí)行。常見的Linux下的.so,Windows下的.dll。
  • 核心轉(zhuǎn)儲文件(Core dump):這個(gè)格式調(diào)試bug時(shí)很有用,進(jìn)程意外終止時(shí)產(chǎn)生的,保留程序終止時(shí)進(jìn)程的信息,Linux下的Core dump。

我們可以使用file命令來獲取文件的格式。

重定位文件
可執(zhí)行文件
動(dòng)態(tài)鏈接庫

目標(biāo)文件內(nèi)部結(jié)構(gòu)##

這節(jié)我們以簡單的ELF目標(biāo)文件作為舉例:

#include<stdio.h>
int global_var1 = 1;
int global_var2;
void func1(int i)
{
    printf("%d\n", i);
}
int main()
{
    static int a1 = 85;
    static int a2;
    int m = 9;
    int n;
    func1(a1+global_var1+m+n);
    return 0;
}

我們默認(rèn)的平臺是32位Intel X86平臺

gcc -c cal.c

產(chǎn)生目標(biāo)文件cal.o
我們可以借助于binutils的工具objdump來查看目標(biāo)文件內(nèi)部結(jié)構(gòu)。

$ objdump -h cal.o

cal.o:     文件格式 elf32-i386

節(jié):
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000064  00000000  00000000  00000034  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  00000000  00000000  00000098  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000004  00000000  00000000  000000a0  2**2
                  ALLOC
  3 .rodata       00000004  00000000  00000000  000000a0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      00000035  00000000  00000000  000000a4  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  00000000  00000000  000000d9  2**0
                  CONTENTS, READONLY
  6 .eh_frame     00000064  00000000  00000000  000000dc  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA


"-h"就是把ELF文件各個(gè)段的基本信息打印出來,也可以到man手冊查詢更多詳細(xì)用法。

cal.o

下面我們來分析上面各段:

代碼段( .text)

程序源代碼編譯后的機(jī)器指令經(jīng)常被放在代碼段(Code Section)里,代碼段常見的名字就是.text或者.code,借助于objdump這個(gè)利器,我們可以進(jìn)一步的分析代碼段的內(nèi)容,-s可以將所有段的內(nèi)容以十六進(jìn)制的方式打印出來,-d可以將所有包含的指令反匯編。

下面使用objdump把代碼段的內(nèi)容提取出來:

$ objdump -s -d cal.o

cal.o:     文件格式 elf32-i386

Contents of section .text:
 0000 5589e583 ec0883ec 08ff7508 68000000  U.........u.h...
 0010 00e8fcff ffff83c4 1090c9c3 8d4c2404  .............L$.
 0020 83e4f0ff 71fc5589 e55183ec 14c745f0  ....q.U..Q....E.
 0030 09000000 8b150400 0000a100 00000001  ................
 0040 c28b45f0 01c28b45 f401d083 ec0c50e8  ..E....E......P.
 0050 fcffffff 83c410b8 00000000 8b4dfcc9  .............M..
 0060 8d61fcc3                             .a..
Contents of section .data:
 0000 01000000 55000000                    ....U...
Contents of section .rodata:
 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 30342e32 2920352e 342e3020 32303136  04.2) 5.4.0 2016
 0030 30363039 00                          0609.
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 017c0801  .........zR..|..
 0010 1b0c0404 88010000 1c000000 1c000000  ................
 0020 00000000 1c000000 00410e08 8502420d  .........A....B.
 0030 0558c50c 04040000 28000000 3c000000  .X......(...<...
 0040 1c000000 48000000 00440c01 00471005  ....H....D...G..
 0050 02750043 0f03757c 06750c01 0041c543  .u.C..u|.u...A.C
 0060 0c040400                             ....

Disassembly of section .text:

00000000 <func1>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   83 ec 08                sub    $0x8,%esp
   6:   83 ec 08                sub    $0x8,%esp
   9:   ff 75 08                pushl  0x8(%ebp)
   c:   68 00 00 00 00          push   $0x0
  11:   e8 fc ff ff ff          call   12 <func1+0x12>
  16:   83 c4 10                add    $0x10,%esp
  19:   90                      nop
  1a:   c9                      leave
  1b:   c3                      ret

0000001c <main>:
  1c:   8d 4c 24 04             lea    0x4(%esp),%ecx
  20:   83 e4 f0                and    $0xfffffff0,%esp
  23:   ff 71 fc                pushl  -0x4(%ecx)
  26:   55                      push   %ebp
  27:   89 e5                   mov    %esp,%ebp
  29:   51                      push   %ecx
  2a:   83 ec 14                sub    $0x14,%esp
  2d:   c7 45 f0 09 00 00 00    movl   $0x9,-0x10(%ebp)
  34:   8b 15 04 00 00 00       mov    0x4,%edx
  3a:   a1 00 00 00 00          mov    0x0,%eax
  3f:   01 c2                   add    %eax,%edx
  41:   8b 45 f0                mov    -0x10(%ebp),%eax
  44:   01 c2                   add    %eax,%edx
  46:   8b 45 f4                mov    -0xc(%ebp),%eax
  49:   01 d0                   add    %edx,%eax
  4b:   83 ec 0c                sub    $0xc,%esp
  4e:   50                      push   %eax
  4f:   e8 fc ff ff ff          call   50 <main+0x34>
  54:   83 c4 10                add    $0x10,%esp
  57:   b8 00 00 00 00          mov    $0x0,%eax
  5c:   8b 4d fc                mov    -0x4(%ebp),%ecx
  5f:   c9                      leave
  60:   8d 61 fc                lea    -0x4(%ecx),%esp
  63:   c3                      ret

看開頭一段Contents of section .text就是一十六進(jìn)制打印出來的內(nèi)容,最左列是偏移量, 看0060那行,只剩下8d61fcc3,所以與對照上面一張圖,.text段的size是0x64字節(jié)。最右列是.text段的ASCII碼格式,對照下面的反匯編結(jié)果,我們可以看到cal.c中的兩個(gè)函數(shù)func1()main()的指令。.text的第一個(gè)字節(jié)0x55就是func1()函數(shù)的第一條push %ebp指令,最后一個(gè)0xc3main()的最后一個(gè)指令ret

數(shù)據(jù)段和只讀數(shù)據(jù)段(.data & .rodata)###

.data段保存的是那些已經(jīng)初始化的全局靜態(tài)變量和局部靜態(tài)變量。代碼中的global_var1a1都是這樣的變量,每個(gè)變量4字節(jié),所以.data段的大小為8個(gè)字節(jié)。

cal.c在調(diào)用printf時(shí),內(nèi)部包含一個(gè)字符串常量"%d\n"用來定義格式化輸出,它是一種只讀數(shù)據(jù),所以保存在.rodata段,我們可以看圖中.rodata段大小為4字節(jié),內(nèi)容為25640a00,翻譯回來就是"%d\n"

.rodata段存放的是只讀數(shù)據(jù),一般程序里面存在只讀變量和字符串常量這兩種只讀類型,單獨(dú)設(shè)置.rodata段有很多好處,支持了C里面的關(guān)鍵字const, 而且操作系統(tǒng)加載程序時(shí)自動(dòng)將只讀變量加載到只讀存儲區(qū),或者映射成只讀,這樣任何修改操作都會被認(rèn)為非法操作,保證了程序的安全性。

BSS段(.bss)###

.bss段存放的是未初始化的全局變量和局部靜態(tài)變量。上面代碼中的global_var2a2就被存放在.bss段。其實(shí)只能說.bss段為他們預(yù)留了空間,實(shí)際上該段大小只有4個(gè)字節(jié),而這兩個(gè)變量應(yīng)該占用8個(gè)字節(jié)。
其實(shí)我們可以通過符號表看到,只有a2被放到了.bss段,global_var2卻沒有放到任何段,只是一個(gè)未定義的“COMMON”符號。其實(shí)這與不同的語言和不同的編譯器實(shí)現(xiàn)有關(guān),有的編譯器不把未定義的全局變量放到.bss段,只是保留一個(gè)符號,直到鏈接成可執(zhí)行文件時(shí)才在.bss段分配空間。

有個(gè)小例子:

static int x1 = 0;
static int x2 = 1;

x1和x2會被放在什么段呢?
答案是x1被放在.bss段 ,而x2被放在.data段。原因在于x1被初始化為0,相當(dāng)于沒有被初始化,未初始化的都是0,所以這里編譯器會把x1優(yōu)化掉,放在.bss段,因?yàn)?bss不占磁盤空間。x2正常的初始化,所以被放到.data段。

其它段###

除了以上各段,ELF文件也包含其它段。下表列舉了一些常見的段。

常用的段名 說明
.rodata1 Read Only Data,這種段里存放的是只讀數(shù)據(jù),比如字符串常量,全局const變量,和".rodata"一樣
.comment 存放的是編譯器版本信息,比如字符串:"GCC:(GUN)4.2.0"
.debug 調(diào)試信息
.dynamic 動(dòng)態(tài)鏈接信息
.hash 符號哈希表
.line 調(diào)試時(shí)的行號表,即源代碼行號和編譯后指令的對應(yīng)表
.note 額外的編譯器信息。比如程序的公司名,發(fā)布版本號
.strtab String Table字符串表,用于存儲ELF文件中用到的各種字符串
.symtab Symbol Table符號表
.shstrtab Section String Table段名表
,plt .got 動(dòng)態(tài)鏈接的跳轉(zhuǎn)表和全局入口表
.init .finit 程序初始化與終結(jié)代碼段

這些段的名字都是“.”作為前綴,一般系統(tǒng)定義的都是"."開頭,如果自己定義的段名則不要以"."開頭,容易與系統(tǒng)保留的產(chǎn)生沖突,如果你打開目標(biāo)文件的段名還有其它一些格式,也許都是以前系統(tǒng)曾經(jīng)用過的,歷史遺留問題。

我們也可以自定義段,GCC提供一個(gè)擴(kuò)展機(jī)制可以讓我們指定變量所處的段:

__attribute__((section("FOO"))) int global = 42;
__attribute__((section("BAR"))) void foo()

在全局變量或者函數(shù)前加上attribute((section("name")))屬性就可以把相應(yīng)的變量和函數(shù)放到以“name”作為段名的段中。

ELF文件結(jié)構(gòu)描述###

1. 文件頭

上面的例子中我們分析了ELF文件的各個(gè)段,位于所有段前面的就是文件頭。我們可以使用readelf命令來查看。

$ readelf -h cal.o
ELF 頭:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  類別:                              ELF32
  數(shù)據(jù):                              2 補(bǔ)碼,小端序 (little endian)
  版本:                              1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  類型:                              REL (可重定位文件)
  系統(tǒng)架構(gòu):                          Intel 80386
  版本:                              0x1
  入口點(diǎn)地址:               0x0
  程序頭起點(diǎn):          0 (bytes into file)
  Start of section headers:          796 (bytes into file)
  標(biāo)志:             0x0
  本頭的大小:       52 (字節(jié))
  程序頭大小:       0 (字節(jié))
  Number of program headers:         0
  節(jié)頭大小:         40 (字節(jié))
  節(jié)頭數(shù)量:         13
  字符串表索引節(jié)頭: 10

從上面的輸出結(jié)果可以看到,ELF的文件頭中定義了ELF魔數(shù),文件數(shù)據(jù)存儲方式,版本,運(yùn)行平臺,ABI版本,系統(tǒng)架構(gòu),硬件平臺,入口地址,程序頭入口和長度,段表的位置和長度,段的數(shù)量等等。

ELF文件頭結(jié)構(gòu)和相關(guān)常數(shù)被定義在"/usr/include/elf.h"里,分為32位和64位版本。我們測試的機(jī)器是32位的,包含"Elf32_Ehdr"的數(shù)據(jù)結(jié)構(gòu)來描述上述輸出的ELF頭。

typedef struct
{
  unsigned char e_ident[EI_NIDENT];     /* Magic number and other info */
  Elf32_Half    e_type;                 /* Object file type */
  Elf32_Half    e_machine;              /* Architecture */
  Elf32_Word    e_version;              /* Object file version */
  Elf32_Addr    e_entry;                /* Entry point virtual address */
  Elf32_Off     e_phoff;                /* Program header table file offset */
  Elf32_Off     e_shoff;                /* Section header table file offset */
  Elf32_Word    e_flags;                /* Processor-specific flags */
  Elf32_Half    e_ehsize;               /* ELF header size in bytes */
  Elf32_Half    e_phentsize;            /* Program header table entry size */
  Elf32_Half    e_phnum;                /* Program header table entry count */
  Elf32_Half    e_shentsize;            /* Section header table entry size */
  Elf32_Half    e_shnum;                /* Section header table entry count */
  Elf32_Half    e_shstrndx;             /* Section header string table index */
} Elf32_Ehdr;

對比Elf32_Ehdr和之前的ELF頭,可以發(fā)現(xiàn)很多字段一一對應(yīng)。不過e_ident這個(gè)成員數(shù)組對應(yīng)了“類型”,“數(shù)據(jù)”,“版本,“OS/ABI”,“ABI版本”這五個(gè)參數(shù),剩下的都一一對應(yīng)。

ELF魔數(shù) 從上面的readelf的輸出可以看到,Magic有16個(gè)字節(jié),對應(yīng)著Elf32_Ehdr的e_ident這個(gè)成員。這個(gè)屬性被用來標(biāo)識ELF文件的平臺屬性。

Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  • 最開始的4個(gè)字節(jié): 所有ELF文件共有的標(biāo)識碼,"0x7F"、"0x45"、"0x4c"、"0x46",其中,"0x7F"對應(yīng)ASCII中的DEL控制符,后面三個(gè)是ELF三個(gè)字母的ASCII碼。這4個(gè)字節(jié)又被稱為ELF文件的魔數(shù)。

基本所有可執(zhí)行文件開始的幾個(gè)字節(jié)都是魔數(shù):
a.out: 0x01、0x07
PE/COFF: 0x4d,0x5a
這些魔數(shù)被操作系統(tǒng)用來確認(rèn)可執(zhí)行文件的類型,如果不對就拒絕加載。

  • 第5個(gè)字節(jié): 表示ELF的文件類,0x01代表是32位的,如果是0x02則表示64位,
  • 第6個(gè)字節(jié): 規(guī)定字節(jié)序,規(guī)定該ELF是大端還是小端的
  • 第7個(gè)字節(jié): 規(guī)定ELF文件的主版本號,一般都是1,因?yàn)闆]有更新過了。
  • 后面的9個(gè)字節(jié):都填充為0, 一般沒意義,有的平臺用來做擴(kuò)展標(biāo)識。

類型 e_type成員用來表示ELF文件類型,系統(tǒng)通過這個(gè)值來判斷文件類型,而不是擴(kuò)展名。

常量 含義
ET_REL 1 可重定位文件,一般是.o文件
ET_EXEC 2 可執(zhí)行文件
ET_DYN 3 共享目標(biāo)文件,一般為.so

機(jī)器類型 ELF文件格式被設(shè)計(jì)成在多平臺下使用,和java不同,ELF文件不能一次編譯處處使用,而是說不同平臺下的ELF文件都遵循一套ELF標(biāo)準(zhǔn)。用e_machine成員表示平臺屬性。

常量 含義
EM_M32 1 AT&T WE32100
EM_SPARK 2 SPARC
EM_386 3 Intel x86
EM_68K 4 Motorola 68000
EM_88K 5 Motorala 88000
EM_860 6 Intel 80860

2016/
未完待續(xù)。。。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容