linux内核分析第七周-Linux内核如何装载和启动一个可执行程序
一、可执行文件的创建
可执行文件的创建就是三步:预处理、编译和链接。
cd Code vi hello.c #写入最简单的helloworld的c程序 gcc -E -o hello.cpp hello.c -m32 #-E参数就是生成预处理后的文件,看到-o后面的是生成的文件hello.cpp,注意它并不是cplusplus,而是随意起的后缀名 vi hello.cpp #查看该文件,发现预处理做了把include的文件包含进来以及宏替换等工作。 gcc -x cpp-output -S -o hello.s hello.cpp -m32 #-x language filename作用是设定文件使用的语言,使后缀名无效。此处就是让刚才的cpp不要让编译器误会为cplusplus,而是当做cpp-output这种文件格式。-s是指生成汇编.s文件 vi hello.s gcc -x assembler -c hello.s -o hello.o -m32 #-c指将.s转为.o文件 vi hello.o gcc -o hello hello.o -m32 #-o指将.o文件链接为可执行的文件 vi hello gcc -o hello.static hello.o -m32 -static #静态链接 ls -l #注意看结果中的各文件的大小,其中静态链接的很大,因为它把所需要的库一次包到进程(可执行文件)中
二、可执行文件的组成
可执行文件属于目标文件之一。目标文件的格式为ELF。ELF的格式以段来组织的二进制代码,所以我们知道:①它已经符合某种机器的ABI了;②为什么进程认为自己占了全部的空间,拥有一套完善的页表,也就是理解了线性地址是什么情况。那么多线程时候是怎么组织的呢?暂时我还不知道。
以ELF为格式的主要有三种文件:①可重定位文件:保持着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者一个共享文件。例如.o文件。
②可执行文件:可以运行的文件。该文件指出了exec(BA_OS)如何来创建进程映象。再来联想下程序和进程的区别。到底这种可执行文件是进程还是程序?我们发现它的段中只含.text和.data一类的段,而不含有堆栈段。所以可以确定它只是程序。当它被操作系统调入内存开始执行时才会真正的成为进程。例如.out文件。
③共享object文件:保存着代码和数据,被两个链接器链接。一个是连接编辑器,可以和其他可重定位和共享object文件来创建其他的object。第二个是动态链接器,联合一个可执行文件和其他共享object文件来创建一个进程映像。
ELF文件的头部:使用命令查看hello文件的头:shiyanlou:Code/ $ readelf -h hello
ELF的头保存的是元数据,也就是路线图,描述了文件的组织情况。比如程序头表(program header table)告诉系统如何来创建一个进程的内存映像。section头表(section header table)包含描述文件sections的信息。每个section在这个表中有一个入口;每个入口给出了该section的名字,大小等等信息。
ELF的剩余部分是sections,包括代码段,数据段。这些在程序变成进程映像时加载到内存的虚拟地址空间中,其中代码段的起始地址就是0x8048000,但它加载的不是纯代码段的内容,而是从ELF头开始加载,所以从图中可以看出真正的代码从0x8048320开始,这才是真正的程序入口。静态链接时程序所需要的代码全部在代码段中,而动态链接就不一样了,它会在运行时候去找内存中间部分加载的库函数。
三、可执行程序的加载
(1)装载:可执行程序的执行环境,shell
shell就是用户键入命令,加载并执行可执行程序的控制台
shell的本质是什么呢?就是提供图形化的界面,将用户写入的字符串解析成真正执行的命令或者说可执行程序。有两个问题:①真正执行程序的是什么程序或指令?答案是execve系统调用(库函数exec*都是execve的封装例程)。②如何给该系统调用传参?也就是传参的默认格式是什么?答:shell会传入execve的参数有两种,一种是程序本身参数,也就是main的参数argc,argv;第二种是shell环境变量的参数,envp字符串数组中。来看下execve的参数类型:
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
当然,有些程序的main函数是不处理环境变量参数的,例如常见的int main(int argc, char *argv[])。但是有时候也支持,例如int main(int argc, char *argv[], char *envp[]),所以这个时候传入的环境变量参数才会被解析使用。
新问题:execve是如何从内核态将参数传给进程(假设该进程是用户态)的用户态堆栈的?
先看两张图:
发现,仍是从用户态的数据段中复制到用户态堆栈中的。那么跟内核什么关系呢?执行execve的进程就是当前的shell,所以参数首先会被压在当前shell进程的内核堆栈中。关键在传入内核的参数是指针,所以内核(sys_execve)要做的就是把指针的值复制回新进程的代码段,再复制到进程的用户堆栈段。初始化后的进程内存地址空间就是第二张图那样。也就解释了sys_execve加载进程并初始化的作用、结果。shell每次都fork一个shell去执行命令,所以,当新进程起来后,启动它的shell结束,曾经保存的参数就不要了。
(2)链接
静态链接就是经过静态链接方式编译的可执行程序,正如上述描述的加载部分,编译后直接加载进程所需信息,而且以后不需要其他信息了。
动态链接不同,它分为装载时的动态链接和运行时的动态链接。
在linux下动态链接文件格式为.so(windows下为.dll)。关于这点的理解,mooc课程中有个例子。写在一篇中篇幅太长,不分析了。
四、分析execve系统调用内核处理函数sys_execve
execve和前面博文分析的fork系统一样,是一种特殊的系统调用。fork的特殊在于系统调用后两次返回,生成了新进程,而不单单是在原来程序的系统调用的下一条语句。而execve的特殊在于它返回之后,执行的是一个新的程序了(例如返回程序的main入口,修改的是elf_entry),而不是以前调用execve的进程shell了。
内核处理函数sys_execve内部会解析可执行文件格式,它的内部执行流程是do_execve -> do_execve_common -> exec_binprm。
gdb断点设置:b sys_execve ;停到该位置,继续设置断点 b load_elf_binary; b start_thread。
其中的一些函数解释:
1)search_binary_handler符合寻找文件格式对应的解析模块,如下:
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
bprm->recursion_depth++;
retval = fmt->load_binary(bprm);
read_lock(&binfmt_lock);
对于ELF格式的可执行文件fmt->load_binary(bprm);执行的应该是load_elf_binary
2)Linux内核是如何支持多种不同的可执行文件格式的?
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary,
.load_shlib = load_elf_library,
.core_dump = elf_core_dump,
.min_coredump = ELF_EXEC_PAGESIZE,
};
static int __init init_elf_binfmt(void)
{
register_binfmt(&elf_format);
return 0;
}
elf_format 和 init_elf_binfmt,就是观察者模式中的观察者。
3)可执行文件开始执行的起点在哪里?如何才能让execve系统调用返回到用户态时执行新程序?
load_elf_binary -> start_thread中通过修改内核堆栈中的EIP的值作为新程序的起点。即修改一开始int 0x80压入内核堆栈的EIP。start_thread中的new_ip是返回到用户态第一条指令的地址,与可执行程序的头中的入口地址相同。