ELF目标檔案格式最前部ELF檔案頭(ELF Header),它包含了描述了整個檔案的基本屬性,比如ELF檔案版本、目标機器型号、程式入口位址等。其中ELF檔案與段有關的重要結構就是段表(Section Header Table)
ELF檔案格式
- 可重定向檔案:檔案儲存着代碼和适當的資料,用來和其他的目标檔案一起來建立一個可執行檔案或者是一個共享目标檔案。(目标檔案或者靜态庫檔案,即linux通常字尾為.a和.o的檔案)
- 可執行檔案:檔案儲存着一個用來執行的程式。(例如bash,gcc等)
- 共享目标檔案:共享庫。檔案儲存着代碼和合适的資料,用來被下連接配接編輯器和動态連結器連結。(linux下字尾為.so的檔案。)
另外的windows下為pe格式的檔案;
ELF視圖
首先,ELF檔案格式提供了兩種視圖,分别是連結視圖和執行視圖。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISPrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdsATOfd3bkFGazxCMx8VesATMfhHLlN3XnxCMwEzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5SNzgTMiNDO1QGNxEDNzUWYwADM2IWOwYDZ0MmZldDM08CXxEzLchDMxIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjL5M3Lc9CX6MHc0RHaiojIsJye.png)
連結視圖是以節(section)為機關,執行視圖是以段(segment)為機關。連結視圖就是在連結時用到的視圖,而執行視圖則是在執行時用到的視圖。上圖左側的視角是從連結來看的,右側的視角是執行來看的。總個檔案可以分為四個部分:
- ELF header: 描述整個檔案的組織。
- Program Header Table: 描述檔案中的各種segments,用來告訴系統如何建立程序映像的。
- sections 或者 segments:segments是從運作的角度來描述elf檔案,sections是從連結的角度來描述elf檔案,也就是說,在連結階段,我們可以忽略program header table來處理此檔案,在運作階段可以忽略section header table來處理此程式(是以很多加強手段删除了section header table)。從圖中我們也可以看出, segments與sections是包含的關系,一個segment包含若幹個section。
- Section Header Table: 包含了檔案各個segction的屬性資訊,我們都将結合例子來解釋。
程式頭部表(Program Header Table),如果存在的話,告訴系統如何建立程序映像。
節區頭部表(Section Header Table)包含了描述檔案節區的資訊,比如大小、偏移等。
如下圖,可以通過執行指令”readelf -S android_server”來檢視該可執行檔案中有哪些section。
通過執行指令readelf –segments android_server,可以檢視該檔案的執行視圖。
這驗證了第一張圖中所述,segment是section的一個集合,sections按照一定規則映射到segment。那麼為什麼需要區分兩種不同視圖?
當ELF檔案被加載到記憶體中後,系統會将多個具有相同權限(flg值)section合并一個segment。作業系統往往以頁為基本機關來管理記憶體配置設定,一般頁的大小為4096B,即4KB的大小。同時,記憶體的權限管理的粒度也是以頁為機關,頁内的記憶體是具有同樣的權限等屬性,并且作業系統對記憶體的管理往往追求高效和高使用率這樣的目标。ELF檔案在被映射時,是以系統的頁長度為機關的,那麼每個section在映射時的長度都是系統頁長度的整數倍,如果section的長度不是其整數倍,則導緻多餘部分也将占用一個頁。而我們從上面的例子中知道,一個ELF檔案具有很多的section,那麼會導緻記憶體浪費嚴重。這樣可以減少頁面内部的碎片,節省了空間,顯著提高記憶體使用率。
檔案頭(ELF header)
我們可以使用readelf指令來詳細檢視elf檔案,代碼如清單3-2所示:
從上面輸出的結構可以看到:ELF檔案頭定義了ELF魔數、檔案機器位元組長度、資料存儲方式、版本、運作平台等。
ELF檔案頭結構及相關常數被定義在“/usr/include/elf.h”,因為ELF檔案在各種平台下都通用,ELF檔案有32位版本和64位版本的ELF檔案的檔案頭内容是一樣的,隻不過有些成員的大小不一樣。它的檔案圖也有兩種版本:分别叫“Elf32_Ehdr”和“Elf64_Ehdr”。
typedef struct {
unsigned char e_ident[16];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
}Elf32_Ehdr;
在ELF檔案頭中,我們需要重點關注以下幾個字段:
- e_entry:程式入口位址
- e_ehsize:ELF Header結構大小
- e_phoff、e_phentsize、e_phnum:描述Program Header Table的偏移、大小、結構。
- e_shoff、e_shentsize、e_shnum:描述Section Header Table的偏移、大小、結構。
- e_shstrndx:這一項描述的是字元串表在Section Header Table中的索引,值25表示的是Section Header Table中第25項是字元串表(String Table)。
段表(Section Header Table)
段表就是儲存ELF檔案中各種各樣段的基本屬性的結構。段表是ELF除了檔案以外的最重要結構體,它描述了ELF的各個段的資訊,ELF檔案的段結構就是由段表決定的。編譯器、連結器和裝載器都是依靠段表來定位和通路各個段的屬性的。段表在ELF檔案中的位置由ELF檔案頭的“e_shoff”成員決定的,比如SimpleSection.o中,段表位于偏移0x118。
typedef struct {
Elf32_Word sh_name; //section的名字
Elf32_Word sh_type; //section類别
Elf32_Word sh_flags; //section在程序中執行的特性(讀、寫)
Elf32_Addr sh_addr; //在記憶體中開始的虛位址
Elf32_Off sh_offset; //此section在檔案中的偏移
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
}
表(Section)
符号表(.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;
重定位表
重定位表在ELF檔案中扮演很重要的角色,首先我們得了解重定位的概念,程式從代碼到可執行檔案這個過程中,要經曆編譯器,彙編器和連結器對代碼的處理。然而編譯器和彙編器通常為每個檔案建立程式位址從0開始的目标代碼,但是幾乎沒有計算機會允許從位址0加載你的程式。如果一個程式是由多個子程式組成的,那麼所有的子程式必需要加載到互不重疊的位址上。重定位就是為程式不同部分配置設定加載位址,調整程式中的資料和代碼以反映所配置設定位址的過程。簡單的言之,則是将程式中的各個部分映射到合理的位址上來。
換句話來說,重定位是将符号引用與符号定義進行連接配接的過程。例如,當程式調用了一個函數時,相關的調用指令必須把控制傳輸到适當的目标執行位址。
具體來說,就是把符号的value進行重新定位。
字元串表(.dynstr)
ELF檔案中用到了許多的字元串,比如段名,變量名等。因為字元串的長度往往是不定的,是以用固定的結構來表示它比較困難。一種常見的做法是把字元串集中起來存放到一個表,然後使用字元串在表中的偏移來引用字元串。
通常用這種方式,在ELF檔案中引用字元串隻需給一個數字下标即可,不用考慮字元串的長度問題。一般字元串标在ELF檔案中國也以段的方式儲存,常見的段名為“.strtab”或“.shstrtab”。這兩個字元串分别表示為字元串表和段表字元串表。
隻有分析ELF檔案頭,就可以得到段表和段表字元串表的位置,進而解析整個ELF檔案。
裝載elf檔案步驟
首先在使用者層面,shell進行會調用fork()系統調用建立一個新程序 - 新程序調用execve()系統調用執行制定的ELF檔案 - 原來的shell程序繼續傳回等待剛才啟動的新程序結束,然後繼續等待使用者輸入。
execve()系統調用的原型如下: c
int execve(const char *filename, char *const argv[], char *const envp[]);
它所對應的三個參數分别是程式檔案名, 執行參數, 環境變量,通過對核心代碼的分析,我們知道execve()系統調用的相應入口是sys_execve(),在sys_execve之後,核心會分别調用
do_execve()
,
search_binary_handle()
load_elf_binary
等等,其中
do_execve()
是最主要的函數,是以接下來我們主要對他來進行具體分析
do_execve
int do_execve(struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
return do_execve_common(filename, argv, envp);
}
//do_execve_common
static int do_execve_common(struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp)
{
// 檢查程序的數量限制
// 選擇最小負載的CPU,以執行新程式
sched_exec();
// 填充 linux_binprm結構體
retval = prepare_binprm(bprm);
// 拷貝檔案名、指令行參數、環境變量
retval = copy_strings_kernel(1, &bprm->filename, bprm);
retval = copy_strings(bprm->envc, envp, bprm);
retval = copy_strings(bprm->argc, argv, bprm);
// 調用裡面的 search_binary_handler
retval = exec_binprm(bprm);
// exec執行成功
}
// exec_binprm
static int exec_binprm(struct linux_binprm *bprm)
{
// 掃描formats連結清單,根據不同的文本格式,選擇不同的load函數
ret = search_binary_handler(bprm);
// ...
return ret;
}
- 如果想要了解elf檔案格式,可以在指令行下面man elf,Linux手冊中有參考.
- 在do_exec()中會調用do_execve_common(),這個函數的參數與do_exec()一模一樣
- 在do_execve_common()中的sched_exec(),會選擇一個負載最小的CPU來執行新程序,這裡我們可以得知Linux核心中是做了負載均衡的.
- 在這段代碼中間出現了變量bprm,這個是一個重要的結構體struct linux_binfmt,下面我貼出此結構體的具體定義:
/*
* This structure is used to hold the arguments that are used when loading binaries.
*/
// 核心中注釋表明了這個結構體是用于儲存載入二進制檔案的參數.
struct linux_binprm {
char buf[BINPRM_BUF_SIZE];
#ifdef CONFIG_MMU
struct vm_area_struct *vma;
unsigned long vma_pages;
#else
//...
unsigned interp_flags;
unsigned interp_data;
unsigned long loader, exec;
};
- 在do_execve_common()中的searchbinaryhandler(),這個函數回去搜尋和比對合适的可執行檔案裝載處理過程,下面這個函數的精簡代碼:
int search_binary_handler(struct linux_binprm *bprm)
{
// 周遊formats連結清單
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
bprm->recursion_depth++;
// 應用每種格式的load_binary方法
retval = fmt->load_binary(bprm);
read_lock(&binfmt_lock);
put_binfmt(fmt);
bprm->recursion_depth--;
// ...
}
return retval;
}
- 這裡需要說明的是,這裡的fmt變量的類型是struct linux_binfmt *, 但是這一個類型與之前在do_execve_common()中的bprm是不一樣的,具體定義如下:
- 這裡的linux_binfmt對象包含了一個單連結清單,這個單連結清單中的第一個元素的位址存儲在formats這個變量中
- list_for_each_entry依次應用load_binary的方法,同時我們可以看到這裡會有遞歸調用,bprm會記錄遞歸調用的深度
- 裝載ELF可執行程式的load_binary的方法叫做load_elf_binary方法,下面會進行具體分析
/*
* This structure defines the functions that are used to load the binary formats that
* linux accepts.
*/
struct linux_binfmt {
struct list_head lh; //單連結清單表頭
struct module *module;
int (*load_binary)(struct linux_binprm *);
int (*load_shlib)(struct file *);
int (*core_dump)(struct coredump_params *cprm);
unsigned long min_coredump; /* minimal dump size */
};
load_elf_binary()
static int load_elf_binary(struct linux_binprm *bprm)
{
// ....
struct pt_regs *regs = current_pt_regs(); // 擷取目前程序的寄存器存儲位置
// 擷取elf前128個位元組,作為魔數
loc->elf_ex = *((struct elfhdr *)bprm->buf);
// 檢查魔數是否比對
if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
goto out;
// 如果既不是可執行檔案也不是動态連結程式,就錯誤退出
if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
//
// 讀取所有的頭部資訊
// 讀入程式的頭部分
retval = kernel_read(bprm->file, loc->elf_ex.e_phoff,
(char *)elf_phdata, size);
// 周遊elf的程式頭
for (i = 0; i < loc->elf_ex.e_phnum; i++) {
// 如果存在解釋器頭部
if (elf_ppnt->p_type == PT_INTERP) {
//
// 讀入解釋器名
retval = kernel_read(bprm->file, elf_ppnt->p_offset,
elf_interpreter,
elf_ppnt->p_filesz);
// 打開解釋器檔案
interpreter = open_exec(elf_interpreter);
// 讀入解釋器檔案的頭部
retval = kernel_read(interpreter, 0, bprm->buf,
BINPRM_BUF_SIZE);
// 擷取解釋器的頭部
loc->interp_elf_ex = *((struct elfhdr *)bprm->buf);
break;
}
elf_ppnt++;
}
// 釋放空間、删除信号、關閉帶有CLOSE_ON_EXEC标志的檔案
retval = flush_old_exec(bprm);
setup_new_exec(bprm);
// 為程序配置設定使用者态堆棧,并塞入參數和環境變量
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack);
current->mm->start_stack = bprm->p;
// 将elf檔案映射進記憶體
for(i = 0, elf_ppnt = elf_phdata;
i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
if (unlikely (elf_brk > elf_bss)) {
unsigned long nbyte;
// 生成BSS
retval = set_brk(elf_bss + load_bias,
elf_brk + load_bias);
// ...
}
// 可執行程式
if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {
elf_flags |= MAP_FIXED;
} else if (loc->elf_ex.e_type == ET_DYN) { // 動态連結庫
// ...
}
// 建立一個新線性區對可執行檔案的資料段進行映射
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, 0);
}
}
// 加上偏移量
loc->elf_ex.e_entry += load_bias;
// ....
// 建立一個新的匿名線性區,來映射程式的bss段
retval = set_brk(elf_bss, elf_brk);
// 如果是動态連結
if (elf_interpreter) {
unsigned long interp_map_addr = 0;
// 調用一個裝入動态連結程式的函數 此時elf_entry指向一個動态連結程式的入口
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias);
// ...
} else {
// elf_entry是可執行程式的入口
elf_entry = loc->elf_ex.e_entry;
// ....
}
// 修改儲存在核心堆棧,但屬于使用者态的eip和esp
start_thread(regs, elf_entry, bprm->p);
retval = 0;
//
}
這段代碼相當之長,我們做了相當大的精簡,雖然對主要部分做了注釋,但是為了友善我還是把主要過程闡述一邊:
- 檢查ELF的可執行檔案的有效性,比如魔數,程式頭表中段(segment)的數量
- 尋找動态連結的.interp段,設定動态連結路徑
- 根據ELF可執行檔案的程式頭表的描述,對ELF檔案進行映射,比如代碼,資料,隻讀資料
- 初始化ELF程序環境
- 将系統調用的傳回位址修改為ELF可執行程式的入口點,這個入口點取決于程式的連接配接方式,對于靜态連結的程式其入口就是e_entry,而動态連結的程式其入口是動态連結器
- 最後調用start_thread,修改儲存在核心堆棧,但屬于使用者态的eip和esp,該函數代碼如下:
start_thread
void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
set_user_gs(regs, 0); // 将使用者态的寄存器清空
regs->fs = 0;
regs->ds = __USER_DS;
regs->es = __USER_DS;
regs->ss = __USER_DS;
regs->cs = __USER_CS;
regs->ip = new_ip; // 新程序的運作位置- 動态連結程式的入口處
regs->sp = new_sp; // 使用者态的棧頂
regs->flags = X86_EFLAGS_IF;
set_thread_flag(TIF_NOTIFY_RESUME);
}
總結
如你所見,執行程式的過程是一個十分複雜的過程,exec本質在于替換fork()後,根據制定的可執行檔案對程序中的相應部分進行替換,最後根據連接配接方式的不同來設定好執行起始位置,然後開始執行程序.