[英]What does `rep ret` mean?
我在 Visual Studio 2008 上測試了一些代碼並注意到security_cookie
。 我能理解它的意思,但我不明白這個指令的目的是什么。
rep ret /* REP to avoid AMD branch prediction penalty */
當然我可以理解評論:) 但是這個前綴 exaclty 在ret
上下文中做什么,如果ecx
是 != 0 會發生什么? 顯然,當我調試它時,來自ecx
的循環計數被忽略,這是意料之中的。
我找到的代碼在這里(由編譯器注入以確保安全):
void __declspec(naked) __fastcall __security_check_cookie(UINT_PTR cookie)
{
/* x86 version written in asm to preserve all regs */
__asm {
cmp ecx, __security_cookie
jne failure
rep ret /* REP to avoid AMD branch prediction penalty */
failure:
jmp __report_gsfailure
}
}
有一個以這條指令命名的完整博客。 第一篇文章描述了其背后的原因: http : //repzret.org/p/repzret/
基本上,AMD 的分支預測器中存在一個問題,即單字節ret
立即跟在您引用的代碼(以及其他一些情況)中的條件跳轉之后,解決方法是添加rep
前綴,該前綴被忽略CPU 但修復了預測器懲罰。
顯然,當分支的目標或失敗是ret
指令時,一些 AMD 處理器的分支預測器表現不佳,添加rep
前綴可以避免這種情況。
至於rep ret
的含義, Intel 指令集參考中沒有提到這個指令序列, rep
的文檔也不是很有幫助:
當與非字符串指令一起使用時,REP 前綴的行為是未定義的。
這至少意味着rep
不必以重復的方式行事。
現在,來自AMD 指令集參考(1.2.6 重復前綴):
前綴只能與此類字符串指令一起使用。
一般來說,重復前綴應該只用於上面表 1-6、1-7 和 1-8 中列出的字符串指令[不包含 ret]。
因此,這看起來確實像是未定義的行為,但可以假設,在實踐中,處理器只是忽略了ret
指令上的rep
前綴。
正如 Trillian 的回答所指出的那樣,當ret
是分支目標或遵循條件分支(作為失敗目標)時, AMD K8 和 K10 存在分支預測問題。 那是因為ret
只有 1 個字節長。
repz ret:為什么這么麻煩? 有一些關於特定微架構原因的額外細節,為什么這會給 K8 和巴塞羅那帶來困難。
避免 1 字節ret
作為可能的分支目標:
AMD 的 K10(巴塞羅那)優化指南在這些情況下建議使用 3 字節ret 0
,這會從堆棧中彈出零字節並返回。 該版本明顯比英特爾上的rep ret
差。 具有諷刺意味的是,在后來的 AMD 處理器(Bulldozer 及以后)上,它也比rep ret
更糟糕。因此,沒有人改變使用基於 AMD 家族 10 優化指南更新的ret 0
是一件好事。
處理器手冊警告說,未來的處理器可能會以不同的方式解釋前綴和它不會修改的指令的組合。 這在理論上是正確的,但沒有人會制造出無法運行大量現有二進制文件的 CPU。
默認情況下,gcc 仍然使用rep ret
(沒有-mtune=intel
或-march=haswell
或其他東西)。 所以大多數 Linux 二進制文件在它們的某個地方都有一個repz ret
。
一旦 K10 徹底過時,gcc 可能會在幾年內停止使用rep ret
。 再過 5 年或 10 年,幾乎所有二進制文件都將使用更新的 gcc 構建。 又過了 15 年,CPU 制造商可能會考慮將f3 c3
字節序列重新用作不同指令(的一部分)。
仍然會有使用rep ret
遺留閉源二進制文件,這些二進制文件沒有更新的可用版本,但有人需要繼續運行。 因此,無論f3 c3 != rep ret
屬於f3 c3 != rep ret
新功能,都需要禁用(例如,使用 BIOS 設置),並讓該設置實際更改指令解碼器行為以將f3 c3
識別為rep ret
。 如果傳統二進制文件的向后兼容性是不可能的(因為它無法在功率和晶體管方面高效地完成),那么 IDK 您會看到什么樣的時間范圍。 遠遠超過 15 年,除非這是僅適用於部分市場的 CPU。
所以使用rep ret
是安全的,因為其他人已經在這樣做了。 使用ret 0
是個壞主意。 在新代碼中,再使用rep ret
幾年可能仍然是個好主意。 可能沒有太多的 AMD PhenomII CPU 仍然存在,但它們足夠慢,沒有額外的返回地址預測錯誤,或者問題是。
成本相當小。 在大多數情況下,它最終不會占用任何額外的空間,因為它通常后面跟着nop
填充。 但是,在它確實導致額外填充的情況下,最壞的情況是需要 15B 的填充才能到達下一個 16B 邊界。 在這種情況下,gcc 只能按 8B 對齊。 (使用.p2align 4,,10;
如果需要 10 個或更少的 nop 字節,則對齊到 16B,然后使用.p2align 3
始終對齊到 8B。使用gcc -S -o-
將 asm 輸出生成到 stdout 以查看何時它這樣做。)
因此,如果我們猜測 16 分之一的rep ret
最終會創建額外的填充,其中ret
剛好達到所需的對齊,並且額外的填充達到 8B 邊界,這意味着每個rep
的平均成本為 8 * 1/ 16 = 半字節。
rep ret
的使用頻率不足以將任何事物相加。 例如,firefox 及其映射的所有庫只有大約 9k 個rep ret
實例。 所以這大約是 4k 字節,跨越許多文件。 (而且 RAM 比這少,因為動態庫中的許多函數從未被調用。)
# disassemble every shared object mapped by a process.
ffproc=/proc/$(pgrep firefox)/
objdump -d "$ffproc/exe" $(sudo ls -l "$ffproc"/map_files/ |
awk '/\.so/ {print $NF}' | sort -u) |
grep 'repz ret' -c
objdump: '(deleted)': No such file # I forgot to restart firefox after the libexpat security update
9649
這計算了 firefox 映射的所有庫中的所有函數中的rep ret
,而不僅僅是它曾經調用過的函數。 這在一定程度上是相關的,因為跨函數的代碼密度較低意味着您的調用分布在更多的內存頁面上。 ITLB 和 L2-TLB 只有有限數量的條目。 本地密度對 L1I$(和英特爾的 uop 緩存)很重要。 無論如何, rep ret
影響非常小。
我花了一分鍾才想到一個原因,即進程的所有者無法訪問/proc/<pid>/map_files/
,但/proc/<pid>/maps
卻可以訪問。 如果 UID=root 進程(例如,來自 suid-root 二進制文件) mmap(2)
sa 0666 文件位於 0700 目錄中,則執行setuid(nobody)
,任何運行該二進制文件的人都可以繞過由於缺少x for other
而施加的訪問限制目錄的x for other
權限。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.