Linux内核装载和启动一个可执行程序
“平安的祝福 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”
理解编译链接的过程和ELF可执行文件格式
a.out基本上已经不用了,分成了两种PE和ELF,linux主要用ELF格式。
COFF 目标文件的格式
PE 目标文件的格式
ELF 目标文件的格式
ELF: 可执行连接格式
可执行连接格式是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary
Interface(ABI)而开发和发布的。工具接口标准委员会(TIS)选择了正在发展中的ELF标准作为工作在32位INTEL体系上不同操作系统
之间可移植的二进制文件格式。
在object文件中有三种主要的类型。
* 一个可重定位(relocatable)文件保存着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者是一个共享文件。
* 一个可执行(executable)文件保存着一个用来执行的程序;该文件指出了exec(BA_OS)如何来创建程序进程映象。
* 一个共享object文件保存着代码和合适的数据,用来被下面的两个链接器链接。第一个是连接编辑器,可以和其他的可重定位和共享object文件来创建其他的object。第二个是动态链接器,联合一个可执行文件和其他的共享object文件来创建一个进程映象。
Object文件格式
Linking 视角 Execution 视角
============ ==============
ELF header ELF header
Program header table (optional) Program header table
Section 1 Segment 1
... Segment 2
Section n ...
Section header table Section header table (optional)
一个ELF头在文件的开始,保存了路线图(road map),描述了该文件的组织情况。
sections保存着object 文件的信息,从连接角度看:包括指令,数据,符号表,重定位信息等等。
注意: 虽然图显示出程序头表立刻出现在一个ELF头后,section头表跟着其他section部分出现,事实是的文件是可以不同的。此外,sections和段(segments)没有特别的顺序。只有ELF头(elf header)是在文件的固定位置。
ELF Header
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT];
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;
* e_entry
该成员是系统第一个传输控制的虚拟地址,在那启动进程。假如文件没有如何关联的入口点,该成员就保持为0。
sections包含了在一个object文件中的所有信息,除了ELF报头,程序报头表(program header table),和section报头表(section header table)。
此外,object文件的sections满足几天条件:
* 每个在object文件中的section都有自己的一个section的报头来描述它。section头可能存在但section可以不存在。
* 每个section在文件中都占有一个连续顺序的空间(但可能是空的)。
* 文件中的Sections不可能重复。文件中没有一个字节既在这个section中又在另外的一个section中。
* object文件可以有"非活动的"空间。不同的报头和sections可以不覆盖到object文件中的每个字节。"非活动"数据内容是未指定的。
链接是一个收集、组织程序所需的不同代码和数据的过程,以便程序能被装入内存并被执行。
链接过程分为两步:
-空间与地址分配
扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步
中,链接器将能获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。
-符号解析与重定位
使用上面第一步中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。事实上第二步是链接过程的核心,特别是重定位过程。
查看ELF文件的头部: readelf -h hello
编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接
execve系统调用
-
命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。
-
$ ls -l /usr/bin 列出/usr/bin下的目录信息
-
Shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身
-
例如,int main(int argc, char *argv[])
-
又如, int main(int argc, char *argv[], char *envp[])
-
Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数
-
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
-
库函数exec*都是execve的封装例程
-
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char * argv[]) { int pid; /* fork another process */ pid = fork(); if (pid<0) { /* error occurred */ fprintf(stderr,"Fork Failed!"); exit(-1); } else if (pid==0) { /* child process */ execlp("/bin/ls","ls",NULL); } else { /* parent process */ /* parent will wait for the child to complete*/ wait(NULL); printf("Child Complete!\n"); exit(0); } }
命令行参数和环境串都放在用户态堆栈中
- 实验截图
-
装载时动态链接和运行时动态链接应用举例
-
动态链接分为可执行程序装载时动态链接和运行时动态链接,如下代码演示了这两种动态链接。
文件在附件:https://files.cnblogs.com/files/pingandezhufu/SharedLibDynamicLink.zip
-
准备.so文件
shlibexample.h (1.3 KB) - Interface of Shared Lib Example
shlibexample.c (1.2 KB) - Implement of Shared Lib Example
编译成libshlibexample.so文件
-
-
$ gcc -shared shlibexample.c -o libshlibexample.so -m32
dllibexample.h (1.3 KB) - Interface of Dynamical Loading Lib Example
dllibexample.c (1.3 KB) - Implement of Dynamical Loading Lib Example
编译成libdllibexample.so文件
-
$ gcc -shared dllibexample.c -o libdllibexample.so -m32
-
分别以共享库和动态加载共享库的方式使用libshlibexample.so文件和libdllibexample.so文件
main.c (1.9 KB) - Main program
编译main,注意这里只提供shlibexample的-L(库对应的接口头文件所在目录)和-l(库名,如libshlibexample.so去掉lib和.so的部分),并没有提供dllibexample的相关信息,只是指明了-ldl
-
-
$ gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32 $ export LD_LIBRARY_PATH=$PWD #将当前目录加入默认路径,否则main找不到依赖的库文件,当然也可以将库文件copy到默认路径下。 $ ./main
使用gdb跟踪分析一个execve系统调用内核处理函数sys_execve
可执行程序的装载
命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。
Shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身
例如,int main(int argc, char *argv[])
又如, int main(int argc, char *argv[], char *envp[])
Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
库函数exec*都是execve的封装例程
sys_execve内部会解析可执行文件格式(在fs/exec.c)
SYSCALL_DEFINE3(execve,
constchar __user *, filename,
constchar __user *const __user *, argv,
constchar __user *const __user *, envp)
{
return do_execve(getname(filename), argv, envp);
}
从这个函数得知sys_execve调用do_execve,do_execve -> do_execve_common -> exec_binprm -> search_binary_handler;
exec_binprm对可执行文件处理;
search_binary_handler符合寻找文件格式对应的解析模块,如下:
1369 list_for_each_entry(fmt, &formats, lh) {
1370 if (!try_module_get(fmt->module))
1371 continue;
1372 read_unlock(&binfmt_lock);
1373 bprm->recursion_depth++;
1374 retval = fmt->load_binary(bprm);
1375 read_lock(&binfmt_lock);
对于ELF格式的可执行文件fmt->load_binary(bprm);执行的应该是load_elf_binary其内部是和ELF文件格式解析的部分需要和ELF文件格式标准结合起来阅读。
load_elf_binary -> start_thread;
Linux内核是如何支持多种不同的可执行文件格式的?(fs/binfmt_elf.c)
82static struct linux_binfmt elf_format = {
83 .module = THIS_MODULE,
84 .load_binary = load_elf_binary,
85 .load_shlib = load_elf_library,
86 .core_dump = elf_core_dump,
87 .min_coredump = ELF_EXEC_PAGESIZE,
88};
在下面的函数中完成注册elf_format
2198static int __init init_elf_binfmt(void)
2199{
2200 register_binfmt(&elf_format);
2201 return 0;
2202}
于是在fmt->load_binary(bprm);执行的应该是load_elf_binary,从而实现多态机制。
实验截图:
刚开始执行exec测试:
设置断点:
运行调试:
特别关注新的可执行程序是从哪里开始执行的?为什么execve系统调用返回后新的可执行程序能顺利执行?
-
庄生梦蝶 —— 醒来迷惑是庄周梦见了蝴蝶还是蝴蝶梦见了庄周?
-
庄周(调用execve的可执行程序)入睡(调用execve陷入内核),醒来(系统调用execve返回用户态)发现自己是蝴蝶(被execve加载的可执行程序)
-
修改int 0x80压入内核堆栈的EIP
-
load_elf_binary -> start_thread
//修改int 0x80压入内核堆栈的EIP,并将内核的堆栈进行切换,方便返回用户态时能够开始执行新的这个程序
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;//new_ip返回用户态的第一条指令,在linux下通常是0x804800a+偏移 regs->sp = new_sp; regs->flags = X86_EFLAGS_IF; /* * force it to the iret return path by making it look as if there was * some work pending. */ set_thread_flag(TIF_NOTIFY_RESUME); }
在load_elf_binary 中查看执行动态链接时的代码:
if (elf_interpreter) { unsigned long interp_map_addr = 0; elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_map_addr, load_bias);//返回的是动态链接库的起点,负责解析当前的动态链接库,递归地广度遍历/加载依赖的动态链接库 if (!IS_ERR((void *)elf_entry)) { /* * load_elf_interp() returns relocation * adjustment */ interp_load_addr = elf_entry; elf_entry += loc->interp_elf_ex.e_entry; } if (BAD_ADDR(elf_entry)) { retval = IS_ERR((void *)elf_entry) ? (int)elf_entry : -EINVAL; goto out_free_dentry; } reloc_func_desc = interp_load_addr; allow_write_access(interpreter); fput(interpreter); kfree(elf_interpreter); } else { elf_entry = loc->elf_ex.e_entry;//静态链接完的入口地址 if (BAD_ADDR(elf_entry)) { retval = -EINVAL; goto out_free_dentry; } }
动态链接主要时用户态的动态连接器来负责完成的。内核态只是加载其可执行的程序。
参考文献: