天天看點

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

轉自看雪論壇作者ID:菜鳥m号

寫在前面

讀《Linux 二進制》,發現作者對 ELF 檔案格式部分并沒有做詳細介紹,為了加深對 ELF 檔案格式了解,我自己着手寫了個解析器, 會和 readelf 工具協同對比。

原理

ELF檔案(目标檔案)格式主要三種:

1. 可重定向檔案(Relocatable file):檔案儲存着代碼和适當的資料,用來和其他的目标檔案一起來建立一個可執行檔案或者是一個共享目标檔案。(目标檔案或者靜态庫檔案,即 linux 通常字尾為 .a 和 .o 的檔案)這是由彙編器彙編生成的 .o 檔案。

後面的連結器(link editor)拿一個或一些 Relocatable object files 作為輸入,經連結處理後,生成一個可執行的對象檔案 (Executable file) 或者一個可被共享的對象檔案(Shared object file),核心可加載子產品 .ko 檔案也是 Relocatable object file。

2. 可執行檔案(Executable file):檔案儲存着一個用來執行的程式。(例如bash,gcc等)

3. 共享目标檔案:

即 .so 檔案。如果拿前面的靜态庫來生成可執行程式,那每個生成的可執行程式中都會有一份庫代碼的拷貝。

如果在磁盤中存儲這些可執行程式,那就會占用額外的磁盤空 間;另外如果拿它們放到 Linux 系統上一起運作,也會浪費掉寶貴的實體記憶體。

如果将靜态庫換成動态庫,那麼這些問題都不會出現。

一般的ELF檔案有三個重要的索引表

1. ELF header:在檔案的開始,描述整個檔案的組織。

2. Program header table:告訴系統如何建立程序映像。用來構造程序映像的目标檔案必須具有程式頭部表,可重定位檔案不需要這個表。

3. Section header table:包含了描述檔案節區的資訊,每個節區在表中都有一項,每一項給出諸如節區名稱、節區大小這類資訊。

用于連結的目标檔案必須包含節區頭部表,其他目标檔案可以有,也可以沒有這個表。

4. sections 或者 segments:segments 是從運作的角度來描述 ELF 檔案,sections 是從連結的角度來描述 ELF 檔案。

也就是說,在連結階段,我們可以忽略 program header table 來處理此檔案,在運作階段可以忽略 section header table 來處理此程式(是以很多加強手段删除了section header table)。

注意:segments與sections是包含的關系,一個segment包含若幹個section。

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

(圖檔來自網絡)

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

了解整體結構之後,我們就可以看一下具體的結構體和指針了。

代碼先寫了一個 help 函數,包含基本資訊和指令結構,效果如下:

void help(){   printf("這是jentle的解析器demo");   printf("-h            :頭部資訊");   printf("-S            :節區表資訊");   printf("-s            :符号表資訊");   printf("-l            :程式頭資訊");   printf("-r            :重定位表資訊"); }
           
elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

1. -h 指令,檢視和列印程式 header 函數。

我們檢視一下 elf headr 結構體:

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;
           

這裡面包括後面的 code 都會涉及到 elf 的資料格式,在這給出:

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

是以列印 ELF 的頭資訊可以設計為:

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

效果展示:

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

readelf 對比:

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

參考前輩圖解:

https://blog.csdn.net/qq_37431937

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

2. -S 指令 列印 section 資訊

段表結構體:

typedef struct {       Elf32_Word st_name;      //符号表項名稱。如果該值非0,則表示符号名的字                                             //符串表索引(offset),否則符号表項沒有名稱。     Elf32_Addr st_value;       //符号的取值。依賴于具體的上下文,可能是一個絕對值、一個位址等等。     Elf32_Word st_size;         //符号的尺寸大小。例如一個資料對象的大小是對象中包含的位元組數。     unsigned char st_info;    //符号的類型和綁定屬性。     unsigned char st_other;  //未定義。     Elf32_Half st_shndx;        //每個符号表項都以和其他節區的關系的方式給出定義。             //此成員給出相關的節區頭部表索引。} Elf32_sym;
           

一些成員參數解釋:

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

代碼設計:

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

效果展示:

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

readelf 對比:

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

3. -s 列印符号表資訊

目标檔案的符号表中包含用來定位、重定位程式中符号定義和引用的資訊。符号表 索引是對此數組的索引。索引 0 表示表中的第一表項,同時也作為 定義符号的索引。

符号是對某些類型的資料或者代碼(如全局變量或函數)的符号引用。例如,printf()函數會在動态符号表 .dynsym 中存有一個指向該函數的符号條目。

在大多數共享庫和動态連結可執行檔案中,存在兩個符号表。如前面使用readelf –S 指令輸出的内容中,可以看到有兩個節:.dynsym 和.symtab。

.dynsym 儲存了引用來自外部檔案符号的全局符号,如 printf 這樣的庫函數,.dynsym 儲存的符号是 .symtab 所儲存符号的子集,.symtab 中還儲存了可執行檔案的本地符号,如全局變量,或者代碼中定義的本地函數等。是以,.symtab 儲存了所有的符号,而 .dynsym 隻儲存動态/全局符号。

是以,就存在這樣一個問題:既然.symtab 中儲存了.dynsym 中所有的符号,那麼為什麼還需要兩個符号表呢?

使用 readelf –S 指令檢視可執行檔案的輸出,可以看到一部分節被标記為了 A(ALLOC) 、WA(WRITE/ALLOC)或者 AX(ALLOC/EXEC)。

.dynsym 是被标記了 ALLOC 的,而.symtab 則沒有标記。ALLOC 表示有該标記的節會在運作時配置設定并裝載進入記憶體,而 .symtab 不是在運作時必需的,是以不會被裝載到記憶體中。

.dynsym 儲存的符号隻能在運作時被解析,是以是運作時動态連結器所需要的唯一符号。

.dynsym 符号表對于動态連結可執行檔案的執行來說是必需的,而 .symtab 符号表隻是用來進行調試和連結的,有時候為了節省空間,會将 .symtab 符号表從生産二進制檔案中删掉。

符号表:.dynsym

符号表包含用來定位、重定位程式中符号定義和引用的資訊,簡單的了解就是符号表記錄了該檔案中的所有符号,所謂的符号就是經過修飾了的函數名或者變量名,不同的編譯器有不同的修飾規則。

例如符号_ZL15global_static_a,就是由 global_static_a 變量名經過修飾而來。

符号表格式如下:

typedef struct {       Elf32_Word st_name;      //符号表項名稱。如果該值非0,則表示符号名的字                                             //符串表索引(offset),否則符号表項沒有名稱。     Elf32_Addr st_value;       //符号的取值。依賴于具體的上下文,可能是一個絕對值、一個位址等等。     Elf32_Word st_size;         //符号的尺寸大小。例如一個資料對象的大小是對象中包含的位元組數。     unsigned char st_info;    //符号的類型和綁定屬性。     unsigned char st_other;  //未定義。     Elf32_Half st_shndx;        //每個符号表項都以和其他節區的關系的方式給出定義。             //此成員給出相關的節區頭部表索引。} Elf32_sym;
           

字元串表 .dynstr 略

void tableheader(const char *pbuff){    //從節區裡面定位到偏移    Elf64_Ehdr* pfilehead = (Elf64_Ehdr*)pbuff;    Elf64_Half eshstrndx = pfilehead->e_shstrndx;    Elf64_Shdr* psecheader = (Elf64_Shdr*)(pbuff + pfilehead->e_shoff);    Elf64_Shdr* pshstr = (Elf64_Shdr*)(psecheader + eshstrndx);    char* pshstrbuff = (char *)(pbuff + pshstr->sh_offset);         for(int i = 0;ie_shnum;++i)    {        if(!strcmp(psecheader[i].sh_name + pshstrbuff, ".dynsym") || !strcmp(psecheader[i].sh_name + pshstrbuff, ".symtab"))        {            Elf64_Sym* psym = (Elf64_Sym*)(pbuff + psecheader[i].sh_offset);            int ncount = psecheader[i].sh_size / psecheader[i].sh_entsize;            char* pbuffstr = (char*)((psecheader + psecheader[i].sh_link)->sh_offset + pbuff);            printf("Symbol table '%s' contains %d entries:", psecheader[i].sh_name + pshstrbuff, ncount);            outputsyminfo(psym, pbuffstr, ncount);            continue;        }    }}
           

效果展示:

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

readelf 對比:

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

參數解釋:

  • STT_NOTYPE:符号類型未定義。
  • STT_FUNC:表示該符号與函數或者其他可執行代碼關聯。
  • STT_OBJECT:表示該符号與資料目标檔案關聯。

符号綁定:

  • STB_LOCAL:本地符号在目标檔案之外是不可見的,目标檔案包含了符号的定義,如一個聲明為 static 的函數。
  • STB_GLOBAL:全局符号對于所有要合并的目标檔案來說都是可見的。一個全局符号在一個檔案中進行定義後,另外一個檔案可以對這個符号進行引用。
  • STB_WEAK:與全局綁定類似,不過比 STB_GLOBAL 的優先級低。被标記為 STB_WEAK 的符号有可能會被同名的未被标記為 STB_WEAK的符号覆寫。

下面是對綁定和類型字段進行打包和解包的宏指令。

  • ELF32_ST_BIND(info) 或者 ELF64_ST_BIND(info):從 st_info 值中提取出一個綁定。
  • ELF32_ST_TYPE(info) 或者 ELF64_ST_TYPE(info):從 st_info 值中提取類型。
  • ELF32_ST_TYPE(bind,type) 或者 ELF64_ST_INFO(bind,type):将一個綁定和類型轉換成 st_info 值。

4. -l 指令 program頭資訊。

程式頭表與段表互相獨立,有 ELF 檔案頭同一管理。

結構資訊:

typedef struct {      Elf32_Word p_type;           //此數組元素描述的段的類型,或者如何解釋此數組元素的資訊。    Elf32_Off  p_offset;           //此成員給出從檔案頭到該段第一個位元組的偏移    Elf32_Addr p_vaddr;         //此成員給出段的第一個位元組将被放到記憶體中的虛拟位址    Elf32_Addr p_paddr;        //此成員僅用于與實體位址相關的系統中。System V忽略所有應用程式的實體位址資訊。    Elf32_Word p_filesz;         //此成員給出段在檔案映像中所占的位元組數。可以為0。    Elf32_Word p_memsz;     //此成員給出段在記憶體映像中占用的位元組數。可以為0。    Elf32_Word p_flags;         //此成員給出與段相關的标志。    Elf32_Word p_align;        //此成員給出段在檔案中和記憶體中如何對齊。} Elf32_phdr;
           

參數解釋:

(1) p_type表示目前描述的段的種類。常見有以下常數。

#define PT_NULL    0  //空段    #define PT_LOAD    1  //可裝載段    #define PT_DYNAMIC 2  //表示該段包含了用于動态連接配接器的資訊    #define PT_INTERP  3  //表示目前段指定了用于動态連接配接的程式解釋器,通常是ld-linux.so    #define PT_NOTE    4  //該段包含有專有的編譯器資訊    #define PT_SHLIB   5  //該段包含有共享庫
           

(2) p_offset給出了該段在二進制檔案中的偏移量,機關為位元組。

(3) p_vaddr給出了該段需要映射到程序虛拟位址空間中的位置。

(4) p_paddr在隻支援實體尋址,不支援虛拟尋址的系統當中才使用。

(5) p_filesz給出了該段在二進制檔案當中的長度,機關為位元組。

(6) p_memsz給出了段在虛拟位址空間當中的長度,機關為位元組。與p_filesz不等時會通過截斷資料或者以0填充的方式處理。

(7) p_flags儲存了标志資訊,定義了該段的通路權限。有如下值

#define PF_R        0x4     //該段可讀  #define PF_W       0x2     //該段可寫  #define PF_X        0x1     //該段可執行
           

(8) p_align指定了段在記憶體和二進制檔案當中的對齊方式,即p_offset和p_vaddr必須是p_align的整數倍。

設計代碼:

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

效果展示:

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

readelf對比:

elf檔案格式_ELF檔案格式解析器 原理 + 代碼寫在前面原理

最後還有一個重定位表的列印函數,暫時不介紹了。

繼續閱讀