课本:第七章 可执行程序工作原理
- ELF目标文件格式
- 目标文件:编译器生成的文件。
- 目标文件的格式:out格式、COFF格式、PE(windows)格式、ELF(Linux)格式。
- ELF(Executable and Linkable Format)即可执行和可链接的格式,是一个目标文件格式的标准。ELF格式的文件用于存储Linux程序。
- ELF文件的3钟类型:可重定位文件、可执行文件、共享目标文件。
- 可重定位文件:这种一般是中间文件,还需要继续处理。由汇编器和编译器创建,一个源代码文件会生成一个可重定位文件。文件中保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件、静态库文件或者共享目标文件(即动态库文件)。如Linux下.c文件都会生成一个同名的.o文件,这就是可重定位目标文件。
- 可执行文件:一般由多个可重定位文件结合生成,是完成了所有重定位工作和符号解析(除了运行时解析的共享库符号)的文件,文件中保存着一个用来执行的程序。
- 共享目标文件:共享库,是指可以被可执行文件或其他库文件使用的目标文件,例如标准C的库文件libc.so。可以简单理解为没有主函数main的“可执行”文件,只有一堆函数可供其他可执行文件调用。Linux下共享库后缀为.so文件,代表shard object。
- ELF文件作用:ELF文件参与程序的链接和程序的执行。
- 如果用于编译和链接(可重定位文件),编译器和链接器将把ELF文件看作节的集合,所有节由节头表描述,程序头表可选。
- 如果用于加载执行(可执行文件),加载器将把ELF文件看作程序头表描述的段的集合,一个段可能包含多个节和节头表可选。
- 如果是共享文件,则两者都含有。
- ELF格式
- 主体是各种节,还有描述这些节属性的信息(Program header table和Section header table),以及ELF文件的整体描述信息(ELF header)。整体如下图所示:
- ELF Header
- Section Header
- Program Header
- 主体是各种节,还有描述这些节属性的信息(Program header table和Section header table),以及ELF文件的整体描述信息(ELF header)。整体如下图所示:
- 对ELF进行研究的相关操作指令
- man elf:详细格式定义。
- readelf:用于显示一个或多个elf格式的目标文件的信息,后面可以使用多个参数,如-a、-h、-S、-l等。
- objdump:显示二进制文件信息,有-f、-h、-r等一系列参数。
- hexdump:用十六进制的数字来显示elf的内容。
- 程序编译
- 预处理:
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
- 预处理:
- 链接与库
- 链接
- 从过程上讲:符号解析、重定位。
- 根据链接时机:静态链接、动态链接。
- 链接
- 程序装载
- exec函数:sys_execve()系统调用
- 调用关系:sys_execve() -> do_execve() -> do_execve_common() -> exec_binprm() -> search_binary_handler() -> load_elf_binary() -> start_thread()
- fork与execve的区别和联系
- 都是比较特殊的系统调用
- fork在陷入内核态后有两次返回,第一次返回到原来父进程的位置继续向下执行,第二次是在子进程返回,这次会返回到ret_from_fork,之后正常返回用户态。
- execve在执行时陷入内核态,在内核中调用execve加载的可执行文件把当前进程的可执行程序给覆盖了,当其返回时,返回的已经不是原来的那个可执行程序了,而是新的程序,返回的是新的可执行程序执行的起点,即main函数的大致位置(一般地址为0x8048xxx,由编译器设定)
- 内核支持多格式是在执行execve时,它加载了文件的头部,来判断文件是什么格式,在链表中寻找能够解析这种文件格式的内核模块
- 两种加载方法
- 静态库:直接执行可执行程序的入口
- 动态库:由ld来动态链接这个程序,再把控制权移交给可执行程序的入口
静态链接与动态链接的可执行文件对比&装载时及运行时动态链接的实例
在实例中,编写了一个hello.c文件,输出“Hello World!”用于实验。在使用gcc -static -o hello.m32.static hello.c
生成了hello.m32.static静态链接的可执行文件,用gcc -o hello.m32.dynamic hello.c
生成了hello.m32.dynamic动态链接的可执行文件后,在shell中使用ls -l hello.m32.*
查看静态链接和动态链接的可执行文件的大小,发现静态链接版本大小大约是动态链接版本的100倍,如下图所示:
动态链接分为可执行程序装载时动态链接和运行时动态链接,下面是这两种动态链接的实例。
装载时动态链接
编写shlibexample.h和shlibexample.c,是一个简单动态库的源码,只提供一个函数ShardLibApi()。源码如下图所示:
之后编译成libshlibexample.so文件:
运行时动态链接
编写dllibexample.h和dllibexample.c文件,里面写了DynamicalLoadingLibApi()函数供运行时动态链接。源码如下图所示:
之后编译成libdllibexample.so文件:
生成了两个so文件之后,开始编写执行函数main,如下图所示:
下面使用gcc编译此main.c文件,使用参数-L指明头文件所在目录(-L.表示头文件就在当前目录),使用-l指明库文件名,如libshlibexample.so去掉lib和.so部分。dllibexample只在程序运行到相关语句时才会访问,在编译时不需要任何相关信息,只是用-ldl指明其需要使用共享库dlopen等函数。当然在实际运行时,也要确保libdllibexample.so是应用可以查找到的,这也是要修改环境变量LD_LIBRARY_PATH的原因,编译结果如下图所示:
实验:Linux内核如何装载和启动一个可执行程序
使用实验楼环境,首先移除LinuxKernel下的menu,从老师的github上clone一个新的menu目录,在打开其test.c文件中发现,在menuOS的功能中增加了一个exec功能,其源代码如下:
并在main函数中调用了Exec函数,如下图所示:
在查看其Makefile的定义规则时看到,在rootfs中,在生成根文件系统时把编写的hello.c生成的hello执行程序一并载入到镜像rootfs.img中了,hello的功能是打印"Hello World!",如下图所示:
使用make rootfs将menuOS在qemu下运行起来,在功能列表下发现增加了exec指令,执行exec指令,在子进程中成功打印了hello world,执行效果如下图所示:
下面使用gdb对这个过程进行跟踪调试:
为避免在加载内核时在某些系统调用函数初始化的时候遇到断点而暂停内核加载,我先在sys_execve一处设置断点,在执行到sys_execve时再分别在load_elf_binary、start_thread处设置断点。
执行到sys_execve时的执行效果如下图所示:
在此时gdb返回的断点信息可以发现,在sys_execve内部调用返回do_execve()函数,如下图所示:
之后让程序继续执行,后发现程序执行到load_elf_binary处的断点,如下图所示:
后继续执行程序,程序来到start_thread断点处,执行到此函数,即开始静态链接hello可执行文件,elf_entry指向了hello中分配的入口地址,使用po new_ip指令打印其指向的地址,new_ip是返回到用户态的第一条指令的地址,理论上是hello的入口地址,如下图所示:
后结束gdb调试,返回shell,在menu文件夹中使用readelf -h hello
来查看hello的elf头部文件信息,查看其入口地址与new_ip所指向的地址是否一致,发现是一致的,同为0x8048d0a,如下图所示:
问题与总结
本周的实验在自己的电脑上遇到了一个问题,无论是在进行预处理、编译、汇编、链接实验中还是在装载和运行时动态链接实验中,只要是在gcc编译源码时在后面加上-m32,编译就会报错,如下图所示:
究其原因,经过查阅相关资料,发现因为自己的环境是64位的linux环境,需要生成32位的目标文件,虽然环境可以向下兼容,但是gcc的开发环境没有安装完备,编译时无法找到生成32位目标文件所需的一系列头文件。
解决方案可以是完善gcc开发环境,安装gcc的多环境库文件,在shell中输入sudoapt-get install gcc-multilib
解决。
对于可执行程序的装载,个人的理解是当调用fork创建子进程后,子进程要调用一种exec函数以执行一个新程序;在调用一种exec函数时,该进程执行的程序被替换为新程序,而新程序则从其main函数处开始执行,但依然始终是同一个进程。课本中使用中国古代的“庄周梦蝶”的故事以类比,确有异曲同工之处。