• 2018-2019-1 20189206 《Linux内核原理与分析》第八周作业


    linux内核分析学习笔记 ——第七章 可执行程序工作原理


    学习目标:了解一个可执行程序是如何作为一个进程工作的。

    ELF文件

    目标文件:是指由汇编产生的(*.o)文件和可执行文件。 即 可执行或可连接的文件目标文件是已经适应某一种CPU体系结构上的二进制指令。

    目标文件的格式可以分为:

    • a.out
    • COFF
    • PE(windows)和ELF(linux)

    ELF就是可执行和可连接的格式,是一个目标文件的标准格式。ELF是一种对象文件格式,用于定义不同类型的对象文件中都有什么内容、以什么样的格式存放这些内容。

    ELF文件的三种类型:

    • 可重定位文件:属于中间文件,需要继续处理。由编译器和汇编器创建。一个源代码会生成一个可重定位文件。用来和其他目标文件一起来创建一个可执行文件、静态库文件或者共享目标文件
      • 可重定位文件后缀为.o ,最后所有.o文件会链接为一个文件。
    • 可执行文件:由多个可重定位文件结合生成,完成了所有重定位工作和符号解析的文件。文件中保存着一个用来执行的程序。
    • 共享目标文件:共享库,是指被可执行文件或其他库文件使用的目标文件。其后缀为.so

    ELF文件的功能:

    ELF文件参与程序的连接(建立一个程序)和程序的执行(运行一个程序),所以可以从不同的角度来看待elf格式的文件:

    • 如果用于编译和链接(可重定位文件),则编译器和链接器将把elf文件看作是节头表描述的节的集合,程序头表可选。
    • 如果用于加载执行(可执行文件),则加载器则将把elf文件看作是程序头表描述的段的集合,一个段可能包含多个节,节头表可选。
    • 如果是共享文件,则两者都含有。

    ELF格式

    ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。

    ELF Header之后可能会有一个程序头部表(Program Header Table),如果存在的话,告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。
    节区头部表(Section Heade Table)包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。
    另外,Sections是文件节区,它包含不同的节区,且节区没有规定的顺序。

    ELF Header

    ELF Header结构体定义:

      #define EI_NIDENT   16
      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;
          Elf32_Half  e_phentsize;
          Elf32_Half  e_phnum;
          Elf32_Half  e_shentsize;
          Elf32_Half  e_shnum;
          Elf32_Half  e_shstrndx;
      } Elf32_Ehdr;
    

    其中e_ident定义:

     e_ident[] Identification Indexes
      Name       Value       Purpose
      ====       =====       =======
      EI_MAG0     0      File identification
      EI_MAG1     1      File identification
      EI_MAG2     2      File identification
      EI_MAG3     3      File identification
      EI_CLASS    4      File class
      EI_DATA     5      Data encoding
      EI_VERSION  6      File version
      EI_PAD      7      Start of padding bytes
      EI_NIDENT   16     Size of e_ident[ ]
    

    其中结构体e_ident[EI_NIDENT]前4个字节叫做一个魔术数(magic number),用来确定该文件是否为ELF的目标文件,所有ELF文件的魔数是相同的。其中 EI_VERSIONELF头的版本号,目前只能设置为‘1’。

    对于ELF Header的部分结构体成员:

    • e_machine该成员变量指出了运行该程序需要的体系结构。
    • e_version这个成员确定object文件的版本。
    • e_entry 程序入口虚地址。
    • e_phoff 文件头偏移,表明文件头紧接在elf head后面。
    • e_shoff 节头表文件偏移;
    • e_flags 处理器相关的标志
    • e_ehsize 该成员保存着ELF头大小(以字节计数)。
    • e_phentsize 该成员保存着在文件的程序头表(program header table)中一个入口的大小(以字节计数)。所有的入口都是同样的大小。
    • e_phnum 该成员保存着在程序头表中入口的个数。
    • e_shentsize 该成员保存着section头的大小(以字节计数)。
    • e_shnum 该成员保存着在section header table中的入口数目.
    • e_shstrndx 该成员保存着跟section名字字符表相关入口的section头表(section header table)索引。

    其中,节头表定义了整个ELF文件的组成,段只是对节的重新组合,将多个节区描述为一段连续区域,对应到一段连续的内存地址中。

    Section Header

    节区头是节区的索引,程序执行时先通过ELF Header找到Section Header,再通过这一索引找到对应的节区。

    typedef struct {
        Elf32_Word	sh_name;
        Elf32_Word	sh_type;
        Elf32_Word	sh_flags;
        Elf32_Addr	sh_addr;
        Elf32_Off 	sh_offset;
        Elf32_Word	sh_size;
        Elf32_Word	sh_link;
        Elf32_Word	sh_info;
        Elf32_Word	sh_addralign;
        Elf32_Word	sh_entsize;
    } Elf32_Shdr;
    
    • sh_name 节名,是在字符串中的索引
    • sh_type 节类型
    • sh_addr 该节对应的虚拟地址
    • sh_offset 该节在文件中的位置
    • sh_size 该节的大小
    • sh_link 与该节连接的其他节
    • sh_addralign 对齐方式

    Program Header

    段头表是和创建进程相关的,描述了连续的几个节在文件中的位置、大小以及它被放入内存后的位置和大小,告诉系统如何创建进程

    /* Program Header */
    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;
    
    • p_type 当前描述的段类型
    • p_offset 段在文件中的偏移
    • p_vaddr 段在内存中的虚拟地址
    • p_paddr 在物理内存定位相关的系统中,此项为物理地址保留
    • p_filesz 段在文件中的长度
    • p_memsz 段在内存中的长度
    • p_align 确定段在文件及内存中如何对齐

    程序编译

    程序从源代码到可执行文件经过以下步骤:

    预处理、编译、汇编、链接。

    • 预处理
      • gcc -E hello.c -o hello.i
      • 预处理的主要工作是
        • 删除所有的注释
        • 删除所有#define,进行替换
        • 处理所有预编译指令
        • 处理#include指令,将被包含的文件插入预编译指令的位置
        • 添加行号和文件名标识
      • 预处理完的文件仍然是文本文件,可以用任意文本编辑器查看。
    • 编译
      • gcc -S hello.i -o hello.s -m32
      • 编译首先会检查代码的规范性、语法错误等
      • 汇编结束的文件是二进制文件,可以用任意编辑器查看
    • 汇编
      • gcc -c hello.s -o hello.o -m32
      • 汇编结束后的文件已经是ELF格式的文件了。至少包含三个节区.text .data .bss
        • .text 代码段,通常用来存放程序执行代码的内存区域。
        • .data 数据段,通常用来存放程序中已经初始化的全局变量的一块内存区域,属于静态内存分配。
        • .bss 通常用来存放程序中未初始化的变量的内存区域,不占用文件空间。
    • 链接
      • gcc hello.o -o hello -m32 -static
      • 主要工作将有关的目标文件彼此相连,使得所有目标文件能够成为一个能够被操作系统装入执行的统一整体。将各种代码和数据部分收集起来并组合成一个单一文件的过程,这个文件可以被加载或复制到内存中并执行。

    链接与库

    • 从过程上讲,链接分为
      • 符号解析
      • 重定位
    • 链接的时机不同,可以分为
      • 静态链接
      • 动态链接

    对于链接过程,都是采用两步链接的方法

    • 空间与地址分配

        扫描所有的输入目标文件,并且获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表中。
      

    这一步中,链接器将能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度和位置,并建立映射关系。

    • 符号解析与重定位

        使用上面第一步中收集的所有信息,读取输入文件中段的数据、重定位信息(有一个重定位表Relocation Table),并且进行符号解析与重定位、调整代码中的地址(外部符号)等。
      

    符号与符号解析

    在链接中,我们将函数和变量统称为符号,函数名或变量名就是符号名,函数或变量的地址就是符号值。

    每一个目标文件都有一个符号表,符号有以下几种:

    • 定义在本目标文件的全局符号,可被其他目标文件引用

        如:全局变量,全局函数
      
    • 在本目标文件中引用的全局符号,却没有定义在本目标文件 -- 外部符号(External Symbol)

         如:extern变量,printf等库函数,其他目标文件中定义的函数
      
    • 段名,这种符号由编译器产生,其值为该段的起始地址

         如:目标文件的.text、.data等
      
    • 局部符号,内部可见

    符号表

    符号表是用来供编译器用于保存有关源程序构造的各种信息的数据结构,这些信息在编译器的分析阶段被逐步收集并放入符号表,在综合阶段用于生成目标文件。

    符号表的功能是找未知函数在其他库文件中的代码段的具体位置。

    查看方法:objdump -t xxx.o 或 readlef -s xxx.o

    • Ndx 该符号对应区节的编号

    其中,可以看到,在链接前main函数没有地址,而在连接后,main函数分配了内存地址。其他属性未改变,因为main函数本身就在hello.o文件中。

    由此可见符号表中的Ndx字段会显示函数表示符号在段在表中的下标,如果是未定义的函数,显示UND;未初始化的全局变量则显示COMMON

    重定位

    重定位就是把程序的逻辑地址空间变换成内存中的实际物理地址空间的过程,也就是说在装入时对目标程序中指令和数据的修改过程。它是实现多道程序在内存中同时运行的基础。

    上图可以看到在0x11处有一个地址,需要被替换为puts将来的内存地址

    通过反汇编后可以看到,call指令之后的fc ff ff ff在链接之后,就会被替换为puts在链接后的地址。

    由此可见符号表记录了目标文件中所有全局函数及其地址;重定位表中记录了所有调用这些函数的代码位置

    静态链接与动态链接

    静态链接

    链接器将函数的代码从其所在地(目标文件或静态链接库中)拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。

    为创建可执行文件,链接器必须要完成的主要任务:
    
    符号解析:把目标文件中符号的定义和引用联系起来;
    
    重定位:把符号定义和内存地址对应起来,然后修改所有对符号的引用。
    

    动态链接

    在编译时不直接复制可执行代码,通过记录一系列的参数和符号,在程序运行或者加载时将这些信息传递给操作系统。

    操作系统将需要的动态库加载到内存中,程序在运行到指定代码时,去共享执行内存中已经加载的动态库去执行代码。

    动态链接分为

    • 装载时动态链接
      • 只需要在代码中调用对应的库函数,在编译时,将动态库的头文件路径标明
    • 运行时动态链接
      • 运行时动态链接的本质就是程序员自己控制整个过程。

    程序装载

    执行环境上下文

    在Shell中输入 ls -l/usr/bin 实际上相当于执行了可执行程序ls,后面带了两个参数。

    shell本身不限制参数的个数,命令行参数受限于命令自身。

    shell程序的工作方式:fork出一个子进程,在子进程中调用execlp来加载可执行程序。

    如果仅仅加载一个静态链接可执行程序,只需要传递一些命令行参数和环境变量就可以正常工作。但是动态链接程序从内核态返回时,首先会执行.interp节区所指向的动态链接器。

    fork和execve内核处理过程

    execve执行概述

    系统调用sys_execve()被用来执行一个可执行文件,整体调用关系为:

    sys_execve -> do_execve() -> do_execve_common() -> exec_binprm() -> search_binary_handler() -> load_elf_binary() -> start_thread()
    
    系统调用内核处理过程

    该系统调用通过宏定义在获得可执行文件的文件名后,直接调用do_execve并传递参数。

    调用do_execve只是对参数进行了类型转换,并传递给do_execve_commom

    首先创建了一个结构体,将环境变量和命令行参数复制到结构体中,在exec_binprm是准备交给真正的可执行文件加载器。

    调用函数search_binary_handler(bprm)根据文件的头部,寻找可执行文件的处理函数。

    search_binary_handler(bprm)中调用了指针load_binary实际上对应的是load_elf_binary

    load_elf_binary用来装载可执行文件,根据静态链接和动态链接的不同,设置不同的elf_entry

    • 调用了start_thread函数,来创建新的进程堆栈,更重要的是修改了中断现场中保存的EIP寄存器。
      • 静态链接:elf_entry指向可执行文件的头部,是新程序执行的起点。
      • 动态链接:elf_entry指向ld(动态连接器)的起点load_elf_interp

    最后就是start_thread在这个设置new_ip即对应的elf_entry等该进程返回用户态时,转而执行elf_entry指向的代码。

    execve和fork的区别

    简单的来说,就是execve是变身,fork是分身

    利用gdb跟踪调试过程

    cd LinuxKernel
    rm menu -rf
    git clone http://github.com/mengning/menu.git
    cd menu
    mv test_exec.c test.c
    make rootfs
    

    重新编译后,使用qemu命令冻结系统执行,进行调试

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

    水平分割一个窗口,启动gdb加载内核,连接到target 1234

     gdb
    (gdb) file linux-3.18.6/vmlinux
    (gdb) target remote:1234
    

    添加断点sys_execve和load_elf_binary和start_thread

    b sys_execve
    b load_elf_binary
    b start_thread
    

    停在了第一个断点sys_execve

    进入第二个断点

    进入第3个断点,即start_thread处,继续执行可以看到修改了eip的值

  • 相关阅读:
    在loader程序中涉及到的CPU模式切换
    python 线程池的实现
    Python 面向对象程序设计
    Haskell语言学习笔记(28)Data.Map
    C++17尝鲜:在 if 和 switch 语句中进行初始化
    趣味编程:24点(Haskell版)
    Haskell语言学习笔记(27)Endo, Dual, Foldable
    Haskell语言学习笔记(26)Identity, IdentityT
    Haskell语言学习笔记(25)MonadState, State, StateT
    Haskell语言学习笔记(24)MonadWriter, Writer, WriterT
  • 原文地址:https://www.cnblogs.com/zz-1226/p/10055367.html
Copyright © 2020-2023  润新知