发布时间:2026/8/4 4:43:03
【Linux系统编程】ELF与动静态链接与加载 1. ELF文件要理解编译链接的细节就要先了解一下ELF文件以下四种文件都是ELF文件可重定位文件Relocatable File即 xxx.o 文件。包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。可执行文件Executable File即可执行程序。共享目标文件Shared Object File即 xxx.so文件。内核转储(core dumps)存放当前进程的执行上下下用于dump信号触发。每个ELF文件又由以下四部分组成ELF头(ELF header) 显示ELF文件的文件头信息。文件头包含了ELF文件的基本信息比如文件类型、机器类型、版本、入口点地址、程序头表和节头表的位置和大小等它的主要目的是定位文件的其他部分。程序头表(Program header table)列举了所有有效的段(segments)和他们的属性。表里记着每个段的开始的位置和位移offset、长度毕竟这些段都是紧密的放在⼆进制文件中需要段表的描述信息才能把他们每个段分割开。节头表(Section header table)包含对节(sections)的描述。节Section ELF文件中的基本组成单位包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中如代码节存储了可执行代码数据节存储了全局变量和静态数据等。查看ELF Header# readelf -h main ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 #ELF文件标识符魔数 Class: ELF64 # ⽂件类64位架构 Data: 2s complement, little endian#数据编码 ⼩端序 ⼆进制补码 Version: 1 (current) # ELF版本当前版本 OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x400640 # ⼊⼝点地址 Start of program headers: 64 (bytes into file) # 程序头表起始偏移 Start of section headers: 7048 (bytes into file) # 节头表起始偏移 Flags: 0x0 Size of this header: 64 (bytes) # ELF头⼤⼩64字节 Size of program headers: 56 (bytes) # 程序头表条⽬⼤⼩ Number of program headers: 9 #程序头表目数 Size of section headers: 64 (bytes) #节头表条目大小 Number of section headers: 31 #节头表条目数 Section header string table index: 30 #节名称字符串表索引对于 ELF HEADER 这部分来说我们只用知道其作用即可它的主要目的是定位文件的其他部分。查看ELF Program Header Table# readelf -l main Elf file type is EXEC (Executable file) Entry point 0x400640 There are 9 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x00000000000001f8 0x00000000000001f8 R E 8 INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x0000000000000d24 0x0000000000000d24 R E 200000 LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x0000000000000254 0x0000000000000258 RW 200000 DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28 0x00000000000001d0 0x00000000000001d0 RW 8 NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254 0x0000000000000044 0x0000000000000044 R 4 GNU_EH_FRAME 0x0000000000000b34 0x0000000000400b34 0x0000000000400b34 0x000000000000005c 0x000000000000005c R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 10 GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x00000000000001f0 0x00000000000001f0 R 1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 08 .init_array .fini_array .jcr .dynamic .got查看ELF Section Header Table# readelf -S main There are 31 section headers, starting at offset 0x1b88: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000400238 00000238 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.ABI-tag NOTE 0000000000400254 00000254 0000000000000020 0000000000000000 A 0 0 4 [ 3] .note.gnu.build-i NOTE 0000000000400274 00000274 0000000000000024 0000000000000000 A 0 0 4 。。。 [10] .rela.plt RELA 0000000000400498 00000498 00000000000000d8 0000000000000018 AI 5 24 8 [11] .init PROGBITS 0000000000400570 00000570 000000000000001a 0000000000000000 AX 0 0 4 。。。 [23] .got PROGBITS 0000000000600ff8 00000ff8 0000000000000008 0000000000000008 WA 0 0 8 。。。 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), l (large), p (processor specific).text节是保存了程序代码指令的代码节。.data节保存了初始化的全局变量和局部静态变量等数据。.rodata节保存了只读的数据如一行C语言代码中的字符串。由于.rodata节是只读的所以只能存在于⼀个可执行文件的只读段中。因此只能是在text段不是data段中找到.rodata节。.BSS节为未初始化的全局变量和局部静态变量预留位置。.symtab节: Symbol Table 符号表就是源码里面那些函数名、变量名和代码的对应关系。.got.plt节全局偏移表-过程链接表.got节保存了全局偏移表。.got节和.plt节⼀起提供了对导入的共享库函数的访问入口由动态链接器在运行时进行 修改。查看具体的Section信息# objdump -S main main: file format elf64-x86-64 Disassembly of section .init: 0000000000400570 _init: 400570: 48 83 ec 08 sub $0x8,%rsp 400574: 48 8b 05 7d 0a 20 00 mov 0x200a7d(%rip),%rax 40057b: 48 85 c0 test %rax,%rax 40057e: 74 05 je 400585 _init0x15 400580: e8 ab 00 00 00 callq 400630 .plt.got 400585: 48 83 c4 08 add $0x8,%rsp 400589: c3 retq Disassembly of section .plt: 0000000000400590 .plt: 400590: ff 35 72 0a 20 00 pushq 0x200a72(%rip) 400596: ff 25 74 0a 20 00 jmpq *0x200a74(%rip) 40059c: 0f 1f 40 00 nopl 0x0(%rax) 00000000004005a0 writeplt: 4005a0: ff 25 72 0a 20 00 jmpq *0x200a72(%rip) 4005a6: 68 00 00 00 00 pushq $0x0 4005ab: e9 e0 ff ff ff jmpq 400590 .plt 00000000004005b0 printfplt: 4005b0: ff 25 6a 0a 20 00 jmpq *0x200a6a(%rip) 4005b6: 68 01 00 00 00 pushq $0x1 4005bb: e9 d0 ff ff ff jmpq 400590 .plt 00000000004005c0 closeplt: 4005c0: ff 25 62 0a 20 00 jmpq *0x200a62(%rip) 4005c6: 68 02 00 00 00 pushq $0x2 4005cb: e9 c0 ff ff ff jmpq 400590 .plt 00000000004005d0 __libc_start_mainplt: 4005d0: ff 25 5a 0a 20 00 jmpq *0x200a5a(%rip) 4005d6: 68 03 00 00 00 pushq $0x3 4005db: e9 b0 ff ff ff jmpq 400590 .plt查看编译后的.o目标文件$ objdump -d hello.o hello.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 main: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: bf 00 00 00 00 mov $0x0,%edi 9: e8 00 00 00 00 callq e main0xe e: b8 00 00 00 00 mov $0x0,%eax 13: e8 00 00 00 00 callq 18 main0x18 18: b8 00 00 00 00 mov $0x0,%eax 1d: 5d pop %rbp 1e: c3 retq图一图二ELF形成可执行过程step1将多份C/C源代码翻译成目标文件动静态库step2将多份文件Section进行合并如图二ELF可执行加载过程⼀个ELF会有多种不同的Section在加载到内存时也会进行Section合并形成segment。合并原则相同属性比如可读可写可执行需要加载时申请空间等。这个合并工作在形成 ELF 的时候合并方式就已经确定了具体合并原则被记录在了 ELF 的 程序头表(Program header table) 中。为什么要将Section合并成段segmentSection合并的主要原因是为了减少页面碎片提高内存使用效率。此外操作系统在加载程序时会将具有相同属性的section合并成⼀个大的segment这样就可以实现不同的访问权限从而优化内存管理和权限访问控制。2. 理解链接和加载2.1 静态链接研究静态链接本质就是研究.o是如何链接的。// hello.c #includestdio.h void run(); int main() { printf(hello world!\n); run(); return 0; } // code.c #includestdio.h void run() { printf(running...\n); } // 编译两个源⽂件 $ gcc -c hello.c $ gcc -c code.c $ ls code.c code.o hello.c hello.o2.1.1 静态链接过程查看编译后的.o文件我们可以看到hello.c中的main函数完全不认识printf和run函数这两个的函数地址设为0。这是因为在编译 hello.c 的时候编译器是完全不知道 printf 和 run 函数的存在的。因此编译器只能将这两个函数的跳转地址先暂时设为0。这个地址会在链接的时候进行修正为了让链接器将来在链接时能够正确定位到这些被修正的地址在代码块.data中还存在⼀个重定位表这张表将来在链接的时候就会根据表里记录的地址将其修正。读取code.o的符号表puts就是printf的实现UND就是undefine说白了就是.o文件找不到读取hello.o的符号表也一样如果将两个.o进行合并并进行统一的编址链接的时候会修改.o中没有确定的函数地址。所以链接其实就是将编译之后的所有目标文件连同用到的⼀些静态库运行时库组合拼装成⼀个独立的可执行文件。其中就包括我们之前提到的地址修正当所有模块组合在⼀起之后链接器会根据我们的.o文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量从而修正它们的地址。这其实就是静态链接的过程。链接过程中会涉及到对.o中外部符号进行地址重定位所以.o文件又叫可重定位文件。2.1.2 ELF加载与进程空间地址逻辑地址/虚拟地址逻辑地址起始地址偏移量一个ELF程序在没有被加载到内存的时候本身就会有地址当代计算机工作都是采用“平坦模式”所以也要求ELF对自己的代码和数据进行统一编址如下图反汇编之后代码最左侧的就是ELF的虚拟地址其实严格意义上应该叫做逻辑地址(起始地址偏移量), 但是我们一般认为起始地址是0。也就是说其实虚拟地址在我们的程序还没有加载到内存的时候就已经把可执行程序进行统一编址了。进程mm_struct、vm_area_struct在进程刚刚创建的时候初始化数据从ELF各个segment来每个segment有自己的起始地址和自己的长度用来初始化内核结构中的[start, end]等范围数据另外在用详细地址虚拟地址和加载到内存后的物理地址填充页表。所以虚拟地址机制既要OS支持也要编译器支持。重新理解进程虚拟地址空间ELF 在被编译好之后会把自己未来程序的入口地址记录在ELF header的Entry字段中将文件加载到物理内存后获取原本在磁盘上就有的逻辑地址和物理内存上的地址填充到页表建立映射关系mm_struct里面的其他数据则从segment当中获取。在CPU内部存在几种硬件EIP存放下一条指令的地址指令也有长度当前地址当前指令长度下一条指令地址MMUEIP内部实际上放的是虚拟地址需要MMU通过页表找到真实的物理地址再交给IRIR存放当前正在执行指令的地址CR3存放当前进程的页表首地址这些统称为当前进程的硬件上下文。所以我们可以看到虚拟空间地址还需要CPU的支持才可以。2.2 动态链接和动态库加载进程如何看到动态库把库文件映射到当前进程的共享区进程间如何共享库的本质是把动态库映射到自己的虚拟地址空间中动态库也叫共享库2.2.1 动态链接静态链接最大的问题在于生成的文件体积大并且相当耗费内存资源。这个时候动态链接的优势就体现出来了我们可以将需要共享的代码单独提取出来保存成⼀个独立的动态链接库等到程序运行的时候再将它们加载到内存这样可以节省空间因为同⼀个模块在内存中只需要保留⼀份副本就可以被不同的进程所共享。那么动态链接是如何工作的动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行⼀个程序操作系统会首先将程序的数据代码连同它用到的⼀系列动态库先加载到内存其中每个动态库的加载地址都是不固定的操作系统会根据当前地址空间的使用情况为它们动态分配⼀段内存。当动态库被加载到内存以后⼀旦它的内存地址被确定我们就可以去修正动态库中的那些函数跳转地址了。在C/C程序中当程序开始执行时它首先并不会直接跳转到 main 函数。实际上程序的入口点是 _start 这是⼀个由C运行时库通常是glibc或链接器如ld提供的特殊函数。在 _start 函数中会执行⼀系列初始化操作这些操作包括设置堆栈为程序创建⼀个初始的堆栈环境初始化数据段将程序的数据段如全局变量和静态变量从初始化数据段复制到相应的内存位置并清零未初始化的数据段。动态链接这是关键的⼀步 _start 函数会调用动态链接器的代码来解析和加载程序所依赖的动态库shared libraries。动态链接器会处理所有的符号解析和重定位确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址。动态链接器动态链接器如ld-linux.so负责在程序运行时加载动态库。当程序启动时动态链接器会解析程序中的动态库依赖并加载这些库到内存中。环境变量和配置文件Linux系统通过环境变量如LD_LIBRARY_PATH和配置文件如/etc/ld.so.conf及其子配置文件来指定动态库的搜索路径。这些路径会被动态链接器在加载动态库时搜索。缓存文件为了提高动态库的加载效率Linux系统会维护⼀个名为/etc/ld.so.cache的缓存文件。该文件包含了系统中所有已知动态库的路径和相关信息动态链接器在加载动态库时会首先搜索这个缓存文件。调用 __libc_start_main⼀旦动态链接完成 _start 函数会调用__libc_start_main 这是glibc提供的⼀个函数。 __libc_start_main 函数负责执行⼀些额外的初始化⼯作比如设置信号处理函数、初始化线程库如果使用了线程等。调用 main 函数最后 __libc_start_main 函数会调用程序的 main 函数此时程序的执行控制权才正式交给用户编写的代码。处理 main 函数的返回值当 main 函数返回时 __libc_start_main 会负责处理这个返回值并最终调用 _exit 函数来终止程序。动态库中的相对地址动态库为了随时进行加载为了支持并映射到任意进程的任意位置对动态库中的方法统⼀编址采用相对编址的方案进行编制的(其实可执行程序也⼀样都要遵守平坦模式只不过exe是直接加载的)。库被映射到了当前进程的地址空间中我们也知道库的虚拟起始地址也知道库中每一个方法的偏移量通过这两点我们可以访问库中的任意方法整个过程从代码区跳转到共享区再回到代码区都是再进程地址空间中完成的。我们还需要对加载到内存中的程序调用的库函数进行地址修改在内存中二次完成地址设置叫做加载地址重定位但是代码区不能修改所以这里的做法是在.data中专门留一片区域来存放函数的跳转地址它也被叫做全局偏移表(Global Offset Table)表中的每一项都是本运行模块要引用的一个全局变量或函数地址因为,data区域是可读写的所以支持动态修改。由于代码段只读我们不能直接修改代码段。但有了GOT表代码便可以被所有进程共享。但在不同进程的地址空间中各动态库的绝对地址、相对位置都不同。反映到GOT表上就是每个进程的每个动态库都有独立的GOT表所以进程间不能共享GOT表。在单个.so下由于GOT表与 .text 的相对位置是固定的我们完全可以利用CPU的相对寻址来找到GOT表。在调用函数的时候会首先查表然后根据表中的地址来进行跳转这些地址在动态库加载的时候会被修改为真正的地址当第二次调用这个函数时就可以直接使用了。这种方式实现的动态链接就被叫做 PIC 地址无关代码 。换句话说我们的动态库不需要做任何修改被加载到任意内存地址都能够正常运行并且能够被所有进程共享这也是为什么之前我们给编译器指定-fPIC参数的原因PIC相对编址GOT。对于GOT表它并不是加载的时候就立即修改的有点写时拷贝的感觉按需修改这样就不会影响进程加载的效率了。GOT是如何进行工作的第一阶段延迟绑定Lazy Binding—— 第一次调用步骤 1代码执行 call printfplt通过过程链接表 PLT 跳转。步骤 2PLT 跳转到 GOT 中对应 printf 的条目。步骤 3此时 GOT 里存的不是printf地址而是指向下一行指令的地址一个“桩”函数。步骤 4执行“桩”函数触发动态链接器去查找 printf 的真实内存地址。步骤 5动态链接器找到地址后把它写入 GOT 对应的条目中覆盖掉原来的“桩”地址。步骤 6跳转执行真正的printf。第二阶段直接跳转 —— 后续调用再次执行 call printfplt 时PLT 再次跳转到 GOT。此时 GOT 里存的已经是 printf的真实地址了直接跳转执行不再触发链接器。为了让共享库.so能被加载到内存的任意地址都能运行编译时需要加上-fPIC参数生成位置无关代码。

相关新闻

2026/8/4 4:43:03

制冷空调行业核心软件工具链全解析:从设计仿真到工程实践

1. 项目概述:制冷行业软件生态全景图 干制冷这行,无论是做产品研发、系统设计,还是搞工程安装、故障诊断,手里没几款趁手的软件工具,那基本等于“盲人摸象”。今天咱们不聊虚的,就实实在在地盘点一下那些在…

2026/8/4 4:43:03

VMware虚拟化入门:从安装到性能调优全指南

1. 为什么选择VMware作为虚拟化入门工具在开始手把手教学之前,有必要先了解为什么VMware Workstation会成为大多数技术人首选的虚拟化工具。作为从业15年的系统架构师,我见证过从Virtual PC到VirtualBox的各种方案,最终沉淀下来的工作流始终以…

2026/8/4 4:38:02

上海企业小程序定制公司哪家好

企业选开发团队时容易先看案例和价格,但项目真正启动以后,最影响体验的往往是沟通、版本管理和问题处理。在上海做本地化交付,沟通效率很重要,但真正决定项目结果的,仍然是需求判断、产品设计和持续交付能力。“哪家好…

2026/8/4 5:28:06

祁木 CAD Translator 东南亚语言翻译效果实测与原理拆解

在处理东南亚地区的工程项目文档时,很多技术团队都会遇到一个棘手的痛点:图纸上的文字识别不准,翻译出来的内容更是让人摸不着头脑。尤其是面对泰语、越南语这些拥有独特字符集的语言,或者是印尼语和马来语中那些高度相似却又含义…

2026/8/4 5:28:06

Intel i5和i7处理器哪个好?电脑装机CPU选择对比指南

组装新电脑,处理器的选择至关重要。尤其是在Intel处理器家族中,i5与i7常常成为用户的焦点。这两款处理器代表着中高端的性能定位,许多用户在挑选电脑或DIY装机时都会疑惑:Intel i5和i7到底哪个好?该如何选择&#xff1…

2026/8/4 5:28:06

Windows路由表配置实现内网隔离的运维指南

1. 项目背景与需求场景在企业办公环境中,经常遇到需要临时断开外网连接,仅保留内网访问权限的特殊场景。比如:涉密计算机需要物理隔离互联网开发测试环境要求纯内网操作安全审计期间的网络访问控制特定业务系统(如财务、HR&#x…

2026/8/4 5:28:06

Unity动态加载外部图片内存优化实战:从原理到解决方案

1. 项目概述:当Unity遇上外部图片的“内存之痛”在Unity项目开发中,尤其是涉及大量美术资源、用户自定义内容或动态下载资源的应用(如相册应用、换装游戏、地图编辑器、UGC社区),动态读取外部图片并转换为Texture2D是一…

2026/8/4 5:23:06

服务器卡顿元凶:你真的搞懂 Linux 交换分区了吗?

文章目录一、计算机存储器层次结构1. CPU 寄存器2. CPU 高速缓存(CPU Cache)3. 主存储器(内存)4. 辅助存储器二、计算机存储器工作原理三、物理内存与虚拟内存核心机制1. 物理内存分页机制2. 虚拟内存的核心价值四、Linux 交换空间…

2026/8/3 21:14:30

如何用免费工具突破游戏窗口限制:SRWE完整使用指南

如何用免费工具突破游戏窗口限制:SRWE完整使用指南 【免费下载链接】SRWE Simple Runtime Window Editor 项目地址: https://gitcode.com/gh_mirrors/sr/SRWE 你是否遇到过这样的困扰?想为心爱的游戏截图,却发现游戏不支持自定义分辨率…

2026/8/4 0:02:01

dealsea是什么?跨境卖家必知的美国deal站入门指南

说实话,第一次听说美国这个老牌折扣网站的跨境卖家,十个有八个会问同一个问题:这个平台到底是干嘛的?我见过一个做家居出口的朋友,他在亚马逊上月销二十万美金,却从来没用过它。我给他看了首页——一屏一屏…

2026/8/3 22:40:58

实测才敢推 AI论文网站 2026最新测评与推荐

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。一、综…

2026/8/3 13:26:41

2026必备!AI论文网站测评:最新推荐与深度对比

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

2026/8/3 16:43:13

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…