可执行程序的装载
谈愈敏 原创作品转载请注明出处 《Linux内核分析》MOOC课程
一、预处理、编译、链接和目标文件的格式
可执行程序是怎么得来的?
- C代码
- 编译器预处理
- 编译成汇编代码
- 汇编器编译成目标代码
- 链接成可执行文件
- 操作系统加载到内存执行
举例说明:
目标文件的格式ELF
常见的目标文件格式:
- A.out -> COFF -> PE(windows)/ELF(linux)
目标文件也叫ABI:应用程序二进制接口
ABI是二进制兼容:目标文件已经适应某种CPU体系结构上的二进制指令
ELF格式中3种目标文件:
可执行文件头部:
静态链接的ELF可执行文件和进程的地址空间
ELF可执行文件加载到进程的地址空间时:
- 默认加载起始地址是0x8048000
- ELF头部大小不同,程序入口点也将不同,程序入口在头部有定义:
Entry point address
,即是可执行文件加载到内存中开始执行的第一行代码 - 一般静态链接会将所有代码放在一个代码段
- 动态链接的进程会有多个代码段,更复杂
二、可执行程序、共享库和动态链接
装载可执行程序之前的工作
了解可执行文件格式ELF,了解执行环境,一般通过shell程序启动可执行程序,提供执行的上下文环境,用户态环境和系统调用相结合。
可执行程序的执行环境
- 命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。
- $ ls -l /usr/bin 列出/usr/bin下的目录信息 //ls是一个可执行程序
- Shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身//也就是main函数
- 例如,int main(int argc, char *argv[])//愿意接受命令行参数,用户输入的
- 又如, int main(int argc, char argv[], char envp[])//*envp[]:shell的环境变量
- Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数
- int execve(const char * filename,char * const argv[ ],char * const envp[ ]);//函数原型
库函数exec*都是execve的封装例程
#include#include #include int main(int argc, char * argv[]){int pid;/* fork another process */pid = fork();//shell先创建子进程if (pid<0) { /* error occurred */ fprintf(stderr,"Fork Failed!"); exit(-1);} else if (pid==0) { /* child process */ execlp("/bin/ls","ls",NULL);//调用execlp加载可执行程序ls,这里是三个参数} else { /* parent process */ /* parent will wait for the child to complete*/ wait(NULL); printf("Child Complete!"); exit(0);}}
命令行参数和环境变量是如何保存和传递的?
创建的子进程完全复制父进程,调用execve时加载的可执行程序把子进程环境覆盖掉了,子进程用户态堆栈也被清空了,命令行参数和环境变量是如何保存在新的可执行程序的用户态堆栈呢?
参数传递过程:shell程序 -> execve系统调用 -> sys_execve 内核处理函数在初始化新程序堆栈时拷贝进去
先函数调用参数传递,再系统调用参数传递:调用execve时,参数压在shell程序当前进程的堆栈上,加载完新的可执行程序时被清空了,内核又创建了新的进程的用户态堆栈,把参数拷贝进去:
装载时动态链接和运行时动态链接应用举例
动态链接分为可执行程序装载时动态链接(用的较多)和运行时动态链接,如下代码演示了这两种动态链接。
1、准备.so文件
共享库:
shlibexample.h (1.3 KB) - Interface of Shared Lib Example
int SharedLibApi(); //定义了一个函数原型
shlibexample.c (1.2 KB) - Implement of Shared Lib Example
#include#include "shlibexample.h" //把shlibexample.h头文件加进来/** Shared Lib API Example* input : none* output : none* return : SUCCESS(0)/FAILURE(-1)**/int SharedLibApi(){ printf("This is a shared libary!\n"); //打印这是一个共享库 return SUCCESS;}
编译成libshlibexample.so文件
$ gcc -shared shlibexample.c -o libshlibexample.so -m32
动态加载库:
dllibexample.h (1.3 KB) - Interface of Dynamical Loading Lib Example
int DynamicalLoadingLibApi(); //声明一个函数
dllibexample.c (1.3 KB) - Implement of Dynamical Loading Lib Example
#include#include "dllibexample.h" //头文件#define SUCCESS 0#define FAILURE (-1)/* * Dynamical Loading Lib API Example * input : none * output : none * return : SUCCESS(0)/FAILURE(-1) * */int DynamicalLoadingLibApi(){ printf("This is a Dynamical Loading libary!\n");//这是一个动态加载库 return SUCCESS;}
编译成libdllibexample.so文件
$ gcc -shared dllibexample.c -o libdllibexample.so -m32
2、分别以共享库和动态加载共享库的方式使用libshlibexample.so文件和libdllibexample.so文件
main.c (1.9 KB) - Main program
#include#include "shlibexample.h" //只加了共享库#include //动态加载/* * Main program * input : none * output : none * return : SUCCESS(0)/FAILURE(-1) * */int main(){printf("This is a Main program!\n");/* Use Shared Lib */printf("Calling SharedLibApi() function of libshlibexample.so!\n");SharedLibApi(); //可直接调用,因为前面include "shlibexample.h" /* Use Dynamical Loading Lib */void * handle = dlopen("libdllibexample.so",RTLD_NOW); //使用dlopen加载起来if(handle == NULL){ printf("Open Lib libdllibexample.so Error:%s\n",dlerror()); return FAILURE;}int (*func)(void); //定义一个函数指针char * error;func = dlsym(handle,"DynamicalLoadingLibApi"); //找到函数名DynamicalLoadingLibApi赋给funcif((error = dlerror()) != NULL){ printf("DynamicalLoadingLibApi not found:%s\n",error); return FAILURE;} printf("Calling DynamicalLoadingLibApi() function of libdllibexample.so!\n");func(); //使用func,即调用的是DynamicalLoadingLibApidlclose(handle); return SUCCESS;}
编译main,注意这里只提供shlibexample的-L(库对应的接口头文件所在目录)和-l(库名,如libshlibexample.so去掉lib和.so的部分),并没有提供dllibexample的相关信息,只是指明了-ldl
$ gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32 //库文件是系统默认给你找,dl:动态加载$ export LD_LIBRARY_PATH=$PWD //将当前目录加入默认路径,否则main找不到依赖的库文件,当然也可以将库文件copy到默认路径下。$ ./main //dllibexample的相关信息在main函数内部指明了,编译时并没有,下面运行结果有This is a Main program!Calling SharedLibApi() function of libshlibexample.so!This is a shared libary!Calling DynamicalLoadingLibApi() function of libdllibexample.so!This is a Dynamical Loading libary!注意这里:shlibexample共享库:装载可执行程序时完成动态链接 dllibexample动态加载共享库:在程序执行过程中有程序自身来装载共享库
三、可执行程序的装载
可执行程序的装载相关关键问题分析
特殊系统调用:
- fork:两次返回,第一次父进程返回,子进程也返回,到特定的点:从ret_ from_fork开始执行然后返回用户态。
- execve:陷入到内核态调用execve加载可执行程序,把当前的程序覆盖掉了,返回的是一个新的可执行程序。返回的是新程序的执行起点,一般是main函数,要构建好执行环境。
sys_execve内部会解析可执行文件格式:
do_ execve -> do_ execve_ common -> exec_binprm
search_ binary_handler寻找符合文件格式对应的解析模块,如下:被观察者模式:
1369 list_for_each_entry(fmt, &formats, lh) {// 在链表中寻找能够解析ELF格式的内核模块1370 if (!try_module_get(fmt->module))1371 continue;1372 read_unlock(&binfmt_lock);1373 bprm->recursion_depth++;1374 retval = fmt->load_binary(bprm);//找到fmt这个链表结点,能够解析ELF格式,加载处理函数1375 read_lock(&binfmt_lock);
对于ELF格式的可执行文件fmt->load_ binary(bprm)
;执行的应该是load_ elf_binary
其内部是和ELF文件格式解析的部分需要和ELF文件格式标准结合起来阅读。
Linux内核是如何支持多种不同的可执行文件格式的? 观察者模式:
82static struct linux_binfmt elf_format = { //elf_format结构体变量83 .module = THIS_MODULE,84 .load_binary = load_elf_binary,//把load_elf_binary赋给了结构体变量中的函数指针85 .load_shlib = load_elf_library,86 .core_dump = elf_core_dump,87 .min_coredump = ELF_EXEC_PAGESIZE,88};2198static int __init init_elf_binfmt(void)2199{2200 register_binfmt(&elf_format); //把elf_format结构体变量注册进内核链表中2201 return 0;2202}
elf_ format 和 init_ elf_binfmt,这里就是观察者模式中的观察者。
execve系统调用返回到用户态从哪里开始执行?
load_ elf_ binary -> start_thread:
load_ elf_ binary中:start_thread(regs,elf_entry,bprm->p); //elf_entry就是可执行文件头部定义的起点start_thread三个参数:pt_regs(内核堆栈的栈底) new_ip new_sp发生中断int 0x80时把sp,ip都压入内核栈了,用新进程起点去替换以前的ip,就是new_ip,而传递进来的参数new_ip = elf_entry 正是可执行程序起点。总结:通过修改内核堆栈中EIP的值作为新程序的起点。
sys_execve的内部处理过程
do_ execve(getname(filename),argv,envp) -> do_ execve_ common(filename,argv,envp) 中:
- 1、do_ open_ exec 打开要加载的可执行文件
- 2、创建结构体bprm,把参数copy进结构体
- 3、exec_ binprm:对可执行文件的处理过程,关键代码search_ binary_ handler寻找可执行文件,这里面关键list_ for_ each_ entry(fmt, &formats, lh)在链表中寻找能够解析ELF格式的内核模块,fmt->load_ binary(bprm)找到fmt这个链表结点,指向能够解析ELF格式的模块,加载处理函数,执行的应该是load_ elf_binary,见上
load_ elf_binary
:严格的解析ELF格式文件,核心工作:把ELF可执行文件映射到进程的地址空间(默认加载起始地址是0x8048000)
start_ thread(regs,elf_ entry,bprm->p):
- 对于静态链接的文件elf_entry是新程序执行的起点(可执行文件头部定义的)
- 需要动态链接的可执行文件先加载连接器ld,装载动态库elf_entry = load_elf_interp,指向动态链接器的起点。
实验
搭建环境:
cd LinuxKernel rm menu -rfgit clone https://github.com/mengning/menu.gitcd menumv test_exec.c test.cvi test.c //增加了execvi MakeFile make rootfs
gdb调试
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -Sgdbfile ../linux-3.18.6/vmlinuxtarget remote:1234b sys_execveb load_elf_binaryb start_threadpo new_ip : 0x8048d0a 返回到用户态的第一条指令的地址readelf -h hello 入口点地址:0x8048d0a
总结
关于静态链接的可执行程序的装载:举一个简单的例子:shell装载hello,进入内核时是shell,返回就变成了一个新的进程hello,类似庄生梦蝶,这是对可执行程序装载的一个简单理解。静态链接时elf_entry直接指向可执行程序的入口。
关于动态链接的可执行程序的装载:ELF格式文件依赖动态链接库,动态链接库.so也可能依赖其他动态链接库,实际上动态链接库的依赖关系会形成一个图。需要动态链接的可执行文件先加载连接器ld,elf_entry指向动态链接器的起点,再看这个动态链接库是否还依赖与其他动态链接库,其实这整个过程是对一个图的遍历,把所有依赖的动态链接库都装载起来之后,ld将CPU的控制权交给可执行程序,主要由ld完成,不是内核。