深入分析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文件结构及其载入过程的全部内容,希望文章能够帮你解决所遇到的问题。