此为个人学习笔记存档
week 7 可执行程序的装载
一、预处理、编译、链接和目标文件的格式
可执行文件的创建——预处理、编译和链接
cd Code vi hello.c gcc -E -o hello.cpp hello.c -m32 vi hello.cpp gcc -x cpp-output -S -o hello.s hello.cpp -m32 vi hello.s gcc -x assembler -c hello.s -o hello.o -m32 vi hello.o gcc -o hello hello.o -m32 vi hello gcc -o hello.static hello.o -m32 -static ls -l -rwxrwxr-x 1 shiyanlou shiyanlou 7292 3u6708 23 09:39 hello -rw-rw-r-- 1 shiyanlou shiyanlou 64 3u6708 23 09:30 hello.c -rw-rw-r-- 1 shiyanlou shiyanlou 17302 3u6708 23 09:35 hello.cpp -rw-rw-r-- 1 shiyanlou shiyanlou 1020 3u6708 23 09:38 hello.o -rw-rw-r-- 1 shiyanlou shiyanlou 470 3u6708 23 09:35 hello.s -rwxrwxr-x 1 shiyanlou shiyanlou 733254 3u6708 23 09:41 hello.static
ELF目标文件格式
查看ELF文件的头部
shiyanlou:Code/ $ readelf -h hello
查看该ELF文件依赖的共享库
shiyanlou:sharelib/ $ ldd main [21:25:56] linux-gate.so.1 => (0xf774e000) # 这个是vdso - virtual DSO:dynamically shared
object,并不存在这个共享库文件,它是内核的一部分,为了解决libc与新版本内核的系统调用不同步的问题,linux-gate.so.1里封装的系统调用与内核支持的系统调用完全匹配,因为它就是内核的一部分嘛。而libc里封装的系统调用与内核并不完全一致,因为它们各自都在版本更新。
libshlibexample.so => /home/shiyanlou/LinuxKernel/sharelib/libshlibexample.so (0xf7749000)
libdl.so.2 => /lib32/libdl.so.2 (0xf7734000)
libc.so.6 => /lib32/libc.so.6 (0xf7588000)
/lib/ld-linux.so.2 (0xf774f000)
shiyanlou:sharelib/ $ ldd /lib32/libc.so.6 [21:37:00]
/lib/ld-linux.so.2 (0xf779e000)
linux-gate.so.1 => (0xf779d000)
# readelf -d 也可以看依赖的so文件
shiyanlou:sharelib/ $ readelf -d main [21:28:04]
Dynamic section at offset 0xf04 contains 26 entries:
0x00000001 (NEEDED) 共享库:[libshlibexample.so]
0x00000001 (NEEDED) 共享库:[libdl.so.2]
0x00000001 (NEEDED) 共享库:[libc.so.6]
0x0000000c (INIT) 0x80484f0
0x0000000d (FINI) 0x8048804
0x00000019 (INIT_ARRAY) 0x8049ef8
1.可执行程序是怎么来的?
c代码,经过预处理,变成汇编代码
经过汇编器,变成目标代码
连接成可执行文件
加载到内核中执行
编译过程
预处理:gcc –E hello.c –o hello.i; gcc –E调用cpp 生成中间文件
编 译:gcc –S hello.i –o hello.s; gcc –S调用ccl 翻译成汇编文件
汇 编:gcc –c hello.s –o hello.o; gcc -c 调用as 翻译成可重定位目标文件
链 接:gcc hello.o –o hello ; gcc -o 调用ld** 创建可执行目标文件
64位机可用参数-m32
cpp是指预处理编译文件
gcc的一些参数:
(1)基本选项
-c 只编译不链接,生成目标文件.o
-S 只编译不汇编,生成汇编代码
-E 只进行预编译,不做其他处理
-g 在可执行程序中包含标准调试信息
-o file 将file文件指定为输出文件
-v 打印出编译器内部编译各过程的命令行信息和编译器的版本
-I dir 在头文件的搜索路径列表中添加dir目录
-x language filename
设定文件使用的语言,这样源程序的后缀名无效了,并对gcc后接的多个编译文件都有效。这样如果存在.c和.cpp文件联编会有问题,解决这个问题用到了下一个参数 -x none filename,在下面做介绍。因为在预处理过程中对于.c和.cpp文件的处理方式是不一样的。可以使用的参数有:'c','objective-c','c-header','c++','cpp-output','assembler','assembler-with-cpp'.编译的时候,如果有这样的一个用C语言写的test.tmp的文件,用gcc编译的时候就用gcc -x c test.tmp就可以让gcc用编译C语言的方式来编译test.tmp.
-x none filename
关掉上一个选项,就是让gcc根据文件名后缀,自动识别文件类型。如用下列方式编译: gcc -x c test.tmp -x none test2.c 这样可以自由地选择编译方式
(2)库选项
-static 进行静态编译,即链接静态库,禁止使用动态库
-shared 1.可以生成动态库文件
2.进行动态编译,尽可能的链接动态库,没有动态库时才会链接同名静态库
-L dir 在库文件的搜索路径列表中添加dir目录
-lname 链接称为libname.a或者libname.so的库文件。
如果两个库文件都存在,根据编译方式是static还是shared进行链接
-fPIC 生成使用相对地址的位置无关的目标代码,
(-fpic) 然后通常使用gcc的-static选项从该pic目标文件生成动态库文件。
2.目标文件的格式ELF
.o文件,可执行文件,都是目标文件,一般使用相同的文件格式。
常用文件格式:
- a.out
- COFF
- PE - WINDOWS上
- ELF - LINUX上
ABI:应用程序二进制接口
ABI和目标文件格式的关系:
ELF文件格式中有三种主要的文件格式:
- 可重定位文件
主要是.o文件,保存有代码和适当数据,用来和其他的object文件一起来创建一个可执行文件或者共享文件 - 可执行文件
保存着一个用来执行的程序,指出exec(BA_OS)如何创建程序进程映象。 - 共享目标文件
保存代码和合适的数据,用来和链接器链接:- 链接编辑器,静态链接,和其他的可重定位、共享目标文件创建其他的目标文件
- 动态链接器,连喝一个可执行文件和其他的共享目标文件来创建一个进程映像
文件格式
Object文件参与程序的联接(创建一个程序)和程序的执行(运行一个程序)。
object 文件格式提供了一个方便有效的方法并行的视角看待文件的内容,
在他们的活动中,反映出不同的需要。例 1-1图显示了一个object文件的
组织图。
-
图1-1: 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文件的信息,从连接角度看:包括指令,数据,符号表,重定位信息等等。特别sections的描述会出项在以后的第一部分。
第二部分讨论了段和从程序的执行角度看文件。假如一个程序头表(program header table)存在,那么它告诉系统如何来创建一个进程的内存映象。被用来建立进程映象(执行一个程序)的文件必须要有一个程序头表(program header table);可重定位文件不需要这个头表。一个section头表(section header table)包含了描述文件sections的信息。每个section在这个表中有一个入口;每个入口给出了该section的名字,大小,等等信息。在联接过程中的文件必须有一个section头表;其他object文件可要可不要这个section头表。
只有ELF头(elf header)是在文件的固定位置。
查看一个可执行文件头部内容:
readelf -h
头部后是代码和数据,等等。
可执行程序加载的主要工作:
当创建或者增加一个进程映像的时候,系统在理论上将拷贝一个文件的段到一个虚拟的内存段
3.静态链接的ELF可执行文件和进程的地址空间
32位x86进程地址空间共4G,1G是内核空间。
如何加载到内存?
默认从0x8048000开始加载,然后头部需要占用一定空间,程序的实际入口可以在0x8048100等地方,即可执行文件加载到内存中开始执行的第一行代码的入口处。
一般静态链接会把所有代码放在一个代码段,
动态链接会有多个代码段。
二、可执行程序、共享库和动态链接
1.装载可执行程序之前的工作
命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。
- $ ls -l /usr/bin 列出/usr/bin下的目录信息【其实是执行了一个可执行参数ls
- Shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身,
- 例如,int main(int argc, char *argv[]) - 又如, int main(int argc, char *argv[], char *envp[]) 【envp是shell的环境变量。
- shell怎么传递和保存命令行参数和环境变量?
——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!");
exit(0);
}
}
命令行参数和环境变量是如何进入新程序的堆栈的?
——命令行参数和环境串都放在用户态堆栈中
实际上是把命令行参数和环境变量通过系统调用传递到内核处理函数,然后内核处理函数在创建新的用户态堆栈时都拷贝进去,来初始化新的可执行程序的堆栈。
即:
shell->execve->sys_execve,然后在初始化新程序堆栈时拷贝进去。
先函数调用参数传递,再系统调用参数传递。
2.装载时动态链接和运行时动态链接应用举例
动态链接分为可执行程序装载时动态链接和运行时动态链接,如下代码演示了这两种动态链接。
(1)准备.so文件【linux下的
例:共享库如何生成
shlibexample.h - 定义了一个函数原型
shlibexample.c - 实现,很简单
#include <stdio.h>
#include "shlibexample.h"
int SharedLibApi()
{
printf("This is a shared libary!
");
return SUCCESS;
}
编译成libshlibexample.so文件,命令如下:
$ gcc -shared shlibexample.c -o libshlibexample.so -m32
动态加载的过程是一样的,只是名字不同:
dllibexample.h
dllibexample.c
编译成libdllibexample.so文件
$ gcc -shared dllibexample.c -o libdllibexample.so -m32
(2)分别以共享库和动态加载共享库的方式使用libshlibexample.so文件和libdllibexample.so文件
/* main.c */
#include <stdio.h>
#include "shlibexample.h" // 共享库
#include <dlfcn.h> // 动态加载
int main()
{
printf("This is a Main program!
");
/* 调用共享库函数 Use Shared Lib */
printf("Calling SharedLibApi() function of libshlibexample.so!
");
SharedLibApi();
/* 调用动态装载库 Use Dynamical Loading Lib */
void * handle = dlopen("libdllibexample.so",RTLD_NOW);
if(handle == NULL)
{
printf("Open Lib libdllibexample.so Error:%s
",dlerror());
return FAILURE;
}
int (*func)(void); // 声明了一个函数指针
char * error;
func = dlsym(handle,"DynamicalLoadingLibApi"); // 找到这个函数指针
if((error = dlerror()) != NULL)
{
printf("DynamicalLoadingLibApi not found:%s
",error);
return FAILURE;
}
printf("Calling DynamicalLoadingLibApi() function of libdllibexample.so!
");
func(); // 使用
dlclose(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
$ export LD_LIBRARY_PATH=$PWD #将当前目录加入默认路径,否则main找不到依赖的库文件,当然也可以将库文件copy到默认路径下。【lib,或usr/lib
$ ./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!
两种方式:
- 在程序执行过程中由程序自身装载共享库
- 在装载可执行程序时完成动态链接过程
三、可执行程序的装载
1.可执行程序的装载相关关键问题分析
可执行程序的装载其实还是系统调用,execve,比较特殊的系统调用.
陷入到内核态,在内核态中加载,把当前进程的可执行程序覆盖掉;execve返回时,就是新的可执行程序了。
sys_execve内核处理过程:
sys_execve内部会解析可执行文件格式:
-
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); //用来解析elf文件的执行到位置。 1375 read_lock(&binfmt_lock);
-
对于ELF格式的可执行文件fmt->load_binary(bprm);执行的应该是load_elf_binary,其内部是和ELF文件格式解析的部分,需要和ELF文件格式标准结合起来阅读。
-
Linux内核是如何支持多种不同的可执行文件格式的?
实现技巧:/* 全局变量elf_format,把函数指针load_elf_binary**赋值**给了.load_binary */ 82 static 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**注册**进了format链表里,就可以在链表里对应elf模式中找到对应模块 */ 2198 static int __init init_elf_binfmt(void) 2199 { 2200 register_binfmt(&elf_format); 2201 return 0; 2202 }
elf_format 和 init_elf_binfmt,这里就相当于观察者模式中的观察者,elf_format是观察者,1369开始的那段代码是被观察者,当elf文件出现的时候,就会自动执行load_elf_binary。
以上这一段是解析部分。
除此之外:
在load_elf_binary中调用了start_thread这个函数:
198 start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)/* pt_regs 是内核堆栈栈底的函数,*/
199 {
200 set_user_gs(regs, 0);
201 regs->fs = 0;
202 regs->ds = __USER_DS;
203 regs->es = __USER_DS;
204 regs->ss = __USER_DS;
205 regs->cs = __USER_CS;
206 regs->ip = new_ip; //起点位置
207 regs->sp = new_sp;
208 regs->flags = X86_EFLAGS_IF;
209 /*
210 * force it to the iret return path by making it look as if there was
211 * some work pending.
212 */
213 set_thread_flag(TIF_NOTIFY_RESUME);
214 }
215 EXPORT_SYMBOL_GPL(start_thread);
- 可执行文件开始执行的起点在哪里?
通过修改内核堆栈中eip的值作为新程序的起点
以上适用于静态库。
2.sys_execve的内部处理过程
1604 SYSCALL_DEFINE3(execve,
1605 const char __user *, filename,
1606 const char __user *const __user *, argv,
1607 const char __user *const __user *, envp)
1608 {
1609 return do_execve(getname(filename), argv, envp);
1610 }
1611 #ifdef CONFIG_COMPAT
1612 COMPAT_SYSCALL_DEFINE3(execve, const char __user *, filename,
1613 const compat_uptr_t __user *, argv,
1614 const compat_uptr_t __user *, envp)
1615 {
1616 return compat_do_execve(getname(filename), argv, envp);
1617 }
1618 #endif
sys_execve函数中返回了一个do_execve:
1549 int do_execve(struct filename *filename,
1550 const char __user *const __user *__argv,
1551 const char __user *const __user *__envp)
1552 {
1553 struct user_arg_ptr argv = { .ptr.native = __argv };
1554 struct user_arg_ptr envp = { .ptr.native = __envp };
1555 return do_execve_common(filename, argv, envp);
1556 }
最后一句中do_execve_common把文件名,参数和环境转换了一下。
该函数do_execve_common打开如下:
1474 file = do_open_exec(filename);
打开了一个要加载的可执行文件,然后会加载一下它的头部,建立一个结构体,把命令行参数和环境变量拷贝到结构体中;
1513 retval = exec_binprm(bprm);
对这个可执行文件的处理过程。
打开exec_binprm这个函数,可以找到一句重要代码:
1416 ret = search_binary_handler(bprm);
寻找这个我们打开的可执行文件的处理函数。
打开search_binary_handler,找到list_for_each_entry如下:
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);
1376 put_binfmt(fmt);
1377 bprm->recursion_depth--;
1378 if (retval < 0 && !bprm->mm) {
1379 /* we got to flush_old_exec() and failed after it */
1380 read_unlock(&binfmt_lock);
1381 force_sigsegv(SIGSEGV, current);
1382 return retval;
1383 }
1384 if (retval != -ENOEXEC || !bprm->file) {
1385 read_unlock(&binfmt_lock);
1386 return retval;
1387 }
1388 }
在这个循环里寻找能够解析这个当前可执行文件的代码模块。
retval = fmt->load_binary(bprm); // 这一句中的load_binary,加载处理函数。这一句是函数指针,实际上是调用的load_elf_binary。
load_elf_binary的赋值和注册:见上一节
load_elf_binary的函数中涉及到很多文件解析的内容。
核心工作是把文件映射到进程的空间中。
ELF可执行文件会被默认映射到0x8048000这个地址。
需要动态链接?
可执行文件先加载链接器ld
动态链接库的执行过程:
887 if (elf_interpreter) {
888 unsigned long interp_map_addr = 0;
889
890 elf_entry = load_elf_interp(&loc->interp_elf_ex,
891 interpreter,
892 &interp_map_addr,
893 load_bias);
需要加载连接器
静态链接的执行过程:
912 else {
913 elf_entry = loc->elf_ex.e_entry;
直接把elf文件的entry地址赋给elf_entry。
但是在start_thread中是直接用的elf_entry:
start_thread(regs,elf_entry, bprm->p);
1.如果是一个静态连接的文件,elf_entry就是指的main函数开始的位置
2.如果是一个需要依赖动态链接库的文件,elf_entry指向的是动态链接器的起点,将cpu控制权交给ld来加载依赖库并完成动态链接。
※ 对于静态链接的文件,elf_entry是新程序执行的起点。
3.使用gdb跟踪sys_execve内核函数的处理过程
这一节内容参见 实验总结 。
4.可执行程序的装载与庄生梦蝶的故事
╮(╯_╰)╭就是一个比喻……
本体喻体对应如下:
- 庄周 调用execve的可执行程序
- 入睡 调用execve陷入内核
- 醒来 系统调用execve返回用户态
- 发现自己是蝴蝶 被execve加载的可执行程序
5.浅析动态链接的可执行程序的装载
-
动态链接的过程内核做了什么?
ELF文件格式需要依赖。
动态链接库也会依赖别的动态链接库。
就相当于一颗树
需要逐步解析然后加载。 -
可执行文件依赖的动态链接库(共享库)是由谁负责加载以及如何递归加载的?
关注ELF文件中.interp和.dynamic
动态链接器ld负责解析,加载,解析,装载和链接后ld再将CPU的控制权交给可执行程序(头部规定的起点位置)。
——这个动作不是由内核完成,是由动态链接器完成的。
动态链接库的装载过程其实是一个图的广度遍历
所以——
两种加载的方法:
- 静态库:直接执行可执行程序的入口
- 动态库:由ld来动态链接这个程序,然后再把控制权移交给可执行程序的入口。