您的位置:首页 > 运维架构 > Linux

Linux内核分析(七)

2016-04-08 21:46 399 查看
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.c
sys_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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息