簡體   English   中英

編譯器對 setjmp/longjmp 的特殊處理

[英]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做了一些特殊處理, 通過名稱sigsetjmpvforkgetcontextsavectx (剝離前導_后)。 在比賽中,它會設置內部標志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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM