2015年2月19日 星期四

[翻譯] Linux 二進位檔中的特殊區段(section)

  • 原文標題:Special sections in Linux binaries
  • 原文網址:http://lwn.net/Articles/531148/
  • 原文作者:Daniel Pierre Bovet
  • 原文發表時間:2013 年 01 月 03 日

譯註:
  • 標題中的 Linux 指的是 Linux 核心。
  • 本文內容與圖片皆自原網址修改。
  • 根據核心的釋出時間,我推論原文應該是使用 3.7 版核心做為依據,因此在相關資料查詢時也都使用 3.7 版做為探索的依據。

↓↓↓↓↓↓ 正文開始 ↓↓↓↓↓↓

一個區段(section)是目的檔(object file)中的一塊區域,包含了對於連結(link,譯註 1)有用的資訊:程式的執行碼、資料、重定位資訊(relocation information)和更多東西。事實表明了,Linux 核心存在某些額外的區段型態,稱為「特殊區段」(special section),它們被用來實作各樣的核心功能(kernel feature),由於特殊區段並不為人所熟知,所以這裡值得對這個主題做點詳細說明。

節區(segment)與區段(section)譯註 2


儘管 Linux 支援許多二進位檔格式,但 ELF(Executable and Linking Format)在設計時所提供的靈活性和可擴充性,再加上不受限於任何處理器與架構的優點,使得它成為了最受歡迎的格式。一個 ELF 二進位檔包含了一個 ELF 標頭(ELF header),其後跟著幾個節區;每個節區依次包含一個或多個區段,而且每個節區和區段的長度都會在 ELF 標頭中指明;大部分的節區,因此也適用於多數的區段,都有一個初始位址(initial address),同樣也會在 ELF 標頭中指明。此外,每個節區都有各自的存取權限。

連結器(linker)會把輸入的目的檔中,凡是相同類別的的區段,合併成單一的區段,並指派初始位址給它,舉例來說,所有目的檔中的 .text 區段會被合併成單一的 .text 區段,這是一個預設包含程式中所有執行碼的區段。GNU 載入器(GNU loader)則會利用某些定義在 ELF 二進位檔中的節區來指派具有特定存取權限的記憶體區塊給行程(process)。

一般所謂的執行檔都含有四個典型的區段,根據慣例,各自名為 .text.data.rodata.bss。區段 .text 包含了可執行碼並且包裹進一個具有讀取和執行權限的節區;而區段 .data.bss 則對應包含了已初始化和未初始化的資料,並包裹進一個具有讀取與寫入權限的節區。

無論應用程式被載入幾次, Linux 只會將區段 .text 載入記憶體一次,這麼做減低了記憶體耗用和啟動時間(launch time),而由於執行碼並不會變更,所以這麼做是安全的;基於同樣的理由,包含了已初始化唯讀資料的區段 .rodata ,也會包裹進含有區段 .text 的同一個節區;區段 .data 則因為包含了在應用程式執行時會變動的資料,所以這個區段必須為每個不同程式實體(instance)進行複製。

readelf -S」命令列出執行檔所含有的區段;而「readelf -l」命令則列出執行檔所含有的節區。

定義一個區段


區段到底在哪宣告的?如果你去觀察一個標準的 C 程式,你不會在其中找到任何與區段相關的東西;然而,如果你去觀察 C 程式的組語版本,你會找到數個定義區段起始的組譯指引(assembly directive),更準確地說,「.text」、「.data」和「.section rodata」這些指引標明了先前提到的三個典型區段的起始,而「.comm」這個指引則定義了未初始化資料所屬的區域。

GNU C 編譯器(GNU C compiler)會將一份原始碼轉換成等價的組合語言檔;下一步則由 GNU 組譯器(GNU assembler)接手,進一步產生目的檔,這個檔案是一個 ELF 可重定位檔案(ELF relocatable file),其中只包含了區段(而節區,因為具有絕對位址,所以無法定義在可重定位檔案之中)。此刻,除了僅具有一個長度與其相關聯的區段 .bss 之外,節區已然在目的檔中填充好了。

組譯器會掃描每行組合語言,將它們轉譯成二進位碼,並將二進位碼插入區段中,每個區段有各自的位移值(offset)告訴組譯器下一個位元組要插在哪裡,而組譯器一次只作用在一個區段上,稱之為當前區段(current section)。在某些情況下,例如替未初始化的全域變數配置記憶體時,組譯器並不會真的在當前區段中加入位元組,僅是遞增其位移值。
組合語言程式是分開各自組譯的,組譯器會假定目的檔的開始位址總是為 0。GNU 連結器(GNU linker)以這樣的一群目的檔做為輸入,然後將它們結合成單一的可執行檔,這樣的連結叫做靜態連結(static linkage),因為它是在程式運行之前進行的。

連結器依賴於所謂的連結器腳本(linker script),來判定對可執行檔中的各個區段指派哪個位址。想取得系統上的預設腳本,可以執行命令:


特殊區段


如果把一個單純的可執行檔(假設產生自 helloworld.c)中出現的區段,跟 Linux 核心執行檔中出現的區段比較一下,會發現 Linux 依靠許多不存在於常見可執行檔之中的特殊區段。這種區段的數量取決於硬體平台,在 x86_64 系統上有超過 30 個定義好的特殊區段;而在 ARM 系統上則有 10 個左右。

你可以利用 readelf 命令來從 vmlinux (就是核心執行檔) 的 ELF 標頭中取出資料,當你在 x86_64 機台上執行這個命令,會取得某些東西,就像這個樣子:


定義一個 Linux 特殊區段


特殊區段定義在 Linux 連結器腳本(Linux linker script)中,這是一個與先前提到的預設連結器腳本有所不同的連接器腳本,其相對應的原始檔儲存在特定於架構的子目錄樹之中的 kernel/vmlinux.ld.S,這個檔案使用一組定義於標頭檔 linux/include/asm_generic/vmlinux.lds.h 的巨集。

用於 ARM 硬體平台的連結器腳本包含了特殊區段的一個易於參考的定義譯註 3


特殊區段 __ex_table 對齊於四個位元組的倍數。此外,連結器創建了一對識別符(identifier),取名為 __start___ex_table__stop___ex_table,並設定它們的位址落在 __ex_table 的開頭與結尾,這些識別符必須以 extern 進行宣告,因為它們是在連結器腳本中定義的譯註 4

因此定義與使用特殊區段可以總結如下:
  • 連同做為界限的識別符對一起,在 Linux 的連結器腳本中定義該特殊區段「.special」。
  • 在 Linux 程式碼中插入組譯指引 .section .special,指明直到下個 .section 組譯指引前的所有位元組,都插入到 .special
  • 在核心內,用該對識別符來操作那些位元組。
這個技巧聽起來似乎只適用於組合語言碼;幸運的是,GNU C 編譯器提供使用非標準的建構符(construct)attribute 來創建特殊區段譯註 5。舉例來說,一個


這樣子的宣告,會告訴編譯器跟在其後的程式碼必須插入到區段 .init.data;為了提升程式碼的可讀性,核心定義了某些適宜的巨集,例如巨集 __initdata,就定義得像這樣:


一些例子


如同在前面 readelf 回應結果中看到的,所有的特殊區段到最後,都會被包裹到某個定義在 vmlinux 的 ELF 標頭中定義的某個節區,並各自實現特定的目的。下方所列,根據儲存的資料類別,對一些 Linux 的特殊區段做了分類,在每當需要的時候,Linux 程式碼中,取代特殊區段名字,而用來指參(refer)區段的巨集名字也會提及。
  • 二進位執行碼
    只在 Linux 初始化期間調用的函式,會以 __init 宣告並放在區段 .init.text;一旦系統已經初始化,Linux 會利用該區段的定界符(delimiter),來釋放配置給區段的頁框(page frame)。

    而以 __sched 宣告的函式則會插入到特殊區段 .sched.text,因此它們將會被在讀取檔案 /proc/PID/wchan 時呼叫的 get_wchan() 函式略過譯註 6,對於任何行程 PID,如果有的話,此檔案含有使其受到阻擋(block)的函式名稱(參見〈WCHAN the waiting channel〉取得更多細節),此區段的定界符圈定了一連串要略過的位址。以函式 down_read() 為例,因為它不能對阻擋行程的事件,給予任何有幫助的資訊,所以它才以 __sched 宣告。
     
  • 已初始化的資料
    只在 Linux 初始化期間使用的全域變數,會以 __initdata 宣告並放在區段 .init.data;一旦系統已經初始化,Linux 會利用該區段的定界符,來釋放配置給區段的頁框。

    巨集 EXPORT_SYMBOL() 讓做為參數傳入的識別符可以被核心模組(kernel module)存取;而識別符的字串則儲存在區段 __ksymtab_strings
     
  • 函式指標
    為了在初始化階段調用 __init 函式,Linux 提供了廣大的一組巨集(定義在 <linux/init.h>),module_init() 就是一個知名的例子;這些函式各個都會將做為參數傳入的函式指標,放到一個 .initcalli.init 區段(__init 函式群組成了數個階級),在系統初始化時,Linux 會利用區段定界符來依次呼叫所有指向的函式。
     
  • 成對的硬體指令指標(instruction pointer)
    巨集 _ASM_EXTABLE(addr1, addr2) 允許分頁錯誤例外處理程序(page fault exception handler)判定一個例外的引發,是否因為在位址 addr1 的核心硬體指令,嘗試讀寫行程位址空間(process address space)的任何位元組所導致,如果確實如此,核心會跳到含有修復程式碼(fixup code)addr2;要不然,就觸發 kernel oops。區段 __ex_table 的界定符(參見先前的連結器腳本範例)則設定了,用來傳遞位元組到/自使用者空間的關鍵核心硬體指令的範圍。
     
  • 成對的位址
    早前提到的巨集 EXPORT_SYMBOL() 在特殊節區 ksymtab(或是 ksymtab_gpl)中,插入的同樣也是一對位址:識別符本身的位址,和在 ksymtab(或 ksymtab_gpl)中相對應字串常數的位址。在與模組(module)進行連結時,這個經由 EXPORT_SYMBOL() 所填充的特殊區段,讓核心得以使用二分搜尋法(binary search),來判定模組中以 extern 宣告的識別符,是否屬於匯出符號(exported symbol)的集合。
     
  • 相對位址
    在 SMP(symmetric multiprocessing,對稱式多處理器)系統上,巨集 DEFINE_PER_CPU(type, varname) 會把未初始化的全域變數 varname 插入到特殊區段 .data..percpu,而這種放在該區段的變數就叫做 per-CPU 變數(per-CPU variable,有人譯為「每 CPU 變數」),因為 .data..percpu 存放在一個初始位址設為 0 的節區中,所以 per-CPU 變數的位址是相對位址,在系統初始化階段,Linux 會配置一塊夠大的記憶體區塊,用來儲存 NR_CPUS 個 per-CPU 變數群組,而區段界定符則用來決定一個群組的大小。
     
  • 結構
    核心的 SMP 備選方案(SMP alternatives)機制允許單一的核心在構建時,對於一個給定的處理器架構的多個版本,都是最佳化的;在系統的處理器能夠執行先進硬體指令的時候,且僅在這種時候,通過開機期執行碼修補(boot-time code patching)的魔法,使得這些先進硬體指令能被利用。這個機制由巨集 alternative() 來控制:


    此巨集首先在常規區段 .text 儲存 oldinstr;之後在特殊區段 .altinstructions 儲存一個結構,含有下列欄位:oldinstr 的位址、newinstr 的位址、旗標 featureoldinstr 的長度和 newinstr 的長度;而 newinstr 則被儲存在特殊區段 .altinstr_replacement。在開機流程的早期,每個被運作中 CPU 所支援的備選指令,都會直接修補到載入的核心映像檔(kernel image)當中;如果有需要,它會填充不做任何事的硬體指令(no-op instruction)。
除了 __ksymtab__ksymtab_strings,還有其他額外的特殊區段是為了處理模組而引進的;*.ko 形式的核心目的檔(kernel object)具有 ELF 可重定位格式(ELF relocatable format),在這種檔案的 ELF 標頭中定義了一對特殊區段,叫做 .modinfo.gnu.linkonce.this_module;不像靜態之核心的特殊區段,這兩個區段是無位址的(address-less),因為核心目的檔並沒有包含節區。

區段 .modinfomodinfo 命令用來顯示關於核心模組的資訊,該區段中儲存的資料不會載入核心位址空間(kernel address space)。而特殊區段 .gnu.linkonce.this_module 含有一個結構 module,其中包含了模組的名字,還有些別的欄位;在插入模組的時候,系統呼叫(system call)init_module() 會從這個特殊區段,將該結構 module 讀入動態記憶體的區塊。

結論


雖然特殊區段也能定義在應用程式內,但毫無疑慮,核心開發者在如何利用它們這方面,已經非常地有創意,然而實際上,上面所列的例子絕對不是全部,並而且在新進的核心釋出版(kernel release)裡也一直冒出新的特殊區段。缺乏了特殊區段,要實作某些像上面那些的核心功能,將會變得頗為困難。

↑↑↑↑↑↑ 正文結束 ↑↑↑↑↑↑

譯註 1:這裡指的是目的檔之間的連結。

譯註 2:關於 segment 與 section 的翻譯,其實有很多種說法,這裡只是取其中一種我覺得比較合適的。

譯註 3:這裡指的是 arch/arm/kernel/vmlinux.lds.S 第 225 行

譯註 4:舉例而言,就像在 kernel/extable.c 第 35 行,有這樣的程式碼:

譯註 5:詳細規定可參考 GCC Manual 中的〈6.33 Attribute Syntax〉、〈6.31 Declaring Attributes of Functions〉和〈6.38 Specifying Attributes of Variables〉。

譯註 6:以 x86_64 為例,可以在 arch/x86/kernel/process_64.c 找到:

沒有留言 :

張貼留言