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

罗冲 + 原创作品转载请注明出处 + 《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跟踪。
Linux内核如何装载和启动一个可执行程序_第1张图片
在弹出窗口中输入相应的命令:
Linux内核如何装载和启动一个可执行程序_第2张图片

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        = 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可执行文件装载完成。

你可能感兴趣的:(Linux内核如何装载和启动一个可执行程序)