1. 背景
長期以來只知道 ELF 是一種廣泛使用的文件格式規(guī)范,常指動態(tài)庫、bin等,一直沒動力深入研究。出于業(yè)務(wù)需求,我花了好些天仔細分析 RTOS Bin 的 Section 和 Symbol 。也是趁著這機會,查閱大量資料,完善了知識脈絡(luò)。
但是可以預(yù)見,業(yè)務(wù)結(jié)束一段時間后,由于不常用,我不可避免會漸漸遺忘。從入門到遺忘,相信只要經(jīng)歷過深度學(xué)習(xí)的小伙伴都會有的痛苦掙扎。為了在將來需要時喚醒記憶,我決定通過這文章打下錨點,也希望此文能幫到更多人更快入門。
內(nèi)容:翻譯 ELF(v1.2) 通用規(guī)范,深入了解 ELF 每個結(jié)構(gòu)的細節(jié),并在最后提供一個實例協(xié)助理解。
2. 基礎(chǔ)概念
2.1 什么是 ELF 文件
ELF 的全稱是 Executable and Linking Format,即“可執(zhí)行可連接格式”,通俗來說,就是二進制程序。ELF 規(guī)定了這二進制程序的組織規(guī)范,所有以這規(guī)范組織的文件都叫 ELF 文件。ELF 文件有以下四類。
ELF 文件類型 | 示例 |
---|---|
可重定位文件(relocatable file) | 例如編譯的過程文件 ".o" |
共享目標(biāo)文件(shared object file) | 例如 ".so" 庫,以及使用動態(tài)鏈接的 bin |
可執(zhí)行文件(executable file) | 例如靜態(tài)鏈接的文件 |
core 文件 | 例如 Linux 上的 coredump 文件 |
我們通過 file 命令可以識別出來
#test.o:gcc-ctest.c-otest.o #test-static-link.bin:gcc--statictest.c-otest #test-dynamic-link.bin:gcctest.c-otest $filetest.otest-static-link.bintest-dynamic-link.bin test.o:ELF...relocatable,... test-dynamic-link.bin:ELF...sharedobject,... test-static-link.bin:ELF...executable,...
除此之外,我們習(xí)慣上叫 ".o" 文件為 目標(biāo)文件(object file),鏈接好的可執(zhí)行文件叫 bin文件。
2.2 ELF 文件結(jié)構(gòu)概覽
ELF 文件主要的用途有兩個,
構(gòu)建程序,鏈接成動態(tài)庫或者bin,一般是目標(biāo)文件 ".o"
運行程序,一般指鏈接好的 ".so" 或者 "bin"
這個 ELF 文件用作不同用途,文件結(jié)構(gòu)的解析角度就有點不一樣,通俗來說,不同用途對需要哪些數(shù)據(jù)的要求不一樣,例如構(gòu)建(鏈接)時 節(jié)表頭(Section Table Header)是必須的,但運行時卻是可選的,例如運行需要段信息,而鏈接只有節(jié)信息。
在上圖中,程序頭表(Program Header Table)緊跟在 ELF 文件頭(File Header)之后,節(jié)頭表(Section Header Table)緊跟在節(jié)信息之后,但在實際的文件中,這個順序并不是固定的。在 ELF 文件的各個組成部分中,只有ELF 文件頭的位置是固定的,其它內(nèi)容的位置全都可變。
這里有一個潛在的概念,很少看到其他文章明確點出來。Section Header Table 描述了所有節(jié)信息表,而 Program Header Table 其實描述的是所有的 段信息表 。
下一章節(jié)會介紹一些關(guān)鍵字段的含義。在這里引入這張圖,主要為了引出兩個重要的概念 節(jié)(Seciton) 和 段(Segment)。由上圖可以發(fā)現(xiàn),鏈接視圖有大量的 節(jié)(Section),而執(zhí)行視圖有 段(Segment)。那么什么是段,什么又是節(jié)?
2.3 節(jié)(Section) Vs. 段(Segment)
或許我們聽說過 bss段,text(代碼)段,或者 data(數(shù)據(jù))段,但其實我們口頭交流的段更多是泛化的 段。為什么呢?
我們隨便拿個 bin,不妨羅列下 程序頭表(Program Header Table):
$readelf-l/bin/ls ElffiletypeisDYN(Sharedobjectfile) Entrypoint0x5850 Thereare9programheaders,startingatoffset64 ProgramHeaders: ...... SectiontoSegmentmapping: ......
Program Headers 下面羅列出了所有的段信息,詳細如下圖所示,共計有 9 個段。
ProgramHeaders: TypeOffsetVirtAddrPhysAddr FileSizMemSizFlagsAlign PHDR0x00000000000000400x00000000000000400x0000000000000040 0x00000000000001f80x00000000000001f8RE0x8 INTERP0x00000000000002380x00000000000002380x0000000000000238 0x000000000000001c0x000000000000001cR0x1 [Requestingprograminterpreter:/lib64/ld-linux-x86-64.so.2] LOAD0x00000000000000000x00000000000000000x0000000000000000 0x000000000001e6e80x000000000001e6e8RE0x200000 LOAD0x000000000001eff00x000000000021eff00x000000000021eff0 0x00000000000012780x0000000000002570RW0x200000 DYNAMIC0x000000000001fa380x000000000021fa380x000000000021fa38 0x00000000000002000x0000000000000200RW0x8 NOTE0x00000000000002540x00000000000002540x0000000000000254 0x00000000000000440x0000000000000044R0x4 GNU_EH_FRAME0x000000000001b1a00x000000000001b1a00x000000000001b1a0 0x00000000000008840x0000000000000884R0x4 GNU_STACK0x00000000000000000x00000000000000000x0000000000000000 0x00000000000000000x0000000000000000RW0x10 GNU_RELRO0x000000000001eff00x000000000021eff00x000000000021eff0 0x00000000000010100x0000000000001010R0x1
Section to Segment mapping 羅列了 各個段(Segment)包含了哪些節(jié)(Section),是的,段是1個或者多個節(jié)的集合,詳細如下文所示。
SectiontoSegmentmapping: SegmentSections... 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.data.rel.ro.dynamic.got.data.bss 04.dynamic 05.note.ABI-tag.note.gnu.build-id 06.eh_frame_hdr 07 08.init_array.fini_array.data.rel.ro.dynamic.got
總的來說,段是沒有名字的,但我們往往把包含 text節(jié) 的段叫做 代碼(text)段,把含有 data節(jié) 的段叫做 數(shù)據(jù)(data)段。一定程度上在口述時習(xí)慣也可以理解為 "段 = 節(jié)",例如 bss段,rodata段,實際他們指 bss節(jié),rodata節(jié)。甚至有時候我們說 text段 就狹義的指 text節(jié),完全不用太糾結(jié)。
3. 關(guān)鍵的文件結(jié)構(gòu)和節(jié)
3.1 一些關(guān)鍵的文件結(jié)構(gòu)
對一個 ELF 格式的文件,有這么幾個特殊的結(jié)構(gòu)我們需要關(guān)注的。
ELF 文件頭(File Header):位于文件最開始,包含了整個文件的結(jié)構(gòu)信息,例如是ELF 幻數(shù),是哪種 ELF 文件,程序頭表、節(jié)頭表的地址等。
程序頭表(Program Header Table):描述了所有段的信息
節(jié)頭表(Section Header Table):描述了所有節(jié)的信息
本文不會解釋結(jié)構(gòu)體每個元素,而是利用 readelf 工具解讀。如果需要詳細到每個字節(jié)的意義,可以查閱章節(jié)1提到的《UnderstandingELF.pdf》
3.1.1 文件頭(File Header)
readelf -h
readelf-h/bin/ls ELFHeader: Magic:7f454c46020101000000000000000000 Class:ELF64 Data:2'scomplement,littleendian Version:1(current) OS/ABI:UNIX-SystemV ABIVersion:0 Type:DYN(Sharedobjectfile) Machine:AdvancedMicroDevicesX86-64 Version:0x1 Entrypointaddress:0x5850 Startofprogramheaders:64(bytesintofile) Startofsectionheaders:132000(bytesintofile) Flags:0x0 Sizeofthisheader:64(bytes) Sizeofprogramheaders:56(bytes) Numberofprogramheaders:9 Sizeofsectionheaders:64(bytes) Numberofsectionheaders:28 Sectionheaderstringtableindex:27
解讀如下:
字段 | 含義 | 備注 |
---|---|---|
Magic | 標(biāo)識是 ELF 文件類型的幻數(shù) | 值只能為 0x7F + 'E' + 'L' + 'F' |
Class | 32Bit/64Bit 類型 | ELF64 / ELF32 |
Data | 小端/大端編碼 | |
Version | 文件頭版本 | |
OS/ABI | 適用的系統(tǒng) 和 ABI | |
Type | 哪種類型的 ELF 文件 | DYN:共享目標(biāo)文件,REL:可重定位文件,EXEC:可執(zhí)行文件 [1] |
Machine | 處理器體系架構(gòu) | 常見例如 ARM,X86-64 |
Version | ELF文件的版本 | |
Entry point address | 指代程序入口的虛擬地址 | 一般是 _start(),一系列調(diào)用后才到 main() |
Start of program headers | 程序頭表在文件的偏移 | 單位 Byte |
Start of section headers | 節(jié)頭表在文件的偏移 | 單位 Byte |
Flags | 處理器特定的標(biāo)志位 | |
Size of this header | ELF 文件頭的大小 | 單位 Byte |
Size of program headers | 程序頭表中每一個表項的大小 | 單位 Byte |
Number of program headers | 程序頭表中表項的數(shù)量,理解為有多少個段 | |
Size of section headers | 節(jié)頭表中每個表項的大小 | 單位 Byte |
Number of section headers | 節(jié)頭表中有表項的數(shù)量,理解為有多少個節(jié) | |
Section header string table index | 節(jié)頭表中與節(jié)名字表相對應(yīng)的表項索引信息 [2] |
[1]. 文件類型見 2.1 章節(jié)
[2]. 段沒名字,但節(jié)是有名字的,節(jié)的名字需要另外保存,對應(yīng)到 .shstrtab 節(jié)
3.1.2 程序頭表(Program Header Table)
readelf -l
$readelf-l/bin/ls ElffiletypeisDYN(Sharedobjectfile) Entrypoint0x5850 Thereare9programheaders,startingatoffset64 ProgramHeaders: TypeOffsetVirtAddrPhysAddr FileSizMemSizFlagsAlign PHDR0x00000000000000400x00000000000000400x0000000000000040 0x00000000000001f80x00000000000001f8RE0x8 INTERP0x00000000000002380x00000000000002380x0000000000000238 0x000000000000001c0x000000000000001cR0x1 [Requestingprograminterpreter:/lib64/ld-linux-x86-64.so.2] LOAD0x00000000000000000x00000000000000000x0000000000000000 0x000000000001e6e80x000000000001e6e8RE0x200000 LOAD0x000000000001eff00x000000000021eff00x000000000021eff0 0x00000000000012780x0000000000002570RW0x200000 DYNAMIC0x000000000001fa380x000000000021fa380x000000000021fa38 0x00000000000002000x0000000000000200RW0x8 NOTE0x00000000000002540x00000000000002540x0000000000000254 0x00000000000000440x0000000000000044R0x4 GNU_EH_FRAME0x000000000001b1a00x000000000001b1a00x000000000001b1a0 0x00000000000008840x0000000000000884R0x4 GNU_STACK0x00000000000000000x00000000000000000x0000000000000000 0x00000000000000000x0000000000000000RW0x10 GNU_RELRO0x000000000001eff00x000000000021eff00x000000000021eff0 0x00000000000010100x0000000000001010R0x1 SectiontoSegmentmapping: SegmentSections... 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.data.rel.ro.dynamic.got.data.bss 04.dynamic 05.note.ABI-tag.note.gnu.build-id 06.eh_frame_hdr 07 08.init_array.fini_array.data.rel.ro.dynamic.got
Program Headers 羅列了所有段的信息,含義如下表:
字段 | 含義 | 備注 |
---|---|---|
Type | 段的類型,暗含了如何解析此段的內(nèi)容 | [1] |
Offset | 本段內(nèi)容在文件的位置 | 單位 Byte |
FileSiz | 本段內(nèi)容在文件中的大小 | 單位 Byte |
VirtAddr | 本段內(nèi)容的開始位置在進程空間中的虛擬地址 | |
MemSiz | 本段內(nèi)容在進程空間中的大小 | 單位 Byte |
PhysAddr | 本段內(nèi)容的開始位置在進程空間中的物理地址 | 由于MMU的存在,物理地址不可知,大多時候等于虛擬地址 |
Flags | 段的權(quán)限 | R/W/E 分別表示 讀/寫/可執(zhí)行 |
Align | 大小對齊信息 | [3] |
[1]. Type 的取值與含義如下
PHDR:此類型的程序頭如果存在的話,它表明的是其自身所在的程序頭表在文件或內(nèi)存中的位置和大小。這樣的段在文件中可以不存在,只有當(dāng)所在程序頭表所覆蓋的段只是整個程序的一部分時,才會出現(xiàn)一次這種表項,而且這種表項一定出現(xiàn)在其它可裝載段的表項之前。
INTERP:本段指向了一個以”null”結(jié)尾的字符串,這個字符串是一個 ELF 解析器的路徑。這種段類型只對可執(zhí)行程序有意義,當(dāng)它出現(xiàn)在共享目標(biāo)文件中時,是一個無意義的多余項。在一個 ELF 文件中它最多只能出現(xiàn)一次,而且必須出現(xiàn)在其它可裝載段的表項之前。
LOAD:此類型表明本程序頭指向一個可裝載的段。段的內(nèi)容會被從文件中拷貝到內(nèi)存中。
DYNAMIC:此類型表明本段指明了動態(tài)連接的信息。
NOTE:本段指向了一個以”null”結(jié)尾的字符串,這個字符串包含一些附加的信息。
[3]. 對于可裝載的段來說,其 虛擬地址 和 文件地址 的值至少要向內(nèi)存頁面大小對齊。此數(shù)據(jù)成員指明本段內(nèi)容如何在內(nèi)存和文件中對齊。如果該值為 0 或 1,表明沒有對齊要求;否則,此值應(yīng)該是一個正整數(shù),并且是 2 的冪次數(shù)。虛擬地址 和 文件地址 在對此值取模后應(yīng)該相等。
Section to Segment mapping 展示了段包含哪些節(jié),常關(guān)注的是代碼段和數(shù)據(jù)段。下表只是個典型的例子,一個實際的更復(fù)雜的代碼段可能包含更多的節(jié)。
代碼段 | 數(shù)據(jù)段 |
---|---|
.text | .data |
.rodata | .dynamic |
.hash | .got |
.dynsym | .bss |
.dynstr | |
.plt | |
.rel.got |
3.1.3 節(jié)頭表(Section Header Table)
readelf -S
$readelf-S/bin/ls Thereare28sectionheaders,startingatoffset0x203a0: SectionHeaders: [Nr]NameTypeAddressOffset SizeEntSizeFlagsLinkInfoAlign [0]NULL000000000000000000000000 00000000000000000000000000000000000 [1].interpPROGBITS000000000000023800000238 000000000000001c0000000000000000A001 ...... 00000000000000180000000000000008AX008 [14].textPROGBITS0000000000003e9000003e90 00000000000124d90000000000000000AX0016 ...... 00000000000003c80000000000000008WA008 [24].dataPROGBITS000000000022000000020000 00000000000002680000000000000000WA0032 [25].bssNOBITS000000000022028000020268 00000000000012e00000000000000000WA0032 ...... KeytoFlags: W(write),A(alloc),X(execute),M(merge),S(strings),I(info), L(linkorder),O(extraOSprocessingrequired),G(group),T(TLS), C(compressed),x(unknown),o(OSspecific),E(exclude), l(large),p(processorspecific)
字段 | 含義 | 備注 |
---|---|---|
Name | 本節(jié)的名字 | |
Size | 本節(jié)的大小 | 單位 Byte,如果節(jié)類型為 NOBITS,此值仍可能非0,但無意義 |
Type | 本節(jié)的類型 | [1] |
EntSize | 如果節(jié)是表類型,則表示表項大小 | 單位 Byte |
Address | 如果需要映射到虛擬內(nèi)存,則表示起始地址 | 單位 Byte |
Offset | 本節(jié)第一個字節(jié)所在文件的偏移 | 單位 Byte |
Flags | 權(quán)限等屬性信息 | |
Link | 略 | 可以查閱章節(jié)1提到的《UnderstandingELF.pdf》 |
Info | 略 | 可以查閱章節(jié)1提到的《UnderstandingELF.pdf》 |
Align | 大小對齊信息 |
[1]. 節(jié)有以下類型
NULL:本節(jié)頭是一個無效的(非活動的)節(jié)頭
PROGBITS:本節(jié)所含有的信息是由程序定義的,本節(jié)內(nèi)容的格式和含義都由程序來決定。
SYMTAB:所有符號表調(diào)試信息,strip 可以干掉,不影響運行
STRTAB:本節(jié)是字符串表。
RELA:本節(jié)是一個重定位節(jié)。
HASH/GNU_HASH:本節(jié)包含一張哈希表。
DYNAMIC:本節(jié)包含的是動態(tài)連接信息。
NOTE:本節(jié)包含的信息用于以某種方式來標(biāo)記本文件。
NOBITS:這一節(jié)的內(nèi)容是空的,節(jié)并不占用實際的空間。
REL:本節(jié)是一個重定位節(jié)。
DYNSYM:動態(tài)鏈接符號表信息
3.1.4 符號表(.symtab 節(jié))
符號表不屬于文件結(jié)構(gòu),但因為非常常用,也單獨拎出來講。
以一段簡單的代碼舉例子:
#includeintmain(intargc,char**argv) { printf("helloworld"); return0; }
編譯出 hello 的 bin 之后,我們通過以下命令查看 .symtab 和 .dynsym
readelf-shello Symboltable'.symtab'contains67entries: Num:ValueSizeTypeBindVisNdxName ... 51:00000000000000000FUNCGLOBALDEFAULTUNDprintf@@GLIBC_2.2.5 ... 61:00000000000006b039FUNCGLOBALDEFAULT14main ...
字段 | 含義 | 備注 |
---|---|---|
Num | 編號 | |
Value | 符號的值 | [1] |
Size | 符號大小 | 單位 Byte |
Type | 符號類型 | NOTYPE:無類型,OBJECT:數(shù)據(jù)對象,例如變量、數(shù)組,F(xiàn)UNC:函數(shù),F(xiàn)ILE:文件符號,SECTION:本符號與一個節(jié)相關(guān)聯(lián),用于重定位 |
Bind | 表示符號的優(yōu)先級和作用范圍 | LOCAL:本地符號,在所屬".o"文件外無效,GLOBAL:全局符號,WEAK:弱符號,例如__attribute__((weak)) 標(biāo)注的弱函數(shù) |
Vis | 略 | |
Ndx | 指定相關(guān)聯(lián)的節(jié)[2] | 數(shù)字:節(jié)在節(jié)頭表中的索引,ABS:絕對值常量,常指文件路徑,UND:未定義的,常指需要外部鏈接的函數(shù)、變量等 |
Name | 符號名字 |
[1]. 在".o" 中,如果 Ndx 不是 UND,則此值表示字符在所在節(jié)中的偏移量,在可執(zhí)行文件和共享庫文件中,則表示虛擬地址
[2]. 任何一個符號表項的定義都與某一個“節(jié)”相聯(lián)系,因為符號是為節(jié)而定義,在節(jié)中被引用。
更多符號表的介紹,請看 3.2.6 章節(jié)。
3.2 一些關(guān)鍵的節(jié)
3.2.1 "編譯 + 節(jié)" 與 "鏈接 + 段"
在 《linux 目標(biāo)文件(*.o) bss,data,text,rodata,堆,棧》(https://blog.csdn.net/sunny04/article/details/40627311) 有張圖非常形象的描述了 text、data、bss 段的作用,如下:
從上圖可以看出,代碼放在 text 段,已經(jīng)初始化的變量放在 data 段,未初始化的變量放在 bss 段。詳細下文再描述,在此章節(jié),我們需要知道一個概念:
所有變量、函數(shù)等都是一個符號,還有字符串、固定的數(shù)據(jù)等,在編譯的時候會按功能、是否初始化等分類放到特定的節(jié)中。
例如 .bss 節(jié)記錄了所有未初始化的變量,.text 節(jié)保存了所有的二進制代碼,.rodata 節(jié)保存了所有只讀的數(shù)據(jù)。
所以我們編譯(不含鏈接)出來的 ".o" 目標(biāo)文件,其實就是對單個 ".c/.cpp" 源碼的符號、數(shù)據(jù)按不同節(jié)分類放好。
這是編譯,那么鏈接成可執(zhí)行程序的時候呢?鏈接雖然涉及多個 ".o",其實也就把相同的節(jié)合并,然后歸類到段中放好。
此節(jié)也就很粗獷的描述,砍掉了所有枝葉,只是為了更好的更直觀的理解主干,實際情況肯定更復(fù)雜。
我們編譯、鏈接的時候,其實可以主動限制某些符號、數(shù)據(jù)放到哪些節(jié)、哪些段的,也可以自己新建個節(jié)、段,這屬于另一個大的課題了。大多時候我們也用不上,采用默認的即可。
下面,我們來看一些關(guān)鍵的節(jié)及其內(nèi)容。
3.2.2 .text
前文一直有帶出來,.text 節(jié)實際就是我們說的代碼段,保存了一系列的可執(zhí)行二進制指令。
.text 節(jié)的內(nèi)容是會占 ROM 空間,并在運行時拷貝到 RAM。
readelf -x .text
$objdump-d-j.text./main main:fileformatelf64-x86-64 Disassemblyofsection.text: ...... 00000000000006b0: 6b0:55push%rbp 6b1:4889e5mov%rsp,%rbp 6b4:4883ec10sub$0x10,%rsp 6b8:897dfcmov%edi,-0x4(%rbp) 6bb:488975f0mov%rsi,-0x10(%rbp) 6bf:488d3d9e000000lea0x9e(%rip),%rdi#764<_IO_stdin_used+0x4> 6c6:b800000000mov$0x0,%eax 6cb:e890feffffcallq560 6d0:b800000000mov$0x0,%eax 6d5:c9leaveq 6d6:c3retq 6d7:660f1f84000000nopw0x0(%rax,%rax,1) 6de:0000 ......
上面的 main 匯編僅僅是 printf("hello")。
3.2.3 .bss
.bss 節(jié)主要保存了未初始化的變量,這里的未初始化起始也包括初始化為 0 的變量。通常是指用來存放程序中未初始化的全局變量和未初始化的局部靜態(tài)變量。
.bss 節(jié)只在運行時,才會從內(nèi)存分配空間,因此近乎不占 ROM 空間。
例如下面示例的兩個變量都會保存到 bss。
intg_int1; intg_int2=0;
對上述的兩個變量,我們還是用 objdump -d -j .bss
$objdump-d-j.bss./main test:fileformatelf64-x86-64 Disassemblyofsection.bss: 0000000000201010<__bss_start>: 201010:0000add%al,(%rax) ... 0000000000201014: 201014:00000000.... 0000000000201018 : ...
3.2.4 .data
.data 節(jié)主要保存了已經(jīng)初始化的變量,通常是指用來存放程序中已初始化的全局變量和已初始化的靜態(tài)變量的一塊內(nèi)存區(qū)域。
既然已經(jīng)初始化,在文件中肯定要記錄初始化的值,因此 .data 節(jié)需要占 ROM 空間的,且相對 .rodata 節(jié)來說,.data 節(jié)的內(nèi)容是可變的,因此運行前需要拷貝到內(nèi)存中可寫的區(qū)間。
objdump 對查看數(shù)據(jù)類型的節(jié)的確方便,我們繼續(xù)用 objdump 看看。
//核心變量:int g_int2 = 1 $objdump-d-j.datatest test:fileformatelf64-x86-64 Disassemblyofsection.data: 0000000000201000<__data_start>: ... 0000000000201008<__dso_handle>: 201008:0810200000000000....... 0000000000201010: 201010:01000000....
代碼 int g_int2 = 1 對變量賦了非0的初始值,就不適合放到 .bss 段了,因此放到了 .data 段。從 dump 的結(jié)果也可以看到變量 g_int2 以及值 0x1 。
3.2.5 .rodata
.rodata 節(jié)的數(shù)據(jù)是會占 ROM 空間的,且(大多時候)在運行時拷貝到內(nèi)存中。
.rodata 存的是只讀數(shù)據(jù),比如字符串常量,全局const變量 和 #define定義的常量。例如:char *p = "123456", 123456 的字符串就存放在 rodata 節(jié) 中。還有一個有意思的例子:
printf("thefuncis%s ",__func__);
"the func is %s " 這個格式化打印字符串以及 __func__ 所指代的函數(shù)名,也是字符串常量,保存到 .rodata 節(jié)中的。
查看字符串類型的節(jié),用 readelf 就方便多了,例如以下代碼:
intmain(intargc,char**argv) { printf("helloworld"); printf("thisfuncis%s",__func__); }
執(zhí)行以下命令可以打印字符串?dāng)?shù)據(jù),如果需要看二進制數(shù)據(jù),例如整型的值,把 -p 改為 -x 即可。
$readelf-p.rodatamain Stringdumpofsection'.rodata': [4]helloworld [10]thisfuncis%s [20]main
這里有個小 Tips,相同的字符串只會保留 1份,因此下面兩段代碼效果一樣,但保存的 .rodata 數(shù)據(jù)是不一樣的。適當(dāng)?shù)膬?yōu)化可以節(jié)省 ROM 空間。自己琢磨:)。
//代碼1 printf("func%s:valis%d ",__func__,val); printf("func%s:openfailed ",__func__); //代碼2 printf("funcmain:valis%d ",val); printf("funcmain:openfailed ");
3.2.6 .symtab 和 .dynsym
.symtab 和 .dynsym 都是符號表,例如函數(shù)、變量等都是符號,甚至 .dynsym 是 .symtab 的子集,但是他們的作用不太一樣。
.symtab,俗稱的符號表,記錄了所有符號,不管是自己定義的變量、函數(shù),還是未定義需要動態(tài)庫提供實現(xiàn)的所有符號。在 ”.o" 鏈接時必須存在,但鏈接成 bin 后就可去掉節(jié)省空間,例如 strip
.dynsym,動態(tài)鏈接才需要的符號表,即可包括對外提供調(diào)用的符號,也包括需要外面提供實現(xiàn)的符號。.dynsym 在 ".so" 或者動態(tài)鏈接的 bin 里是必須的。
.symtab 和 .dynsym 是占 ROM 空間的,.dynsym 會加載進內(nèi)存,但 .symtab 不會。我們也可以通過 strip 去掉符號表,然后通過 file 判斷符號表是否已經(jīng) striped,例如下面的例子:
$gcctest.c-otest $filetest test:ELF64-bitLSBsharedobject,...,notstripped $striptest test:ELF64-bitLSBsharedobject,...,stripped
4. 利用工具解析 ELF
在上文的示例中頻繁使用 readelf 和 objdump 來讀取各種頭表和節(jié)內(nèi)容,除了這兩個之外,還有一個 nm 工具,3者的功能非常相近。吃多嚼不爛,咱們以 readelf 為主,以 objdump 為輔講解如何用工具解析 ELF 。
上文已有示例,本文主要做個匯總記錄,方便將來查閱。字段的具體含義,可以查看章節(jié)3中具體的節(jié)解析。
獲取 ELF 文件頭
readelf-h
獲取程序頭表(段表)
readelf-l
獲取節(jié)表(獲取有哪些節(jié))
readelf-S
獲取符號表(列出函數(shù)、變量符號)
#獲取所有符號表(含.symtab和.dynsym) readelf-s#獲取動態(tài)符號表 readelf--dyn-syms
獲取節(jié)內(nèi)容
#打印節(jié)中的字符串,常用于含字符串類型的節(jié),例如.rodata節(jié) readelf-p#以二進制打印節(jié),常用于非字符串類型的節(jié),例如.bss,.data節(jié) readelf-x #以匯編打印二進制代碼 objdump-d-j
5. ELF 在磁盤 Vs. ELF 加載到內(nèi)存
在 《完全剖析 - Linux虛擬內(nèi)存空間管理》(https://cloud.tencent.com/developer/article/1835295) 一文有張圖畫出了虛擬內(nèi)存的空間分布情況,如下:
我們關(guān)注最底下3個區(qū)間,其實跟 ELF 的內(nèi)容是能對應(yīng)上的。用一張新圖來表示兩者的關(guān)系:
可執(zhí)行文件的 代碼段、數(shù)據(jù)段等會拷貝到內(nèi)存中,BSS 段雖然沒數(shù)據(jù),但也記錄了有哪些變量,會拷貝到內(nèi)存可寫區(qū)域,而動態(tài)庫是 map 到 mmap 區(qū)的。
-
程序
+關(guān)注
關(guān)注
117文章
3785瀏覽量
81003 -
代碼
+關(guān)注
關(guān)注
30文章
4779瀏覽量
68521 -
深度學(xué)習(xí)
+關(guān)注
關(guān)注
73文章
5500瀏覽量
121111 -
elf
+關(guān)注
關(guān)注
0文章
12瀏覽量
2181
原文標(biāo)題:搞懂 ELF - 從入門到遺忘
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論