夜雨聆风学习资料网

ARTICLE · 1018840

ELF在LLVM编译中的位置与C源码要素的落地

ELF在LLVM编译中的位置与C源码要素的落地

函数、全局变量、static、extern、匿名字符串——C源码里的每个要素都能在ELF里找到对应的落点。

— — — — — — — — — — — —

📌 ELF横跨编译到运行的四个阶段

ELF文件格式在软件构建运行过程中的存在,涵盖编译、链接、加载、运行等阶段,参看图7-4。它不是某一步的中间产物,而是从.o开始一路贯穿到程序崩溃时的现场快照。

1. 编译阶段。C源码经过gcc/clang这些编译器的编译,生成ELF Relocatable文件,即平时大家看到的.o文件,又称为Object目标文件。ELF Relocatable的特点是此时文件中的全局变量和函数都还没有被分配地址。

2. 链接阶段。ELF Relocatable文件经由链接器(GNU的ld或LLVM的lld)链接,生成可执行文件Executable(后面简称EXE)或者动态链接库文件Dynamic Shared Object(后面简称DSO)。

3. 加载阶段。加载器将EXE或DSO加载进内存,形成程序的进程内存镜像。

4. 运行阶段。在该阶段如果出现错误,系统可以保留程序运行现场,包括栈、寄存器、其他要素,生成ELF Core文件,供调试分析。

图7-4 编译流程中ELF形态的位置

可以看出,ELF文件格式这一形态纵向承担了"实现高级编程语言运行到物理机器上"这一工程长跑的最后几棒,其重要性并不亚于各类高级语言本身。事实上,ELF文件格式是SYSTEM V ABI系统标准的最主要内容,它是可执行文件和操作系统之间的关键接口。

🔑 REL、DSO、EXE的三点区别

ELF在软件过程中的存在子形态,包括Relocatable可重定位文件、DSO动态链接库文件、EXE可执行文件、以及Core核心文件。对于编译器和链接器而言,主要关注的是Relocatable、DSO、EXE这三种,它们的关键区别在这几点:

• Relocatable文件不可加载。需要链接生成DSO或者EXE之后才可以加载运行。

• DSO和EXE都可以被系统加载器直接加载运行。这里有个冷知识:DSO动态链接库也是可加载运行的,只要在它的ELF Header中标注清楚入口地址就行。

• DSO和EXE的区别在于地址是否固定。EXE中各个程序部分(Program或Section)的地址是固定的,加载起始地址不可调整;而DSO的加载只要求其中各个程序部分的相对地址不变,整体的加载起始地址可以是任意的,也就是我们常说的位置无关代码,PIC(Position Independent Code)概念。

🤔 假设ELF标准还不存在

不妨换一个视角来考察ELF:假设ELF标准还未存在,但整个软件开发过程中和ELF直接连接的那些实体和概念都先一步已经存在,应用场景对文件格式的需求也就呼之欲出。在这个假设下可以问自己四个问题:

• What。我们希望ELF具备哪些能力?包括功能和性能两个方面。

• How。对于这些需求,已然成为现有标准的ELF是如何应对的?

• Where。对这些需求的回应,具体体现在ELF整体标准的哪一个细节中?

• What If。同样的问题,在未涉猎ELF的情况下,由你来设计解决方案,又会打算怎么做?

这样的思考方式不只针对ELF。在学习某项技术的初期提出一些引导性问题,能够牵引自己的思维主动向前走,在学习过程中和既有的技术体系互动比对,兼以一个批判比较的角度来看待现有方案设计,也能把新知识点融进自己既有的知识框架。

回到对ELF的需求,对一种可执行文件格式的基本需求应当包括功能和性能两方面。功能角度看,ELF纵向涵盖了程序构建执行过程的最后几个子过程,那么对每个子过程中的形态转换和信息传递都应当有足够的信息支撑;性能角度看,它在设计上要对编译、链接、加载、运行的高效执行都能起到支撑。展开来看就是图7-5。

图7-5 软件工程对可执行文件格式的需求

从图中看,要定义一种实用的可执行文件格式,要考虑的点还是很多的:软件过程上要涵盖编译、链接、加载、运行等多个子过程;信息组织上要支持文件内和跨文件的符号分析,要为各个符号定义足够的信息来支撑链接和加载;还要支持跨架构,不能和特定硬件绑定;性能上更是要处处为链接和加载性能作考虑。

🎯 需求清单摊开之后,ELF内部那些看起来繁琐的设计就有了解释——一个要同时满足功能覆盖、跨架构、链接与加载性能的格式,其内部必然会有丰富的特性和精巧的构思。

🔬 libed.so:把源码要素找出来

ELF Header、Section表、Program表这些框架性要素把ELF文件的架子支撑了起来,它们让链接器和加载器能够便捷地索引访问到文件中的各个Program和Section。而具体反映C源码中各种实体要素的这一职能,则是由种类丰富的Sections实现的。

延续上节libed.so的例子,把ELF动态链接库文件的内容结构再往深看一层,着重看C源码中函数和全局变量这两个顶层要素在ELF文件中的体现。关注的源码主干概念包括:函数实体、全局函数、static函数、对外部函数的引用;变量实体、static变量、全局变量、引用外部变量、匿名字符串、变量是否静态初始化、变量名。

这些C语言要素对应到ELF中的主要section,包括代码段.text、数据段.data和.bss、符号表段.symtab和.dynsym。用llvm-readelf查看这些和函数、全局变量有关的信息,打印中加入了//标识的注释:

$ llvm-readelf -a libed.so

ELF Header:// ELF总表头

Type:DYN(Shared object file)

Machine:Sparcv9

Entrypoint address:0x0

Startof program headers:64 (bytes into file)

Startof section headers:2744 (bytes into file)

Numberof program headers:9

Numberof section headers:19

Sectionheader string table index: 17

. . .

// 重定位表

Relocation section '.rel.dyn' at offset 0x420 contains 5 entries:

OffsetInfoTypeSymbol'sName

0000000000300778000000000016 R_SPARC_RELATIVE

0000000000300770000100000014 R_SPARC_GLOB_DATval2

0000000000300760000500000014 R_SPARC_GLOB_DATval0

0000000000300768000600000014 R_SPARC_GLOB_DATval1

// 函数用的重定位表

Relocation section '.rel.plt' at offset 0x470 contains 3 entries:

00000000004007a8000800000015 R_SPARC_JMP_SLOTfunc5

00000000004007b0000200000015 R_SPARC_JMP_SLOTfunc6

00000000004007b8000300000015 R_SPARC_JMP_SLOTfunc9

. . .

ELF打出的信息是相当丰富的,并且section和section之间存在着各种引用联系。在往下看分析之前,读者也可以结合ed01.c和ed02.c这两份C源码,自己先试着回答几个问题:

• 定位问题。给定一个ELF文件,如何在文件中找到某个特定的section或某个特定的program?Section和Program之间有什么关系?可以从逻辑功能以及地址分布两个角度分析,并找到数字层面的支撑。

• 内容问题。该ELF中每个section的功能是什么,主要包含了什么具体信息?C源程序中出现的函数和全局变量,又体现在了ELF的哪里?

• 数字问题。ELF信息中的每个数字是什么含义,是一个地址、一个序号、还是某个枚举值?如果是地址,那么它归属到哪个地址体系:虚拟地址、物理地址、还是文件位置offset?

• 外部符号问题。C语言中用extern标记的外部变量,在ELF中的展现和本地变量有何区别?.rel.dyn和.rel.plt这两个section用来干什么,其中的Type位域意味着什么,如何被加载器使用?

上面也只是一部分有分析价值的问题。读者还可以根据自己的理解就这个例子提出更多问题,也可以再编写更多例子来挖掘ELF的丰富特性。对于理解ELF这个目的而言,把挖掘分析过程同技术文档的阅读结合起来,能够获得更扎实的理解。

🗂️ 变量和函数的索引:查符号表

全局变量和函数在ELF文件中的位置,以及它们在本ELF所定义虚拟地址空间中的位置,可以通过.symtab和.dynsym这两个符号表section索引查找。其中.symtab是ELF文件中符号的全集,包含了.dynsym中的所有符号:

Symbol table '.symtab' contains 16 entries:

Num:ValueSizeTypeBindVisNdx Name

. . .

6:00000000001004a884 FUNCGLOBAL DEFAULT8 func2

7:00000000004007884 OBJECTGLOBAL DEFAULT12 val0

8:00000000004007c04 OBJECTGLOBAL DEFAULT14 val1

9:00000000000000000 NOTYPEGLOBAL DEFAULTUND val2

10:00000000001004fc28 FUNCGLOBAL DEFAULT8 func3

11:000000000010055848 FUNCGLOBAL DEFAULT8 func5

12:00000000000000000 NOTYPEGLOBAL DEFAULTUND func6

13:000000000010051820 FUNCGLOBAL DEFAULT8 func7

14:000000000010052c44 FUNCGLOBAL DEFAULT8 func8

15:00000000000000000 NOTYPEGLOBAL DEFAULTUND func9

.symtab中每一行的Value值,就是该符号(函数或全局变量)在ELF所定义虚拟地址空间中的位置;Name则是该符号在C代码中的名字。把这些Value和Section表里各个section的地址范围对起来看:

Section Headers:

[Nr]NameTypeAddressOffSizeES Flg

. . .

[8] .textPROGBITS00000000001004a8 0004a8 0000e0 00AX

. . .

[12].dataPROGBITS0000000000400788 000788 000008 00WA

. . .

[14].bssNOBITS00000000004007c0 0007c0 000004 00WA

• 函数进.text。在默认编译设定下,func2、func3等等这些C函数被编译为ELF后呈现为连续的机器指令码,被置于.text section,Ndx值都是8。指令机器码的具体编码方式由x86/ARM这些CPU架构定义。

• 有初值的全局变量进.data。例如val0,地址0x400788正好落在编号12的.data section里。

• 无初值的全局变量进.bss。例如val1,地址0x4007c0对应编号14的.bss section。

.bss是一种特殊的section。由于置于该section的全局变量不具有初值,所以在ELF文件中只给这些变量以及.bss本身分配一段虚拟地址空间,但不占用ELF文件的文件空间——注意上面.bss的Type是NOBITS而不是PROGBITS。C程序中出现一个较大的无初值结构体或数组时,这种设计无疑能降低ELF文件对文件系统的空间占用。

再用llvm-objdump具体查看代码段,可以看到.text section就是C源码翻译成的汇编指令流,起始地址0x1004a8和上面符号表里func2的Value完全对得上:

$ llvm-objdump -S libed.so

libed.so:file format elf64-sparc

Disassembly of section .text:

00000000001004a8:

1004a8:9d e3 bf 80save %sp, -128, %sp

1004ac:40 00 00 02call 8

1004b0:31 00 08 00sethi 2048, %i0

1004b4:b0 16 22 b4or %i0, 692, %i0

1004b8:b0 06 00 0fadd %i0, %o7, %i0

. . .

00000000001004fc:

1004fc:9d e3 bf 50save %sp, -176, %sp

100500:40 04 00 44call 1048848

100504:01 00 00 00nop

100508:40 04 00 4acall 1048872

10050c:01 00 00 00nop

. . .

🏠 global和static的差异:看Bind

C语言中标记了static的全局变量或函数,只能被本源文件中的函数或变量访问,而不能跨C源文件访问。这个语义差异在ELF里落在了符号表的Bind字段上。对比例子中val0和val4这两个全局变量:

Symbol table '.symtab' contains 16 entries:

Num:ValueSizeTypeBindVisNdx Name

. . .

3:000000000040078c4 OBJECTLOCALDEFAULT12 val4

. . .

7:00000000004007884 OBJECTGLOBAL DEFAULT12 val0

global和static符号在ELF文件中的差异体现在Bind字段:global变量被标记为GLOBAL,static符号被标记为LOCAL,意为本地符号。注意两者的Ndx都是12,都在.data section里,位置上是紧挨着的——0x400788和0x40078c,差4个字节。区别只在可见性,不在存放位置。

🌍 so内部和外部符号:看Ndx

一个编译好的so动态链接库文件,在加载时可能会被分析出依赖其他动态链接库文件,也就是它其中的符号引用了外部的函数或全局变量。例子中val2就是外部变量,它在ed01.c和ed02.c中都没有定义,只是以extern作了外部符号声明。跨DSO的符号分析基于.dynsym这个符号表:

Symbol table '.dynsym' contains 11 entries:

Num:ValueSizeTypeBindVisNdx Name

. . .

1:00000000000000000 NOTYPEGLOBAL DEFAULTUND val2

. . .

5:00000000004007884 OBJECTGLOBAL DEFAULT12 val0

ELF的.dynsym section区分内部和外部符号,用的是Ndx字段。该字段表示的是本符号的实体所在section的编号:val0实体位于编号12的section即.data,而val2的Ndx值为UND,意为Undefined,即该符号的实体不在本ELF文件中,是外部符号。

函数func6和func9也是同样的情况,它们没有在ed01.c和ed02.c中定义,只是声明而已,.dynsym中的Ndx字段同样是UND。Ndx为UND的这些外部符号,在程序加载时是需要进行重定位的,即分析出该符号实体的加载地址,然后填入.got和.got.plt这些全局重定位表section中。

🔑 Bind和Ndx回答的是两个不同的问题:Bind管"这个符号能不能被别人看到"(GLOBAL还是LOCAL),Ndx管"这个符号的实体在不在本文件里"(section编号还是UND)。static对应前者,extern对应后者。

📝 匿名字符串与函数局部变量

匿名字符串是一种特殊变量,最常见于给printf()函数传入的第一个字符串常量参数。本例中func8()调用func9()传入的参数就是这样一个常量字符串"hello",用objdump可以查看到,这个匿名常量字符串位于.rodata section中:

$ llvm-objdump --full-contents libed.so

. . .

Contents of section .rodata:

04a0 68656c6c 6f0a00hello..

实际编译执行中,所有的匿名字符串都会被放入.rodata section。需要访问引用这些常量字符串的函数或者全局变量,会通过绝对地址或者相对地址方式来寻址。

函数的局部变量则不同于全局变量,它们不会直接体现在ELF文件的某个section中。局部变量的实体存在仅在程序运行阶段,它对应于各个函数中分配出的栈空间的一个slot。某个函数开始运行、栈空间得以分配,相应局部变量的生命周期开始;该函数运行结束、栈空间被回收,该局部变量的生命周期就完全结束了。

当然,也可以更精确地说,局部变量的生命周期从它被声明开始,到它不再被任何语句访问结束。这也解释了为什么调试器要靠调试信息(而不是符号表)才能报出局部变量——ELF的符号表里根本就没有它们的位置。

🧾 小结

• ELF横跨四个阶段。编译出Relocatable(.o,地址未分配)、链接出EXE或DSO、加载成进程内存镜像、运行出错时留下Core。它是SYSTEM V ABI的最主要内容,也是可执行文件和操作系统之间的关键接口。

• REL不可加载,EXE与DSO的区别是地址能不能挪。EXE各程序部分地址固定,DSO只要求相对地址不变、整体起始地址任意,也就是PIC。

• 函数进.text,有初值的全局变量进.data,无初值的进.bss。.bss的Type是NOBITS,只占虚拟地址空间不占文件空间,大数组和大结构体因此不会把.o撑大。

• static和global的差异在Bind字段。val4是LOCAL、val0是GLOBAL,但两者都在编号12的.data里,地址只差4字节——差别在可见性,不在存放位置。

• 内部和外部符号的差异在Ndx字段。val0的Ndx是12,extern声明的val2以及func6、func9的Ndx是UND,加载时要重定位并把地址填进.got和.got.plt。

• 匿名字符串在.rodata,局部变量哪儿都不在。"hello\n"以68656c6c 6f0a00躺在.rodata里;局部变量只是运行期栈上的一个slot,不出现在任何section。

相关学习资料

返回首页浏览学习资料