可执行程序工作原理
ELF目标文件格式
-
ELF概述
- “目标文件”指编译器生成的文件,“目标”指类似x86或x64的目标平台,它决定编译器使用的机器指令集。
- “目标文件”也叫ABI,它和“目标平台”是二进制兼容的。
- 最古老的目标文件格式是a.out,后来发展为COFF格式,现在linux常用的格式为ELF。
- ELF(Executable and Linkable Format)即可执行并可链接的格式,是一个目标文件格式的标准。
- ELF是一种对象文件的格式,用于定义不同类型的对象文件中都有什么内容,以什么样的格式放这些内容。
- ELF在首部会描绘整个文件的组织结构,还包括了很多系统定义的以及用户自定义的节。
-
ELF文件的3种类型
- 可重定位文件:一般是中间文件,还需要继续处理。由编译器和汇编器创建,一个源代码文件会生成一个可重定位文件。
- 内核编译中.o文件便是可重定位目标文件,最后所有的.o文件链接为一个文件,即linux内核
- 可执行文件:一般由多个可重定位文件结合生成,是完成了所有重定位工作和符号解析的文件。
- 共享目标文件:共享库,指可以被可执行文件或其他库文件使用的目标文件,例如标准C的库文件libc.so。
- 可以理解为没有主函数main的“可执行”文件,只有一堆函数可供其他可执行文调用。
- linux下共享库后缀为.so文件,so代表shared object。
- 可重定位文件:一般是中间文件,还需要继续处理。由编译器和汇编器创建,一个源代码文件会生成一个可重定位文件。
-
ELF文件的索引表
-
ELF Header结构
- ELF表头首先会给出很多关于本ELF文件的属性信息,如前文提到过的3种ELF类型就是通过e_type来实现的
- e_type值1、2、3、4分别代表可重定位文件、可执行文件、共享目标文件、核心文件。
- 节头表基本定义了整个ELF文件的组成,可以说是整个ELF就是由若干个节组成的。
-
Section Header结构
-用于链接的目标文件必须包含节区头部表,其他目标文件有没有这个表皆可。
-
program Header结构
- 段头表是和创建进程相关的,描述了连续的几个节在文件中的位置、大小以及它被放进内存后的位置和大小。
-
相关操作指令
- man elf:查看elf详细的格式定义
- readelf:用于显示一个或多个elf格式的目标文件的信息,可以通过它的选项来控制显示哪些信息。
- objdump:显示二进制文件信息,用于查看目标文件或者可执行的目标文件的构成的gcc工具。
程序编译
-
程序从源代码到可执行文件经过四个步骤,具体如下(用hello.c示例):
- 预处理:gcc -E hello.c -o hello.i
- 编译:gcc -S hello.i -o hello.s -m32
- 汇编:gcc -C hello.s -o hello.o -m32
- 链接:gcc hello.o -o hello -m32 -static
-
预处理:gcc -E hello.c -o hello.i
- 删除所有的注释
- 删除所有的“#define”,展开所有的宏定义
- 处理所有的条件预编译指令
- 处理“#include”预编译指令
- 添加行号和文件名标识
-
编译:gcc -S hello.i -o hello.s -m32
- 编译时,gcc首先检查代码的规范性、是否有语法错误。以确定代码实际要做的工作。
- 在检查代码无误后,gcc讲代码翻译成汇编语言
-
汇编:gcc -C hello.s -o hello.o -m32
- -m32表示生成32位的目标文件
- 汇编后形成的.o格式的文件已经是ELF格式文件。
- 程序编译后生成的目标文件至少含有3个节区(.text .data .bss)
-
链接:gcc hello.o -o hello -m32 -static
- 链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可以被加载到内存种并执行。
- 上述代码是将编译出的.o文件与libc库文件进行链接,生成最终的可执行文件。
- 链接从过程上讲分为符号解析和重定位两部分
- 根据链接时机的不同,又分为静态链接和动态连接两种
链接与库
-
符号与符号解析
- 符号
- 符号包含全局变量和全局函数。例如printf就是一个符号,hello程序需要在函数库中找到这个符号。
- 符号表
- 供编译器用于保存有关源程序构造的各种信息的数据结构。
- 符号表的功能是找未知函数在其他库文件中的代码段的具体位置。
- 查看方式是objdump -t xxx.o或readelf -s xxx.o
- 符号表中的函数
- 链接前后,内存地址和符号对应的节区编号会发生改变。
- 链接前后,符号表个数的差异会很大
- 符号
-
重定位
- 重定位是把程序的逻辑地址空间变换成内存中的实际物理地址空间的过程,即装入时对目标程序中指令和数据的修改过程
- 它是实现多道程序在内存中同时运行的基础。
-
静态链接与动态链接
- 静态链接
- 在编译链接时直接将需要的执行代码复制到最终可执行文件中。
- 代码的装载速度快,执行速度也比较快,对外部的环境依赖度低。
- 编译时它会把需要的所有代码都链接进去,应用程序相对比较大。
- 动态链接
- 在编译时不直接复制可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统。
- 装载时动态链接
- 这种用法的前提是在编译之前已经明确知道要调用DLL中的哪几个函数,编译时在目标文件中只保留必要的链接信息,而不含DLL函数的代码。
- 当程序执行时,调用函数的时候利用链接信息加载DLL函数代码并在内存中将其链接入调用程序的执行空间中(全部函数加载进内存),其主要目的是便于代码共享。
- 运行时动态链接
- 这种方式是指在编译之前并不知道将会调用哪些DLL函数,完全是在运行过程中根据需要决定应调用哪个函数,将其加载到内存中(只加载调用的函数进内存),并标识内存地址,其他程序也可以使用该程序,并用LoadLibrary和GetProcAddress动态获得DLL函数的入口地址。
- 静态链接
实验
-
实验要求
- 理解编译链接的过程和ELF可执行文件格式
- 编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接,编程练习动态链接库的这两种使用方式。
- 使用gdb跟踪分析一个execve系统调用内核处理函数sys_execve ,验证您对Linux系统加载可执行程序所需处理过程的理解。
- 特别关注新的可执行程序是从哪里开始执行的?为什么execve系统调用返回后新的可执行程序能顺利执行?对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时会有什么不同?
-
实验步骤
- 将menu目录删除,利用git命令克隆一个新的menu目录,然后用test_exec.c将test.c覆盖。
rm -rf menu git clone https://github.com/mengning/menu.git
- 将menu目录删除,利用git命令克隆一个新的menu目录,然后用test_exec.c将test.c覆盖。
![](https://img2018.cnblogs.com/blog/1800808/201911/1800808-20191109231642984-364100017.png)
![](https://img2018.cnblogs.com/blog/1800808/201911/1800808-20191109231859464-432890259.png)
![](https://img2018.cnblogs.com/blog/1800808/201911/1800808-20191109232103861-377262856.png)
![](https://img2018.cnblogs.com/blog/1800808/201911/1800808-20191109232413463-870565543.png)
![](https://img2018.cnblogs.com/blog/1800808/201911/1800808-20191109232649624-1781610654.png)
![](https://img2018.cnblogs.com/blog/1800808/201911/1800808-20191109232756856-1776944147.png)
![](https://img2018.cnblogs.com/blog/1800808/201911/1800808-20191109233301959-1462653557.png)
- exec()函数分析
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);
}
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执行成功
}
static int exec_binprm(struct linux_binprm *bprm)
{
// 扫描formats链表,根据不同的文本格式,选择不同的load函数
ret = search_binary_handler(bprm);
// ...
return ret;
}
总结
本实验中,我首先了解到ELF文件的类型,有可重定位文件(一般是中间文件,还需要继续处理。由编译器和汇编器创建,一个源代码文件会生成一个可重定位文件)、可执行文件(一般由多个可重定位文件结合生成,是完成了所有重定位工作和符号解析的文件)、共享目标文件(共享库,指可以被可执行文件或其他库文件使用的目标文件,例如标准C的库文件libc.so)。程序从源代码到可执行文件经过四个步骤,分别是预处理(负责把include的文件包含进来,宏替换)、编 译( –S调用ccl,编译成汇编代码)、汇 编(调用as,得到二进制文件)、链 接(调用ld形成目标可执行文件)