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的值

2018-2019-1 20189206 《Linux内核原理与分析》第八周作业的更多相关文章

  1. 2019-2020-1 20199329《Linux内核原理与分析》第九周作业

    <Linux内核原理与分析>第九周作业 一.本周内容概述: 阐释linux操作系统的整体构架 理解linux系统的一般执行过程和进程调度的时机 理解linux系统的中断和进程上下文切换 二 ...

  2. 2019-2020-1 20199329《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 一.上周问题总结: 未能及时整理笔记 Linux还需要多用 markdown格式不熟练 发布博客时间超过规定期限 二.本周学习内容: <庖丁解 ...

  3. 20169212《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 这一周学习了MOOCLinux内核分析的第一讲,计算机是如何工作的?由于本科对相关知识的不熟悉,所以感觉有的知识理解起来了有一定的难度,不过多查查资 ...

  4. 20169210《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 本周作业分为两部分:第一部分为观看学习视频并完成实验楼实验一:第二部分为看<Linux内核设计与实现>1.2.18章并安装配置内核. 第 ...

  5. 2018-2019-1 20189221 《Linux内核原理与分析》第九周作业

    2018-2019-1 20189221 <Linux内核原理与分析>第九周作业 实验八 理理解进程调度时机跟踪分析进程调度与进程切换的过程 进程调度 进度调度时机: 1.中断处理过程(包 ...

  6. 2017-2018-1 20179215《Linux内核原理与分析》第二周作业

    20179215<Linux内核原理与分析>第二周作业 这一周主要了解了计算机是如何工作的,包括现在存储程序计算机的工作模型.X86汇编指令包括几种内存地址的寻址方式和push.pop.c ...

  7. 2019-2020-1 20209313《Linux内核原理与分析》第二周作业

    2019-2020-1 20209313<Linux内核原理与分析>第二周作业 零.总结 阐明自己对"计算机是如何工作的"理解. 一.myod 步骤 复习c文件处理内容 ...

  8. 2018-2019-1 20189221《Linux内核原理与分析》第一周作业

    Linux内核原理与分析 - 第一周作业 实验1 Linux系统简介 Linux历史 1991 年 10 月,Linus Torvalds想在自己的电脑上运行UNIX,可是 UNIX 的商业版本非常昂 ...

  9. 《Linux内核原理与分析》第一周作业 20189210

    实验一 Linux系统简介 这一节主要学习了Linux的历史,Linux有关的重要人物以及学习Linux的方法,Linux和Windows的区别.其中学到了LInux中的应用程序大都为开源自由的软件, ...

  10. 2018-2019-1 20189221《Linux内核原理与分析》第二周作业

    读书报告 <庖丁解牛Linux内核分析> 第 1 章 计算工作原理 1.1 存储程序计算机工作模型 1.2 x86-32汇编基础 1.3汇编一个简单的C语言程序并分析其汇编指令执行过程 因 ...

随机推荐

  1. linux下批量杀死进程

    ps aux|grep python|grep -v grep|cut -c 9-15|xargs kill -15 管道符“|”用来隔开两个命令,管道符左边命令的输出会作为管道符右边命令的输入.下面 ...

  2. 使用redis接管cookie

    class RedisCookie { // 默认配置名称(使用load_config加载) private $_default_config_path = 'package/cache/redis_ ...

  3. php跨域发送请求原理以及同步异步问题

    <script async type="text/javascript" src="http://lisi.com/data.php?flag=1"> ...

  4. 【转载】解决gridview空行时不显示的问题

    问题: GridView控件应用很是广泛,通常将它与DataSourceControl搭配使用,当然也可以手工指定DataSource属性来完成数据绑定.如果数据源返回一个空行的数据集(例如查询不到指 ...

  5. Python学习之旅(三十五)

    Python基础知识(34):电子邮件(Ⅰ) 几乎所有的编程语言都支持发送和接收电子邮件 在使用Python收发邮件前,请先准备好至少两个电子邮件,如xxx@163.com,xxx@sina.com, ...

  6. Herriott池的设计

    0.矩阵法计算光路 1.谐振腔和透镜组的等效,计算x和x’ 2.近轴光路的近似计算和矩阵法. 3.相邻光线的角度 4.为啥分模式 5.椭圆模式 6.要考虑的其他问题,相邻光斑的干涉

  7. css学习_css伪元素的本质

    1.伪元素的本质(插入了一个元素(行内元素/标签/盒子) 案例1: 案例2:

  8. 目标检测(七)YOLOv3: An Incremental Improvement

    项目地址 Abstract 该技术报告主要介绍了作者对 YOLOv1 的一系列改进措施(注意:不是对YOLOv2,但是借鉴了YOLOv2中的部分改进措施).虽然改进后的网络较YOLOv1大一些,但是检 ...

  9. linux 逆向映射机制浅析

    2017-05-20 聚会回来一如既往的看了会羽毛球比赛,然后想到前几天和朋友讨论的逆向映射的问题,还是简要总结下,免得以后再忘记了!可是当我添加时间……这就有点尴尬了……520还在写技术博客…… 闲 ...

  10. stellar.js 视差滚动

    1.引入包 <script src="js/jquery.min.js"></script> <script src="js/jquery. ...