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 文件的格式
7-1
为什么会有两种不同的格式呢?
(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头部结构中对应的数据类型。
7-2

用readelf 可以看可执行文件的ELF信息

~$ readelf -h  hello   #查看hello文件的头部结构

7-3

(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的程序表

7-4

注意:更多的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文件(如下)
7-5
c. 运行make rootfs
d. 使用gdb调试:

qemu -kernel ../../Lab3/linux-3.18.6/arch/x86/boot/bzImage -initrd ./rootfs.img -s -S

(2)设置断点,并运行
7-6
在QEMU模拟器中输入以下命令

MenuOS>> exec

gdb中将停在断点处,如下
7-7
(3)进行跟踪
7-8
7-9
7-10
7-11
7-12
7-13
大致的运行流程如下:

// 文件路径: 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

0 0
原创粉丝点击