前言
-
本篇繼續研究 musl libc ldso 的動態加載過程中遇到的關鍵性的概念:到底要加載ELF 文件的哪些內容到 內存
-
當前如果遇到 ELF 動態加載,當前系統需要有【文件系統】,并且有較大的內存,因為 ELF 文件是無法直接運行的,首先通過解析 ELF 頭部 獲取入口函數,把需要載入到內存中的文件內容復制到指定內存區域,然后執行ELF 的入口函數,通常不是 ELF的 main 函數,而是更早的執行函數,如
_start
或者_dlstart
函數。此時 PC 指針指向 ELF 加載的基地址 + ELF 入口函數。
ELF 加載基地址
- 一個 ELF 文件,是否可以隨意的加載?
當前驗證發現: ELF 文件包括我們通常見到的 可以執行的文件,以及 共享庫(如 xx.so)。共享庫沒有連接地址,基址是 0,但入口函數不一定是 0,如果遇到入口函數也是 0 的,需要注意這個 偏移地址 為 0 的入口函數,是否只是個空的符號,無法執行
-
為何有的 xxx.so 也稱作 ELF 文件? 比如 musl libc.so,本身是個 庫,但是它 有入口函數,并且可以執行。 當前 musl libc.so 確實如此,通常我們一般區分 執行文件與 庫,庫不用于執行。但是 musl libc.so 具備執行的功能,就像是我們見到的普通的執行文件,但是它依舊具備普通庫的功能,為其他動態編譯的應用程序提供共享庫。
-
作為 共享庫與可執行 集成在一起的 musl libc.so,基地址:0,入口函數不為0,基地址為0 可以重定位加載,如手動把 libc.so 加載到 0x200000 地址, 那么 libc.so 的入口函數就是: 0x200000 + libc.so 入口地址
-
普通靜態或者動態鏈接的ELF 文件,由于基地址 不為0,就無法手動加載到 隨意的地址。
-
如下 ELF 文件:基地址 0x200000,這個基地址跟鏈接腳本中的鏈接地址有關系,可以查看這個 elf 的連接腳本配置
-
入口點:入口地址,這個地址 已經是基于基地址 0x200000的,所以這個地址就不能隨機加載了。如果想改變 這個 elf 的基地址,需要更改 相應的 鏈接腳本 鏈接地址的設置
動態加載需要加載哪些 ELF 內容到內存
-
有的 ELF 文件特別的大,尤其是開啟了 【DEBUG】的,比如編譯時使用
-O0 -g
, gdb 的調試信息都加入 ELF 文件了, ELF 文件不同于單片機的燒寫文件 bin 文件,里面還有一些內容,如調試信息,是不需要加載到內存的,那么到底需要加載什么內容呢? -
這部分可以查看 Linux 內核代碼 elf 加載部分,如
linux-6.3.8/fs/binfmt_elf.c
中的load_elf_binary
-
Linux 系統由于默認支持 mmu,執行文件的 mmap 映射,所以沒有文件沒有使用常規的 內存分配,不過依舊是先把文件內容映射 到用戶地址空間,之所以不填充,是因為 Linux文件mmap 有缺頁異常機制,需要訪問時才會真正載入文件內容到內存,這樣有很多好處,開始只映射(占位子)不加載,這樣節省了加載時間,一個 ELF 文件,不可能上來全部執行到,可能只會執行部分內容,這樣采用 訪問時再加載,將會節省數量可觀的內存,節省大量的加載時間。加上文件 mmap 有 cache 功能,如果加載過后,緩存暫時不清掉,這樣下次執行就不再重復加載了。Linux 這個文件mmap 映射加載機制,對于 ELF 加載非常的有用。
-
經過熟悉 Linux 的
load_elf_binary
,發現只需要 加載PT_LOAD
段 -
那么 ELF 的
PT_LOAD
段,真的覆蓋 ELF 的所有需要加載到內存中的內容范圍嗎?有沒有漏下的?或者說 elf 不是還有 重定位、符號、.text
、.data
、等等嗎?這些包含在里面嗎? -
通過 elf 查看工具,加上對實際加載到內存的內容進行反向 dump 出來,肯定的一點就是: ELF 的
PT_LOAD
段 包含了所有需要加載到內存的文件內容,是所有,如果在其他的系統上,發現動態加載后, 內存中的文件內容不正確,或者部分內容為0,需要查看文件加載部分是否有處理不當的地方。
查看 PT_LOAD
段
- 可以使用
Die
這個工具,查看 ELF文件
-
這里了解到,
PT_LOAD
段 第一個段 文件偏移是 0,也就是把 ELF 文件頭部也加入了內存 -
兩個
PT_LOAD
段 的大小:Program 中的 p_filesz 就是當前的段大小,總大小: 0x23528 + 0x9f8 = 0x23f20,之所以這么計算,是因為 當前的兩個段 是連在一起的。 -
由于段有多個節(section),可以查看 節 信息,
-
通過 計算
PT_LOAD
段的總大小,知道 這個 elf 文件 前面0 ~ (0x23f20 -1)
,也就是 0x23f20 個字節已經加載到內存,剩下 的節,.bss
沒有實際內容,但內存中需要留位置,并且清 0。其他的節全部是 調試信息 debug 相關的。 -
所以通過加載
PT_LOAD
段,確實實現了整個 ELF 必需文件內容的全部加載
加載大小
-
這里需要提一下:段的加載大小,不是 段的 p_filesz,而是 段的 p_memsz, p_memsz 一般等于或者大于 p_filesz,超出的大小,就是
.bss
section 的大小,這部分大小需要手動清零,不清零,可能引發程序啟動后的異常,比如定義了一個變量,但是沒有初始化就使用,而程序員默認沒有初始化的變量會被初始化 為 0。 清零.bss
就是清零PT_LOAD
段 中p_memsz - p_filesz
大小的區域,這個區域的起始地址應該是:base +elf_ppnt->p_vaddr + elf_ppnt->p_filesz
,如果是靜態連接編譯的 elf 程序, base 是0,也就是elf_ppnt->p_vaddr + elf_ppnt->p_filesz
。elf_ppnt->p_vaddr
是這個文件段的起始地址。 -
這里需要提一下: 段的 p_offset,這個是相對文件本身的偏移,通過情況下,
p_offset
與p_vaddr
是相同的,但也有不相同的。所以在文件填充時,需要把 文件內容 偏移p_offset
后,讀取到內存地址p_vaddr
的位置,也就是說: 文件內容的存放位置 與 文件映射到內存的地址,并非一一對應。
小結
-
本篇注意講解一下 ELF文件在 動態加載時需要加載哪些內容到內存,注意這里的動態加載,是動態加載 ELF 文件,這個 ELF文件,不單是 動態編譯鏈接的 ELF,也包括靜態編譯鏈接的 ELF 以及 經常遇到的 動態共享庫 (xx.so)
-
需要熟悉 ELF 的 頭部、Program Header、了解 各個 Segment 段,了解 Section 節信息,這樣對理解 動態加載程序,熟悉 動態加載非常有用。
-
需要了解操作系統的進程、線程機制,文件映射 mmap 機制。注意需要反復確認 內存的文件內容是否正確、完整。可以同 dump 的方式,把內存中的文件內容 dump 成一個文件,然后與實際的文件進行內容對比。
-
需要深刻了解 文件段的本身的偏移 :p_offset 與 內存地址 p_vaddr 的關系,也需要了解 段真實文件大小 p_filesz 與 p_memsz 的關系,也就是
.bss
節的存在