Linux------可执行程序的装载

来源:互联网 发布:二手房增值税算法 编辑:程序博客网 时间:2024/05/22 13:21

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

一、基础知识
(1)可执行程序是怎么来的?
一个.c文件经过编译器预处理(.cpp),
然后编译成汇编代码(.S/.asm),
由汇编器生成目标代码(.o二进制),
由链接器链接成可执行文件,
最后由操作系统加载到内存中然后执行
用gcc执行的过程:
1.预处理:gcc -E -o hello.cpp hello.c -m32
2.编译:gcc会检查代码(是否有语法错误等),将代码翻译成汇编语言
gcc -x cpp-ouput -S -o hello.s hello.cpp -m32
3.汇编:将编译阶段生成的.S文件转变为目标文件(.o)
gcc -x assembler -c hello.s -o hello.o -m32
4.链接:将编译输出.o文件链接成最终的可执行文件(hello也是一个二进制文件)
gcc -o hello-static hello.o -m32 -static
(2)可执行文件的内部是怎样的?
1.目标文件的格式ELF
1)目标文件格式分类
这里写图片描述
2)ABI
ABI:应用程序二进制接口,在目标文件中二进制兼容模式适应到某一种cpu体系结构上的二进制指令
3)ELF中三种目标文件
1.可重定位文件(.o文件):用来和其他的object文件一起创建一个可执行文件或一个共享文件
2.可执行文件:用来保存一个可执行的程序,该文件指出了exec(BA_OS)如何来创建程序进程映像(操作系统如何把程序加载起来,并且从哪里开始执行)
3.共享目标文件(.so):保存着代码和合适的数据,用来被下面的两个链接器链接:第一种是链接编辑器,第二种是动态链接器
4)ELF头(保存了很多关键信息)
这里写图片描述
5)可执行的文件加载的工作:当创建或者增加一个进程映像时,系统在理论上将拷贝一个文件的段到虚拟的内存段
6)静态链接的ELF可执行文件与进程的地址空间
这里写图片描述
1.一个进程加载了新的可执行文件开始的入口点
2.一般静态链接会将所有的代码放在一个代码段
3.动态链接的进程有多个代码段
(3)可执行程序、共享库和动态进程
1、装载可执行程序之前的工作
可执行程序的执行环境:shell命令行、main函数参数、execve参数
1)命令行参数和shell环境

  • 列出/usr/bin下的目录信息:
    $ ls -l /usr/bin
  • Shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身
    int main(int argc, char *argv[], char *envp[])
    //envp接受shell命令行的相关变量
  • Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数:
    int execve(const char * filename,char * const argv[ ],char * const envp[ ])
    Shell会先定义一个子进程,在子进程中调用execlp(“/bin/ls”,”ls”,NULL);
  • 库函数exec*都是execve的封装例程
    (4)命令行参数和环境变量是如何保存和传递的?是如何进入新程序的堆栈的
    1)当fork时,子进程复制父进程的堆栈,调用execv时,在加载的可执行程序前将原来的进程用要加载的可执行程序覆盖掉,覆盖掉后用户栈和堆栈会被清空。
    2)命令行参数和环境变量都存放在用户堆栈中
  • shell程序 —> execve —> sys_execve
  • 初始化新程序堆栈时拷贝进去(execve在创建可执行程序堆栈时,帮我们拷贝进去)
    这里写图片描述
    新的程序从main函数开始讲对应的参数接收进来然后先函数调用参数传递,再系统调用参数传递
    3)装载时动态链接和运行时动态链接应用
    动态链接分为可执行程序装载时动态链接和运行时动态链接
    这里写图片描述
    对于动态链接库,可以作为在进程装载的时候动态链接。也可以作为运行时装载起来
    头文件 # include < dlfcn.h > //动态加载

编译main:-L :库对应的接口头文件所在的目录
-l:苦命,如Linshlibexample.so,去掉lib和.so部分

gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32

-ldl:动态加载
(5)可执行程序的装载
1、execve系统调用的内核处理过程(execve也是一种特殊的系统调用)
1)新的可执行程序起点——一般是地址空间为0x8048000或0x8048300
2)execve和fork都是特殊的系统调用——一般的都是陷入到内核态再返回到用户态

  • fork两次返回,第一次返回到父进程继续向下执行,第二次是子进程返回到ret_from_fork然后正常返回到用户态。
  • execve执行的时候陷入到内核态,用execve中加载的程序把当前正在执行的程序覆盖掉,当系统调用返回的时候也就返回到新的可执行程序起点(不是原来的位置了)
  • sys_execve内部会解析可执行文件格式
    do_ execve —> do_ execve_common —> exec _binprm
      search_ binary _ handler符合寻找文件格式对应的解析模块
      对于ELF格式的可执行文件fmt->load_ binary(bprm);执行的应该是load_ elf _binary其内部是和ELF文件格式解析的部分需要和ELF文件格式标准结合起来阅读
    2.search_binary _handle符合寻找文件格式对应的解析模块,根据ELF文件头部信息寻找对应的文件格式处理模块
    寻找能解析ELF格式的模块
    对于ELF格式的可执行文件fmt->load_ binary(bprm):执行的应该是load_elf _ library,其内部是和ELF文件格式解析的部分(和ELF标准相联系)
    3.Linux内核是如何支持多种不同的可执行文件合适的?
    elf_ format全局变量:将load_ elf_ binary赋给了全局变量的指针load_ library,在init elf binfmt时register _ binfmt( &elf _ format),将它注册到内核链表(fmt链表),(elf format和init _elf binfat像是观察者模式中的观察者)
    当出现elf文件时,elf format 自动执行 load elf _ binary 实际上执行了 retval _fmt ->load _binary (bprm),(多态机制)
    在load _ elf_ bimary 中调用了 start _thread(struct pt _regs *regs,unsigned long new _ip,unsigned long new _sp);
    修改了pt_ regs
    load_ elf_binary中,调用了start _thread()函数,通过修改内核堆栈中EIP的值作为新程序的起点
    将flags,ip,sp都压栈,regs->ip = new_ip,regs.sp = new _ sp,
    其中new_ ip来自:在load elf _binary中,start thread(regs,elf _ entry,bprm->p),在新的可执行程序返回到用户态之前,要修改int $0x80压人内核堆栈的EIP,用新的可执行文件来修改
    (6)sys_execve的内部处理过程

  • 系统调用的入口:do_execve
    return do_execve(getname(filename), argv, envp);

  • 转到do _ execve _ common函数
    return do_ execve_ common(filename, argv, envp);
      file = do_ open_exec(filename); //打开要加载的可执行文件,加载它的文件头部
      bprm->file = file;
      bprm->filename = bprm->interp = filename->name; //创建了一个结构体bprm,把环境变量和命令行参数都copy到结构体中
  • exec_binprm(对可执行文件的处理过程)
      ret = search_binary_handler(bprm);  //寻找此可执行文件的处理函数 在其中关键的代码
      list_ for each entry(fmt, &formats, lh);
      retval = fmt->load_ binary(bprm);
       //在这个循环中寻找能够解析当前可执行文件的代码并加载出来,实际调用的是load_elf _binary函数

  • 文件解析相关模块:核心的工作就是把文件映射到进程的空间,对于ELF可执行文件会被默认映射到0x8048000。

  • 需要动态链接的可执行文件先加载链接器ld​(load _ elf _ interp 动态链接库动态链接文件),动态链接器的起点
  • 如果它是一个静态链接,可直接将文件地址入口进行赋值
    (7)结构体变量如何进入到内核的处理模块?
    在init _ elf binfmt中,函数register binfmt(&elf _ format)。
    需要动态链接库的可执行文件先加载动态链接器ld,
    if(elf_ interpreter)需要加载其他的动态库
    执行elf_load _elf _interp<——加载动态链接器
    else
    如果是静态链接文件执行
    elf _ entry = loc->elf _ex.e _entry
    在start _thread中直接使用elf _entry
    1.如果elf _entry是动态链接文件,elf指向链接器的起点
    2.如果elf _entry是静态链接文件,elf指向可执行文件中规定的头(main函数的位置)
    将cpu的控制权交给ld来加载依赖库并完成动态链接
    对于静态链接的文件elf_ entry是新程序执行的起点
    (8)用庄生梦蝶的典故理解可执行程序的加载
    庄周(调用execve的可执行程序)入睡(调用execve陷入内核),醒来(系统调用execve返回用户态)发现自己是蝴蝶(被execve加载的可执行程序)(醒来时发现自己不是原来的“自己”了)。
    (9)动态链接的可执行程序的装载

  • 实际上动态链接库的依赖关系会形成一个“依赖树”

  • 动态链接库的装载过程一般是一个图的广度遍历
    将所有依赖的动态链接库装载起来,装载和链接之后ld将cpu的控制权交给可执行程序。
  • 动态链接是由动态链接器完成而不是内核

总之:静态链接:直接执行可执行程序的入口
动态链接:装载和链接之后ld将CPU的控制权交给可执行程序


二、实验部分 ——Linux内核如何装载和启动一个可执行程序
(一)搭建环境
(查看代码时,可以使用shift+G直接跳到文件末尾)
这里写图片描述
修改Makefile文件
这里写图片描述
(生成根文件系统时,将init hello放入rootfs地址中,这样在执行exec文件时,就自动加载hello文件)
这里写图片描述
这里写图片描述
(二)使用gdb跟踪sys_execve内核函数的处理过程
1、加载符号表,并连接到端口1234
2、设置断点
这里写图片描述
3、执行
这里写图片描述
这里写图片描述
输入c继续运行,进入到sys_execve系统调用:
这里写图片描述
输入s进行跟踪:
这里写图片描述
new_ip是返回到用户态的第一条指令的地址:
这里写图片描述
用readelf -h hello 查看信息,入口点地址为0x8048doa


二、实验总结
由静态链接和动态链接对可执行文件的加载过程进行了解,通过对execve系统调用的功能和执行的分析,了解了可执行文件的加载过程,应记忆函数的功能和特点以及参数传递的方法,动态链接库和静态链接库的区别等等。

0 0
原创粉丝点击