您的位置:首页 > 运维架构 > Linux

Linux内核如何装载和启动一个可执行程序

2016-04-10 00:22 519 查看
罗冲 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

1. 实验准备

下载课件中准备的代码。

int Execstaic(int argc, char *argv[])
{
int pid;
/* fork another process */
asm volatile(
"mov $0x78, %%eax\n\t"
"int $0x80\n\t"
"mov %%eax, %0\n\t"
:"=m"(pid)
:
);
if (pid < 0)
{
/* error occurred */
fprintf(stderr,"Fork Failed!");
exit(-1);
}
else if (pid == 0)
{
/* child process */
printf("This is Child Process!\n");
execlp("/hellostaic","hello",NULL);
}
else
{
/* parent process  */
printf("This is Parent Process!\n");
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!\n");
}
return 0;
}
int main()
{
PrintMenuOS();
SetPrompt("MenuOS>>");
MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
MenuConfig("quit","Quit from MenuOS",Quit);
MenuConfig("time","Show System Time",Time);
MenuConfig("exec","exec progress",Exec);
MenuConfig("execStaic","exec static progress",Execstaic);

ExecuteMenu();
}


其中hellpStatic的代码如下:

#include <stdio.h>

int main()
{
printf("hello world.\n");
return 0;
}


然后将它编译成静态文件:

[root@localhost menu]# gcc -o hellostatic hello.c -static

编译完成后,将静态文件hellostatic与init拷贝到rootfs中,并在rootfs中执行命令:

find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img

按照课件描述,启动linux跟踪。



在弹出窗口中输入相应的命令:



2. 代码分析

当输入命令后,代码进入跟踪:



而do_execve()最终是调用到函数do_execve_common,在此函数中,有两条比较关键的代码:

static int do_execve_common(...)
{
//1. 打开对应的二进制文件
file = do_open_exec(filename);
//2. 创建一个结构体,在这个结构体中保存了需要加载的二进制文件的信息
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
//3. 加载二进制文件,并设置eip等参数
retval = exec_binprm(bprm);

}


其中exec_binprm函数为其中比较关键代码,而在这一函数中,比较关键的代码为:

static int exec_binprm(struct linux_binprm *bprm)
{
... ...
//查找二进制文件的加载方式
ret = search_binary_handler(bprm);
... ...

}


再次查看search_binary_handler:

int search_binary_handler(struct linux_binprm *bprm)
{

struct linux_binfmt *fmt;
... ...
retry:
read_lock(&binfmt_lock);
//循环遍历查找可以解析需要加载的文件的代码。fmt
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
bprm->recursion_depth++;
//加载文件,而此函数最终通过函数指针调用到load_elf_binary
retval = fmt->load_binary(bprm);
... ....
}


这里需要注意一个问题。 linux对于linux_binfmt,有多种不同的定义,因而最终实际调用load_binary也不同,系统会根据加载文件读取128个字节的文件头部后,决定了linux_binfmt的实际定义。a.out可执行文件的装载过程叫做load_aout_binary;而装载可执行脚本程序的处理过程叫做load_script()。在本例中,elf格式的文件的定义:

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,
};


因此程序也就是调用load_elf_binary将程序加载起来。其中load_elf_binary()的主要步骤是:

1. 检查ELF可执行文件格式的有效性,比如魔数、程序头表中段(segment)的数量

2. 寻找动态链接”.interp”段,设置动态链接器路径(与动态链接有关)

3. 根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码、数据、只读数据。

4. 初始化ELF进程环境,比如进程启动时EDX寄存器的地址应该是DT_FINI的地址

5. 将系统调用返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的ELF可执行文件,这个程序入口就是ELF文件的文件头中e_entry所指的地址;对于动态链接的ELF可执行文件,程序入口点是动态链接器。

注:摘自《程序员的自我修养–链接、装载与库》

其中第5步是能过函数start_thread()实现的

void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
set_user_gs(regs, 0);
regs->fs        = 0;
regs->ds        = __USER_DS;
regs->es        = __USER_DS;
regs->ss        = __USER_DS;
regs->cs        = __USER_CS;
regs->ip
4000
= new_ip; //修改相应的值
regs->sp        = new_sp;
regs->flags     = X86_EFLAGS_IF;
/*
* force it to the iret return path by making it look as if there was
* some work pending.
*/
set_thread_flag(TIF_NOTIFY_RESUME);
}


3. 总结

程序加载通过do_execve()完成,而do_execve()执行过程:

1) 首查找被执行的文件,如果找到文件,则读取文件的前128个字节。 用于判断文件的格式。 开头的4个字节,称为魔数。通过对魔数的判断可以确定文件的格式和类型。

2) 读取128个字节的文件头部后,调用search_binary_handle去搜索和匹配合适的可执行文件装载处理过程。通过文件头部的魔数确定文件的格式,并调用相应的装载处理过程。比如ELF可执行文件的装载处理过程叫做load_elf_binary();a.out可执行文件的装载过程叫做load_aout_binary;而装载可执行脚本程序的处理过程叫做load_script()

3)当load_elf_binary()执行完毕,返回至do_execve()再返回至sys_execve()时, 上面的第5步已经把系统调用返回地址改成了被装载的ELF程序的入口地址了。所以当sys_execve()系统调用从内核态返回到用户态时,EIP寄存器直接跳转到ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件装载完成。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: