ELF可执行与可链接格式(Executable and Linkable Format)

一、定义

ELF(Executable and Linkable Format,可执行与可链接格式) 是一种用于静态存储在磁盘上的二进制文件列如可执行文件、目标代码、共享库和核心转储(core dump  程序崩溃时生成的调试信息文件)的标准文件格式。它被广泛应用于类 Unix 系统(如 Linux、FreeBSD)、嵌入式系统以及部分其他操作系统中,是程序编译、链接和运行过程中的关键格式。

二、结构

  • ELF 头部(ELF Header):位于文件开头,包含文件的基本信息,如 ELF 格式版本、目标架构(32 位 / 64 位)、文件类型(可执行文件 / 共享库等)、入口点地址(程序开始执行的位置)、各个段(Section)和节区(Segment)的偏移量等,是系统识别和解析 ELF 文件的入口。

  • 节区表(Section Header Table):描述文件中的各个 “节区”(Section),根据不同的文件内容分区,如代码段(.text,存放机器指令)、数据段(.data,存放已初始化的全局变量)、未初始化数据段(.bss)、符号表(.symtab)、字符串表(.strtab)等。

  • 程序头表(Program Header Table):描述文件中的各个 “段”(Segment)根据不同的文件属性分区,如代码段(包含.text等可执行节区)、数据段(包含.data等可读写数据节区)、动态链接信息段等。

数据区:实际存放代码、数据、符号等内容的区域,由节区表和程序头表指向。

                       

三、编译,链接,运行

简单的代码初略的观察在编译链接运行下elf格式的文件是如何变化的

代码如下

$ cat code1.h
#pragma once

void TestCode1();

$ cat code1.cpp
#include<iostream>
#include "code1.h"

void TestCode1(){
    std::cout<<"Linked with the static library successfually"<<std::endl;
}

$ cat code2.h
#pragma once

void TestCode2();

$ cat code2.cpp
#include<iostream>
#include "code2.h"

void TestCode2(){
    std::cout<<"Linked with the dynamic library successfully"<<std::endl;
}

$ cat test.cpp
#include"code1.h"
#include"code2.h"
#include<iostream>

int main(){
    TestCode1();
    TestCode2();
    return 0;
}
 

$ cat makefile

libcode1.a:code1.o

    @ar -rc $@ $^                                    #archive/replace/create

    @echo "build $^ to $@ ... done"

libcode2.so:code2.o

    @g++ -shared -fPIC -o $@ $^            #GUN c++ compiler/Position-Independent Code

    @echo "build $^ to $@ ... done"

code1.o:code1.cpp

    @g++ -c -g -o $@ $^                           #compile/debug information

    @echo "compiling $^ to $@ ... done"

code2.o:code2.cpp

    @g++ -fPIC -c -g  -o $@ $^

    @echo "compiling $^ to $@ ... done"


 

test.o:test.cpp

    @g++ -c -g -o $@ $^

    @echo "compiling $^ to $@ ... done"

test.exe: test.o libcode1.a libcode2.so      

    @g++ -o $@ $< -L. -lcode1 -lcode2          #这里-L. 作用于-lcode1,

                                                                        -lcode2默认到系统标准库路径寻找

   @echo "link $^ to produce $@ ...done"

.PHONY: all

all: libcode1.a libcode2.so test.exe

.PHONY: run                                                          #PHONY伪目标,make run时创建子进程

run: test.exe

    LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH ./$^  # 仅在运行时临时设置环境变量

                                                                                         优先搜索当前目录动态库

    @echo "Running $<..."

.PHONY:clean

clean:

    @rm -rf *o *.a *.so stdc* *.exe

    @echo "clean ... done"

.PHONY:output

output:

    @mkdir -p stdc/include

    @mkdir -p stdc/lib

    @cp -f *.h stdc/include

    @cp -f *.a  *.so stdc/lib

    @tar -czf stdc.tgz stdc

    @echo "output stdc ... done"

3.1编译

$make

code1.cpp、code2.cpp test.cpp编译形成.o文件,.o文件以elf格式存在磁盘上

$objdump -d code1.o

//object dump(转储,即提取并显示文件内容)disassemble(反汇编)

code1.o调用动态库函数std::cout和std::endl时还没有链接,不知道库函数的实际位置,偏移量用00 00 00 00占位,链接阶段才会更改为真实地址。

$readelf -s code1.o

UND表示undefine,未链接调用的库函数是未定义的

code2.o的情况同上

未来把code1.o文件归档成.a静态库文件,code2.o文件动态链接成.so动态库文件,test.o调用这两个库里面的函数时

$objdump -d -S test.o

编译时调用动静态库偏移量都是00 00 00 00占位。

3.2链接

链接前准备:

        code1.o归档形成.a文件,不再是 ELF 格式,而是采用 ar 归档格式(由 ar 工具创建)

      所有 .o 中的代码段(.text)、数据段(.data)、重定位表(.rel.text)等 ELF 段完全保留在.a文件,不发生合并或修改。

链接:

  code2.o动态链接形成.so文件,.so文件以elf格式存在磁盘上

        动态链接生成 .so 时,会对多个 .o 的 ELF 内容进行合并、重定位和特殊处理。

段(Section)的合并与优化:

        多个 .o 中的相同类型段会被合并(如所有 .text 代码段合并为一个 .text 段,.data 数据段合并为一个 .data 段)。这里一个.o文件形成动态库演示不了。

        新增动态链接专用段:

1).dynamic:存储动态链接信息(如依赖的其他库、符号表位置、重定位信息等)。

2).got(全局偏移表)和 .plt(过程链接表):用于运行时动态绑定符号地址,实现延迟加载。

3).dynsym 和 .dynstr:动态符号表及其字符串表,仅包含运行时需要的全局符号(比 .symtab 精简)。

符号与重定位的处理

内部符号(仅在 .so 内部使用的函数 / 变量)会被重定位到合并后的段地址,解决内部依赖。

外部符号(如依赖的 std::cout)仍保留为未定义符号(UND),但会记录在 .dynsym 中,等待运行时由动态链接器(如 ld-linux.so)解析。

重定位表从 .rel.text 转为动态重定位表 .rel.dyn 或 .rel.plt,用于运行时修正全局变量和函数调用的地址。

ELF 头的修改

ELF 头中的 e_type 字段从目标文件的 ET_REL(可重定位文件)改为 ET_DYN(动态共享对象)。

新增 PT_DYNAMIC 程序头,指向 .dynamic 段,告知系统这是一个需要动态链接的库。

将test.o链接.a 、.so文件生成.exe文件

静态库链接时,代码段与数据段合并:可执行文件的 .text 段(代码)会合并所有参与链接的 .o 文件(包括 test.o 和静态库中被引用的 .o,如 code1.o)的 .text 段;.data(已初始化数据)、.bss(未初始化数据)等段也会相应合并,形成连续的内存布

$readelf -s *exe

静态库链接时,符号表整理.symtab(完整符号表)中,原 .o 中的未定义符号(如 TestCode1())会被替换为合并后的实际地址,标记为 DEFINED;未被引用的局部符号可能被精简,仅保留必要的全局符号和调试符号

这里动态库函数的UND说明还不知道动态库函数的地址

$ objdump -d -S *exe

由于静态库代码已经被合并到可执行文件中,链接时链接器会完成所有地址重定位(将 .o 中函数 / 变量的占位符地址替换为可执行文件中的绝对地址),因此原 .o 中的重定位表(.rel.text.rel.data)会被完全删除,可执行文件中不再保留重定位信息。

但是这里链接时调用的动态库函数拥有偏移量并不指向动态库函数的实际地址,而是plt(中间桩Procedure Linkage Table)的偏移量。PLT 是可执行文件自身 .text 段的一部分,编译时由编译器 / 链接器生成。每个动态库函数调用对应一个 PLT 条目。有时候这个偏移量可能是GOT的偏移量,plt和GOT的区别执行时会讲。

3.3运行

分配虚拟地址:

启动一个程序时,操作系统的进程创建机制会先创建一个新的进程。在创建进程的过程中,操作系统会为这个新进程分配一个独立的虚拟地址空间。虚拟地址空间是一个连续的、逻辑上的地址范围,它被划分成不同的区域,包括代码段(.text)、数据段(.data.bss)、堆、栈以及共享库映射区域等。

加载内存:

运行⼀个程序,操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配⼀段内存。

形成页表:

此时通过进程分配的虚拟空间知道了库的起始虚拟地址,通过库的ELF格式内容知道动态库函数方法偏移量,于是就可以通过库的起始虚拟地址+方法偏移量即可定位库中的方法,关联了库的虚拟空间与物理空间,形成了页表。整个调用过程,是从代码区跳转到共享区,调⽤完毕在返回到代码区,整个过程完全在进程地址空间中进行的。

加载地址重定位:

在 .data(可执行程序或者库自己)中专门预留一片区域用来存放函数的跳转地址,它也被叫做全局偏移表 GOT,表中每一项都是本运行模块要引用的一个全局变量或函数的地址。而data 区域是可读写的,所以可以支持动态进行修改,会对加载到内存中的程序的库函数调用进行地址修改,在内存中二次完成地址设置(这个叫做加载地址重定位)。由于同用ELF格式内容,库调用库也像俄罗斯套娃一样通过该方法进行地址重定位,如下图。

由于动态链接在程序加载的时候需要对大量函数进行重定位,这一步显然是非常耗时的。为了进一步降低开销,我们的操作系统还做了一些其他的优化,比如延迟绑定,或者也叫 PLT(过程连接表 Procedure Linkage Table)。与其在程序一开始就对所有函数进行重定位,不如将这个过程推迟到函数第一次被调用的时候,因为绝大多数动态库中的函数可能在程序运行期间一次都不会被使用到。其思路是:GOT 中的跳转地址默认会指向一段辅助代码,它也被叫做桩代码 /stup。在我们第一次调用函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新 GOT 表。于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现,如下图。

四、作用

        作用由格式的名字ELF可执行与可链接两个方面思考

1.链接

        链接在合并目标文件时,会根据节区表(Section Header Table)区分的文件内容相同的section合并。会进行符号去重数据整合,其目的是消除冗余、统一布局,为后续程序运行或进一步链接做准备。

        

2.执行

        可执行文件加载到内存,需要将文件根据程序头表(Program Header Table)区分的不同属性(如:可读,可写,可执行)的section合并成segment加载到内存,减少了储存的页片碎片,相同的属性合并优化了内存管理和权限访问控制。

五、优势

  • 跨平台兼容性:作为标准格式,ELF 被多种系统支持,便于软件在不同类 Unix 系统间移植。
  • 灵活性:支持动态链接(共享库)、位置无关代码(PIC,使共享库可在内存任意位置加载)、调试信息嵌入等功能,满足复杂程序的开发和运行需求。
  • 可扩展性:设计时预留了扩展空间,可通过新增节区或段支持新功能(如安全相关的标记、自定义元数据等)。

总之,ELF 是现代类 Unix 系统中程序生命周期(编译、链接、加载、运行)的核心格式,理解 ELF 有助于深入掌握程序的执行机制和系统底层原理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值