含大量图文解析及例程

您所在的位置:网站首页 elf怎么说读 含大量图文解析及例程

含大量图文解析及例程

2023-12-31 16:25| 来源: 网络整理| 查看: 265

常用工具

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

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

ELF文件详解ELF文件的三种形式

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

可重定位(relocatable)目标文件:通常是.o文件。包含二进制代码和数据,其形式可以再编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。可执行(executable)目标文件:是完全链接的可执行文件,即静态链接的可执行文件。包含二进制代码和数据,其形式可以被直接复制到内存并执行。共享(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文件看作是程序头表描述的段的集合,一个段可能包含多个节,节头部表可选。如果是共享目标文件,则两者都含有。因为链接器在链接的时候需要节头部表来查看目标文件各个 section 的信息然后对各个目标文件进行链接;而加载器在加载可执行程序的时候需要程序头表 ,它需要根据这个表把相应的段加载到进程自己的的虚拟内存(虚拟地址空间)中。

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

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

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

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

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

其中各个节的含义如下:

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

静态链接编译、链接的需求

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

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

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

// a.c int foo(int a, int b){  return a + b; }// b.c int x = 100, y = 200;// main.c extern int x, y; int foo(int a, int b); int main(){  printf("%d + %d = %d\n", x, y, foo(x, y)); }

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

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

CFLAGS := -Os a.out: a.o b.o main.o  gcc -static -Wl,--verbose a.o b.o main.o a.o: a.c  gcc $(CFLAGS) -c a.c b.o: b.c  gcc $(CFLAGS) -c b.c main.o: main.c  gcc $(CFLAGS) -c main.c clean:  rm -f *.o a.out

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

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

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

// main.c (changed) #include  extern int x, y; // int foo(int a, int b); extern int foo; int main(){         printf("%x\n", foo);         foo += 1;         // printf("%d + %d = %d\n", 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

输出(节选):

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

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

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

编译链接的需求:允许引用其他文件(C标准成为编译单元,Compilation Unit)里定义的符号。C语言中不禁止你随便声明符号的类型,但是类型不匹配是Undefined Behavior。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

输出:

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

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

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

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

readelf -r main.o

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

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

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

程序的静态链接

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

段的合并

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

重定位

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

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

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

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

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

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

objdump -d a.out

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

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

静态链接库的构建与使用

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

// addvec.c int addcnt = 0; void addvec(int *x, int *y, int*z, int n){  int i;  addcnt++;  for (i=0; i


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3