深入分析ELF文件结构及其载入过程
文章目錄
- 前言
- ELF目標文件類型
- 以下面例子深入分析ELF
- 詳解file命令結果的各個部分
- ELF的文件結構
- ELF知識擴展
- Linux系統裝載ELF的過程
- 用戶層面
- 系統層面
前言
一般程序符號和數據,包括:全局變量,靜態全局變量,全局函數,靜態全局函數,外部符號(函數/變量),局部變量,局部靜態變量,字面量(常量)等。程序從源碼(如:C語言)到ELF二進制可執行文件,一般需要通過編譯器和鏈接器來處理并生產。
ELF文件由4部分組成,分別是ELF頭(ELF header)、程序頭表(Program header table)、節(Section)和節頭表(Section header table)。實際上,一個文件中不一定包含全部內容,而且它們的位置也未必如同所示這樣安排,只有ELF頭的位置是固定的,其余各部分的位置、大小等信息由ELF頭中的各項值來決定。
ELF目標文件類型
(1)可重定位的對象文件(Relocatable file)
Linux中.o文件。這類文件包含了代碼和數據,可以用來鏈接生成可執行或共享目標文件,靜態鏈接庫也可以歸為這一類。
(3)可執行的對象文件(Executable file)
ELF可執行文件。
(3)可共享庫文件(Shared object file)
Linux中.so文件。這類文件可以跟其他的重定位文件和.so文件鏈接,產生新的.so文件。第二種是動態鏈接器可以將幾個這種.so文件與可執行文件結合,作為進程映像的一部分來運行。
(4) Linux下的核心轉存文件(Core Dump File)
當進程意外終止時,系統可以將該進程的地址空間的內容及終止時的一些其它信息轉存到此Dump File。
以下面例子深入分析ELF
以下面的C程序為例:
#include <pthread.h> #include <stdio.h>const char *FLAG = "[INFO]";char *infoprefixstr = "ThdID:";const int const_num = 111; int gbl_num = 222;static void * static_func(){printf("static_func be called.\n");return NULL; }void *thread_start(void *args) {printf("%s%s%ld. const_num:%d. gbl_num:%d\n",FLAG, infoprefixstr, *((pthread_t *) args),const_num, gbl_num);return static_func(); }int main(int argc, char **argv) {pthread_t thds[argc - 1];for (int i = 0; i < argc; i++) {pthread_create(&thds[i], NULL, &thread_start, &thds[i]);}for (int i = 0; i < argc; i++) {pthread_join(thds[i], NULL);}static int static_scope_var = 333;printf("main exitting......static_scope_var:%d\n",static_scope_var);return 0; }CMakeList.txt配置如下:
cmake_minimum_required(VERSION 3.15) project(test1 C)set(CMAKE_C_STANDARD 99)add_executable(test1 main.c) target_link_libraries(test1 PUBLIC -lpthread)編譯構建產生test1二進制程序。
詳解file命令結果的各個部分
使用file命令查看test1的文件詳情,得到如下結果:
$ file test1/cmake-build-debug/test1 test1/cmake-build-debug/test1: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=45d6523007a7906dfb699d5f6fc66f3f4b7ec720, with debug_info, not strippedELF 64-bit表示文件是64位ELF格式的。
LSB shared object表示ELF文件是一個共享對象。
注:“LSB executable”(ET_EXEC)和"LSB shared object"(ET_DYN)的區別是什么?
- 在Linux內核/動態加載程序中ET_EXEC與ET_DYN的主要作用是通知可執行文件是否可以通過ASLR放置在隨機存儲器中。GCC在編譯時,默認會增加-pie選項,使得生成的ELF是ET_DYN的。PIE可執行文件是DYN的,它們可以被地址隨機化,就像共享庫so一樣。
注:-pie、-fpie、-fPIE、-fpie、fPIC的區別是什么?
-
-fPIE與-fpie是等價的。
-
-pie,往往和-fpie或-fPIE配合使用,用于在目標機器上生成與位置無關的可執行文件。-pie選項在鏈接時指定,-fpie或-fPIE選項在編譯時指定。PIE(Position-Independent-Executable)是Binutils,glibc和gcc的一個功能,能用來創建能像共享庫一樣可重分配地址的程序,這種程序須連接到Scrt1.o。標準的可執行程序需要固定的地址,并且只有被裝載到這個地址時,程序才能正確執行。PIE能使程序像共享庫一樣在主存任何位置裝載,這需要將程序編譯成位置無關,并鏈接為ELF共享對象。
-
-fpic,使用于在目標機支持編譯共享庫時使用。編譯出的代碼將通過全局偏移表(Global Offset Table)中的常數地址訪存,動態裝載器將在程序開始執行時解析GOT表項(注意,動態裝載器操作系統的一部分,連接器是GCC的一部分)。而gcc中的-fPIC選項則是針對某些特殊機型做了特殊處理,比如適合動態鏈接并能避免超出GOT大小限制之類的錯誤。
-
-fPIC與-fpic都是在編譯時加入的選項,用于生成位置無關的代碼(Position-Independent-Code)。這兩個選項都是可以使代碼在加載到內存時使用相對地址,所有對固定地址的訪問都通過全局偏移表(GOT)來實現。-fPIC和-fpic最大的區別在于是否對GOT的大小有限制。-fPIC對GOT表大小無限制,所以如果在不確定的情況下,使用-fPIC是更好的選擇。
x86-64表示目標機CPU指令集架構。
version 1 (SYSV)表示操作系統和ABI標識符,ELF規范中包含如下幾類:
Table 5. Operating System and ABI Identifiers, e_ident[EI_OSABI] Name Value Meaning ELFOSABI_SYSV 0 System V ABI ELFOSABI_HPUX 1 HP-UX operating system ELFOSABI_STANDALONE 255 Standalone (embedded) applicationdynamically linked表示ELF是動態鏈接的。
interpreter /lib64/ld-linux-x86-64.so.2表示程序的加載器。
for GNU/Linux 3.2.0表示操作系版本號。
BuildID[sha1]=45d6523007a7906dfb699d5f6fc66f3f4b7ec720表示文件的構建碼。個人理解是m
with debug_info表示ELF文件帶有debug信息。
not stripped表示保留ELF的所有符號表信息,未刪除一些符號表信息。如果輸出的是stripped表示已經刪除了ELF中一些符號表信息。
注:一般編譯出來的ELF中都有符號表(symbol table),該表中包括所有的符號(程序的入口點還有變量的地址等等)。這些符號表可以用 strip工具去除,這樣的話這個文件就無法讓debug程序跟蹤了,但是會生成比較小的可執行文件。ELF可執行文件中的符號表可以部分去除,由于部分符號在加載運行時起著重要的作用,所以用strip永遠不可能完全去除elf格式文件中的符號表。對未連接的目標文件來說如果用strip去掉符號表的話,會導致連接器無法連接。
ELF文件中除了包含指令、數據,還包括符號表、調試信息、字符串等,如果是可重定位對象文件還包含鏈接時所須的一些信息。一般目標文件將這些信息按不同的屬性以Section(節)的形式存儲,有時候也叫Segment(段),在一般情況下,它們都表示一個一定長度的區域,基本上不加以區別。后面將統一稱為“段”。
ELF的文件結構
基本結構如下所示:
+====================+ + ELF header + // 包含了整個文件的基本屬性,如:文件版本,目標機器型號,入口地址。 +====================+ +Program header table+ // 程序標頭表是一組程序標頭,它們定義了運行時程序的內存布局。對于.obj文件可選的 +====================+ + .interp + // 可執行文件所需要的動態鏈接器的位置。 +--------------------+ + .note.ABI-tag + // 用于聲明ELF的預期運行時ABI。包括操作系統名稱及其運行時版本。 +--------------------+ + .note.gnu.build-id + // 表示唯一的構建ID位串。 +--------------------+ + .gnu.hash + // 符號hash表。若段名是.hash,則使用的是SYSV hash,其比gnu hash性能差。 +--------------------+ + .dynsym + // 動態符號表用來保存與動態鏈接相關的導入導出符號,不包括模塊內部的符號。 +--------------------+ + .dynstr + // 動態符號字符串表,用于保存符號名的字符串表。靜態鏈接時為.strtab。 +--------------------+ + .gnu.version + // 表中條目與.dynsym動態符號表相同。每個條目指定了相應動態符號定義或版本要求。 +--------------------+ + .gnu.version_r + // 版本定義。 +--------------------+ + .rela.dyn + // 包含共享庫(PLT除外)所有部分的RELA類型重定位信息。 +--------------------+ + .rela.plt + // 包含共享庫或動態鏈接的應用程序的PLT節的RELA類型重定位信息。 +--------------------+ + .init + // 程序初始化段。 +--------------------+ + .plt + // 過程鏈接表(Procedure Linkage Table),用來實現延遲綁定。 +--------------------+ + .plt.got + // 暫無。。。。。 +--------------------+ + .text + // 代碼段 +--------------------+ + .fini + // 程序結束段 +--------------------+ + .rodata + // 只讀變量(const修飾的)和字符串變量。 +--------------------+ + .rodata1 + // 據我所知,.rodata和.rodata1是相同的。一些編譯器會.rodata分為2個部分。 +--------------------+ + .eh_frame_hdr + // 包含指針和二分查找表,(一般在C++)運行時可以有效地從eh_frame中檢索信息。 +--------------------+ + .eh_frame + // 它包含異常解除和源語言信息。此部分中每個條目都由單個CFI(呼叫幀信息)表示。 +--------------------+ + .init_array + // 包含指針指向了一些初始化代碼。初始化代碼一般是在main函數之前執行的。 +--------------------+ + .fini_array + // 包含指針指向了一些結束代碼。結束代碼一般是在main函數之后執行的。 +--------------------+ + .dynamic + // 保存動態鏈接器所需的基本信息。 +--------------------+ + .got + // 全局偏移表,存放所有對于外部變量引用的地址。 +--------------------+ + .got.plt + // 保存所有對于外部函數引用的地址。延遲綁定主要使用.got.plt表。 +--------------------+ + .data + // 全局變量和靜態局部變量。 +--------------------+ + .data1 + // 據我所知,.data和.data1是相同的。一些編譯器會.data分為2個部分。 +--------------------+ + .bss + // 未初始化的全局變量和局部局部變量。 +--------------------+ + .comment + // 存放編譯器版本信息 +--------------------+ + .debug_aranges + // 內存地址和編譯之間的映射 +--------------------+ + .debug_info + // 包含DWARF調試信息項(DIE)的核心DWARF數據 +--------------------+ + .debug_abbrev + // .debug_info部分中使用的縮寫 +--------------------+ + .debug_line + // 程序行號 +--------------------+ + .debug_str + // .debug_info使用的字符串表 +--------------------+ + .symtab + // 靜態鏈接時的符號表,保存了所有關于該目標文件的符號的定義和引用。 +--------------------+ + .strtab + // 默認字符串表。 +--------------------+ + .shstrtab + // 字符串表。 +====================+ +Section header table+ // 用于引用Sections的位置和大小,并且主要用于鏈接和調試目的。對于Exec文件可選 +====================+ELF知識擴展
關于ELF格式說明的更多信息,點擊查看ELF Specification、Object File Format。
關于Program Header Table的更多信息,點擊查看Program Header、Program Header Table。
關于Section header table的更多信息,點擊查看Section header table。
關于.debug_xxx段的更多信息,點擊查看[DWARF調試格式介紹](http://www.dwarfstd.org/doc/Debugging using DWARF-2012.pdf)。
Linux系統裝載ELF的過程
用戶層面
bash進程會調用fork()系統調用創建一個新的進程,然后在新的進程調用execve()系統調用執行指定的ELF文件。進入execve()系統調用之后,Linux內核就開始進行真正的裝載工作。
系統層面
注:以下分析將使用linux-3.18.6的內核,其他版本大同小異。
在內核中execve()系統調用相應的入口是sys_execve(),它被定義在linux-3.18.6/include/linux/syscalls.h。sys_execve()函數將調用linux-3.18.6/fs/exec.c文件中第1430行的do_execve_common函數進行處理
1427 /* 1428 * sys_execve() executes a new program. 1429 */ 1430 static int do_execve_common(struct filename *filename, 1431 struct user_arg_ptr argv, 1432 struct user_arg_ptr envp) 1433 { ... 1474 file = do_open_exec(filename); // 打開可執行文件 1475 retval = PTR_ERR(file); 1476 if (IS_ERR(file)) 1477 goto out_unmark; 1478 1479 sched_exec(); // 是一個寶貴的平衡機會,因為此時任務具有最小的有效內存和高速緩存占用空間。 1480 1481 bprm->file = file; 1482 bprm->filename = bprm->interp = filename->name; 1483 1484 retval = bprm_mm_init(bprm); // 創建一個新的mm_struct(將賦值給bprm->mm字段),并使用臨時堆棧vm_area_struct填充它。 此時我們沒有足夠的上下文來設置堆棧標志,權限和偏移量,因此我們使用臨時值。稍后將在setup_arg_pages()中對其進行更新。 1485 if (retval) 1486 goto out_unmark; 1487 1488 bprm->argc = count(argv, MAX_ARG_STRINGS); // 參數個數 1489 if ((retval = bprm->argc) < 0) 1490 goto out; 1491 1492 bprm->envc = count(envp, MAX_ARG_STRINGS); // 環境變量 1493 if ((retval = bprm->envc) < 0) 1494 goto out; 1495 1496 retval = prepare_binprm(bprm); // 檢查文件權限,并讀取文件前128個byte確定文件格式和類型 1497 if (retval < 0) 1498 goto out; ... 1513 retval = exec_binprm(bprm); // 執行 1514 if (retval < 0) 1515 goto out; ... 1547 }do_execve_common中1496行,將調用linux-3.18.6/fs/exec.c文件中prepare_binprm函數,讀取文件首128個字節來判斷文件格式。(注:每種可執行文件格式的開頭幾個字節都是很特殊的,特別是開頭的魔數Magic Number,通過對魔數的判斷可以確定文件的格式和類型。)如下:
1253 /* 1254 * Fill the binprm structure from the inode. 1255 * Check permissions, then read the first 128 (BINPRM_BUF_SIZE) bytes 1256 * 1257 * This may be called multiple times for binary chains (scripts for example). 1258 */ 1259 int prepare_binprm(struct linux_binprm *bprm) 1260 { 1261 struct inode *inode = file_inode(bprm->file); 1262 umode_t mode = inode->i_mode; 1263 int retval; 1264 1265 1266 /* clear any previous set[ug]id data from a previous binary */ 1267 bprm->cred->euid = current_euid(); // 清除之前的信任證 1268 bprm->cred->egid = current_egid(); // 清除之前的信任證 ... 1292 /* fill in binprm security blob */ 1293 retval = security_bprm_set_creds(bprm); // 設置安全信任證 1294 if (retval) 1295 return retval; 1296 bprm->cred_prepared = 1; 1297 1298 memset(bprm->buf, 0, BINPRM_BUF_SIZE); 1299 return kernel_read(bprm->file, 0, bprm->buf, BINPRM_BUF_SIZE); // BINPRM_BUF_SIZE定義為128 1300 }do_execve_common中1513行,調用exec_binprm函數執行文件。exec_binprm函數中1416行調用search_binary_handler函數,來搜索和匹配合適的可執行文件裝載處理程序。
1405 static int exec_binprm(struct linux_binprm *bprm) 1406 { 1407 pid_t old_pid, old_vpid; 1408 int ret; 1409 1410 /* 需要在load_binary更改之前獲取pid */ 1411 old_pid = current->pid; 1412 rcu_read_lock(); 1413 old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent)); 1414 rcu_read_unlock(); 1415 1416 ret = search_binary_handler(bprm); // 搜索和匹配合適的可執行文件裝載處理過程。 1417 if (ret >= 0) { 1418 audit_bprm(bprm); 1419 trace_sched_process_exec(current, old_pid, bprm); 1420 ptrace_event(PTRACE_EVENT_EXEC, old_vpid); 1421 proc_exec_connector(current); 1422 } 1423 1424 return ret; 1425 }看一下search_binary_handler函數是如何搜索匹配,并執行加載的。search_binary_handler函數。
注意:
-
search_binary_handler函數第1369行中的formats是一個靜態全局變量,formats是struct list_head類型。實際上formats的作用是一個列表的頭,列表中每個struct linux_binfmt元素是經過register_binfmt/insert_binfmt函數注冊/插入進列表的。linux-3.18.6內核版本中注冊的文件加載器有:
- register_binfmt(&elf_fdpic_format); // 將fdpic二進制文件加載到內存。load an fdpic binary into various bits of memory
- register_binfmt(&aout_format); // 這些是用于加載a.out樣式的可執行文件和共享庫的函數。 在其他任何地方都沒有二進制相關代碼。These are the functions used to load a.out style executables and shared libraries. There is no binary dependent code anywhere else.
- register_binfmt(&elf_format); // 加載elf二進制文件。load elf binary
- register_binfmt(&em86_format); //
- register_binfmt(&som_format); // 這些是用于加載SOM可執行文件和共享庫的功能。 在其他任何地方都沒有二進制相關代碼。These are the functions used to load SOM executables and shared libraries. There is no binary dependent code anywhere else.
- **register_binfmt(&script_format); ** // 加載腳本文件。load script file
- register_binfmt(&flat_format); // 這些是用于加載flat樣式可執行文件和共享庫的函數。 在其他任何地方都沒有二進制相關代碼。These are the functions used to load flat style executables and shared libraries. There is no binary dependent code anywhere else.
代碼如下:
1349 /* 1350 * cycle the list of binary formats handler, until one recognizes the image 1351 */ 1352 int search_binary_handler(struct linux_binprm *bprm) 1353 { 1354 bool need_retry = IS_ENABLED(CONFIG_MODULES); 1355 struct linux_binfmt *fmt; ... 1367 retry: 1368 read_lock(&binfmt_lock); 1369 list_for_each_entry(fmt, &formats, lh) { // 循環便利formats列表,fmt是每個元素的指針 1370 if (!try_module_get(fmt->module)) 1371 continue; 1372 read_unlock(&binfmt_lock); 1373 bprm->recursion_depth++; 1374 retval = fmt->load_binary(bprm); // load_binary是struct linux_binfmt結構體中的一個成員,指定加載函數的指針。 1375 read_lock(&binfmt_lock); ... 1388 } 1389 read_unlock(&binfmt_lock); ... 1400 1401 return retval; 1402 } 1403 EXPORT_SYMBOL(search_binary_handler);最終search_binary_handler函數將在1374行調用./linux-3.18.6/fs/binfmt_elf.c文件中571行的load_elf_binary函數,load_elf_binary函數將對ELF文件進行裝載。
load_elf_binary函數主要做的事情包括:
當load_elf_binary()執行完畢,返回到do_execve_common函數,再返回到sys_execve()函數時,load_elf_binary()中已經把系統調用的返回地址改成了被裝載的ELF程序的入口地址了。
所以當sys_execve()系統調用從內核態返回到用戶態時,RIP寄存器直接跳到了ELF程序的入口地址,于是新的程序開始執行,ELF可執行文件加載完成。
總結
以上是生活随笔為你收集整理的深入分析ELF文件结构及其载入过程的全部內容,希望文章能夠幫你解決所遇到的問題。