罗冲 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
下载课件中准备的代码。
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跟踪。
在弹出窗口中输入相应的命令:
当输入命令后,代码进入跟踪:
而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 = 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);
}
程序加载通过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可执行文件装载完成。