Linux内核分析(七)

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.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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值