Linux内核分析(七)
来源:互联网 发布:手机图章制作软件 编辑:程序博客网 时间:2024/04/27 20:22
Linux 内核分析——【实验七:如何装载和启动一个可执行程序】
一 什么是可执行文件(程序)
在windows环境下,我们都知道只要双击一个.exe的文件就可以执行一个程序,这个以.exe结尾的文件就是一个可执行文件。在andriod系统下,一个.apk的文件就是一个可执行文件,那么在linux系统下,可执行文件是怎么样的呢?实际上,可执行文件在linux环境下并没有什么特殊的后缀标记,只是在生成该文件时,它的属性设置了可执行(就是‘x’),那么他就是属于可执行文件。
二 可执行文件的格式
linux系统中,可执行文件的格式为elf(Executable and Linking Format)格式。
1 ELF文件有三种类型:
(1)可重定位文件
也就是通常称的目标文件,后缀为.o。链接器将它作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件。
(2)共享文件
这些就是所谓的动态库文件,也即 .so 文件。如果拿前面的静态库来生成可执行程序,那每个生成的可执行程序中都会有一份库代码的拷贝。如果在磁盘中存储这些可执行程序,那就会占用额外的磁盘空间;另外如果拿它们放到Linux系统上一起运行,也会浪费掉宝贵的物理内存。如果将静态库换成动态库,那么这些问题都不会出现。
(3)可执行文件
2 elf 文件的格式
为什么会有两种不同的格式呢?
(1) Linking View: 组成不同的可重定位文件,以参与可执行文件或者可被共享的对象文件的链接构建;
(2) Execution View: 组成可执行文件或者可被共享的对象文件,以在运行时内存中进程映像的构建。
我们从Execution View进行分析:
(1) ELF头部结构Elf32_Ehdr
typedef struct{ unsigned char e_ident[EI_NIDENT]; /* 魔数和相关信息 */ Elf32_Half e_type; /* 目标文件类型 */ Elf32_Half e_machine; /* 硬件体系 */ Elf32_Word e_version; /* 目标文件版本 */ Elf32_Addr e_entry; /* 程序进入点 */ Elf32_Off e_phoff; /* 程序头部偏移量 */ Elf32_Off e_shoff; /* 节头部偏移量 */ Elf32_Word e_flags; /* 处理器特定标志 */ Elf32_Half e_ehsize; /* ELF头部长度 */ Elf32_Half e_phentsize; /* 程序头部中一个条目的长度 */ Elf32_Half e_phnum; /* 程序头部条目个数 */ Elf32_Half e_shentsize; /* 节头部中一个条目的长度 */ Elf32_Half e_shnum; /* 节头部条目个数 */ Elf32_Half e_shstrndx; /* 节头部字符表索引 */} Elf32_Ehdr;e_ident[0]-e_ident[3]包含了文件的魔数 依次是 0x7f, 'E', 'L', 'F'e_ident[4] 表示硬件的位数 1表示32位, 2表示64位e_ident[5] 表示数据编码方式
下面是ELF头部结构中对应的数据类型。
用readelf 可以看可执行文件的ELF信息
~$ readelf -h hello #查看hello文件的头部结构
(2) ELF头的是程序表
typedef struct { Elf32_Word p_type; /* 段类型 */ Elf32_Off p_offset; /* 段位置相对于文件开始处的偏移量 */ Elf32_Addr p_vaddr; /* 段在内存中的地址 */ Elf32_Addr p_paddr; /* 段的物理地址 */ Elf32_Word p_filesz; /* 段在文件中的长度 */ Elf32_Word p_memsz; /* 段在内存中的长度 */ Elf32_Word p_flags; /* 段的标记 */ Elf32_Word p_align; /* 段在内存中对齐标记 */ } Elf32_Phdr;
用readelf 可以看ELF头的是程序表信息
~$ readelf -l hello #查看hello的程序表
注意:更多的readelf命令可以使用:
~$ readelf --help
三 使用exec*库函数加载一个可执行程序
1 exec* 库函数
#include <unistd.h>int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);
其中,只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
(1)函数名与参数的关系:
细看一下,这6个函数都是以exec开头(表示属于exec函数组),前3个函数接着字母l的,后3个接着字母v的,我的理解是l表示list(列举参数),v表示vector(参数向量表)。
(2)区别
execv开头的函数是以”char *argv[]”(vector)形式传递命令行参数,而execl开头的函数采用了罗列(list)的方式,把参数一个一个列出来,然后以一个NULL表示结束。这里的NULL的作用和argv数组里的NULL作用是一样的。
字母p是指在环境变量PATH的目录里去查找要执行的可执行文件。2个以p结尾的函数execlp和execvp,看起来,和execl与execv的差别很小,事实也如此,它们的区别从第一个参数名可以看出:除execlp和execvp之外的4个函数都要求,它们的第1个参数path必须是一个完整的路径,如”/bin/ls”;而execlp和execvp 的第1个参数file可以仅仅只是一个文件名,如”ls”,这两个函数可以自动到环境变量PATH指定的目录里去查找。
字母e是指给可执行文件指定环境变量。在全部6个函数中,只有execle和execve使用了char *envp[]传递环境变量,其它的4个函数都没有这个参数,这并不意味着它们不传递环境变量,这4个函数将把默认的环境变量不做任何修改地传给被执行的应用程序。而execle和execve用指定的环境变量去替代默认的那些。
(3)返回值
与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只有进程ID等一些表面上的信息仍保持原样。调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。
(4)常见的错误
与其他系统调用比起来,exec很容易失败,被执行文件的位置,权限等很多因素都能导致调用失败。因此,使用exec函数族时,一定要加错误判断语句。
a.找不到文件或路径,此时errno被设置为ENOENT;
b.数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT;
c.没有对要执行文件的运行权限,此时errno被设置为EACCES。
2 exec*()函数和fork()函数的区别
(1)fork
fork函数创建一个新的进程,这个进程是当前进程的一个拷贝:子进程和父进程使用相同的代码段,子进程复制父进程的堆栈段和数据段。但是,他们属于两个进程,只不过执行的代码一样罢了。
(2)execve
execve()是对当前进程的替换,替换者为一个指定的程序,其参数包括替换者文件名(filename)、参数列表(argv)以及环境变量(envp)。替换者的执行会中止当前进程,而且替换者处理其他任务,不必和父进程执行一样的任务。
3 使用gdb跟踪exec*函数的执行过程
(1)配置实验环境(与实验三相似)
a.下载文件menu
b.解压,修改makefile文件(如下)
c. 运行make rootfs
d. 使用gdb调试:
qemu -kernel ../../Lab3/linux-3.18.6/arch/x86/boot/bzImage -initrd ./rootfs.img -s -S
(2)设置断点,并运行
在QEMU模拟器中输入以下命令
MenuOS>> exec
gdb中将停在断点处,如下
(3)进行跟踪
大致的运行流程如下:
// 文件路径: linux-3.18.6/fs/exec.csys_execve(){ //系统调用execve do_execve(){ do_execve_common(){ exec_binprm(){ search_binary_handler(){ load_elf_binary(){ start_thread(){ } } } } } }}
倒数第三张图中,在load_elf_binary函数中,会设置程序静态链接活动态链接的入口地址elf_entry,从最后两张图中,可以看到为进程设置了新的ip(也就是elf_entry)和sp,之后返回用户态就会从这里设置的ip开始执行。
=========== 王杰 原创作品转载请注明出处==============
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
- Linux内核分析(七)
- Linux内核分析(七)
- Linux内核分析(七)
- Linux内核分析(七)之待时而动
- Linux内核分析实验七
- Linux内核分析:实验七
- Linux学习笔记(七)-Linux内核分析
- Linux内核源码分析--文件系统(七、Namei.c)
- 网易云课堂 Linux内核分析(七)
- Linux内核分析(七):可执行程序的装载
- Linux内核分析 - 网络[七]:NetFilter
- Linux内核分析 - 网络[七]:NetFilter
- 《Linux内核分析》(七)——Linux可执行程序浅析
- [网易云课堂]Linux内核分析(七)—— Linux内核如何装载和启动一个可执行程序
- Linux内核--网络栈实现分析(七)--数据包的传递过程(下)
- Linux内核--网络栈实现分析(七)--数据包的传递过程(下)
- Linux内核--网络栈实现分析(七)--数据包的传递过程(下)
- linux内核部件分析(七)——设备驱动模型之driver
- 最短路径问题Dijkstra(图论算法)
- 最小花费(图论算法)
- 2000年分区联赛普级组之一 计…
- 信使(图论算法)
- 热浪(图论算法)
- Linux内核分析(七)
- 营养膳食(贪心算法)
- 谁是组长
- 分糖果(图论算法)
- 观光旅游(图论算法)
- 刻录光盘(图论算法)
- oracle patch打不上的常见问题
- 字符组合
- 亲戚(图论算法)