深入浅出ELF(Executeable and Linkable Format,可执行与可链接格式)

一、参考资料

深入浅出ELF

Linux加油站 - ELF文件

二、相关介绍

1. ELF简介

在Linux下面,二进制的程序也要有严格的格式,这个格式称为ELF(Executeable and Linkable Format,可执行与可链接格式),包含了ELF所需要支持的两个功能——执行链接。不管是ELF,还是Windows的PE,抑或是MacOS的Mach-O,其根本目的都是为了能让处理器正确执行我们所编写的代码。

2. ELF文件结构

深入浅出ELF(Executeable and Linkable Format,可执行与可链接格式)_第1张图片

从大局上看,ELF文件主要分为3个部分:

  • ELF Header
  • Section Header Table
  • Program Header Table

其中,ELF Header是文件头,包含了固定长度的文件信息;Section Header Table则包含了链接时所需要用到的信息;Program Header Table中包含了运行时加载程序所需要的信息。

ELF文件的主要组成也就是Section Header Table和Program Header Table两部分,整体框架相当简洁。而ELF中体现拓展性的地方则是在Section和Segment的类型上(s_type和p_type),这两个字段的类型都是ElfN_Word,在32位系统下大小为4字节,也就是说最多可以支持高达2^32 - 1种不同的类型

三、代码示例

重要说明:本文通过以下代码示例,介绍不同ELF格式的差异和用法。

1. cal.h

定义头文件 cal.h ,定义两个声明。

//cal.h
#include
 
// 加法声明
int add(int a, int b);
//乘法声明
int mult(int a , int b);

2. add.c

// add.c
#include "cal.h"
 
int add(int a , int b){
    return a+b;
}

3. mult.c

//mult.c
#include "cal.h"
 
int mult(int a , int b){
    return a*b;
}

4. main.c

//main.c
#include
#include "cal.h"
 
int main(){
    
    int a = 20;
    int b = 12;
    printf("a = %d , b = %d\n", a, b);
    printf("a + b = %d\n", add(a, b));
    return 0;
}

四、不同ELF格式的文件

根据编译的结果不同,可以将ELF分为以下三种格式:

  1. 可重定位文件:编译后生成 .o 文件 。

  2. 可执行文件:编译后且用静态库或者动态库完成链接后的文件,可直接运行的程序。

  3. 共享对象文件:单独的一个动态库文件 .so 。

1. 可重定位文件

1.1 生成可重定位文件

# -c参数是gcc编译器的可选参数,表示只编译源文件但不链接
gcc -c add.c mult.c 

深入浅出ELF(Executeable and Linkable Format,可执行与可链接格式)_第2张图片

1.2 可重定位文件描述图

深入浅出ELF(Executeable and Linkable Format,可执行与可链接格式)_第3张图片

  • .text:放编译好的二进制可执行代码
  • .data:已经初始化好的全局变量
  • .rodata:只读数据,例如字符串常量、const的变量
  • .bss:未初始化全局变量,运行时会置0
  • .symtab:符号表,记录的则是函数和变量
  • .strtab:字符串表、字符串常量和变量名

1.3 总结

为什么叫可重定位文件?可以想象一下,这个编译好的代码和变量,将来加载到内存里面的时候,都是要加载到一定位置的。比如说,调用一个函数,其实就是跳到这个函数所在的代码位置执行;再比如修改一个全局变量,也是要到变量的位置那里去修改。但是现在这个时候还是一个 .o文件,不是一个可以直接运行的程序,这里面只是部分代码片段。

例如这里的 add(),mult() 函数,将来被谁调用,在哪里调用都不清楚,就更别提确定位置了。所以,.o里面的位置是不确定的,但是必须是可重新定位的,因为它将来是要做函数库的,就是一块砖,哪里需要哪里搬,搬到哪里就重新定位这些代码、变量的位置

2. 可执行文件

要想让add(),mult() 两个函数作为库文件被重用,不能以.o的形式存在,而是要形成库文件,最简单的类型是 静态链接库.a文件(Archives)。

2.1 生成 .a 静态链接库文件

拿上面的两个编译后的 .o 文件,执行以下代码创建一个我们自己的 cal.a 静态库:

ar -cr libcal.a add.o mult.o

参数解释

  • ar,是创建静态库命令;
  • -cr,是可选参数;
  • libcal.a,这个.a 文件就是自己创建好的静态库文件 .a(Achive);
  • add.o mult.o`,将两个文件进行打包,作为静态库文件以便使用。

深入浅出ELF(Executeable and Linkable Format,可执行与可链接格式)_第4张图片

2.2 生成可执行文件

创建好了静态库,就可以与 main 进行链接,main就可以使用静态库里面的函数了,执行以下:

gcc -o mainexe main.c -L . -l cal

参数解释

  • -o,gcc编译器可选参数,用于指定编译链接完成后的可执行文件名;
  • -L,指定静态库的所在存储位置, .表示当前路径;
  • -l,指定静态库的名称,不需要前面的lib和扩展名.a,他会自动加上,只需要中间的库名字部分。

深入浅出ELF(Executeable and Linkable Format,可执行与可链接格式)_第5张图片

编译生成的二进制文件 mainexe 称为可执行文件

2.3 可执行文件描述图

深入浅出ELF(Executeable and Linkable Format,可执行与可链接格式)_第6张图片

可执行文件格式和.o文件大致相似,还是分成一个个的section,并且被节头表描述。只不过这些section是多个.o文件合并过的。

这些section被分成了需要加载到内存里面的代码段、数据段和不需要加载到内存里面的部分,将小的section合成了大的段segment(例如代码段就属于一个segment)。

3. 共享对象文件

3.1 问题引入

静态链接库一旦链接进去,代码和变量的section都合并了,因而程序运行的时候,就不依赖于这个库是否存在。但是这样有一个缺点,就是相同的代码段,如果被多个程序使用的话,在内存里面就有多份,而且一旦静态链接库更新了,如果二进制执行文件不重新编译,也不随着更新。

动态链接库 .so 文件,不仅是一组对象文件的简单归档,而且是多个对象文件的重新组合,可被多个程序共享。

3.2 生成 .so 动态链接库文件

执行以下代码,先编译两个库文件,然后创建一个动态链接库 cal.so文件:

# 编译源文件生成 .o目标文件
gcc -c -fPIC add.c mult.c

gcc -shared add.o mult.o -o libcal.so

参数解释

  • -fPIC ,gcc编译器可选参数作用于编译阶段,告诉编译器产生与位置无关代码,产生的代码中没有绝对地址,全部使用相对地址,故代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是动态库所要求的,动态库被加载时,在内存的位置不是固定的;
  • -shared,gcc编译器可选参数,作用是告诉编译器需要链接的文件。
  • -o,生成的动态库文件名字。

深入浅出ELF(Executeable and Linkable Format,可执行与可链接格式)_第7张图片

3.3 生成可执行文件

动态库文件也属于可执行文件,将 main.c 文件与动态库进行链接:

 #各参数含义与静态库链接含义一样。
gcc -o mainexe main.c -L. -l cal

3.4 总结

动态链接库,就是ELF的共享对象文件(Shared Object)。

你可能感兴趣的:(Linux,linux,ELF)