天天看點

技術文章:Linux系統的可執行檔案,ELF格式

作者:底層技術棧

ELF檔案,是Linux系統上的可執行檔案(類似windows上的exe檔案)。

ELF的全稱是可執行與可重定位檔案。

在Linux上,不管是編譯完的目标檔案(.o檔案),還是連接配接後的可執行檔案,都是elf格式。

ELF在設計時同時相容了目标檔案、可執行檔案、動态庫三種場景。

ELF檔案的構成,分為如下幾個部分:

1,ELF檔案頭,

它是ELF檔案的“總目錄”,管理着整個檔案的資料該怎麼解釋。

ELF檔案頭的資料結構,如下圖:

技術文章:Linux系統的可執行檔案,ELF格式

ELF檔案頭

幾個重要的項目已經加了注釋。

它的前16個位元組是ELF檔案的“魔數”:0x7f,‘E’,'L','F'。

它是ELF檔案的識别碼,Linux系統就是靠這幾個字元來判斷是不是ELF檔案的。

e_type:

指的是ELF檔案的類型,包括目标檔案ET_REL、可執行檔案ET_EXE、動态庫ET_DYN三種。

e_machine:

指的是CPU類型,常用的也隻有3種:EM_386、EM_ARM、EM_X86_64,分别是32位的x86、ARM、和64位的x86_64。

當然也支援一些其他型号的CPU,但并不常見。

e_entry:

指的是程式的入口位址,Linux系統把程式加載到記憶體之後,就跳轉到這個位址運作,然後才會調用main()函數。

如果有電腦病毒修改了e_entry,就可以讓linux系統先運作病毒代碼了。

e_phoff:

程式頭表的檔案偏移量,即離檔案的第1個位元組有多少位元組。

按照C語言的慣例,檔案第1個位元組的檔案偏移量是0。

程式頭,是Linux系統加載可執行檔案和動态庫時使用的,在目标檔案裡沒有程式頭。

e_shoff:

“節頭表”的檔案偏移量,也是離檔案第1個位元組的位元組數。

“節頭”,是程式具體資料位置的索引。

例如資料段.data在哪裡,代碼段.text在哪裡,都要檢視對應的“節頭”。

e_phnum和e_shnum:

分别是程式頭和“節頭”的個數。

e_shstrndx:

節名字元串的索引,它表示節名字元串是(所有節裡的)哪個節,一般情況下是最後一個節。

這一項也是很重要的,通過它才可以知道每個節的名字是什麼,才可以知道每個節是做什麼的。

2,程式頭,

在可執行檔案和動态庫加載時,作業系統根據程式頭的資訊把代碼和資料放在合适的記憶體位置,然後程式才可以正常運作。

例如有個全局變量a,代碼裡有一行a = 10,要給a指派必須知道它準确的記憶體位置。

這個記憶體位置是在連接配接時确定的,在加載時也必須保持同樣的記憶體鏡像才可以,否則這個10就不知道指派到哪裡去了。

程式頭的作用,就是告訴Linux系統把資料段加載到哪裡,把代碼段加載到哪裡,讓代碼可以 正确地讀寫資料。

技術文章:Linux系統的可執行檔案,ELF格式

程式頭

3,“節頭”,

“節頭”,顧名思義,是每個節的頭[呲牙]也就是每個節的資料的索引資訊。

每個“節”的内容都是程式的代碼、資料、符号表、或調試資訊,它們在ELF檔案裡的存放位置就是通過節頭管理的。

節,英文是section。

在ELF的文檔裡一般用sh表示節頭,section header的縮寫。

技術文章:Linux系統的可執行檔案,ELF格式

節頭

sh_name:

表示“節”的名字,但它隻是給出在(節名)字元串表裡的偏移量,字元串是以0結尾的,這樣才可以找到真正的名字。

之是以不直接給出節名,是為了把字元串集中存放在一個連續的區域,以節省空間。

sh_type:

表示“節”的類型,例如重定位節、符号表、字元串表、調試資訊,等等。

代碼段、資料段也是“節”的一種,它們都需要加載到記憶體,差別隻是讀、寫、執行的權限不同。

代碼段是隻讀的,資料段不可執行。

當程式動态連接配接時需要的PLT表和GOT表,也是一個節。它們也需要加載到記憶體,并且Linux的加載器會修改GOT表,讓它指向動态庫裡的函數位址。

動态連接配接還是比較複雜的,哪天單獨再說吧。

sh_entsize:

當節的資料内容是一個數組時,sh_entsize表示每項的大小。

例如,符号表就是一個數組,每一項就是一個符号(函數或全局變量),每項的大小都是固定的,它們的“節頭”裡就有這一項sh_entsize。

如果用不到這一項,則填為0。

sh_offset和sh_addr:

每個節的資料實際上有2個位址:一個是資料在檔案裡的位址(sh_offset),一個是資料要加載到記憶體裡的位址(sh_addr)。

這兩個位址不是一樣的,而且記憶體裡的位址一般還有對齊要求,但檔案裡不需要對齊。

sh_link和sh_info:

這兩項的具體解釋需要根據“節”的類型來。

例如,重定位節重定位的是函數或全局變量的位址,而函數或全局變量的資訊又在符号表裡,是以重定位節要關聯到符号表:

這樣就可以知道它重定位的符号表是哪一個“節”,這個節的索引号就寫在這兩項裡。

4,資料區,

資料區,是程式真正的代碼、資料、符号表、重定位資訊、調試資訊等有效資料。

剛編譯完的檔案并不能直接運作,因為裡面的函數、全局變量的位址都不是真實位址,而隻是給連接配接器使用的重定位資訊。

這些重定位資訊,必須被連接配接器計算出真實位址來進行填充之後,才可以運作。

符号表,是一個記錄函數和全局變量的名稱及位址的表格。

連接配接器會讀取符号表,我們平時檢視動态庫包含哪些函數時也會讀取它。

編譯器在生成機器碼之後,需要按照ELF格式把二進制代碼和資料寫入檔案裡,然後用連接配接器把函數和全局變量處理成真實位址,就可以被Linux系統加載運作了。

scf編譯器架構的ELF檔案格式、連接配接器代碼、gdb調試資訊的實作,都在scf/elf目錄。

繼續閱讀