本文出處: www.jollen.org
已取得原作者授權使用
#1: Segment 的觀念
「Program Loading」的議題在討論「如何將程式載入記憶體」,以便後續的「執行」。在「ELF(Executable and Linking Format)格式教學文件」第 1~8 篇文章裡,我們了解基本的 ELF 觀念,並建立所謂的「節區」知識。
本系列日記「ELF 之 Program Loading 教學文件」將會介紹有關程式載入(program loading)的核心主題,在此之前,請先閱讀 Jollen 的「Executable and Linking Format」專欄,以基本基礎的先備知識。
Segments
由 "execution view" 的角度來看程式(即 process),所謂的節區(section)已經被進化成區段(segment)的觀念了。廣義來說,section 可被分為以下 3 種 segment:
- Text segment - 指存放唯讀(read-only)程式碼與資料的所有 section 。
- Data segment - 指存放可寫(writable data)程式碼與資料的所有 section。
- BSS segment - 即 .bss section。
另外,還有一類的 segment:
- Dynamic segment - 用來存放 dynamic linking 資訊的 section。
不過,當我們講到「process segment」時,指的是 text segment、data segment 與 BSS segment;process address space 就是由這三種 segment 所構成。
Text segment 是由以下的 section 組成:
- .text
- .rodata
- .hash
- .dynsym
- .dynstr
- .plt
- .rel.got
Data segment 是由以下的 section 組成:
- .data
- .dynamic
- .got
- .bss
依照 SystemV ABI 規格,我們看到 .bss section 被放到 data segment 裡,但嚴格來說,我們應把 .bss section 另外獨立出來討論。基本的 .bss section 觀念,可參考 Jollen 的「BSS Section Concepts」專欄。
此外,在 text segment 與 data segment 裡,包含了 4 個與 dynamic linking 有關的 section,整理如下圖。
與 dynamic linking 有關的 4 個 section 並非另外組成所謂的 dynamic segment,這只是我們在研究 dynamic linking 時邏輯上的用語。
到這裡,我們了解了一個相當重要的一個觀念:當程式載入後,是以 "segment" 的觀念存在於記憶體中,並且也是以 segment 的觀念被 Linux kernel 管理。ELF 執行檔被載入到記憶體的過程中,最重要的資訊就是前導專欄裡所提到的「Program Header Table」;就 ELF execution view 來說,program header table 是必要的,下篇日記會針對此議題做介紹。
#2: Program Header Table
了解系統行為的研究方法,我認為有效的步驟應分成三個階段來進行。
初次入門:一開始進行研究時,因為對於系統的基本觀念還不夠完備,因此「學中做、做中學」成了最有效率的入門方式,透過「概念的實作」與「實作讀到的概念」的過程,最可以幫助我們在短時間內掌握重要的核心知識。以 ELF 專欄為例,在入門時期,我用了 loader 0.1~0.5 共 5 個小範例來陳述 ELF 的格式以及 section 的觀念。
掌握觀念:首先發表一個自己的看法。「將所有理論或概念全部動手實作一遍」,在我看來,並不是很有效率的辦法,這種做法應當很有幫助,但是卻會延緩學習速度,因此,這個時 期的重點如果是在「通盤掌握整體的重要關鍵」,那麼「善用工具來做分析」自然是最有成 效的方式。以「工具來操作並驗證觀念」是建議的做法,另外一個理由是,這種做法比較能貼近實務面。在「ELF 之 Program Loading 教學文件」的日記裡,我將會以此做法來分享教學文件。
思考與研究:這個階段有點像是「實驗室」的做法,在這裡不再贅述。
Program Header Table
這裡有幾個重要的觀念:
1. Program header table 是程式要能執行的重要資訊,program header table 紀錄 ELF image 裡的 'segment' 分佈,請參考 Jollen's Blog「ELF 之 Program Loading 教學文件, #1: Segment 的觀念」的說明。
2. 對 dynamic loader/linker 來說,ELF 的 'section' 是通透性的(transparent),也就是在整個載入的過程裡,dynamic loader/linker 並不會知道他所載入的 'segment' 與實際的 section 有何關係。整個載入過程只讀取 program header table 所紀錄的 'segment'。
3. 也就是說,section header table 在此時期是用不到的,就算沒有 section header table 也不影響程式載入與執行;這個觀念可參考 Jollen's Blog「「Truncate It」小技倆的原始碼與原理」。
使用 readelf 來觀察執行檔的 program header table:
# readelf -l helloElf file type is EXEC (Executable file)Entry point 0x8048278There are 6 program headers, starting at offset 52Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x000c0 0x000c0 R E 0x4
INTERP 0x0000f4 0x080480f4 0x080480f4 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x00408 0x00408 R E 0x1000
LOAD 0x000408 0x08049408 0x08049408 0x00100 0x00104 RW 0x1000
DYNAMIC 0x000414 0x08049414 0x08049414 0x000c8 0x000c8 RW 0x4
NOTE 0x000108 0x08048108 0x08048108 0x00020 0x00020 R 0x4
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
03 .data .dynamic .ctors .dtors .jcr .got .bss
04 .dynamic
05 .note.ABI-tag
Program header table 的 data structure 如下(/usr/include/elf.h):
/* Program segment header. */typedef struct{Elf32_Word p_type; /* Segment type */Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;
'readelf' 的列表與 program header table 的 data structure 相符,每個 field 所紀錄的資訊可參考註解說明,或是參照完整的 System V ABI 文件。
幾個重要資訊說明如下。
1. Segment 的個數與大小紀錄在 ELF 檔頭裡:
# readelf -h helloELF Header:Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048278
Start of program headers: 52 (bytes into file)
Start of section headers: 1784 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 6
Size of section headers: 40 (bytes)
Number of section headers: 25
Section header string table index: 24
2. Program header table 的 p_type 欄位必須先進行研究(/usr/include/elf.h):
/* Legal values for p_type (segment type). */#define PT_NULL 0 /* Program header table entry unused */#define PT_LOAD 1 /* Loadable program segment */#define PT_DYNAMIC 2 /* Dynamic linking information */#define PT_INTERP 3 /* Program interpreter */
#define PT_NOTE 4 /* Auxiliary information */
#define PT_SHLIB 5 /* Reserved */
#define PT_PHDR 6 /* Entry for header table itself */
#define PT_TLS 7 /* Thread-local storage segment */
#define PT_NUM 8 /* Number of defined types */
#define PT_LOOS 0x60000000 /* Start of OS-specific */
#define PT_GNU_EH_FRAME 0x6474e550 /* GCC .eh_frame_hdr segment */
#define PT_LOSUNW 0x6ffffffa
#define PT_SUNWBSS 0x6ffffffa /* Sun Specific segment */
#define PT_SUNWSTACK 0x6ffffffb /* Stack segment */
#define PT_HISUNW 0x6fffffff
#define PT_HIOS 0x6fffffff /* End of OS-specific */
#define PT_LOPROC 0x70000000 /* Start of processor-specific */
#define PT_HIPROC 0x7fffffff /* End of processor-specific */
標示紅色的部份是我們在 'readelf -l' 列表中所看到的 type。
3. p_flags 的意義也要先行了解(/usr/include/elf.h):
/* Legal values for p_flags (segment flags). */#define PF_X (1 << 0) /* Segment is executable */#define PF_W (1 << 1) /* Segment is writable */#define PF_R (1 << 2) /* Segment is readable */#define PF_MASKOS 0x0ff00000 /* OS-specific */
#define PF_MASKPROC 0xf0000000 /* Processor-specific */
Segment Permissions
Segment permission 是 p_flags 的組合,在 'readelf -l' 列表中則是相對應於 Flg 一欄。
Segment Contents
Segment 由哪些 section 所構成,同樣可由 'readelf -l' 列表的「Section to Segment mapping」部份觀察。
小結
以下表格整理目前為止所得到的結論。
Segment | Sections | Type | Flg |
00 | PHDR | PF_R + PF_X | |
01 | .interp | INTERP | PF_R |
02 | .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame | LOAD | PF_R + PF_X |
03 | .data .dynamic .ctors .dtors .jcr .got .bss | LOAD | PF_R + PF_W |
04 | .dynamic | DYNAMIC | PF_R + PF_W |
05 | .note.ABI-tag | NOTE | PF_R |
下則日記將會針對 segment type 所討論。
#3: Segment Type 與 Kernel Space Loader
Segment Type
Program loading 時期會處理以下 4 種 segment:
#define PT_LOAD 1 /* Loadable program segment */#define PT_DYNAMIC 2 /* Dynamic linking information */
#define PT_INTERP 3 /* Program interpreter */
#define PT_PHDR 6 /* Entry for header table itself */
簡要說明這 4 種 segment 的作用如下。
PT_LOAD:即 text segment 或 data segment。
PT_DYNAMIC:若 ELF 有此 segment,則表示 .dynamic section 會獨立成一個實體的 segment。PT_DYNAMIC 僅包含 .dynamic section。
PT_INTERP:紀錄 program interpreter 路徑與檔名的 segment。PT_INTERP 僅包含 .interp section。
PT_PHDR:Program header table segment。
Segment type 是很重要的資訊,kernel 的 ELF loader 與 program interpreter(ld.so)都是透過 segment type 來判斷 segment 的用途。
Kernel 與 program interpreter 對不同的 segment 都有不同的處理方式。
另外,PT_NOTE 並不是一個必要的 segment,實際上它也沒有重要的作用,因此忽略不做討論。
更多有關 Kernel ELF Loader
回到 Jollen's blog「Process Creation」專欄所談論 到的「Program 載入流程」裡,相當重要的一個觀念就是 "exec" system call。程式的載入涉及 exec systegm call service,exec system call 是 machine-dependent 的實作,以 x86 來說,此服務實作於 linux/arch/i386/kernel/process.c 檔案;在 SystemV ABI 規格中,program loading 的章節 被撰寫在 'processor-specific' 文件裡。
在目前所討論的 ELF loading 主題中,我們還看不到與 processor 相關的議題,例如 .got/.plt 節區的用途;相關的技術細節在此較不適合做深入討論,未來會以分享一份技術簡報的方式來呈現,並且以 ARM 做為討論標地。
Exec system call 叫用 kernel space 的 ELF loader 載入 ELF image,此觀念請參考 Jollen's Blog「Process Creation, #6:Exec System Call 的觀念」。我們整理過 kernel 的 ELF loader 所做的工作有:
1. 讀取 ELF image 的 segment 資訊(由 program header table)。
2. 讀出 segment 內容。
3. 判斷是否有 program interpreter;若有,再讀取 program interpreter。
4. 將讀取到的 segment 內容取代掉 current 的內容,這個動作就是 exec system call 的重要特性。
項目 1. 是由 ELF image 的 program header table 分析執行檔的 segment 資訊,並判斷 segment type;ELF loader 會依照 segment 的類型來做不同的處理,說明如後(項目 2.)。
項目 3. 則是在找到 PT_INTERP segment 時,呼叫 interpreter loader 將 interpreter 載入。Program interpreter 我們先前曾提過,就是 ld.so(dynamic loader/linker),這個部份將在下一篇日記再做說明。
PT_INTERP
若 ELF loader 判斷此 segment 為 PT_INTERP,則叫用 interpreter 的 loading 來做載入的工作,在標準的 GNU/Linux 系統底下,由於 program interpreter 也是 ELF 格式,因此叫用 kernel 的 load_elf_interp() API(fs/binfmt_elf.c)將 program interpreter 讀取並載入。
PT_LOAD
若 ELF loader 判斷此 segment 為 PT_LOAD,表示這是 text segment 或 data segment,也就是最主要的程式碼與資料區段,此時便透過 do_mmap() 來將 text/data segment mapping 到記憶體。
依照 ELF 規格,text segment 會由 virtual address 0x0804_8000 的地方開始 mapping,data segment 則是緊接在 text segment 之後。
另外,text segment 與 data segment 都是 PT_LOAD 類型,因此需要根據 Flg 欄位的屬性來知道該 segment 是 text segment 還是 data segment。詳見前一篇日記有關 segment permissions 的說明。
小結
總結本專欄到目前為止所得到的資訊如下。灰色部份是 interpreter 負責的工作,將在下篇日記討論。
請注意,本日記是針對 kernel 本身所做的 ELF loading 工作來做整理,即 kernel-space 的 program loading。另外一半的 program loading 是由 user-space 的 dynamic loader/linker 所完成,這裡所提的 dynamic loader/linker 就是我們不斷提到的 program interpreter。
Segment | Type | Kernel or Program Interpreter |
00 | PHDR | Interpreter 用來計算 base address 用。 |
01 | INTERP | Kernel ELF loader 用來載入 interpreter,並交由 interpreter 做 shared library 的 dynamic linking。 |
02 | LOAD | Kernel 將此 segment mapping 為新的 text segment 或 data segment。 |
03 | LOAD | Kernel 將此 segment mapping 為新的 text segment 或 data segment。 |
04 | DYNAMIC | 由 interpreter 處理 |
#4: Program Loader 整體流程
Program loader 的整體流程如下:
1. 使用者在 shell 模式下執行外部程式(stored program)。
2. shell 以 fork+exec system call 的方式執行外部程式。
3. 透過 0x80H 軟體中斷(x86)叫用 kernel 的 exec(sys_execvp)system call service。
Kernel Space Program Loader4. Exec system call 呼叫 program loader(ELF loader),將 process image(ELF image)載入。5. Program loader 找到 PT_INTERP segment。6. Program loader 將 PT_LOAD segment mapping 為新的 text/data segment,text segment 由 vaddr. 0x0804_8000 開始,data segment 緊接其後。7. Program loader 呼叫 interpreter loader 將 program interpreter(ld.so)載入,並 mapping 到 process memory;interpreter 的 text segment 由 vaddr. 0x4000_0000 開始,interpreter 的 data segment 緊接其後。8. Program loader 將 BSS segment 準備好。 9. Program loader 將 process 的 register %eip(user-mode)修改為 program interpreter 的進入點;並將 %sp 設定為 user mode 的 stack。 |
10. 所以任何 shared library 的外部程式,一開始都是由 program interpreter 開始執行!
User Space Program Loader / Linker11. Program interpreter 會找到 process 所需的 shared library(名稱及其路徑)。12. Program interpreter 使用 mmap system call 將 shared library mapping 到 process memory,以完成整個 process image 的建立。13. 更新 shared library 的符號表。14. Program interpreter 'jump' 到 process 的進入點(紀錄在 ELF header 裡的 entry point)。 |
15. 真正開始執行程式!
步驟 1.~6. 已在以下的日記介紹過:
- 2007.03.08: Process Creation, #6:Exec System Call 的觀念
- 2007.03.05: ELF 之 Program Loading 教學文件, #1: Segment 的觀念
- 2007.03.08: ELF 之 Program Loading 教學文件, #2: Program Header Table
- 2007.03.09: ELF 之 Program Loading 教學文件, #3: Segment Type 與 Kernel Space Loader
步驟 7.~15. 將會在後續的日記再做整理。以整體的 program loader 流程來說,可分為 kernel-space 與 user-space 二大階段:
- 步驟 4.~9. 由 kernel 的 ELF loader 所負責。
- 步驟 11.~14 由 user-space 的 program interpreter(dynamic loader/linker,也就是 /lib/ld-linux.so.2)所負責。
Program interpreter 需要 dynamic segment 裡的相關資訊來完成後續的工作,此部份日後再做整理分享。
來源: Jollen’s Blog
–jollen
0 comments:
Post a Comment