# ELF 文件分类

Linux中,ELF文件全称为:Executable and Linkable Format,主要有三种形式,分别是:

  • 可执行文件
  • 动态库文件(共享文件 .so)
  • 目标文件(可重定位文件 .o)

写个脚本测试一下:

准备两个 C 程序:a.c 和 b.c,内容如下:

// a.c
#include <stdio.h> void hello(void); int main(void) {
hello();
return 0;
}
// b.c
#include <stdio.h> void hello(void) {
printf("hello a, b!\n");
}

接下来将b.c编译成动态链接库:

gcc -shared -o libb.so b.c -fPIC

a.c编译成可执行文件:

gcc a.c ./libb.so

得到 4 个文件:

a.c    a.out    b.c    libb.so

执行 ./a.out,可以输出:hello a, b!

为了测试,可以执行gcc -c a.c -o a.o,多编一个a.o,虽然用不到,权当对照。

此时可以用file命令查看文件信息:

file a.out
# 输出:a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked...
file a.o
# 输出:a.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
file libb.so
# 输出:libb.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked...

可以看到,以上三个文件分属于不同的 ELF 种类。

# ELF 文件格式

ELF 文件的结构分为两个重要的部分:ELF头部分和ELF节表部分,其中节表部分被分成两种类型:节和程序段。ELF文件通过一个节表和程序头表指向这两个部分。具体结构如图:

即,同样的数据区域,既可以被视为节(sections),也可以被视为程序段(segments),其在不同的ELF文件中有所区分:

  • 可执行文件:加载器则将把elf文件看作是程序头表描述的段的集合,一个段可能包含多个节,节头部表可选;
  • 可重定位文件(.o):一般编译器和链接器将把elf文件看作是节头表描述的节的集合,程序头表可选;
  • 动态库文件(.so):一般两者都有,因为链接器在链接的时候需要节头部表来查看目标文件各个 section 的信息然后对各个目标文件进行链接;而加载器在加载可执行程序的时候需要程序头表 ,它需要根据这个表把相应的段加载到进程自己的的虚拟内存(虚拟地址空间)中。

可以通过readelf工具查看 ELF 文件的内容:

# 查看 ELF 文件头
readelf -h [elf_file]
# 查看 ELF 文件 sections
readelf -S [elf_file]
# 查看 ELF 文件 segments
readelf -l [elf_file]

这里仅做抛砖引玉,具体的 ELF 文件各个字段的解释,以及动态链接 ELF 文件如何寻址填充,生成可执行文件,可以移步这篇文章:https://cloud.tencent.com/developer/article/2058294

# ELF 文件的执行流程

当执行./a.out命令时,首先开始工作的,是Linux集成的Bash程序。Bash 进程会做两件事情:

  • 调用 fork() 系统调用,创建出一个新的进程,用来执行a.out任务;
  • 调用 execve() 系统调用,执行这个 ELF 可执行文件a.out

execve() 系统调用在内核源码fs/exec.c文件中被定义(kernel 版本 4.19):

SYSCALL_DEFINE3(execve,
const char __user *, filename, // ELF 文件名
const char __user *const __user *, argv, // ELF 文件执行参数
const char __user *const __user *, envp) // 环境参数
{
return do_execve(getname(filename), argv, envp);
}

execve() 系统调用接收三个参数:文件名、执行参数和环境参数,其调用链为:

// execve 系统调用:fs/exec.c
SYSCALL_DEFINE3(execve, ...)
|-> do_execve()
|-> do_execveat_common()
|-> __do_execve_file() // (A)
|-> prepare_binprm(bprm)
|-> kernel_read(bprm->file, bprm->buf, BINPRM_BUF_SIZE, &pos)
|-> exec_binprm(bprm) // (B)
|-> search_binary_handler(bprm)
|-> security_bprm_check(bprm) // (C) lsm hook (include/linux/lsm_hooks.h)
|-> list_for_each_entry(fmt, &formats, lh) {
fmt->load_binary(bprm) // (D) load_elf_binary
}

值得注意的:

在内核中,一个 ELF 可执行文件会被解析为一个brpm结构,结构体为linux_binprm,定义在include/linux/binfmts.h中,核心字段如下:

struct linux_binprm {
char buf[BINPRM_BUF_SIZE]; // 存储 ELF 文件头,大小 128 字节
struct mm_struct *mm;
unsigned long p; // mem top 指针
struct file * file; // ELF 可执行文件指针
int argc, envc; // argv、envp 参数数量
const char * filename; // ELF 可执行文件名
}

在步骤(A)中:留意两件事情

  • 调用prepare_binprm(bprm),后者执行kernel_read(bprm->file, bprm->buf, BINPRM_BUF_SIZE, &pos),将 ELF 文件的前BINPRM_BUF_SIZE大小(128字节)的内容填充到bprm->buf中;
  • 对传入的参数进行处理,即,为运行参数 argv和环境参数envp分配内存页面(函数copy_strings());

随后调用(B)。

步骤(B)会调用search_binary_handler(bprm)选择合适的可执行文件处理器后,最终会调用load_elf_binary()函数真正加载这个 ELF 文件(步骤(D))。

Linux 支持其他不同格式的可执行程序, elf就是其中常见的一种可执行文件格式。在这种方式下, Linux 能运行其他操作系统所编译的程序, 如 MS-DOS 程序, 活 BSD Unix 的 COFF 可执行格式。

这里选择的是 ELF 二进制文件处理器。

引自:https://zhuanlan.zhihu.com/p/287863861

不过在步骤(D)执行之前,会进行一个security_bprm_check(bprm)过程(步骤(C))。该过程是 LSM 框架预设的 hook 点,用于在真正加载 ELF 文件前执行自定义的 check 回调,来实现安全控制。

步骤(D)中调用的其实是fmt->load_binary(bprm),此乃linux_binfmt在初始化时,其成员函数指针内核预设的值,具体如下:

// 在文件 fs/binfmts.h 中
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary,
.load_shlib = load_elf_library,
.core_dump = elf_core_dump,
.min_coredump = ELF_EXEC_PAGESIZE,
}; // elf_binfmt 初始化注册
static int __init init_elf_binfmt(void)
{
register_binfmt(&elf_format);
return 0;
}
// initcall
core_initcall(init_elf_binfmt);
# ELF文件的加载

在函数load_elf_binary()中,完成ELF文件的加载过程。

1)获取 ELF 头进行检查
/* Get the exec-header */
loc->elf_ex = *((struct elfhdr *)bprm->buf); /* First of all, some simple consistency checks */
if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
goto out; if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
goto out;
if (!elf_check_arch(&loc->elf_ex))
goto out;
if (elf_check_fdpic(&loc->elf_ex))
goto out;

这一步骤,首先从 bprm->buf中读取 ELF 头(prepare_binprm(bprm)),并判断文件前SELFMAG个字节是否为ELFMAG

注:include/uapi/linux/elf.h

#define ELFMAG 		"\177ELF"
#define SEELFMAG 4

随后判断其文件类型是否为 “ET_EXEC” 和 “ET_DYN”,即,内核仅允许可执行ELF动态链接ELF的加载。

2)加载程序头表

这一过程是通过load_elf_phdrs()函数完成的。该函数主要作用是,调用kernel_read()读取 ELF 文件的 程序头表:

// in load_elf_binary
elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
if (!elf_phdata)
goto out; // in load_elf_phdrs 保留关键逻辑
static struct elf_phdr *load_elf_phdrs(struct elfhdr *elf_ex,
struct file *elf_file)
{
/* Sanity check the number of program headers... */
if (elf_ex->e_phnum < 1 ||
elf_ex->e_phnum > 65536U / sizeof(struct elf_phdr))
goto out; /* ...and their total size. */
size = sizeof(struct elf_phdr) * elf_ex->e_phnum;
elf_phdata = kmalloc(size, GFP_KERNEL); /* Read in the program headers */
retval = kernel_read(elf_file, elf_phdata, size, &pos); return elf_phdata;
}

在这个函数中,有几个细节值得注意:

  • ELF 文件至少有一个程序段,才能被成功加载;
  • 所有段的大小不超过 65536U,即 64k
  • 最终 ELF 程序头表被保存在elf_phdata中。
3)处理动态链接的 ELF

如果当前加载的 ELF 文件是需要动态链接的,那么,程序最终会交给解释器执行,由解释器填充为链接库预留的程序段后,再真正交由程序执行。

因此,在这一步中,如果对 ELF 中定义的解释器段进行提取和解析,并加载到内存中。

需要动态链接的程序需要经由解释器来执行。例如上述的 a.out文件,其中动态链接了一个名为libb.so的共享库——具体而言,其代码中调用了libb.sohello()函数。

这部分的核心代码逻辑为:

// in load_elf_binary
for (i = 0; i < loc->elf_ex.e_phnum; i++) {
if (elf_ppnt->p_type == PT_INTERP) {
// (A)
elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
pos = elf_ppnt->p_offset;
retval = kernel_read(bprm->file, elf_interpreter,
elf_ppnt->p_filesz, &pos);
// (B)
interpreter = open_exec(elf_interpreter);
// (C)
pos = 0;
retval = kernel_read(interpreter, &loc->interp_elf_ex,
sizeof(loc->interp_elf_ex), &pos);
// ...
}
}

这一步骤,遍历 ELF 文件的所有程序段,寻找PT_INTERP程序段,如果找到了,则主要做三件事:

(A)

elf_interpreter代表 解释器文件名,它是硬编码到 ELF 文件PT_INTERP程序段中的。举例来看:

执行readelf -l a.out查看上述的a.out ELF 可执行文件,结果如下:

可以看到,其中INTERP程序段中,从0x000238开始,大小为0x00001C的内容填充了一段名为/lib64/ld-linux-x86-64.so.2的字符串,代表了 Linux 系统的解释器。

/lib64/ld-linux-x86-64.so.2也是一个.so文件,但它是静态链接的,其本身不依赖任何其他的共享对象也不能使用全局和静态变量。这是合理的,试想,如果解释器都是动态链接的话,那么由谁来完成它的动态链接呢?

这里的解释器在32为系统上的路径名为:/lib/ld-linux.so.2

因此,步骤(A)仅是把PT_INTERP程序段的内容读取出来,存放到elf_interpreter变量中。

(B)找到了elf_interpreter后,尝试打开它。

(C)读取解释器(/lib64/ld-linux-x86-64.so.2)的 ELF 文件头。

4)处理可执行栈

该步骤的逻辑:

// in load_elf_binary
for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
switch (elf_ppnt->p_type) {
case PT_GNU_STACK:
// ...
break;
// ...
}
}

gcc编译选项中,开始/关闭可执行栈的选项是 -z execstack/noexecstack,默认情况下gcc是关闭可执行栈的。在加载 ELF 文件时,会遍历所有的segment,找到PT_GNU_STACK,即栈段,检查flags。

具体可参考:https://mudongliang.github.io/2015/10/23/elf.html

5)解释器的检查工作

这一步骤主要检查刚刚打开的解释器的合法性,主要包括以下几个方面:

  • 是否是一个 ELF 解释器?
  • 架构信息是否合法?
  • 加载解释器程序头表
  • 执行前的最后校验(arch_check_elf(),此函数节点是执行前的最后确认,在此之前,exec系统调用仍然可以发挥一个 error code)
6)重建用户空间映射

这一步骤中,ELF 文件即将蜕变为一个真正的进程,首先为其重建用户空间:

// in load_elf_binary
/* Flush all traces of the currently running executable */
retval = flush_old_exec(bprm);
// ...
setup_new_exec(bprm);
install_exec_creds(bprm);
/* Do this so that we can load the interpreter, if need be. We will
change some of these later */
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack);
current->mm->start_stack = bprm->p;

在这一过程中,首先调用flush_old_exec释放当前进程的所有用户空间页面映射;紧接着,进行必要的setupinstall过程;最后,调用setup_arg_pages(),将前文(copy_strings())为argvenvp分配的页面重新映射回用户空间。

7)载入 LOAD 程序段

此为关键步骤,仍然是遍历所有的程序段,寻找PT_LOAD段,并将其载入到某个地址上(实际上是建立映射关系)。

// in load_elf_binary
for(i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
if (elf_ppnt->p_type != PT_LOAD)
continue;
if (elf_ppnt->p_flags & PF_R) elf_prot |= PROT_READ;
if (elf_ppnt->p_flags & PF_W) elf_prot |= PROT_WRITE;
if (elf_ppnt->p_flags & PF_X) elf_prot |= PROT_EXEC;
// ...
vaddr = elf_ppnt->p_vaddr;
// ...
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, total_size);
}

这一过程中,首先定位PT_LOAD程序段,然后,保存 R/W/X 权限信息,最后进行地址映射。

8)定位程序的入口

进行到此步骤时,当前 ELF 可执行程序 和解释器均已加载完成,并且各类准备工作也已经执行完毕,接下来要做的,就是找到程序的入口。

// in load_elf_binary
if (elf_interpreter) {
unsigned long interp_map_addr = 0;
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias, interp_elf_phdata);
} else {
elf_entry = loc->elf_ex.e_entry;
}

很简单,若当前 ELF 依赖解释器,则入口地址设置为解释器的入口地址;否则设置为 ELF 本身的入口地址。

9)准备执行
  • 进程栈的设置(参数、环境变量...)
  • current->mm 的设置
  • ...
  • 调用start_thread(regs, elf_entry, bprm->p)开始执行
# ELF 文件的执行

load_elf_binary()函数最终调用start_thread(regs, elf_entry, bprm->p)启动执行流程。

对于x86架构而言,start_thread()定义在arch/x86/k ernel/process_64.c文件中:

void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
start_thread_common(regs, new_ip, new_sp,
__USER_CS, __USER_DS, 0);
}
EXPORT_SYMBOL_GPL(start_thread);

其中,new_ip 就是 ELF 文件的入口地址:elf_entry,后续指令将跳转此处开始执行。

【内核】ELF 文件执行流程的更多相关文章

  1. Yii2 源码分析 入口文件执行流程

    Yii2 源码分析  入口文件执行流程 1. 入口文件:web/index.php,第12行.(new yii\web\Application($config)->run()) 入口文件主要做4 ...

  2. 一篇看懂JVM底层详解,利用class反编译文件了解文件执行流程

    JVM之内存结构详解 JVM内存结构 java虚拟机在执行程序的过程中会将内存划分为不同的区域,具体如图1-1所示. 五个区域 JVM分为五个区域:堆.虚拟机栈.本地方法栈.方法区(元空间).程序计数 ...

  3. PHP7内核剖析之执行流程

    以fpm为例: 1.fpm启动时,会先执行 module_startup, 并随着fpm进程常驻 2.当一个请求到达之后,会执行 request_startup, 进行一些请求初始化工作,然后执行代码 ...

  4. 关于ELF文件和BIN文件

    ELF文件执行过程 ELF文件有操作系统的加载器loader执行,比如linux,windows,对于3803处理器是grmon的load命令. 加载器会读取ELF文件program header,比 ...

  5. PHP解释器引擎执行流程 - [ PHP内核学习 ]

    catalogue . SAPI接口 . PHP CLI模式解释执行脚本流程 . PHP Zend Complile/Execute函数接口化(Hook Call架构基础) 1. SAPI接口 PHP ...

  6. debian内核代码执行流程(三)

    接续<debian内核代码执行流程(二)>未完成部分 下面这行输出信息是启动udevd进程产生的输出信息: [ ]: starting version 175是udevd的版本号. 根据& ...

  7. debian内核代码执行流程(一)

    本文根据debian开机信息来查看内核源代码. 系统使用<debian下配置dynamic printk以及重新编译内核>中内核源码来查看执行流程. 使用dmesg命令,得到下面的开机信息 ...

  8. phpcms-v9 前台模板文件中{pc}标签的执行流程

    前台pc标签的使用:{pc:content 参数名="参数值" 参数名="参数值" 参数名="参数值"} 如: {pc:content ac ...

  9. Spring 文件上传MultipartFile 执行流程分析

    在了解Spring 文件上传执行流程之前,我们必须知道两点: 1.Spring 文件上传是基于common-fileUpload 组件的,所以,文件上传必须引入此包 2.Spring 文件上传需要在X ...

  10. debian内核代码执行流程(二)

    继续上一篇文章<debian内核代码执行流程(一)>未完成部分. acpi_bus_init调用acpi_initialize_objects,经过一系列复杂调用后输出下面信息: [ IN ...

随机推荐

  1. PDF 补丁丁 1.0 正式版

    经过了一年多的测试和完善,PDF 补丁丁发布第一个开放源代码的正式版本了. PDF 补丁丁也是国内首先开放源代码.带有修改和阅读PDF的功能的 PDF 处理程序之一. 源代码网址:https://gi ...

  2. 给你的模糊测试开开窍——定向灰盒模糊测试(Directed Greybox Fuzzing)综述

    ​ 本文系原创,转载请说明出处 Please Subscribe Wechat Official Account:信安科研人,获取更多的原创安全资讯 原论文:<The Progress, Cha ...

  3. .NET周刊【9月第1期 2023-09-03】

    国内文章 如何正确实现一个自定义 Exception https://www.cnblogs.com/kklldog/p/how-to-design-exception.html 最近在公司的项目中, ...

  4. KRPano多屏互动原理

    KRPano可以实现多个屏幕之间的同步显示,主要应用到Websocket技术进行通信. 在控制端,我们需要发送当前KRPano场景的实时的视角和场景信息,可以使用如下的代码: embedpano({ ...

  5. Linux挂载新磁盘

    Linux挂载新磁盘 1. 查看磁盘 # df -lh # 查看磁盘占用情况,同时可以查看已挂载的磁盘及其挂载位置 # fdisk -l # 查看所有的磁盘分区 图中 /dev/sdb 下无分区信息, ...

  6. IP协议的发展历程

    1. IP协议 1.1为什么需要IP协议 好像ip地址就像每个人的家门号一样家喻户晓,被大家默认用来作为寻址的门牌号,起初,设计IP地址也是为了寻找某台主机,但是作为世界上家喻户晓的IPv4,大家不应 ...

  7. Chapter 6. Build Script Basics

    Chapter 6. Build Script Basics 6.1. Projects and tasks Everything in Gradle sits on top of two basic ...

  8. 软件开发人员 Kubernetes 入门指南|Part 2

    在第 1 部分中,我们讲解了 Kubernetes 的核心组件,Kubernetes 是一种开源容器编排器,用于在分布式环境中部署和扩展应用程序:我们还讲解了如何在集群中部署一个简单的应用程序,然后更 ...

  9. exgcd|扩展欧几里得算法|扩展欧几里得算法证明|exgcd求逆元 一文说明白

    exgcd 扩展欧几里得算法(Extended Euclidean algorithm, EXGCD),常用于求 \(ax+by=\gcd(a,b)\) 的一组可行解. 部分选自OI Wiki 扩展欧 ...

  10. 各种flex布局,拿来即用用过的都说好

    开发过程中,很多布局,用antd的栅格还是不灵活,flex弹性布局会更好用 Flex 是 Flexible Box 的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性. 注意 ...