前言
移植某個專案到 Linux 的時候,注意到原本在 Windows 跟 macOS 都運作的好好的,但在 Linux 上 gRPC 卻會遇到遍歷全域 Linked List 時出現無窮迴圈問題。網路上諸多討論[1][2],幾年前有人建議改善插入節點的程式,但卻遲遲沒有正式修正,這是為什麼呢?
雖然知道用 Floyd Tortoise and Hare (LeetCode 142)方法可以找到迴圈起點,但感覺對案情沒什麼幫助,最後在亂試 CMake 的 link 方法後,才發現問題根源在於 dynamic loader 對全域變數的處理差異。
簡單的 PoC
會有這樣的結果,主要是有兩個動態連結檔 C & D 引用了同份 B (其全域變數來自 A,也就是 gRPC 的 root_tracer_)。
那問題來了,當執行檔引用了 C & D 這兩個動態連結檔,全域變數會不會發生衝突?答案是不會。
在 Project A 中定義了全域變數,程式碼如下,可以看到會將參數插入到 linked list 最前面:
TraceFlag *TraceFlagList::root_tracer_ = nullptr;
void TraceFlagList::Add(TraceFlag *flag)
{
flag->next_tracer_ = root_tracer_;
root_tracer_ = flag;
}
Project C & D 程式碼範例如下,在插入自身節點後,印出自己與 next 的地址:
namespace ProjectC
{
void CallRegisterTraceFlag()
{
static TraceFlag flag;
ProjectB::RegisterTraceFlag(&flag);
std::cout << "C flag addr: " << &flag << ", next: " << flag.next_tracer_ << std::endl;
}
}
Project E 會呼叫 C 與 D 的函式來進行節點插入
ProjectC::CallRegisterTraceFlag();
ProjectD::CallRegisterTraceFlag();
來看一下 Windows 結果 ⇒ 每個 DLL 有自己獨立的一份全域變數
C flag addr: 00007FFC80AED170, next: 0000000000000000
D flag addr: 00007FFC80B0D170, next: 0000000000000000
macOS、Linux 結果 ⇒ 共享同一個全域變數
C flag addr: 0x77e767095030, next: 0000000000000000
D flag addr: 0x77e767090030, next: 0x77e767095030
可以看到,Windows 的規則是 DLL 裡的全域變數不會在不同 DLL 之間共享,每個 DLL 有自己的一份。macOS 與 Linux 會視為同一份。
但是呢…如果把 Project B 的 SHARED 調整成 STATIC,那 Windows 與 Linux 結果不變,但 macOS 變成各自一份,和 Windows 行為一致。
C flag addr: 0x10053c000, next: 0x0
D flag addr: 0x10054c000, next: 0x0
簡單結論
Windows
DLL 的全域變數是「每個 DLL 各自擁有一份」,不同 DLL 之間不共享。
macOS
- 若透過 shared library (
.dylib
) 導出:全域變數會共享。 - 若 static archive (
.a
) 被靜態打包進多個 dylib:則各自一份。
- 若透過 shared library (
Linux (ELF/ld.so)
- 編譯時每個 shared lib 都會有一份,但 dynamic loader 在建立全域符號表時,會採用 第一次載入的定義,後續的同名符號會被忽略。
- 因此多個 shared lib 看似共用同一份全域變數。
平台 | 多個 shared lib 各自靜態鏈入同一份連結檔時,關於全域變數? | 全域變數共享? |
---|---|---|
Windows | 會產生多份 | 否 |
macOS with STATIC | 會產生多份 | 否 |
macOS with SHARED | 共用同一份 | 是 |
Linux | 會產生多份,但動態載入時只會用第一個符號 | 看起來共享 |
相關文件
Windows
Variables that are declared as global in a DLL source code file are treated as global variables by the compiler and linker, but each process that loads a given DLL gets its own copy of that DLL’s global variables.
macOS
Symbol Exporting Strategies • The
static
storage class: This is the easiest way to indicate that you don’t want to export a symbol.
The dynamic loader doesn’t detect naming conflicts between the symbols exported by the dynamic libraries it loads. When a client contains a reference to a symbol that two or more of its dependent libraries export, the dynamic loader binds the reference to the first dependent library that exports the symbol in the client’s dependent library list. The dependent library list is a list of the client’s dependent libraries in the order they were specified when the client was linked with them.
Linux
Note that there is no problem if the scope contains more than one definition of the same symbol. The symbol lookup algorithm simply picks up the first definition it finds.
可以看到 Linux 的方法是只用第一個找到的同名 symbol,這也就是為什麼 LD_PRELOAD 可以在程式執行前替換掉其他同名的 symbol 哦。
參考
comments powered by Disqus