2

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

关于【linuxelf文件加载过程】,今天小编给您分享一下,如果对您有所帮助别忘了关注本站哦。

  • 内容导航:
  • 1、linuxelf文件加载过程:含大量图文解析及例程 | Linux下的ELF文件、链接、加载与库(上)
  • 2、linuxelf文件加载过程,链接的基石-ELF文件

1、linuxelf文件加载过程:含大量图文解析及例程 | Linux下的ELF文件、链接、加载与库(上)

常用工具

我们首先列出一些在接下来的介绍过程中会频繁使用的分析工具,如果从事操作系统相关的较底层的工作,那这些工具应该再熟悉不过了。不熟悉的读者可以先看一下这里的简单的功能介绍,我们会在后文中介绍一些详细的参数选项和使用场景。

另外,建议大家在遇到自己不熟悉的命令时,通过 man 命令来查看手册,这是最权威的、第一手的资料。

ELF文件详解

ELF文件的三种形式

在Linux下,可执行文件/动态库文件/目标文件(可重定向文件)都是同一种文件格式,我们把它称之为ELF文件格式。虽然它们三个都是ELF文件格式但都各有不同。以下文件的格式信息可以通过 file 命令来查看。

  1. 可重定位(relocatable)目标文件:通常是.o文件。包含二进制代码和数据,其形式可以再编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
  2. 可执行(executable)目标文件:是完全链接的可执行文件,即静态链接的可执行文件。包含二进制代码和数据,其形式可以被直接复制到内存并执行。
  3. 共享(shared)目标文件:通常是.so动态链接库文件或者动态链接生成的可执行文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。注意动态库文件和动态链接生成的可执行文件都属于这一类。会在最后一节辨析时详细区分。

因为我们知道ELF的全称:Executable and Linkable Format,即 ”可执行、可链接格式“,很显然这里的三个ELF文件形式要么是可执行的、要么是可链接的。

其实还有一种core文件,也属于ELF文件,在core dumped时可以得到。我们这里暂且不提。

注意:在Linux中并不以后缀名作为区分文件格式的绝对标准。

节头部表和程序头表和ELF头

在我们的ELF文件中,有两张重要的表:节头部表(Section Tables)和程序头表(Program Headers)。可以通过readelf -l [fileName]和readelf -S [fileName]来查看。

但并不是所有以上三种ELF的形式都有这两张表,

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

我们在后面的还会详细介绍这两张表。

此外,整个ELF文件的前64个字节,成为ELF头,可以通过readelf -h [fileName]来查看。我们也会在后面详细介绍。

可重定位ELF文件的内容分析

#include <elf.h>,该头文件通常在/usr/include/elf.h,可以自己vim查看。

首先有一个64字节的ELF头Elf64_Ehdr,其中包含了很多重要的信息(可通过readelf -h [fileName]来查看),这些信息中有一个很关键的信息叫做Start of p headers,它指明了节头部表,Section Headers Elf64_Shdr的位置。段表中储存了ELF文件中各个的偏移量以记录其位置。ELF中的各个段可以通过readelf -S [fileName]来查看。

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

其中各个节的含义如下:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

这样我们就把一个可重定位的ELF文件中的每一个字节都搞清楚了。

静态链接

编译、链接的需求

嵌入式物联网需要学的东西真的非常多,千万不要学错了路线和内容,导致工资要不上去!

无偿分享大家一个资料包,差不多150多G。里面学习内容、面经、项目都比较新也比较全!某鱼上买估计至少要好几十。

点击这里找小助理0元领取:https://s.pdb2.com/l/cnklSITCGo24eIn

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

为了节省空间和时间,不将所有的代码都写在同一个文件中是一个很基本的需求。

为此,我们的C语言需要实现这样的需求:允许引用其他文件(C标准成为编译单元,Compilation Unit)里定义的符号。C语言中不禁止你随便声明符号的类型,但是类型不匹配是Undefined Behavior。

假如我们有三个c文件,分别是a.c,b.c,main.c:

// a.cint foo(int a, int b){ return a + b;}

// b.cint x = 100, y = 200;

// main.cextern int x, y;int foo(int a, int b);int main(){ printf("%d + %d = %d", x, y, foo(x, y));}

我们在main.c中声明了外部变量x,y和函数foo,C语言并不禁止我们这么做,并且在声明时,C也不会做什么类型检查。当然,在编译main.c的时候,我们看不到这些外部变量和函数的定义,也不知道它们在哪里。

我们编译链接这些代码,Makfile如下:

CFLAGS := -Osa.out: a.o b.o main.o gcc -static -Wl,--verbose a.o b.o main.oa.o: a.c gcc $(CFLAGS) -c a.cb.o: b.c gcc $(CFLAGS) -c b.cmain.o: main.c gcc $(CFLAGS) -c main.cclean: rm -f *.o a.out

结果生成的可执行文件可以正常地输出我们想要的内容。

make./a.out# 输出:# 100 + 200 = 300

我们知道foo这个符号是一个函数名,在代码区。但这时,如果我们将main.c中的foo声明为一个整型,并且直接打印出这个整型,然后尝试对其加一。即我们将main.c改写为下面这样,会发生什么事呢?

// main.c (changed)#include <stdio.h>extern int x, y;// int foo(int a, int b);extern int foo;int main(){ printf("%x", foo); foo += 1; // printf("%d + %d = %d", x, y, foo(x, y));}

输出:

c337048d

Segmentation fault (core dumped)

我们发现,其实是能够打印出四个字节(整型为4个字节),但这四个字节是什么东西呢?

C语言中的类型:C语言中的其实是可以理解为没有类型的,在C语言的眼中只有内存和指针,也就是内存地址,而所谓的C语言中的类型,其实就是对这个地址的一个解读。比如有符号整型,就按照补码解读接下来的4个字节地址;又比如浮点型,就是按照IEEE754的浮点数规定来解读接下来的4字节地址。

那我们这里将符号foo定义为了整型,那编译器也会按照整型4个自己来解读它,而这个地址指针指向的其实还是函数foo的地址。那这四个字节应该就是函数foo在代码段的前四个字节。我们不妨用objdump反汇编来验证我们的想法:

objdump -d a.out

输出(节选):

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

我们看到,foo函数在代码段的前四个字节的地址确是就是我们上面打印输出的c3 37 04 8d(注意字节序为小端法)。

那我们接下来试图对foo进行加一操作相当于是对代码段的写操作,而我们知道内存中的代码段是 可读可执行不可写 的,这就对应了上面输出的Segmentation fault (core dumped)。

总结一下,通过这个例子,我们应当理解:

  1. 编译链接的需求:允许引用其他文件(C标准成为编译单元,Compilation Unit)里定义的符号。C语言中不禁止你随便声明符号的类型,但是类型不匹配是Undefined Behavior。
  2. C语言中类型的概念:C语言中的其实是可以理解为没有类型的,在C语言的眼中只有内存和指针,也就是内存地址,而所谓的C语言中的类型,其实就是对这个地址的一个解读。

程序的编译 - 可重定向文件

我们先用file命令来查看main.c编译生成的main.o文件的属性:

file main.o

输出:

main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

我们看到这里的main.o文件是可重定向( relocatable) 的ELF文件,这里的重定向指的就是我们链接过程中对外部符号的引用。也就是说,编译过的main.o文件对于其中声明的外部符号如foo,x,y,是不知道的。

既然外部的符号是在链接时才会被main程序知道,那在编译main程序,生成可重定向文件时这些外部的符号是怎么处理的呢?我们同样通过objdump工具来查看编译出的main.o文件(未修改的原版本):

objdump -d main.o

输出:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

main在编译的时候,引用的外部符号就只能 ”留空(0)“ 了。

我们看到,在编译但还未链接的main.o文件中,对于引用的外界符号的部分是用留空的方式用0暂时填充的。即上图中红框框出来的位置。注意图中的最后一列是笔者添加的注释,指明了本行中留空的地方对应那个外部符号。

另外注意这里的%rip相对寻址的偏移量都是0,一会儿我们会讲到,在静态链接完成之后,它们的偏移量会被填上正确的数值。

我们已经知道在编译时生成的文件中外部符号的部分使用0暂时留空的,这些外部符号是待链接时再填充的。那么,我们在链接时究竟需要填充哪些位置呢?我们可以使用readelf工具来查看ELF文件的重定位信息:

readelf -r main.o

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

这个图中上方是readelf的结果,下面是objdump的结果,笔者在这里已经将前两个外部符号的偏移量的对应关系用红色箭头指了出来,其他的以此类推。这种对应也可以证明我们上面的分析是正确的的。

应当讲,可重定向ELF文件(如main.o)已经告诉了我们足够多的信息,指示我们应该将相应的外部符号填充到哪个位置。

另外,注意%rip寄存器指向了当前指令的末尾,也就是下一条指令的开头,所以上图中最后的偏移量要减4(如 y - 4)。

程序的静态链接

简单讲,程序的静态链接是会把所需要的文件链接起来生成可执行的二进制文件,将相应的外部符号,填入正确的位置(就像我们上面查看的那样)。

  1. 段的合并

首先会做一个段的合并。即把相同的段(比如代码段 .text)识别出来并放在一起。

  1. 重定位

重定位表,可用objdump -r [fileName] 查看。

简单讲,就是当某个文件中引用了外部符号,在编译时编译器是不会阻止你这样做的,因为它相信你会在链接时告诉它这些外部符号是什么东西。但在编译时,它也不知到这些符号具体在什么地址,因此这些符号的地址会在编译时被留空为0。此时的重定位,就是链接器将这些留空为0的外部符号填上正确的地址。

具体的链接过程,可以通过ld --verbose来查看默认的链接脚本,并在需要的时候修改链接脚本。

我们可以通过使用gcc的 -Wl,--verbose将--verbose传递给链接器ld,从而直接观察到整个静态链接的过程,包括:

  • ldscript里面各个p是按照何种顺序 “粘贴”
  • ctors / dtors (constructors / destructores) 的实现,( 我们用过__attribute__((contructor)) )
  • 只读数据和读写数据之间的padding,. = DATA_SEGMENT_ALIGN …

我们可以通过objdump来查看静态链接完成以后生成的可执行文件a.out的内容:

objdump -d a.out

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

注意,这个a.out的objdump结果图要与我们之前看到的main.o的objdump输出对比着来看。

我们可以看到,之前填0留空的地方都被填充上了正确的数值,%rip相对寻址的偏移量以被填上了正确的数值,而且objdump也能够正确地解析出我们的外部符号名(最后一列)的框。

静态链接库的构建与使用

假如我们要制作一个关于向量的静态链接库libvector.a,它包含两个源代码addvec.c和multvec.c如下:

// addvec.cint addcnt = 0;void addvec(int *x, int *y, int*z, int n){ int i; addcnt++; for (i=0; i<n; i++) z[i] = x[i] + y[i];}

// multvec.vint multcnt = 0;void multvec(int *x, int *y, int*z, int n){ int i; multcnt++; for (i=0; i<n; i++) z[i] = x[i] * y[i];}

我们只需要这样来进行编译:

gcc -c addvec.c multvec.c

ar rcs libvector.a addvec.o multvec.o

假如我们有个程序main.c要调用这个静态库libvector.a:

// main.c#include <stdio.h>#include "vector.h"int x[2] = {1, 2};int y[2] = {3, 4};int z[2];int main(){ addvec(x, y, z, 2); printf("z = [%d %d]", z[0], z[1]); return 0;}

// vector.hvoid addvec(int*, int*, int*, int);void multvec(int*, int*, int*, int);

只需要在这样编译链接即可:

gcc -c main.c

gcc -static main.o ./libvector.a

静态链接过程图示

我们以使用刚才构建的静态库libvector.a的程序为例,画出静态链接的过程。

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

原文作者:人人极客

2、linuxelf文件加载过程,链接的基石-ELF文件

初次见面

大家好,我是 ELF 文件,大名叫 Executable and Linkable Format。

经常在 Linux 系统中开发的小伙伴们,对于我肯定是再熟悉不过了,特别是那些需要了解编译、链接的家伙们,估计已经把我研究的透透的。

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

为了结识更多的小伙伴,今天呢,就是我的开放日,我会像洋葱一样,一层一层地拨开我的心,让更多的小伙伴来了解我,欢迎大家前来围观。

以前啊,我看到有些小伙伴在研究我的时候,看一下头部的汇总信息,然后再瞅几眼 Section 的布局,就当做熟悉我了。

从科学的态度上来说,这是远远不够的,未达究竟。

当你面对编译、链接的详细过程时,还是会一脸懵逼。

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

今天,我会从字节码的颗粒度,毫无保留、开诚布公、知无不言、言无不尽、赤胆忠心、一片丹心、鞠躬尽瘁、死而后已的把自己剖析一遍,让各位看官大开眼界、大饱眼福。

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

您了解这些知识之后呢,在今后继续学习编译、链接的底层过程,以及一个可执行程序在从硬盘加载到内存、一直到 main 函数的执行,心中就会非常的敞亮。

也就是说,掌握了 ELF 文件的结构和内容,是理解编译、链接和程序执行的基础。

你们不是有一句俗话嘛:磨刀不误砍柴工!

好了,下面我们就开始吧!

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

文件很单纯,复杂的是人

作为一种文件,那么肯定就需要遵守一定的格式,我也不例外。

从宏观上看,可以把我拆卸成四个部分:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

图中的这几个概念,如果不明白的话也没关系,下面我会逐个说明的。

在 Linux 系统中,一个 ELF 文件主要用来表示 3 种类型的文件:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

既然可以用来表示 3 种类型的文件,那么在文件中,肯定有一个地方用来区分这 3 种情况。

也许你已经猜到了,在我的头部内容中,就存在一个字段,用来表示:当前这个 ELF 文件,它到底是一个可执行文件?是一个目标文件?还是一个共享库文件?

另外,既然我可以用来表示 3 种类型的文件,那么就肯定是在 3 种不同的场合下被使用,或者说被不同的家伙来操作我:

  1. 可执行文件:被操作系统中的加载器从硬盘上读取,载入到内存中去执行;

  2. 目标文件:被链接器读取,用来产生一个可执行文件或者共享库文件;

  3. 共享库文件:在动态链接的时候,由 ld-linux.so 来读取;

就拿链接器和加载器来说吧,这两个家伙的性格是不一样的,它们看我的眼光也是不一样的。

链接器在看我的时候,它的眼睛里只有 3 部分内容:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

也就是说,链接器只关心 ELF header, Sections 以及 Section header table 这 3 部分内容。

加载器在看我的时候,它的眼睛里是另外的 3 部分内容:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

加载器只关心 ELF header, Program header table 和 Segment 这 3 部分内容。

对了,从加载器的角度看,对于中间部分的Sections, 它改了个名字,叫做Segments(段)。换汤不换药,本质上都是一样一样的。

可以理解为:一个 Segment 可能包含一个或者多个 Sections,就像下面这样:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

这就好比超市里的货架上摆放的商品:有矿泉水、可乐、啤酒,巧克力,牛肉干,薯片。

从理货员的角度看:它们属于 6 种不同的商品;但是从超市经理的角度看,它们只属于 2 类商品:饮料和零食。

怎么样?现在对我已经有一个总体的印象了吧?

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

其实只要掌握到2点内容就可以了:

  1. 一个 ELF 文件一共由 4 个部分组成;

  2. 链接器和加载器,它们在使用我的时候,只会使用它们感兴趣的部分;

还有一点差点忘记给你提个醒了:在Linux系统中,会有不同的数据结构来描述上面所说的每部分内容。

我知道有些小伙伴比较性急,我先把这几个结构体告诉你。

初次见面,先认识一下即可,千万不要深究哦。

描述 ELF header 的结构体:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

描述 Program header table 的结构体:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

描述 Section header table 的结构体:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

ELF header(ELF 头)

头部内容,就相当于是一个总管,它决定了这个完整的 ELF 文件内部的所有信息,比如:

  1. 这是一个 ELF 文件;

  2. 一些基本信息:版本,文件类型,机器类型;

  3. Program header table(程序头表)的开始地址,在整个文件的什么地方;

  4. Section header table(节头表)的开始地址,在整个文件的什么地方;

你是不是有点纳闷,好像没有说 Sections(从链接器角度看) 或者 Segments(从加载器角度看) 在 ELF 文件的什么地方。

为了方便描述,我就把SectionsSegments全部统一称为 Sections 啦!

其实是这样的,在一个 ELF 文件中,存在很多个 Sections,这些 Sections 的具体信息,是在 Program header table或者Section head table中进行描述的。

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

就拿Section head table来举例吧:

假如一个 ELF 文件中一共存在 4个 Section:.text、.rodata、.data、.bss,那么在Section head table中,将会有4个 Entry(条目)来分别描述这 4 个 Section 的具体信息(严格来说,不止 4 个 Entry,因为还存在一些其他辅助的 Sections),就像下面这样:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

在开头我就说了,我要用字节码的粒度,扒开来给你看!

为了不耍流氓,我还是用一个具体的代码示例来描述,只有这样,你才能看到实实在在的字节码。

程序的功能比较简单:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

// mymath.cint my_add(int a, int b){return a b;}

// main.c#include <stdio.h>extern int my_add(int a, int b);int main{int i = 1;int j = 2;int k = my_add(i, j);printf("k = %d ", k);}

从刚才的描述中可以知道:动态库文件libmymath.so, 目标文件main.o和 可执行文件main,它们都是 ELF 文件,只不过属于不同的类型。

这里就以可执行文件 main 来拆解它!

我们首先用指令readelf -h main来看一下 main 文件中,ELF header的信息。

readelf 这个工具,可是一个好东西啊!一定要好好的利用它。

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

这张图中显示的信息,就是ELF header中描述的所有内容了。这个内容与结构体Elf32_Ehdr中的成员变量是一一对应的!

有没有发现图中第 15 行显示的内容:Size of this header: 52 (bytes)

也就是说:ELF header部分的内容,一共是52个字节。那么我就把开头的这52个字节码给你看一下。

这回,我用od -Ax -t x1 -N 52 main这个指令来读取 main 中的字节码,简单解释一下其中的几个选项:

-Ax: 显示地址的时候,用十六进制来表示。如果使用 -Ad,意思就是用十进制来显示地址;

-t -x1: 显示字节码内容的时候,使用十六进制(x),每次显示一个字节(1);

-N 52:只需要读取 52 个字节;

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

52个字节的内容,你可以对照上面的结构体中每个字段来解释了。

首先看一下前 16 个字节。

在结构体中的第一个成员是unsigned char e_ident[EI_NIDENT];EI_NIDENT的长度是16,代表了EL header 中的开始16个字节,具体含义如下:

0 - 15 个字节

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

怎样样?我以这样的方式彻底暴露自己,向你表白,足以表现出我的诚心了吧?!

如果被感动了,别忘记在文章的最底部,点击一下在看和收藏,也非常感谢您转发给身边的小伙伴。赠人玫瑰,手留余香!

为了权威性,我把官方文档对于这部分的解释也贴给大家看一下:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

关于大端、小端格式,这个main文件中显示的是1,代表小端格式。啥意思呢,看下面这张图就明白了:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

那么再来看一下大端格式:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

好了,下面我们继续把剩下的36个字节(52 - 16 = 32),也以这样的字节码含义画出来:

16 - 31 个字节:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

32 - 47 个字节:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

48 - 51 个字节:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

具体的内容就不用再解释了,一切都在感情深、一口闷,话不多说,都在酒里~~ 哦不对,重点都在图里!

字符串表表项 Entry

在一个ELF文件中,存在很多字符串,例如:变量名、Section名称、链接器加入的符号等等,这些字符串的长度都是不固定的,因此用一个固定的结构来表示这些字符串,肯定是不现实的。

于是,聪明的人类就想到:把这些字符串集中起来,统一放在一起,作为一个独立的Section来进行管理。

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

在文件中的其他地方呢,如果想表示一个字符串,就在这个地方写一个数字索引:表示这个字符串位于字符串统一存储地方的某个偏移位置,经过这样的按图索骥,就可以找到这个具体的字符串了。

比如说啊,下面这个空间中存储了所有的字符串:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

在程序的其他地方,如果想引用字符串 “hello,world!”,那么就只需要在那个地方标明数字 13就可以了,表示:这个字符串从偏移 13 个字节处开始。

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

那么现在,咱们再回到这个main文件中的字符串表,

ELF header的最后 2 个字节是0x1C 0x00,它对应结构体中的成员e_shstrndx,意思是这个 ELF 文件中,字符串表是一个普通的 Section,在这个 Section 中,存储了ELF文件中使用到的所有的字符串。

既然是一个Section,那么在Section header table中,就一定有一个表项 Entry 来描述它,那么是哪一个表项呢?

这就是0x1C 0x00这个表项,也就是第28个表项。

这里,我们还可以用指令readelf -S main来看一下这个ELF文件中所有的Section信息:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

其中的第28个 Section,描述的正是字符串表 Section:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

可以看出来:这个SectionELF文件中的偏移地址是0x0016ed,长度是0x00010a个字节。

下面,我们从ELF header的二进制数据中,来推断这信息。

读取字符串表 Section 的内容

那我就来演示一下:如何通过ELF header中提供的信息,把字符串表这个Section给找出来,然后把它的字节码打印出来给各位看官瞧瞧。

要想打印字符串表Section的内容,就必须知道这个SectionELF文件中的偏移地址。

要想知道偏移地址,只能从Section head table中第28个表项描述信息中获取。

要想知道第28个表项的地址,就必须知道Section head tableELF文件中的开始地址,以及每一个表项的大小。

正好最后这2个需求信息,在ELF header中都告诉我们了,因此我们倒着推算,就一定能成功。

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

ELF header中的第3235字节内容是:F8 17 00 00(注意这里的字节序,低位在前),表示的就是Section head table在 ELF 文件中的开始地址(e_shoff)。

0x000017F8 = 6136,也就是说Section head table的开始地址位于ELF文件的第6136个字节处。

知道了开始地址,再来算一下第28个表项 Entry 的地址。

ELF header中的第46、47字节内容是:28 00,表示每个表项的长度是0x0028 = 40个字节。

注意这里的计算都是从0开始的,因此第28个表项的开始地址就是:6136 28 * 40 = 7256,也就是说用来描述字符串表这个Section的表项,位于ELF文件的7256字节的位置。

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

既然知道了这个表项 Entry 的地址,那么就扒开来看一下其中的二进制内容:

执行指令:od -Ad -t x1 -j 7256 -N 40 main

其中的-j 7256选项,表示跳过前面的7256个字节,也就是我们从main这个ELF文件的7256字节处开始读取,一共读40个字节。

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

40个字节的内容,就对应了Elf32_Shdr结构体中的每个成员变量:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

这里主要关注一下上图中标注出来的4个字段:

sh_name: 暂时不告诉你,马上就解释到了;

sh_type:表示这个 Section 的类型,3 表示这是一个 string table;

sh_offset: 表示这个 Section,在 ELF 文件中的偏移量。0x000016ed = 5869,意思是字符串表这个 Section 的内容,从 ELF 文件的 5869 个字节处开始;

sh_size:表示这个 Section 的长度。0x0000010a = 266 个字节,意思是字符串表这个 Section 的内容,一共有 266 个字节。

还记得刚才我们使用readelf工具,读取到字符串表Section在 ELF 文件中的偏移地址是0x0016ed,长度是0x00010a个字节吗?

与我们这里的推断是完全一致的!

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

既然知道了字符串表这个SectionELF文件中的偏移量以及长度,那么就可以把它的字节码内容读取出来。

执行指令:od -Ad -t c -j 5869 -N 266 main,所有这些参数应该不用再解释了吧?!

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

看一看,瞧一瞧,是不是这个Section中存储的全部是字符串?

刚才没有解释sh_name这个字段,它表示字符串表这个Section本身的名字,既然是名字,那一定是个字符串。

但是这个字符串不是直接存储在这里的,而是存储了一个索引,索引值是0x00000011,也就是十进制数值17

现在我们来数一下字符串表Section内容中,第17个字节开始的地方,存储的是什么?

不要偷懒,数一下,是不是看到了:“.shstrtab” 这个字符串(是字符串的分隔符)?!

好了,如果看到这里,你全部都能看懂,那么关于字符串表这部分的内容,说明你已经完全理解了,给你一百个赞!!!

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

读取代码段的内容

从下面的这张图(指令:readelf -S main):

可以看到代码段是位于第14个表项中,加载(虚拟)地址是0x08048470,它位于ELF文件中的偏移量是0x000470,长度是0x0001b2个字节。

那我们就来试着读一下其中的内容。

首先计算这个表项Entry的地址:6136 14 * 40 = 6696

然后读取这个表项Entry,读取指令是od -Ad -t x1 -j 6696 -N 40 main:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

同样的,我们也只关心下面这5个字段内容:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

sh_name: 这回应该清楚了,表示代码段的名称在字符串表 Section 中的偏移位置。0x9B = 155 字节,也就是在字符串表 Section 的第 155 字节处,存储的就是代码段的名字。回过头去找一下,看一下是不是字符串 “.text”;

sh_type:表示这个 Section 的类型,1(SHT_PROGBITS) 表示这是代码;

sh_addr:表示这个 Section 加载的虚拟地址是 0x08048470,这个值与 ELF header 中的 e_entry 字段的值是相同的;

sh_offset: 表示这个 Section,在 ELF 文件中的偏移量。0x00000470 = 1136,意思是这个 Section 的内容,从 ELF 文件的 1136 个字节处开始;

sh_size:表示这个 Section 的长度。0x000001b2 = 434 个字节,意思是代码段一共有 434 个字节。

以上这些分析结构,与指令readelf -S main读取出来的完全一样!

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

PS: 在查看字符串表 Section中的字符串时,不要告诉我,你真的是从0开始数到155啊!可以计算一下:字符串表的开始地址是5869(十进制),加上155,结果就是6024,所以从6024开始的地方,就是代码段的名称,也就是 “.text”。

知道了以上这些信息,我们就可以读取代码段的字节码了.使用指令:od -Ad -t x1 -j 1136 -N 434 main即可。

内容全部是黑乎乎的的字节码,我就不贴出来了。

Program header

文章的开头,我就介绍了:我是一个通用的文件结构,链接器和加载器在看待我的时候,眼光是不同的。

为了对Program header有更感性的认识,我还是先用readelf这个工具来从总体上看一下main文件中的所有段信息。

执行指令:readelf -l main,得到下面这张图:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

显示的信息已经很明白了:

  1. 这是一个可执行程序;

  2. 入口地址是 0x8048470;

  3. 一共有 9 个 Program header,是从 ELF 文件的 52 个偏移地址开始的;

布局如下图所示:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

开头我还告诉过你:SectionSegment本质上是一样的,可以理解为:一个 Secgment 由一个或多个 Sections 组成。

从上图中可以看到,第2program header这个段,由那么多的Section组成,这下更明白一些了吧?!

从图中还可以看到,一共有2LOAD类型的段:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

我们来读取第一个 LOAD 类型的段,当然还是扒开其中的二进制字节码。

第一步的工作是,计算这个段表项的地址信息。

ELF header中得知如下信息:

  1. 字段e_phoff:Program header table 位于 ELF 文件偏移 52 个字节的地方。

  2. 字段e_phentsize: 每一个表项的长度是 32 个字节;

  3. 字段e_phnum: 一共有 9 个表项 Entry;

通过计算,得到可读、可执行的LOAD段,位于偏移量116字节处。

执行读取指令:od -Ad -t x1 -j 116 -N 32 main

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

按照上面的惯例,我还是把其中几个需要关注的字段,与数据结构中的成员变量进行关联一下:

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

p_type: 段的类型,1: 表示这个段需要加载到内存中;

p_offset: 段在 ELF 文件中的偏移地址,这里值为 0,表示这个段从 ELF 文件的头部开始;

p_vaddr:段加载到内存中的虚拟地址 0x08048000;

p_paddr:段加载的物理地址,与虚拟地址相同;

p_filesz: 这个段在 ELF 文件中,占据的字节数,0x0744 = 1860 个字节;

p_memsz:这个段加载到内存中,需要占据的字节数,0x0744= 1860 个字节。注意:有些段是不需要加载到内存中的;

经过上述分析,我们就知道:从ELF文件的第1到 第1860个字节,都是属于这个LOAD段的内容。

在被执行时,这个段需要被加载到内存中虚拟地址为0x08048000这个地方,从这里开始,又是一个全新的故事了。

再回顾一下

到这里,我已经像洋葱一样,把自己的层层外衣都扒开,让你看到最细的颗粒度了,这下子,您是否对我有足够的了解了呢?

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库

其实只要抓住下面2个重点即可:

  1. ELF header 描述了文件的总体信息,以及两个 table 的相关信息(偏移地址,表项个数,表项长度);

  2. 每一个 table 中,包括很多个表项 Entry,每一个表项都描述了一个 Section/Segment 的具体信息。

链接器和加载器也都是按照这样的原理来解析 ELF 文件的,明白了这些道理,后面在学习具体的链接、加载过程时,就不会迷路啦!

------ End ------

本文关键词:linux内存加载elf并执行,linux文件系统,linux file-nr,linux运行elf文件的命令,linux filezilla使用教程。这就是关于《linuxelf文件加载过程,Linux下的ELF文件、链接、加载与库》的所有内容,希望对您能有所帮助!

本文来自网络,不代表本站立场。转载请注明出处: https://tj.jiuquan.cc/a-2233780/
1
上一篇 利害攸关的意思,从俄乌冲突理解“性命攸关”
下一篇 沙虫冻笋制作方法,散文:闽南土笋冻

为您推荐

联系我们

联系我们

在线咨询: QQ交谈

邮箱: alzn66@foxmail.com

关注微信

微信扫一扫关注我们

返回顶部