說明
LLVM 的陰影呼叫堆疊功能是一種編譯器模式,可強化產生的程式碼,防範堆疊破壞攻擊,例如濫用緩衝區過度執行錯誤。
上方連結的 Clang/LLVM 說明文件頁面說明瞭配置。封裝摘要是指函式傳回地址絕不會從一般堆疊中重新載入,而只會從獨立的「陰影呼叫堆疊」中重新載入。這屬於額外的堆疊,但並不是包含每個函式所需的大小的整個堆疊框架,而是在每個記錄的呼叫框架中僅包含一個地址字詞,只是傳回地址。由於陰影呼叫堆疊是獨立於其他堆疊或堆積區塊之外配置,而這些區塊很少使用,但其指標很少,因此不太可能有某些緩衝區超出或釋放後使用會在記憶體中覆寫回傳地址,導致程式傳回攻擊者的指令。
陰影呼叫堆疊和安全堆疊檢測配置與 ABI 相關且相近,但也有關係。每個函式都可單獨啟用或停用,供任何函式使用。Fuchsia 的編譯器 ABI 和 libc 一律會與使用或未採用任一檢測設備建構的程式碼互通,無論檢測作業在特定 libc 版本中為何都不考慮。
互通性與 ABI 效果
一般來說,陰影呼叫堆疊不會影響 ABI。機器專屬的呼叫慣例維持不變。它可以在使用陰影呼叫堆疊建構的程式中使用某些函式,有些則否。合併兩者來自於直接編譯的 .o
檔案、封存程式庫 (.a
檔案) 或共用程式庫 (.so
檔案) 的任何組合。
雖然每個執行緒都有一些額外的狀態 (陰影呼叫堆疊指標,請見下文),但未使用遮蔽呼叫堆疊的程式碼不需要針對此狀態執行任何作業,可在呼叫或由使用安全堆疊的程式碼呼叫時保持正確狀態。其中唯一可能的例外狀況,是程式碼實作自己的非本機離開或情境切換 (例如協同程式)。Zircon C 程式庫的 setjmp
/longjmp
程式碼會自動儲存並還原此額外狀態,因此即使呼叫 setjmp
和 longjmp
的程式碼並不知道陰影呼叫堆疊,任何以 longjmp
為基礎的項目都能正確處理所有內容。
如果是 AArch64 (ARM64),x18
註冊機制在 ABI 中通常已保留為「已修正」。根據預設,如果程式碼不知道 ABI 的陰影呼叫堆疊擴充功能,只要從未碰到 x18
,就能與陰影呼叫堆疊 ABI 互通。
其他架構尚未支援此功能。
用於 Zircon 和福奇亞
Aarch64 (ARM64) 的 Zircon 同時在核心和使用者模式程式碼中支援陰影呼叫堆疊。Clang 編譯器可透過 -fsanitize=shadow-call-stack
指令列選項啟用這項功能。aarch64-fuchsia
(ARM64) 目標預設為啟用。如要針對特定編譯停用此功能,請使用 -fno-sanitize=shadow-call-stack
指令列選項。
與安全堆疊一樣,陰影呼叫堆疊大小並無獨立的設施。相反地,在舊版 API (例如 pthread_attr_setstacksize
) 中為「堆疊」指定的大小,而 ABI (例如 PT_GNU_STACK
) 會做為「每個」類型堆疊的大小。由於不同類型的堆疊會根據特定程式行為,以不同的比例使用,因此您無法根據傳統的單一堆疊大小選擇陰影呼叫堆疊大小。因此,每種堆疊的範圍都不大,取決於經過調整的「unitary」堆疊大小,所預期的最糟情況。雖然這看似浪費資源的情況,但這只會稍微浪費:在最糟的情況下,一個頁面就會根據各種堆疊類型浪費掉一個頁面,而且頁面表格也會耗用更多位址空間給不曾存取的網頁。
導入作業詳細資料
支援陰影呼叫堆疊程式碼的基本功能是「陰影呼叫堆疊指標」。這是供全域使用的登錄,例如傳統堆疊指標。但每個呼叫框架會推送並彈出單一傳回位址字詞,而非如一般堆疊框架中的任何資料。
對於 AArch64 (ARM64),x18
註冊會保存函式項目中的陰影呼叫堆疊指標。陰影呼叫堆疊透過遞增後的語意向上成長,因此 x18
一律會指向下一個免費運算單元。編譯器絕對不會碰觸註冊器,但會濺起並重新載入回傳地址暫存器 (x30
,又稱 LR)。Fuchsia ABI 規定 x18
必須隨時包含有效的陰影堆疊指標。也就是說,「一律」必須有效將新位址推送至 x18
的陰影呼叫堆疊 (模數堆疊溢位)。
低層級和組合程式碼的注意事項
大部分程式碼甚至不需要考慮陰影呼叫堆疊問題,即使在組合中也是如此。呼叫慣例維持不變。對堆疊 (和/或不安全的堆疊) 的所有使用行為在有或沒有陰影呼叫堆疊時相同;啟用框架指標後,回傳地址會按預期儲存在影格指標旁的機器堆疊上。針對 AArch64 (ARM64),函式呼叫仍會照常使用 x30
做為傳回地址,不過 Crybber x30
的函式可以選擇外洩並重新載入使用其他記憶體。在理想情況下,組件寫入的非分葉函式應透過外洩及重新載入返回地址註冊 (而非機器堆疊) 來使用陰影呼叫堆疊 ABI。
主要例外狀況是程式碼會實作某些項目,例如非本機離開或內容切換。這類程式碼可能需要儲存或還原陰影呼叫堆疊指標。longjmp
函式和 C++ throw
均已直接處理這個問題,因此使用這些建構函式的 C 或 C++ 程式碼不需要執行任何新功能。
導入某些新類型非本機結束或內容切換功能的新程式碼,必須處理陰影呼叫堆疊指標的方式,類似處理傳統機器堆疊指標註冊和不安全的堆疊指標的方式。這類程式碼都應使用 #if __has_feature(shadow_call_stack)
在編譯期間進行測試,指出在特定建構作業中是否使用陰影呼叫堆疊。這個預先處理工具的建構作業可用於 C、C++ 或組件 (.S
) 來源檔案。