李辰希 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一、预处理、编译、链接和目标文件的格式
1、可执行程序是怎么得来的
编译链接的过程
1.预处理阶段
gcc -E -o XX.cpp XX.c -m32
XX.cpp是预处理文件
2.编译器生成汇编代码阶段
gcc -x cpp-output -S -o hello.s hello.cpp -m32
XX.s是汇编代码
3.汇编器生成目标代码阶段
gcc -x assembler -c hello.s -o hello.o -m32
XX.o是目标代码
4.链接器生成可执行文件阶段
gcc -o hello.static hello.c -m32 -static
5.查看hello和hello.static的区别:
ls -l
6.补充:
- hello和hello.o都是ELF文件
- .static文件会将所有用到C库文件都放到这一个可执行程序中(所以占用空间比较多)
二.目标文件的格式ELF
1.常见的目标文件格式:
- A.out -> COFF -> PE(windows)/ELF(linux)
2.目标文件也叫ABI:应用程序二进制接口
3.ABI是二进制兼容:目标文件已经适应某种CPU体系结构上的二进制指令
4.ELF格式中3种目标文件:
- 可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件
- 可执行目标文件:包含二进制代码和数据,其形式可以被直接拷贝到存储器并执行
- 共享目标文件:一种特殊类型的可重定位目标文件,可以在加载或运行时被动态地加载到存储器并链接
5.静态链接的ELF可执行文件和进程的地址空间
ELF可执行文件加载到进程的地址空间时:
- 默认加载起始地址是0x8048000
- ELF头部大小不同,程序入口点也将不同,程序入口在头部有定义:
Entry point address
,即是可执行文件加载到内存中开始执行的第一行代码 - 一般静态链接会将所有代码放在一个代码段
- 动态链接的进程会有多个代码段,更复杂
三.可执行文件、共享库和动态链接
- 可执行程序的执行环境
- 一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。
- Shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身
- 例如,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[ ]);
2.命令行参数和环境串都放在用户态堆栈中
3.命令行参数和环境变量是如何保存和传递的?
创建的子进程完全复制父进程,调用execve时加载的可执行程序把子进程环境覆盖掉了,子进程用户态堆栈也被清空了,命令行参数和环境变量是如何保存在新的可执行程序的用户态堆栈呢?
参数传递过程:shell程序 -> execve系统调用 -> sys_execve 内核处理函数在初始化新程序堆栈时拷贝进去
先函数调用参数传递,再系统调用参数传递:调用execve时,参数压在shell程序当前进程的堆栈上,加载完新的可执行程序时被清空了,内核又创建了新的进程的用户态堆栈,把参数拷贝进去。
4.装载时动态链接和运行时动态链接应用
- 动态链接分为可执行程序装载时动态链接和运行时动态链接(大部分使用前者);
- 举例1(共享库的动态链接)
- 准备.so文件(在Linux下动态链接文件格式,在Windows中是.dll)
#ifndef _SH_LIB_EXAMPLE_H_ #define _SH_LIB_EXAMPLE_H_ #define SUCCESS 0 #define FAILURE (-1) #ifdef __cplusplus extern "C" { #endif /* * Shared Lib API Example * input : none * output : none * return : SUCCESS(0)/FAILURE(-1) * */ int SharedLibApi();//内容只有一个函数头定义 #ifdef __cplusplus } #endif #endif /* _SH_LIB_EXAMPLE_H_ */ /*------------------------------------------------------*/ #include <stdio.h> #include "shlibexample.h" int SharedLibApi() { printf("This is a shared libary!\n"); return SUCCESS; }/* _SH_LIB_EXAMPLE_C_ */
- 编译成.so文件
$ gcc -shared shlibexample.c -o libshlibexample.so -m32
- 准备.so文件(在Linux下动态链接文件格式,在Windows中是.dll)
5.编译
1.$ gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32 #这里只提供shlibexample的-L(库对应的接口头文件所在目录,也就是path to your dir)和-l(库名,如libshlibexample.so去掉lib和.so的部分),并没有提供dllibexample的相关信息,只是指明了-ldl
2.$ export LD_LIBRARY_PATH=$PWD #将当前目录加入默认路径,否则main找不到依赖的库文件,当然也可以将库文件copy到默认路径下。
3.$ ./main
4.This is a Main program!
5.Calling SharedLibApi() function of libshlibexample.so!
6.This is a shared libary!
7.Calling DynamicalLoadingLibApi() function of libdllibexample.so!
8.This is a Dynamical Loading libary!
四.可执行程序的装载
1.特殊系统调用:
- fork:两次返回,第一次父进程返回,子进程也返回,到特定的点:从ret_ from_fork开始执行然后返回用户态。
- execve:陷入到内核态调用execve加载可执行程序,把当前的程序覆盖掉了,返回的是一个新的可执行程序。返回的是新程序的执行起点,一般是main函数,要构建好执行环境。
2.sys_execve内部会解析可执行文件格式:
do_ execve -> do_ execve_ common -> exec_binprm
3.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,指向动态链接器的起点。
五.实验
1.搭建环境:
cd LinuxKernel
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
vi test.c //增加了exec
vi MakeFile
make rootfs
2.更新menu内核
查看test.c文件
直接e hello.c切换到hello.c
查看Makefile
启动内核并验证execv函数
冻结内核,启动GDB调试
进行调试
- 先停在sys_execve处,再设置其它断点;按c一路运行下去直到断点sys_execve
- 按s跳入函数内单步执行
- new_ip是返回到用户态的第一条指令
退出调试状态,输入redelf -h hello可以查看hello的EIF头部
六.总结
分析exec*函数对应的系统调用处理过程:
1.关于动态链接的可执行程序的装载:ELF格式文件依赖动态链接库,动态链接库.so也可能依赖其他动态链接库,实际上动态链接库的依赖关系会形成一个图。需要动态链接的可执行文件先加载连接器ld,elf_entry指向动态链接器的起点,再看这个动态链接库是否还依赖与其他动态链接库,其实这整个过程是对一个图的遍历,把所有依赖的动态链接库都装载起来之后,ld将CPU的控制权交给可执行程序,主要由ld完成,不是内核。
2.庄周梦蝶——庄周(调用execve的可执行程序)入睡(调用execve陷入内核),醒来(系统调用execve返回用户态)发现自己是蝴蝶(被execve加载的可执行程序)