Linux内核如何装载和启动一个可执行程序
20135311傅冬菁 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
Linux内核如何装载和启动一个可执行程序
一、内容分析
一、预处理、编译、链接和目标文件的格式
1.预处理阶段 :编译器将C源代码中包含的头文件编译进来和执行宏替换等工作。
gcc -E -o XX.cpp XX.c -m32 (XX.cpp是预处理文件)
2.编译器生成汇编代码阶段:gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。
gcc -x cpp-output -S -o hello.s hello.cpp -m32 (XX.s是汇编代码)
3.汇编器生成目标代码阶段:把编译阶段生成的XX.S文件
转成二进制目标代码。
gcc -x assembler -c hello.s -o hello.o -m32 (XX.o是目标代码)
4.链接器生成可执行文件阶段(将编译输出XX.o文件
链接成最终的可执行文件)。
gcc -o hello.static hello.c -m32 -static
5.运行(若链接没有-o指明,则生成可执行文件默认为a.out)
./a.out
PS: hello和hello.static的区别:ls -l
目标文件的格式ELF:
A.out是最古老的可执行文件,目前Windows系统上多是PE,Linux系统上多是ELF。ELF文件已经是适应到某一种CPU体系结构的二进制兼容文件了
ELF格式分类:
- 可重定位文件.o,用来和其他object文件一起创建可执行文件和共享文件
- 可执行文件,指出应该从哪里开始执行
- 共享文件,主要是.so文件,用来被链接编辑器和动态链接器链接
对ELF头的描述告诉系统如何创建一个进程的内存映像,section头表包含了描述文件sections的信息。当创建或增加一个进程映像时,理论上它会把程序段拷贝到虚拟内存中某个段
ELF文件的头部规定了许多与二进制兼容性相关的信息。所以在加载ELF文件的时候,必须先加载头部,分析ELF的具体信息
entry代表刚加载过新的可执行文件之后的程序的入口地址,头部后是代码和数据,进程的地址空间是4G,上面的1G是内核用,下面的3G是程序使用。默认的ELF头加载地址是0x8048000
静态链接的ELF可执行文件和进程的地址空间:
1. 可执行文件加载到内存时:
- 加载效果:将代码段数据加载到内存中,再把数据加载到内存,默认从0x8048000地址开始加载
- 启动一个刚加载过可执行文件的进程时,可执行文件加载到内存之后执行的第一条代码地址
- 一般静态链接会将所有代码放在一个代码段,而动态链接的进程会有多个代码段
2. 流程
- 分析头部
- 查看是否需要动态链接。如果是静态链接的ELF文件,那么直接加载文件即可。如果是动态链接的可执行文件,那么需要加载的是动态链接器
- 装载文件,为其准备进程映像
- 为新的代码段设定寄存器以及堆栈信息
默认从0x8048000处开始加载件加载到内存中开始执行的第一行代码
一般静态链接会将所有代码放在一个代码段
动态链接的进程会有多个代码段
可执行程序、共享库和动态加载
1.装载可执行程序之前的工作
(1)可执行程序的执行环境
一般我们执行一个程序的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[ ]);
库函数exec*都是execve的封装例程
shell中使用fork()来创建新进程。
命令行参数和环境串都放在用户态堆栈中。
在创建一个新的用户态堆栈的时候,实际上是把命令行参数的内容和环境变量的内容通过指针的方式传递到系统调用的内核处理函数的,内核处理函数在创建一个新的可执行堆栈的时候会将命令行参数的内容和环境变量的内容拷贝到用户态堆栈里面来初始化新的可执行程序执行的上下文环境
shell程序 -> execve -> sys_execve
先函数调用参数传递,在系统调用参数传递
装载时动态链接和运行时动态链接应用举例:
动态链接分为可执行程序装载时动态链接和运行时动态链接,如下代码演示了这两种动态链接。
- 准备.so文件
- 编译成libshlibexample.so文件
- 编译成libdllibexample.so文件
- 分别以共享库和动态加载共享库的方式使用libshlibexample.so文件和libdllibexample.so文件
- 编译main
可执行程序的装载:
可执行程序的装载相关关键问题分析
sys_execve内部会解析可执行文件格式
do_execve -> do_execve_common -> exec_binprm
sys_execve的内部处理过程:
装载和启动一个可执行程序依次调用以下函数:
sys_execve() -> do_execve() -> do_execve_common() -> exec_binprm() -> search_binary_handler() -> load_elf_binary() -> start_thread()
二、实验过程
test.c文件的代码
三、总结
在创建一个新的用户态堆栈的时候,实际上是把命令行参数的内容和环境变量的内容通过指针的方式传递到系统调用的内核处理函数的,内核处理函数在创建一个新的可执行堆栈的时候会将命令行参数的内容和环境变量的内容拷贝到用户态堆栈里面来初始化新的可执行程序执行的上下文环境
shell程序 -> execve -> sys_execve
先函数调用参数传递,在系统调用参数传递
sys_execve的内部处理过程:
装载和启动一个可执行程序依次调用以下函数:
sys_execve() -> do_execve() -> do_execve_common() -> exec_binprm() -> search_binary_handler() -> load_elf_binary() -> start_thread()