深入浅出ELF
Linux加油站 - ELF文件
在Linux下面,二进制的程序也要有严格的格式,这个格式称为ELF(Executeable and Linkable Format,可执行与可链接格式),包含了ELF所需要支持的两个功能——执行和链接。不管是ELF,还是Windows的PE
,抑或是MacOS的Mach-O
,其根本目的都是为了能让处理器正确执行我们所编写的代码。
从大局上看,ELF文件主要分为3个部分:
其中,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格式的差异和用法。
cal.h
定义头文件 cal.h ,定义两个声明。
//cal.h
#include
// 加法声明
int add(int a, int b);
//乘法声明
int mult(int a , int b);
add.c
// add.c
#include "cal.h"
int add(int a , int b){
return a+b;
}
mult.c
//mult.c
#include "cal.h"
int mult(int a , int b){
return a*b;
}
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分为以下三种格式:
可重定位文件:编译后生成 .o 文件 。
可执行文件:编译后且用静态库或者动态库完成链接后的文件,可直接运行的程序。
共享对象文件:单独的一个动态库文件 .so 。
# -c参数是gcc编译器的可选参数,表示只编译源文件但不链接
gcc -c add.c mult.c
为什么叫可重定位文件?可以想象一下,这个编译好的代码和变量,将来加载到内存里面的时候,都是要加载到一定位置的。比如说,调用一个函数,其实就是跳到这个函数所在的代码位置执行;再比如修改一个全局变量,也是要到变量的位置那里去修改。但是现在这个时候还是一个 .o文件,不是一个可以直接运行的程序,这里面只是部分代码片段。
例如这里的 add(),mult() 函数,将来被谁调用,在哪里调用都不清楚,就更别提确定位置了。所以,.o里面的位置是不确定的,但是必须是可重新定位的,因为它将来是要做函数库的,就是一块砖,哪里需要哪里搬,搬到哪里就重新定位这些代码、变量的位置。
要想让add(),mult() 两个函数作为库文件被重用,不能以.o的形式存在,而是要形成库文件,最简单的类型是 静态链接库.a文件(Archives)。
.a
静态链接库文件拿上面的两个编译后的 .o 文件,执行以下代码创建一个我们自己的 cal.a 静态库:
ar -cr libcal.a add.o mult.o
参数解释
ar
,是创建静态库命令;-cr
,是可选参数;libcal.a
,这个.a 文件就是自己创建好的静态库文件 .a(Achive);add.o
mult.o`,将两个文件进行打包,作为静态库文件以便使用。创建好了静态库,就可以与 main
进行链接,main就可以使用静态库里面的函数了,执行以下:
gcc -o mainexe main.c -L . -l cal
参数解释
-o
,gcc编译器可选参数,用于指定编译链接完成后的可执行文件名;-L
,指定静态库的所在存储位置, .表示当前路径;-l
,指定静态库的名称,不需要前面的lib和扩展名.a,他会自动加上,只需要中间的库名字部分。编译生成的二进制文件 mainexe
称为可执行文件。
可执行文件格式和.o文件大致相似,还是分成一个个的section,并且被节头表描述。只不过这些section是多个.o文件合并过的。
这些section被分成了需要加载到内存里面的代码段、数据段和不需要加载到内存里面的部分,将小的section合成了大的段segment(例如代码段就属于一个segment)。
静态链接库一旦链接进去,代码和变量的section都合并了,因而程序运行的时候,就不依赖于这个库是否存在。但是这样有一个缺点,就是相同的代码段,如果被多个程序使用的话,在内存里面就有多份,而且一旦静态链接库更新了,如果二进制执行文件不重新编译,也不随着更新。
动态链接库 .so 文件,不仅是一组对象文件的简单归档,而且是多个对象文件的重新组合,可被多个程序共享。
.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
,生成的动态库文件名字。动态库文件也属于可执行文件,将 main.c
文件与动态库进行链接:
#各参数含义与静态库链接含义一样。
gcc -o mainexe main.c -L. -l cal
动态链接库,就是ELF的共享对象文件(Shared Object)。