1. 背景
長(zhǎng)期以來(lái)只知道 ELF 是一種廣泛使用的文件格式規(guī)范,常指動(dòng)態(tài)庫(kù)、bin等,一直沒(méi)動(dòng)力深入研究。出于業(yè)務(wù)需求,我花了好些天仔細(xì)分析 RTOS Bin 的 Section 和 Symbol 。也是趁著這機(jī)會(huì),查閱大量資料,完善了知識(shí)脈絡(luò)。
但是可以預(yù)見(jiàn),業(yè)務(wù)結(jié)束一段時(shí)間后,由于不常用,我不可避免會(huì)漸漸遺忘。從入門(mén)到遺忘,相信只要經(jīng)歷過(guò)深度學(xué)習(xí)的小伙伴都會(huì)有的痛苦掙扎。為了在將來(lái)需要時(shí)喚醒記憶,我決定通過(guò)這文章打下錨點(diǎn),也希望此文能幫到更多人更快入門(mén)。
2. 基礎(chǔ)概念
2.1 什么是 ELF 文件
ELF 的全稱(chēng)是 Executable and Linking Format,即“可執(zhí)行可連接格式”,通俗來(lái)說(shuō),就是二進(jìn)制程序。ELF 規(guī)定了這二進(jìn)制程序的組織規(guī)范,所有以這規(guī)范組織的文件都叫 ELF 文件。ELF 文件有以下四類(lèi)。
ELF 文件類(lèi)型 | 示例 |
---|---|
可重定位文件(relocatable file) | 例如編譯的過(guò)程文件 ".o" |
共享目標(biāo)文件(shared object file) | 例如 ".so" 庫(kù),以及使用動(dòng)態(tài)鏈接的 bin |
可執(zhí)行文件(executable file) | 例如靜態(tài)鏈接的文件 |
core 文件 | 例如 Linux 上的 coredump 文件 |
我們通過(guò) file 命令可以識(shí)別出來(lái)
#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è),
構(gòu)建程序,鏈接成動(dòng)態(tài)庫(kù)或者bin,一般是目標(biāo)文件 ".o"
運(yùn)行程序,一般指鏈接好的 ".so" 或者 "bin"
這個(gè) ELF 文件用作不同用途,文件結(jié)構(gòu)的解析角度就有點(diǎn)不一樣,通俗來(lái)說(shuō),不同用途對(duì)需要哪些數(shù)據(jù)的要求不一樣,例如構(gòu)建(鏈接)時(shí) 節(jié)表頭(Section Table Header)是必須的,但運(yùn)行時(shí)卻是可選的,例如運(yùn)行需要段信息,而鏈接只有節(jié)信息。
在上圖中,程序頭表(Program Header Table)緊跟在 ELF 文件頭(File Header)之后,節(jié)頭表(Section Header Table)緊跟在節(jié)信息之后,但在實(shí)際的文件中,這個(gè)順序并不是固定的。在 ELF 文件的各個(gè)組成部分中,只有ELF 文件頭的位置是固定的,其它內(nèi)容的位置全都可變。
這里有一個(gè)潛在的概念,很少看到其他文章明確點(diǎn)出來(lái)。Section Header Table 描述了所有節(jié)信息表,而 Program Header Table 其實(shí)描述的是所有的 段信息表 。
下一章節(jié)會(huì)介紹一些關(guān)鍵字段的含義。在這里引入這張圖,主要為了引出兩個(gè)重要的概念 節(jié)(Seciton) 和 段(Segment)。由上圖可以發(fā)現(xiàn),鏈接視圖有大量的 節(jié)(Section),而執(zhí)行視圖有 段(Segment)。那么什么是段,什么又是節(jié)?
2.3 節(jié)(Section) Vs. 段(Segment)
或許我們聽(tīng)說(shuō)過(guò) bss段,text(代碼)段,或者 data(數(shù)據(jù))段,但其實(shí)我們口頭交流的段更多是泛化的 段。為什么呢?
我們隨便拿個(gè) bin,不妨羅列下 程序頭表(Program Header Table):
$readelf-l/bin/ls ElffiletypeisDYN(Sharedobjectfile) Entrypoint0x5850 Thereare9programheaders,startingatoffset64 ProgramHeaders: ...... SectiontoSegmentmapping: ......
Program Headers 下面羅列出了所有的段信息,詳細(xì)如下圖所示,共計(jì)有 9 個(gè)段。
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 羅列了 各個(gè)段(Segment)包含了哪些節(jié)(Section),是的,段是1個(gè)或者多個(gè)節(jié)的集合,詳細(xì)如下文所示。
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
總的來(lái)說(shuō),段是沒(méi)有名字的,但我們往往把包含 text節(jié) 的段叫做 代碼(text)段,把含有 data節(jié) 的段叫做 數(shù)據(jù)(data)段。一定程度上在口述時(shí)習(xí)慣也可以理解為 "段 = 節(jié)",例如 bss段,rodata段,實(shí)際他們指 bss節(jié),rodata節(jié)。甚至有時(shí)候我們說(shuō) text段 就狹義的指 text節(jié),完全不用太糾結(jié)。
3. 關(guān)鍵的文件結(jié)構(gòu)和節(jié)
3.1 一些關(guān)鍵的文件結(jié)構(gòu)
對(duì)一個(gè) ELF 格式的文件,有這么幾個(gè)特殊的結(jié)構(gòu)我們需要關(guān)注的。
ELF 文件頭(File Header):位于文件最開(kāi)始,包含了整個(gè)文件的結(jié)構(gòu)信息,例如是ELF 幻數(shù),是哪種 ELF 文件,程序頭表、節(jié)頭表的地址等。
程序頭表(Program Header Table):描述了所有段的信息
節(jié)頭表(Section Header Table):描述了所有節(jié)的信息
本文不會(huì)解釋結(jié)構(gòu)體每個(gè)元素,而是利用 readelf 工具解讀。如果需要詳細(xì)到每個(gè)字節(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)識(shí)是 ELF 文件類(lèi)型的幻數(shù) | 值只能為 0x7F + 'E' + 'L' + 'F' |
Class | 32Bit/64Bit 類(lèi)型 | ELF64 / ELF32 |
Data | 小端/大端編碼 | |
Version | 文件頭版本 | |
OS/ABI | 適用的系統(tǒng) 和 ABI | |
Type | 哪種類(lèi)型的 ELF 文件 | DYN:共享目標(biāo)文件,REL:可重定位文件,EXEC:可執(zhí)行文件 [1] |
Machine | 處理器體系架構(gòu) | 常見(jiàn)例如 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 | 程序頭表中每一個(gè)表項(xiàng)的大小 | 單位 Byte |
Number of program headers | 程序頭表中表項(xiàng)的數(shù)量,理解為有多少個(gè)段 | |
Size of section headers | 節(jié)頭表中每個(gè)表項(xiàng)的大小 | 單位 Byte |
Number of section headers | 節(jié)頭表中有表項(xiàng)的數(shù)量,理解為有多少個(gè)節(jié) | |
Section header string table index | 節(jié)頭表中與節(jié)名字表相對(duì)應(yīng)的表項(xiàng)索引信息 [2] |
[1]. 文件類(lèi)型見(jiàn) 2.1 章節(jié)
[2]. 段沒(méi)名字,但節(jié)是有名字的,節(jié)的名字需要另外保存,對(duì)應(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 | 段的類(lèi)型,暗含了如何解析此段的內(nèi)容 | [1] |
Offset | 本段內(nèi)容在文件的位置 | 單位 Byte |
FileSiz | 本段內(nèi)容在文件中的大小 | 單位 Byte |
VirtAddr | 本段內(nèi)容的開(kāi)始位置在進(jìn)程空間中的虛擬地址 | |
MemSiz | 本段內(nèi)容在進(jìn)程空間中的大小 | 單位 Byte |
PhysAddr | 本段內(nèi)容的開(kāi)始位置在進(jìn)程空間中的物理地址 | 由于MMU的存在,物理地址不可知,大多時(shí)候等于虛擬地址 |
Flags | 段的權(quán)限 | R/W/E 分別表示 讀/寫(xiě)/可執(zhí)行 |
Align | 大小對(duì)齊信息 | [3] |
[1]. Type 的取值與含義如下
PHDR:此類(lèi)型的程序頭如果存在的話,它表明的是其自身所在的程序頭表在文件或內(nèi)存中的位置和大小。這樣的段在文件中可以不存在,只有當(dāng)所在程序頭表所覆蓋的段只是整個(gè)程序的一部分時(shí),才會(huì)出現(xiàn)一次這種表項(xiàng),而且這種表項(xiàng)一定出現(xiàn)在其它可裝載段的表項(xiàng)之前。
INTERP:本段指向了一個(gè)以”null”結(jié)尾的字符串,這個(gè)字符串是一個(gè) ELF 解析器的路徑。這種段類(lèi)型只對(duì)可執(zhí)行程序有意義,當(dāng)它出現(xiàn)在共享目標(biāo)文件中時(shí),是一個(gè)無(wú)意義的多余項(xiàng)。在一個(gè) ELF 文件中它最多只能出現(xiàn)一次,而且必須出現(xiàn)在其它可裝載段的表項(xiàng)之前。
LOAD:此類(lèi)型表明本程序頭指向一個(gè)可裝載的段。段的內(nèi)容會(huì)被從文件中拷貝到內(nèi)存中。
DYNAMIC:此類(lèi)型表明本段指明了動(dòng)態(tài)連接的信息。
NOTE:本段指向了一個(gè)以”null”結(jié)尾的字符串,這個(gè)字符串包含一些附加的信息。
[3]. 對(duì)于可裝載的段來(lái)說(shuō),其 虛擬地址 和 文件地址 的值至少要向內(nèi)存頁(yè)面大小對(duì)齊。此數(shù)據(jù)成員指明本段內(nèi)容如何在內(nèi)存和文件中對(duì)齊。如果該值為 0 或 1,表明沒(méi)有對(duì)齊要求;否則,此值應(yīng)該是一個(gè)正整數(shù),并且是 2 的冪次數(shù)。虛擬地址 和 文件地址 在對(duì)此值取模后應(yīng)該相等。
Section to Segment mapping 展示了段包含哪些節(jié),常關(guān)注的是代碼段和數(shù)據(jù)段。下表只是個(gè)典型的例子,一個(gè)實(shí)際的更復(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é)類(lèi)型為 NOBITS,此值仍可能非0,但無(wú)意義 |
Type | 本節(jié)的類(lèi)型 | [1] |
EntSize | 如果節(jié)是表類(lèi)型,則表示表項(xiàng)大小 | 單位 Byte |
Address | 如果需要映射到虛擬內(nèi)存,則表示起始地址 | 單位 Byte |
Offset | 本節(jié)第一個(gè)字節(jié)所在文件的偏移 | 單位 Byte |
Flags | 權(quán)限等屬性信息 | |
Link | 略 | 可以查閱章節(jié)1提到的《UnderstandingELF.pdf》 |
Info | 略 | 可以查閱章節(jié)1提到的《UnderstandingELF.pdf》 |
Align | 大小對(duì)齊信息 |
[1]. 節(jié)有以下類(lèi)型
NULL:本節(jié)頭是一個(gè)無(wú)效的(非活動(dòng)的)節(jié)頭
PROGBITS:本節(jié)所含有的信息是由程序定義的,本節(jié)內(nèi)容的格式和含義都由程序來(lái)決定。
SYMTAB:所有符號(hào)表調(diào)試信息,strip 可以干掉,不影響運(yùn)行
STRTAB:本節(jié)是字符串表。
RELA:本節(jié)是一個(gè)重定位節(jié)。
HASH/GNU_HASH:本節(jié)包含一張哈希表。
DYNAMIC:本節(jié)包含的是動(dòng)態(tài)連接信息。
NOTE:本節(jié)包含的信息用于以某種方式來(lái)標(biāo)記本文件。
NOBITS:這一節(jié)的內(nèi)容是空的,節(jié)并不占用實(shí)際的空間。
REL:本節(jié)是一個(gè)重定位節(jié)。
DYNSYM:動(dòng)態(tài)鏈接符號(hào)表信息
3.1.4 符號(hào)表(.symtab 節(jié))
符號(hào)表不屬于文件結(jié)構(gòu),但因?yàn)榉浅3S?,也單?dú)拎出來(lái)講。
以一段簡(jiǎn)單的代碼舉例子:
#includeintmain(intargc,char**argv) { printf("helloworld"); return0; }
編譯出 hello 的 bin 之后,我們通過(guò)以下命令查看 .symtab 和 .dynsym
readelf-shello Symboltable'.symtab'contains67entries: Num:ValueSizeTypeBindVisNdxName ... 51:00000000000000000FUNCGLOBALDEFAULTUNDprintf@@GLIBC_2.2.5 ... 61:00000000000006b039FUNCGLOBALDEFAULT14main ...
字段 | 含義 | 備注 |
---|---|---|
Num | 編號(hào) | |
Value | 符號(hào)的值 | [1] |
Size | 符號(hào)大小 | 單位 Byte |
Type | 符號(hào)類(lèi)型 | NOTYPE:無(wú)類(lèi)型,OBJECT:數(shù)據(jù)對(duì)象,例如變量、數(shù)組,F(xiàn)UNC:函數(shù),F(xiàn)ILE:文件符號(hào),SECTION:本符號(hào)與一個(gè)節(jié)相關(guān)聯(lián),用于重定位 |
Bind | 表示符號(hào)的優(yōu)先級(jí)和作用范圍 | LOCAL:本地符號(hào),在所屬".o"文件外無(wú)效,GLOBAL:全局符號(hào),WEAK:弱符號(hào),例如__attribute__((weak)) 標(biāo)注的弱函數(shù) |
Vis | 略 | |
Ndx | 指定相關(guān)聯(lián)的節(jié)[2] | 數(shù)字:節(jié)在節(jié)頭表中的索引,ABS:絕對(duì)值常量,常指文件路徑,UND:未定義的,常指需要外部鏈接的函數(shù)、變量等 |
Name | 符號(hào)名字 |
[1]. 在".o" 中,如果 Ndx 不是 UND,則此值表示字符在所在節(jié)中的偏移量,在可執(zhí)行文件和共享庫(kù)文件中,則表示虛擬地址
[2]. 任何一個(gè)符號(hào)表項(xiàng)的定義都與某一個(gè)“節(jié)”相聯(lián)系,因?yàn)榉?hào)是為節(jié)而定義,在節(jié)中被引用。
更多符號(hào)表的介紹,請(qǐng)看 3.2.6 章節(jié)。
3.2 一些關(guān)鍵的節(jié)
3.2.1 "編譯 + 節(jié)" 與 "鏈接 + 段"
在 《linux 目標(biāo)文件(*.o) bss,data,text,rodata,堆,?!罚╤ttps://blog.csdn.net/sunny04/article/details/40627311) 有張圖非常形象的描述了 text、data、bss 段的作用,如下:
從上圖可以看出,代碼放在 text 段,已經(jīng)初始化的變量放在 data 段,未初始化的變量放在 bss 段。詳細(xì)下文再描述,在此章節(jié),我們需要知道一個(gè)概念:
所有變量、函數(shù)等都是一個(gè)符號(hào),還有字符串、固定的數(shù)據(jù)等,在編譯的時(shí)候會(huì)按功能、是否初始化等分類(lèi)放到特定的節(jié)中。
例如 .bss 節(jié)記錄了所有未初始化的變量,.text 節(jié)保存了所有的二進(jìn)制代碼,.rodata 節(jié)保存了所有只讀的數(shù)據(jù)。
所以我們編譯(不含鏈接)出來(lái)的 ".o" 目標(biāo)文件,其實(shí)就是對(duì)單個(gè) ".c/.cpp" 源碼的符號(hào)、數(shù)據(jù)按不同節(jié)分類(lèi)放好。
這是編譯,那么鏈接成可執(zhí)行程序的時(shí)候呢?鏈接雖然涉及多個(gè) ".o",其實(shí)也就把相同的節(jié)合并,然后歸類(lèi)到段中放好。
此節(jié)也就很粗獷的描述,砍掉了所有枝葉,只是為了更好的更直觀的理解主干,實(shí)際情況肯定更復(fù)雜。
我們編譯、鏈接的時(shí)候,其實(shí)可以主動(dòng)限制某些符號(hào)、數(shù)據(jù)放到哪些節(jié)、哪些段的,也可以自己新建個(gè)節(jié)、段,這屬于另一個(gè)大的課題了。大多時(shí)候我們也用不上,采用默認(rèn)的即可。
下面,我們來(lái)看一些關(guān)鍵的節(jié)及其內(nèi)容。
3.2.2 .text
前文一直有帶出來(lái),.text 節(jié)實(shí)際就是我們說(shuō)的代碼段,保存了一系列的可執(zhí)行二進(jìn)制指令。
.text 節(jié)的內(nèi)容是會(huì)占 ROM 空間,并在運(yùn)行時(shí)拷貝到 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 的變量。通常是指用來(lái)存放程序中未初始化的全局變量和未初始化的局部靜態(tài)變量。
.bss 節(jié)只在運(yùn)行時(shí),才會(huì)從內(nèi)存分配空間,因此近乎不占 ROM 空間。
例如下面示例的兩個(gè)變量都會(huì)保存到 bss。
intg_int1; intg_int2=0;
對(duì)上述的兩個(gè)變量,我們還是用 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)初始化的變量,通常是指用來(lái)存放程序中已初始化的全局變量和已初始化的靜態(tài)變量的一塊內(nèi)存區(qū)域。
既然已經(jīng)初始化,在文件中肯定要記錄初始化的值,因此 .data 節(jié)需要占 ROM 空間的,且相對(duì) .rodata 節(jié)來(lái)說(shuō),.data 節(jié)的內(nèi)容是可變的,因此運(yùn)行前需要拷貝到內(nèi)存中可寫(xiě)的區(qū)間。
objdump 對(duì)查看數(shù)據(jù)類(lèi)型的節(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 對(duì)變量賦了非0的初始值,就不適合放到 .bss 段了,因此放到了 .data 段。從 dump 的結(jié)果也可以看到變量 g_int2 以及值 0x1 。
3.2.5 .rodata
.rodata 節(jié)的數(shù)據(jù)是會(huì)占 ROM 空間的,且(大多時(shí)候)在運(yùn)行時(shí)拷貝到內(nèi)存中。
.rodata 存的是只讀數(shù)據(jù),比如字符串常量,全局const變量 和 #define定義的常量。例如:char *p = "123456", 123456 的字符串就存放在 rodata 節(jié) 中。還有一個(gè)有意思的例子:
printf("thefuncis%s ",__func__);
"the func is %s " 這個(gè)格式化打印字符串以及 __func__ 所指代的函數(shù)名,也是字符串常量,保存到 .rodata 節(jié)中的。
查看字符串類(lèi)型的節(jié),用 readelf 就方便多了,例如以下代碼:
intmain(intargc,char**argv) { printf("helloworld"); printf("thisfuncis%s",__func__); }
執(zhí)行以下命令可以打印字符串?dāng)?shù)據(jù),如果需要看二進(jìn)制數(shù)據(jù),例如整型的值,把 -p 改為 -x 即可。
$readelf-p.rodatamain Stringdumpofsection'.rodata': [4]helloworld [10]thisfuncis%s [20]main
這里有個(gè)小 Tips,相同的字符串只會(huì)保留 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 都是符號(hào)表,例如函數(shù)、變量等都是符號(hào),甚至 .dynsym 是 .symtab 的子集,但是他們的作用不太一樣。
.symtab,俗稱(chēng)的符號(hào)表,記錄了所有符號(hào),不管是自己定義的變量、函數(shù),還是未定義需要?jiǎng)討B(tài)庫(kù)提供實(shí)現(xiàn)的所有符號(hào)。在 ”.o" 鏈接時(shí)必須存在,但鏈接成 bin 后就可去掉節(jié)省空間,例如 strip
.dynsym,動(dòng)態(tài)鏈接才需要的符號(hào)表,即可包括對(duì)外提供調(diào)用的符號(hào),也包括需要外面提供實(shí)現(xiàn)的符號(hào)。.dynsym 在 ".so" 或者動(dòng)態(tài)鏈接的 bin 里是必須的。
.symtab 和 .dynsym 是占 ROM 空間的,.dynsym 會(huì)加載進(jìn)內(nèi)存,但 .symtab 不會(huì)。我們也可以通過(guò) strip 去掉符號(hào)表,然后通過(guò) file 判斷符號(hào)表是否已經(jīng) striped,例如下面的例子:
$gcctest.c-otest $filetest test:ELF64-bitLSBsharedobject,...,notstripped $striptest test:ELF64-bitLSBsharedobject,...,stripped
4. 利用工具解析 ELF
在上文的示例中頻繁使用 readelf 和 objdump 來(lái)讀取各種頭表和節(jié)內(nèi)容,除了這兩個(gè)之外,還有一個(gè) nm 工具,3者的功能非常相近。吃多嚼不爛,咱們以 readelf 為主,以 objdump 為輔講解如何用工具解析 ELF 。
上文已有示例,本文主要做個(gè)匯總記錄,方便將來(lái)查閱。字段的具體含義,可以查看章節(jié)3中具體的節(jié)解析。
獲取 ELF 文件頭
readelf-h
獲取程序頭表(段表)
readelf-l
獲取節(jié)表(獲取有哪些節(jié))
readelf-S
獲取符號(hào)表(列出函數(shù)、變量符號(hào))
#獲取所有符號(hào)表(含.symtab和.dynsym) readelf-s#獲取動(dòng)態(tài)符號(hào)表 readelf--dyn-syms
獲取節(jié)內(nèi)容
#打印節(jié)中的字符串,常用于含字符串類(lèi)型的節(jié),例如.rodata節(jié) readelf-p#以二進(jìn)制打印節(jié),常用于非字符串類(lèi)型的節(jié),例如.bss,.data節(jié) readelf-x #以匯編打印二進(jìn)制代碼 objdump-d-j
5. ELF 在磁盤(pán) Vs. ELF 加載到內(nèi)存
在 《完全剖析 - Linux虛擬內(nèi)存空間管理》(https://cloud.tencent.com/developer/article/1835295) 一文有張圖畫(huà)出了虛擬內(nèi)存的空間分布情況,如下:
我們關(guān)注最底下3個(gè)區(qū)間,其實(shí)跟 ELF 的內(nèi)容是能對(duì)應(yīng)上的。用一張新圖來(lái)表示兩者的關(guān)系:
可執(zhí)行文件的 代碼段、數(shù)據(jù)段等會(huì)拷貝到內(nèi)存中,BSS 段雖然沒(méi)數(shù)據(jù),但也記錄了有哪些變量,會(huì)拷貝到內(nèi)存可寫(xiě)區(qū)域,而動(dòng)態(tài)庫(kù)是 map 到 mmap 區(qū)的。
審核編輯:劉清
-
處理器
+關(guān)注
關(guān)注
68文章
19286瀏覽量
229852 -
BIN文件
+關(guān)注
關(guān)注
0文章
26瀏覽量
8291 -
elf
+關(guān)注
關(guān)注
0文章
12瀏覽量
2187
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論