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

概念总结

1.可执行程序是怎么得来的

C代码——预处理——汇编代码——目标代码——可执行文件

2.可执行文件的格式

 可执行文件最初为A.out格式,后来演化为COFF格式,再后来变成PE(windows系统)和ELF(linux系统)。ELF:executable and linkable format,即可执行可链接格式。

 

3.可执行程序的执行环境

  • 命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。

    • $ ls -l /usr/bin 列出/usr/bin下的目录信息

    • Shell本身不限制命令行参数的个数, 命令行参数的个数受限于命令自身

      • 例如,int main(int argc, char *argv[])

      • 又如, int main(int argc, char *argv[], char *envp[])

    • Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数

      • int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

      • 库函数exec*都是execve的封装例程

 gdb调试

在原有的功能增加了exec

 

设置断点

运行两次之后在child处停下

s进入do_execve内部

load_elf_binary

new_ip是返回到用户态的第一条指令的地址,通过readelf -h hello我们发现hello的入口点地址和new_ip的地址相同

start_thread修改了内核堆栈regs->ip等,从而改变了上下文环境

关于几个问题的回答

1.新的可执行程序是从哪里开始执行的?
默认elf文件从0x8048000出开始加载,前面是elf头部信息,大小一般各不相同。实际的入口在0x8048x000(x为不定值),即程序的入口点地址。

2.为什么execve系统调用返回后新的可执行程序能顺利执行?

在创建一个新的用户态堆栈时,实际上是把命令行参数内容和环境变量的内容通过指针传递到系统调用内核处理函数,然后内核处理函数在创建一个新的可执行程序的用户态堆栈时将参数拷贝到户态堆栈,以此来初始化新的可执行程序的上下文环境。先函数调用参数传递,再系统调用参数传递。

命令行参数和环境串都放在用户态堆栈中

 

3.对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时会有什么不同?

静态链接方法:#pragma comment(lib, "test.lib") ,静态链接的时候,载入代码就会把程序会用到的动态代码或动态代码的地址确定下来
静态库的链接可以使用静态链接,动态链接库也可以使用这种方法链接导入库

load_elf_binary中调用了函数start_thread,其中的参数pt_regs就是内核堆栈的栈底
start_thread(regs, elf_entry, bprm->p)
静态链接的elf_entry就是可执行文件的entry,新的程序在返回用户态之前需要修改int 0x80压入内核堆栈的eip

动态链接方法:LoadLibrary()/GetProcessAddress()和FreeLibrary(),使用这种方式的程序并不在一开始就完成动态链接,而是直到真正调用动态库代码时,载入程序才计算(被调用的那部分)动态代码的逻辑地址,然后等到某个时候,程序又需要调用另外某块动态代码时,载入程序又去计算这部分代码的逻辑地址,所以,这种方式使程序初始化时间较短,但运行期间的性能比不上静态链接的程序。

总结

execve和fork都是特殊的系统调用,陷入到内核态在返回到用户态继续执行。fork比较特殊,父进程和一般的系统调用一样,子进程从ret_from_fork开始执行返回到用户态。execve陷入到内核态,用加载的新的可执行文件将当前进程的可执行程序覆盖,当其返回时已经不是原来的可执行程序,而是新的可执行程序了。因为调用exec并不创建新进程,只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段,所以前后的进程ID并未改变

 

 

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

posted @ 2016-04-10 23:26  yuxiaochuan  阅读(1079)  评论(1编辑  收藏  举报