文章目錄
爲什麼要有elf格式的文件?
1)可執行程序要解決的問題
- 一句話回答,要解決怎麼讓被調用者把自己加載到內存,並執行自己代碼段的問題。
如果沒有源碼中的僞指令指示,彙編器把第一條指令碼的地址設置成0,之後的代碼和數據以此爲準進行計算。 - BIOS引導操作系統時,首先加載引導盤的前512字節(MBR)到和MBR約定的內存地址0x7c00,然後pc指向0x7c00開始執行代碼。彙編器在編譯MBR的程序時,按照約定把第一條指令碼的地址設置成0x7c00,之後的代碼和數據地址的計算,都是指令碼在文件中的實際偏移加上0x7c00,以此得到。這種思路可以解決運行可執行程序的運行問題,但是有個缺點,調用者必須事先知道被調用程序期望被加載到的內存地址。如果所有程序都用這種解決方法,那麼需要額外提供一張各個程序運行地址的表格,每調用一個程序,就去這個表格裏面查找其期望加載到的內存地址和其它信息。無疑,這個方法可以解決問題,但是需要多維護一張描述程序的元數據表格。
- 爲了不維護這張表,有一種方法是把元數據寫到被調用程序的頭部,調用者和被調用者約定讀取這個頭部信息的規則,告訴調用者通過怎麼樣的方式可以找到我期望被加載的內存地址。elf格式的二進制文件就是這樣做的。
2)沒有elf格式文件的世界
- 重新認識Linux進程地址空間中通過bios加載軟盤固件,解釋了沒有elf格式的文件時,程序調用怎麼實現。
3)小結
- elf二進制程序比裸的二進制程序或者固件,多出了程序被加載、執行時需要的元數據。這些數據都放在文件的頭部,讀取這些程序,調用者和被調用者可以約定各種各樣的規則,elf程序的規則就是其中一種。
編譯源碼生成elf格式文件,到底對文件做了什麼?
1)一個裸的二進制程序長什麼樣?
- 源代碼mbr.asm,這段程序沒有什麼具體功能,就是在屏幕中打印一段"Hello, OS World"
org 07c00h ; 告訴編譯器程序加載到7c00處
jmp 07c0h:DispStrOff
code:
times 10 db 0
; never reach here
DispStrOff equ $ - $$
DispStr:
mov edx, code ; 取code標號的地址給edx,測試彙編器計算地址
; 如果沒有第一條org指令,nasm計算得到的code標號地址=0+jmp指令長度,
; 如果有第一條org指令,nasm計算得到的code標號地址=07c00h+jmp指令長度
mov ax, BootMessage
mov bp, ax ; ES:BP = 串地址
mov cx, 16 ; CX = 串長度
mov ax, 01301h ; AH = 13, AL = 01h
mov bx, 000ch ; 頁號爲0(BH = 0) 黑底紅字(BL = 0Ch,高亮)
mov dl, 0
int 10h ; 10h 號中斷
jmp $
BootMessage: db "Hello, OS world!"
-
利用匯編器將其彙編成二進制程序
nasm -o mbr.bin mbr.asm -
反彙編查看其內容
ndisasm -o 0x7c00 mbr.bin
-
xxd查看文件實際內容
xxd -u -a -g 1 -c 16 mbr.bin
-
對比反彙編代碼和文件的數據,兩個是一樣的。裸的二進制程序包含的僅僅是二進制格式的代碼指令,這個程序能跑起來嗎?可以的,參見重新認識intel段機制尋址的實驗。
2)一個ELF格式的對象文件長什麼樣?執行nasm -f的時候我們在做什麼?
- 對上面的源碼稍加改動
[SECTION .s16]
[BITS 16]
global _start
_start:
jmp 07c0h:OffDispStr
OffDispStr equ $ - $$
DispStr:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
mov ax, BootMessage
mov bp, ax
mov cx, 16
mov ax, 01301h
mov bx, 000ch
mov dl, 0
int 10h
jmp $
BootMessage: db "Hello, OS world!"
times 510-($-$$) db 0
dw 0xaa55
......
-
使用
nasm -f elf mbr.asm -o mbr.o
將源碼編譯成elf格式的可重定向文件,反彙編查看其代碼段內容objdump -D mbr.o
和實際的彙編代碼有點兒不一樣,沒關係,這是由於[section .16]
告訴編譯器把彙編代碼編譯成16bit 寄存器模式的二進制碼,而objdump -D 是按照32bit 寄存器模式來反彙編二進制碼,所以不大一樣。我不知道怎麼讓objdump 按照16bit反彙編二進制程序,但nasm可以設置,所以下面的方法,可以正確反彙編出elf文件格式中的代碼段- 通過
readelf -S mbr.o
找到elf文件中.s16段在mbr.o中的位置和長度,off=0x130=304,size=0x200=512
- 將.s16段的數據拷貝到文件
dd if=mbr.o ibs=1 skip=304 of=mbr.s16 seek=0 count=512
,然後反彙編ndisasm -b 16 mbr.s16 | head -n 20
,得到反匯編出來的代碼
- xdd查看文件實際內容,
xxd -u -a -g 1 -c 16 mbr.o
,紅色方框內的數據是前一步反彙編的代碼,也是objdump反彙編代碼所用的數據。可以看到,ELF文件除了包含.s16這段代碼彙編出的數據,還有其它的數據。這些其它的數據,就是描述這段程序的元數據。查看ELF的規範,可以進一步讀懂這些元數據表達的意思。
- 通過
3)ELF規範
對照elf文件格式的規範手冊,分析這個數據
- 頭部信息總體圖
elf文件元數據包括4個部分:- ELF header
- 爲達到程序可以被執行的目的,可以設計各種不同的元數據格式規範,elf只是其中一種格式,爲了區分其它格式的文件,elf header的第一個字段時magic,固定不變。
- elf文件有三個用途,一做可執行程序直接加載到內存運行、二做可重定向程序和別的重定向程序一起組成可執行程序、三做共享庫程序,用來鏈接成重定向程序或者動態鏈接到內存中的其它進程中。elf header中設計了e_type字段用於區分這些不同用途的程序。
- elf設計目標是可以在不同架構下運行。elf header中設計了e_machine字段用於指明這個二進制程序在哪個架構下運行。
- elf文件元數據除了header還有其它部分。elf header還作爲路標,提供找到其它元數據的地址。
- Program Header Table
- elf程序最終目的是被加載到內存,告訴被調用者怎樣把自己加載到內存,加載到什麼地址,拷貝多長的數據,這些是elf存在的意義,program header table就提供這個信息。
- 這類信息對系統加載一個可執行程序中有用,對系統鏈接一個可重定向文件沒有,因此這部分內容可能爲空,當header中的e_type是ET_EXEC時,文件是個可執行的二進制程序,這段信息存在。當header中的e_type時ET_REL時,文件是個用於重定向的對象文件,這段信息不需要,可以爲空。
-
- Sections
- ELF全稱Executable and Linkable Format,除了爲被調用者提供加載執行程序的信息,還有一個特點是提供可鏈接的信息,Section的設計就是爲鏈接器提供這些信息。
- 手寫的彙編的源代碼可以有自己定義的section,分別用來存放代碼或者數據。但高級語言編譯後的彙編程序,代碼和數據混雜在一起,需要統一整理,可以將可執行的代碼放在一個section,未初始化的數據放在一個section,常量放在一個section,最後生成可重定位的對象文件。鏈接程序在處理這些文件時,就可以把section當做基本操作單位,將不同對象文件的同類型的section放在一起,組成segment,並對segment進行地址綁定,告訴被調用這這段segment期望加載到的內存地址。
- Section Header Table
- section可以有不同的作用,用於放代碼的section,用於放數據的section,一個section有多長,它在elf文件的什麼位置,這些都需要元數據去描述,Section Header Table就是這個作用。
- ELF header
4)可重定向文件元數據分析實例
下面是mbr.o的elf頭部元數據分析,對照elf header 和section header,可以理解其含義
-
elf header
-
section header
-
elf 頭部元數據
-
日常應用中不可能通過查看二進制數據分析元數據,elf提供了readelf工具解析這個頭部。可以對照驗證上面的分析。
- 讀取elf header
readelf -h mbr.o
- 讀取program header table
readelf -l mbr.o
,由於是用於重定向的對象文件,這部分數據爲空
- 讀取section header table
readelf -S mbr.o
- 讀取elf header
5)一個ELF格式的二進制程序長什麼樣?執行ld的時候我們在做什麼?
- 執行
ld -o mbr mbr.o -Tmbr.ld
生成可執行的elf文件,mbr.ld內容。
這段代碼的意思只有一個:把所有輸入的對象文件(例子中只有一個mbr.o)中的.s16 section集合起來,統一放到.boot section中,將.boot section的期望內存加載地址設置成0。並設置elf文件的入口點爲_start標號處的指令。
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)
SECTIONS
{
. = 0;
.boot : {*(.s16)}
. = ASSERT(. <= 512, "Boot too big!");
/* For Load Memory Address test
* . = 0x10;
* .boot : {*(.s16)}
**/
}
- elf文件比對象文件多出了什麼?有什麼不同?
elf因爲要提供程序加載信息,所以肯定多出了programe header table,elf文件是對1個及以上對象文件section的重新安排並設置加載地址,所以輸出文件中的section都由加載地址(LMA),例子中LMA=0。
- 改變LMA 意味着什麼?
LMA是elf期望調用者加載自己到內存的地址,如果調用者不按這個期望值加載會有什麼後果?換句話說,改變LMA,elf程序會有什麼變化?- 對ld鏈接腳本做如下改動,同時在源代碼中添加一句獲取符號的mov指令
1)mbr.ld
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)
SECTIONS
{
. = 0x10;
.boot : {*(.s16)}
}
2)mbr.asm
[SECTION .s16]
[BITS 16]
global _start
_start:
jmp 07c0h:OffDispStr
OffDispStr equ $ - $$
DispStr:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
mov eax, DispStr ; for test
mov ax, BootMessage
- 重新編譯生成mbr,查看其section內容
readelf -S mbr
,addr 地址變了,.boot section在文件中的偏移也變了,seciont大小沒變
- 拷貝.boot section的數據
dd if=mbr ibs=1 skip=4112 of=mbr.s16 seek=0 count=512
並反彙編。如果程序第一條指令地址按照0來算,DispStr
標號的地址應該是jmp
指令的下一條指令地址0x5
,但mov eax, DispStr
語句被彙編成了mov eax,0x15
,可見,ld會以LMA爲依據,重新計算源代碼中的標號值。如果源代碼中有位置相關的語句,那麼調用者就必須按照elf給定的LMA加載這個程序,否則程序會執行錯誤,反之,如果源代碼中所有語句都位置無關,那麼調用者就可以忽略elf中的LMA地址,隨便加載程序到某段內存執行。
實驗源碼見 my github gdb調試elf程序
執行一個elf程序,系統做了什麼?
/* TODO */