陈铁 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000


    学习过程其实就是模仿老师的过程,万一足够熟练了,就变成自己的了。内核代码部分的确有些痛苦,好在本周回到了用户shell层面,毕竟有些了解。将整个学习过程记录如下,也是自己的成长经历。

    

一、可执行文件的生成过程。我们说的可执行文件是给cpu执行的二进制代码,那它又是我们人所编辑的。这个过程我就简单点,有下面的shell命令说明一下。结果如图。

  245  vi hello.c
  246  gcc -E -o hello.cpp hello.c -m32
  247  gcc -x cpp-output -S -o hello.s hello.cpp -m32
  248  gcc -x assembler -c hello.s -o hello.o -m32
  249  gcc -0 hello hello.o -m32
  250  gcc -o hello hello.o -m32
  251  gcc -o hello.static hello.o -m32 -static

wKiom1UvrPOAmTUnAAHur_bXTzc580.jpg

我们在windows上的可执行文件是PE文件。在linux下是elf格式。

文本编辑器编辑源代码文件->预编译处理->汇编成.s文件->编译生成目标.o文件->链接程序将目标文件连接成可执行文件。


二、对于静态链接的elf文件,基本上在加载时对应加上程序入口地址,将相应的代码数据加载到对应的内存空间中,然后逐步执行代码。以下是我的ELF Header情况。

wKioL1UvtEKj9qreAANbzIAsmK0653.jpg

我用gdb跟一下看看执行时的代码入口地址。由于实际上我们的main函数是由_start调用的,所以我们设置断点 break _start。结果如下:

wKiom1UvtlfwSy6zAAHP1T1CPl4233.jpg


三、通常的linux下的可执行程序都是在shell下执行的,所以会进行相应的执行前处理过程。比如/bin/ls -l这个命令,就是先fork出一个进程,调用execve系统调用,然后再调用具体的execlp调用将相关的命令行参数传递给程序的main函数的。


四、通常我们的程序还需要使用动态链接库。分为装载时动态链接和运行时动态链接。演示代码

    通过这样的命令生成共享库:

gcc -shared shlibexample.c -o libshlibexample.so -m32

    以下命令生成运行时链接库:

gcc -shared dllibexample.c -o libdllibexample.so -m32

生成文件及运行情况如下:

wKiom1UvwPXiBp1xAASMH3nWbE8396.jpg

五、execve系统调用和fork系统调用一样,都是特殊的系统调用。fork系统调用返回两次,一次返回父进程执行,一回返回特定的点ret_from_fork执行,然后返回到用户态。execve系统调用在内核中将当前的可执行程序覆盖掉了,当返回时不再是原来的可执行程序了。

Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数:
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
而所有的库函数exec*都是execve的封装例程。

系统调用sys_execve会解析可执行文件格式do_execve->do_execve_common->exec_binprm然后执行search_binary_handler找到符合文件头部标明的文件格式的解析模块。对于linux下的ELF文件,fmt->load_binary(bprm)实际执行的就是static int load_elf_binary(struct linux_binprm *bprm)。

执行start_thread(regs, elf_entry, bprm->p)时,如果是静态链接的,elf_entry就是文件头部标明的入口。进入内核后

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

execve在返回前用新的ip和sp更新了进程的ip和sp。对于需要动态链接的程序,elf_entry就会加载动态链接器ld的入口地址。


总结,在linux环境下,可执行文件是以ELF格式存在的,文件头部标明了文件在加载到内存中需要的相关信息,随后的部分是以段的形式存在的代码和数据,段的划分主要依据加载到内存中的读写属性。系统调用execve负责可执行文件的调度工作,先进行相关参数的传递和调用前环境的处理,然后加载可执行文件的信息,查找相应的可执行文件解析模块,对于ELF格式的可执行文件,按照格式要求加载到内存中相应的地址空间,如果是静态链接的就将文件头部标明的入口地址作为开始;如果是依赖动态链接库的可执行文件则需要将动态链接器ld的入口地址作为开始。