[英]Special treatment of setjmp/longjmp by compilers
在為什么 volatile 適用於 setjmp/longjmp中,用戶greggo評論:
實際上,現代 C 編譯器確實需要知道 setjmp 是一種特殊情況,因為通常存在由 setjmp 引起的流變化可能會嚴重破壞事物的優化,這些需要避免。 回到 K&R 時代,setjmp 不需要特殊處理,也沒有得到任何處理,因此適用於當地人的警告。 由於該警告已經存在並且(應該!)被理解 - 當然,setjmp 的使用非常罕見 - 現代編譯器沒有動力去任何額外的長度來解決“clobber”問題 - 它仍然是在語言中。
是否有任何參考資料對此進行了詳細說明,如果這是真的,是否可以安全地存在(行為不會比標准 setjmp/longjmp 更容易出錯)setjmp/longjmp 的定制實現(例如,也許我想要保存一些命名不同的額外(線程本地)上下文)? 反正有沒有告訴編譯器“這個函數實際上是 setjmp/longjmp”?
GCC 確實對setjmp
做了一些特殊處理, 通過名稱與sigsetjmp
、 vfork
、 getcontext
和savectx
。 (剝離前導_
后)。 在比賽中,它會設置內部標志ECF_RETURNS_TWICE
。 我認為這等效於隱式__attribute__((returns_twice))
(您可以將其用於您自己的功能)。 glibc 標頭不使用它,它們僅依賴於名稱匹配。 (這個答案的早期版本被愚弄了,認為它們根本不是特殊情況。)
longjmp
不需要太多特殊處理; 它看起來就像任何其他__attribute__((noreturn))
函數調用。 Glibc 以這種方式聲明longjmp
,這應該在調用它之前對局部變量產生副作用,例如避免在int foo(){ if(x) return y; longjmp(jmpbuf); }
int foo(){ if(x) return y; longjmp(jmpbuf); }
setjmp
/ longjmp
不能保證比優化器的任何不透明函數(不可內聯)看起來更多。 (但一個關鍵的區別是當setjmp
再次返回時可以返回范圍時,不為單獨的本地人重用堆棧空間,請參閱@amonakov 的回答。)
對volatile
局部變量的副作用可能已在編譯時重新排序。 setjmp
(或longjmp
)如果轉義分析可以顯示沒有全局變量可以有它們的地址。
在調用setjmp
期間,仍然允許優化將本地變量保存在寄存器中而不是內存中。 這意味着當longjmp
將調用保留寄存器恢復到jmp_buf
中的保存狀態時,在setjmp
之后、 longjmp
之前完成的volatile
變量的副作用可能會或可能不會回滾。
setjmp(3)
的 Linux 手冊頁列出了規則:
編譯器可以將變量優化到寄存器中,
longjmp()
可以恢復除堆棧指針和程序計數器之外的其他寄存器的值。 因此,如果自動變量的值滿足以下所有條件,則在調用longjmp()
后未指定它們的值:
- 它們對於進行相應
setjmp()
調用的函數是本地的;- 它們的值在
setjmp()
和longjmp()
調用之間發生變化; 和- 它們沒有被聲明為
volatile
。
來自 glibc 的/usr/include/setjmp.h
// earlier CPP macros to define __THROWNL as __attribute__ ((__nothrow__)) in C++ mode
extern int setjmp (jmp_buf __env) __THROWNL;
extern void longjmp (struct __jmp_buf_tag __env[1], int __val)
__THROWNL __attribute__ ((__noreturn__));
extern void siglongjmp (sigjmp_buf __env, int __val)
__THROWNL __attribute__ ((__noreturn__));
有一堆 C 預處理器的東西來定義一個_
版本(無信號setjmp
)等等。
順便說一句,有一個__builtin_setjmp
。 但它的工作方式有些不同: GCC 手冊建議不要在用戶代碼中使用它,並且 ISO C setjmp/longjump 庫函數不能根據它來定義。
C 語言將 setjmp 定義為一個宏,並對它可能出現的上下文施加了嚴格的限制,而不會調用未定義的行為。 它不是一個正常的函數:您不能獲取它的地址並期望通過結果指針的調用表現為正確的 setjmp 調用。
特別是,setjmp 調用的匯編代碼通常不遵循與普通函數相同的調用約定。 Linux 和 Solaris 上的 SPARC 提供了一個反例:它的 setjmp 不會恢復所有調用保留的寄存器(vfork 也不會)。 就在 2018 年( gcc-patches thread , bugzilla entry ),它讓 GCC 感到意外。
但是即使考慮到 setjmp 入口點遵循通常約定的“編譯器友好”平台,仍然有必要將其識別為“返回兩次”的函數。 GCC 按名稱識別類似 setjmp 的函數(包括 vfork),並提供__attribute__((returns_twice))
用於在自定義代碼中注釋此類函數。
這樣做的原因是 longjmp'ing 回到 setjmp 可以將控制從某個變量或臨時出現死的點(並且編譯器將其存儲重新用於不相關的東西)轉移回它原來的位置(但它的存儲現在被“破壞”了,哎呀)。
構建一個示例來演示這是如何發生的有點棘手:被破壞的存儲不能是寄存器,因為如果它被調用破壞,它將不會在 setjmp 點被使用,如果它是調用保存的 longjmp 將恢復它(除了 SPARC 例外)。 所以它需要被強制堆棧,而不會使兩個變量的地址以一種使它們的生命周期重疊的方式暴露,防止堆棧槽的重用,並且不讓其中一個在 longjmp 之前超出范圍。
幸運的是,我設法得到了以下測試用例,當使用-O2 -mtune-ctrl=^inter_unit_moves_from_vec
編譯時(在 Compiler Explorer 上查看):
//__attribute__((returns_twice))
int my_setjmp(void);
__attribute__((noreturn))
void my_longjmp(int);
static inline
int float_as_int(float x)
{
return (union{float f; int i;}){x}.i;
}
float f(void);
int g(void)
{
int ret = float_as_int(f());
if (__builtin_expect(my_setjmp(), 1)) {
int tmp = float_as_int(f());
my_longjmp(tmp);
}
return ret;
}
產生以下程序集:
g:
sub rsp, 24
call f
movss DWORD PTR [rsp+12], xmm0
call my_setjmp
test eax, eax
je .L2
call f
movss DWORD PTR [rsp+12], xmm0
mov edi, DWORD PTR [rsp+12]
call my_longjmp
.L2:
mov eax, DWORD PTR [rsp+12]
add rsp, 24
ret
-mtune-ctrl=^inter_unit_moves_from_vec
標志導致 GCC 通過堆棧實現 SSE-to-gpr 移動,並且兩個移動使用相同的堆棧槽,因為據編譯器所知,沒有沖突(計算 'tmp' 導致noreturn 函數,因此不再需要臨時用於計算“ret”)。 但是,如果 my_longjmp 將控制權轉移回 my_setjmp,則在分支到標簽 .L2 之后,我們會嘗試從被覆蓋的插槽中讀取 'ret' 的值。
首先,為什么volatile
在鏈接的帖子中起作用的正確答案是“因為 C 標准明確說明了這一點”。 我不認為引用的部分是正確的,因為 C 明確列出了許多與setjmp
/ longjmp
相關的定義不明確的行為。 相關部分可以在 C17 7.13.2.1 中找到:
所有可訪問對象都有值,並且抽象機的所有其他組件都有狀態,截至調用
longjmp
函數時,除了包含調用相應setjmp
的函數的本地自動存儲持續時間的對象的值沒有volatile
限定類型並且已在setjmp
調用和longjmp
調用之間更改的宏是不確定的。
甚至 C90 說的也差不多。 因此,無論現代與否,編譯器都不需要“修復這個”的原因是因為 C 從未要求它們這樣做。 在引用評論的示例中,第二次執行if ( foo != 5 )
時, foo
的值是不確定的(並且foo
永遠不會被占用),所以嚴格來說,該行只是調用未定義的行為和編譯器可以從那里隨心所欲地做 - 這是由應用程序程序員而不是優化器創建的錯誤。
通常,任何使用setjmp.h
的應用程序程序員都會得到他們的結果。 這是最糟糕的意大利面條編程形式。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.