6. 核心棧

6.1. x86-64 位上的核心棧

大部分文字來自 Keith Owens,由 AK 修改

x86_64 頁大小 (PAGE_SIZE) 為 4K。

與所有其他架構一樣,x86_64 具有每個活動執行緒的核心棧。 這些執行緒棧的大小為 THREAD_SIZE (4*PAGE_SIZE)。 只要執行緒處於活動狀態或殭屍狀態,這些棧就包含有用的資料。 當執行緒位於使用者空間時,核心棧是空的,除了底部的 thread_info 結構。

除了每個執行緒的棧之外,還有與每個 CPU 關聯的專用棧。 這些棧僅在核心控制該 CPU 時使用;當 CPU 返回到使用者空間時,專用棧不包含有用的資料。 主要的 CPU 棧有

  • 中斷棧。 IRQ_STACK_SIZE

    用於外部硬體中斷。 如果這是第一個外部硬體中斷(即不是巢狀的硬體中斷),則核心從當前任務切換到中斷棧。 就像 i386 上的分離執行緒和中斷棧一樣,這為核心中斷處理提供了更多空間,而無需增加每個執行緒棧的大小。

    中斷棧也用於處理軟中斷。

切換到核心中斷棧是由軟體基於每個 CPU 中斷巢狀計數器完成的。 這是必需的,因為 x86-64 “IST” 硬體棧無法在沒有競爭的情況下巢狀。

x86_64 還具有 i386 上不可用的功能,即能夠自動切換到新棧以處理指定的事件,例如雙重錯誤或 NMI,這使得在 x86_64 上處理這些不尋常的事件變得更加容易。 此功能稱為中斷棧表 (IST)。 每個 CPU 最多可以有 7 個 IST 條目。 IST 程式碼是任務狀態段 (TSS) 的索引。 TSS 中的 IST 條目指向專用棧;每個棧的大小可能不同。

IST 由中斷門描述符的 IST 欄位中的非零值選擇。 發生中斷並且硬體載入此類描述符時,硬體會自動根據 IST 值設定新的棧指標,然後呼叫中斷處理程式。 如果中斷來自使用者模式,則中斷處理程式序言將切換回每個執行緒的棧。 如果軟體希望允許巢狀的 IST 中斷,則處理程式必須在進入和退出中斷處理程式時調整 IST 值。(偶爾會這樣做,例如對於除錯異常。)

具有不同 IST 程式碼(即具有不同棧)的事件可以巢狀。 例如,除錯中斷可以安全地被 NMI 中斷。 arch/x86_64/kernel/entry.S::paranoidentry 在進入和退出所有 IST 事件時調整棧指標,理論上允許具有相同程式碼的 IST 事件巢狀。 但是,在大多數情況下,分配給 IST 的棧大小假定同一程式碼沒有巢狀。 如果該假設被打破,則棧將損壞。

當前分配的 IST 棧是

  • ESTACK_DF。 EXCEPTION_STKSZ (PAGE_SIZE)。

    用於中斷 8 - 雙重錯誤異常 (#DF)。

    當處理一個異常導致另一個異常時呼叫。 當核心非常混亂時發生(例如核心棧指標損壞)。 使用單獨的棧允許核心在許多情況下都能很好地從中恢復,從而仍然可以輸出 oops。

  • ESTACK_NMI。 EXCEPTION_STKSZ (PAGE_SIZE)。

    用於不可遮蔽中斷 (NMI)。

    NMI 可以在任何時間傳遞,包括當核心正在切換棧的中間。 將 IST 用於 NMI 事件可以避免對核心棧的先前狀態進行假設。

  • ESTACK_DB。 EXCEPTION_STKSZ (PAGE_SIZE)。

    用於硬體除錯中斷(中斷 1)和軟體除錯中斷 (INT3)。

    除錯核心時,除錯中斷(硬體和軟體)可以在任何時間發生。 將 IST 用於這些中斷可以避免對核心棧的先前狀態進行假設。

    為了正確處理巢狀的 #DB,存在兩個 DB 棧例項。 在 #DB 進入時,#DB 的 IST 棧指標切換到第二個例項,因此巢狀的 #DB 從乾淨的棧開始。 巢狀的 #DB 將 IST 棧指標切換到保護孔以捕獲三重巢狀。

  • ESTACK_MCE。 EXCEPTION_STKSZ (PAGE_SIZE)。

    用於中斷 18 - 機器檢查異常 (#MC)。

    MCE 可以在任何時間傳遞,包括當核心正在切換棧的中間。 將 IST 用於 MCE 事件可以避免對核心棧的先前狀態進行假設。

有關更多詳細資訊,請參閱 Intel IA32 或 AMD AMD64 架構手冊。

6.2. 在 x86 上列印回溯

關於 x86 堆疊跟蹤中函式名稱前出現的“?”的問題不斷湧現,這裡有一個深入的解釋。 如果讀者仔細閱讀 print_context_stack() 以及 arch/x86/kernel/dumpstack.c 及其周圍的整個機制,這將有所幫助。

改編自 Ingo 的郵件,訊息 ID:<20150521101614.GA10889@gmail.com>

我們總是掃描整個核心棧,以查詢儲存在核心棧上的返回地址 [1],從棧頂到棧底,並打印出任何“看起來像”核心文字地址的內容。

如果它適合幀指標鏈,我們會列印它而沒有問號,因為我們知道它是真實回溯的一部分。

如果地址不適合我們期望的幀指標鏈,我們仍然會列印它,但我們會列印一個“?”。 這可能意味著兩件事

  • 或者該地址不是呼叫鏈的一部分:它只是核心棧上的陳舊值,來自之前的函式呼叫。 這是常見的情況。

  • 或者它是呼叫鏈的一部分,但幀指標未在函式中正確設定,因此我們無法識別它。

這樣,無論幀指標是否正確設定,我們始終會打印出真實呼叫鏈(以及更多條目) - 但在大多數情況下,我們也會正確獲得呼叫鏈。 列印的條目嚴格按照棧順序排列,因此您也可以從中推斷出更多資訊。

此方法最重要的屬性是我們 _never_ 丟失資訊:我們始終努力列印棧上的 _all_ 地址,這些地址看起來像核心文字地址,因此如果除錯資訊不正確,我們仍然會打印出真實的呼叫鏈 - 只是問號比理想情況下更多。