[英]Why use the Global Offset Table for symbols defined in the shared library itself?
考慮以下簡單的共享庫源代碼:
庫.cpp:
static int global = 10;
int foo()
{
return global;
}
-fPIC
clang 中的-fPIC
選項編譯,生成此對象程序集 (x86-64):
foo(): # @foo()
push rbp
mov rbp, rsp
mov eax, dword ptr [rip + global]
pop rbp
ret
global:
.long 10 # 0xa
由於符號是在庫中定義的,編譯器按預期使用 PC 相對尋址: mov eax, dword ptr [rip + global]
但是,如果我們更改static int global = 10;
到int global = 10;
使其成為具有外部鏈接的符號,生成的程序集為:
foo(): # @foo()
push rbp
mov rbp, rsp
mov rax, qword ptr [rip + global@GOTPCREL]
mov eax, dword ptr [rax]
pop rbp
ret
global:
.long 10 # 0xa
正如您所看到的,編譯器使用全局偏移表添加了一個間接層,在這種情況下這似乎完全沒有必要,因為符號仍然定義在同一個庫(和源文件)中。
如果符號是在另一個共享庫中定義的,則 GOT 是必要的,但在這種情況下,它感覺是多余的。 為什么編譯器還在 GOT 中添加這個符號?
注意:我相信這個問題與此類似,但是由於缺乏細節,答案不相關。
全局偏移表有兩個目的。 一種是允許動態鏈接器“插入”與可執行文件或其他共享對象不同的變量定義。 第二個是允許生成位置無關代碼以引用某些處理器架構上的變量。
ELF 動態鏈接將整個進程、可執行文件和所有共享對象(動態庫)視為共享一個全局命名空間。 如果多個組件(可執行文件或共享對象)定義了相同的全局符號,那么動態鏈接器通常會選擇該符號的一個定義,並且所有組件中對該符號的所有引用都引用該定義。 (但是,ELF 動態符號解析很復雜,並且由於各種原因,不同的組件最終可能會使用同一全局符號的不同定義。)
為了實現這一點,在構建共享庫時,編譯器將通過 GOT 間接訪問全局變量。 對於每個變量,將在 GOT 中創建一個包含指向該變量的指針的條目。 如您的示例代碼所示,編譯器將使用此條目來獲取變量的地址,而不是嘗試直接訪問它。 當共享對象加載到進程中時,動態鏈接器將確定是否有任何全局變量已被另一個組件中的變量定義取代。 如果是這樣,那些全局變量將更新其 GOT 條目以指向替代變量。
通過使用“隱藏的”或“受保護的”ELF 可見性屬性,可以防止全局定義的符號被另一個組件中的定義取代,從而消除在某些體系結構上使用 GOT 的需要。 例如:
extern int global_visible;
extern int global_hidden __attribute__((visibility("hidden")));
static volatile int local; // volatile, so it's not optimized away
int
foo() {
return global_visible + global_hidden + local;
}
當使用-O3 -fPIC
與 GCC 的 x86_64 端口編譯時生成:
foo():
mov rcx, QWORD PTR global_visible@GOTPCREL[rip]
mov edx, DWORD PTR local[rip]
mov eax, DWORD PTR global_hidden[rip]
add eax, DWORD PTR [rcx]
add eax, edx
ret
如您所見,只有global_visible
使用 GOT, global_hidden
和local
不使用它。 “受保護”可見性的工作原理類似,它防止定義被取代,但使其對動態鏈接器仍然可見,以便其他組件可以訪問它。 “隱藏”可見性完全隱藏了動態鏈接器中的符號。
使代碼可重定位以允許共享對象在不同進程中加載到不同地址的必要性意味着靜態分配的變量,無論它們具有全局作用域還是局部作用域,在大多數體系結構上都不能通過單個指令直接訪問。 我所知道的唯一例外是 64 位 x86 架構,如上所示。 它支持既與 PC 相關的內存操作數,又具有大的 32 位位移,可以到達同一組件中定義的任何變量。
在所有其他架構上,我熟悉以位置相關方式訪問變量需要多條指令。 具體如何因架構而異,但通常涉及使用 GOT。 例如,如果您使用-m32 -O3 -fPIC
選項使用 GCC 的 x86_64 端口編譯上面的示例 C 代碼,您將得到:
foo():
call __x86.get_pc_thunk.dx
add edx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
push ebx
mov ebx, DWORD PTR global_visible@GOT[edx]
mov ecx, DWORD PTR local@GOTOFF[edx]
mov eax, DWORD PTR global_hidden@GOTOFF[edx]
add eax, DWORD PTR [ebx]
pop ebx
add eax, ecx
ret
__x86.get_pc_thunk.dx:
mov edx, DWORD PTR [esp]
ret
GOT 用於所有三個變量訪問,但如果仔細觀察global_hidden
和local
的處理方式與global_visible
不同。 對於后者,指向變量的指針通過 GOT 訪問,前兩個變量通過 GOT 直接訪問。 在 GOT 用於所有位置獨立變量引用的體系結構中,這是一個相當常見的技巧。
32 位 x86 體系結構在這方面的一個方面是特殊的,因為它具有大的 32 位位移和 32 位地址空間。 這意味着可以通過 GOT 庫訪問內存中的任何地方,而不僅僅是 GOT 本身。 大多數其他架構只支持更小的位移,這使得某些東西與 GOT 基礎的最大距離要小得多。 使用此技巧的其他體系結構只會將小(本地/隱藏/受保護)變量放在 GOT 本身中,大變量存儲在 GOT 之外,並且 GOT 將包含一個指向該變量的指針,就像普通可見性全局變量一樣。
除了羅斯嶺答案中的詳細信息。
這是外部與內部聯系。 如果沒有static
,該變量具有外部鏈接,因此可以從任何其他翻譯單元訪問。 任何其他翻譯單元都可以將其聲明為extern int global;
並訪問它。
聯動:
外部聯動。 該名稱可以從其他翻譯單元的范圍中引用。 具有外部鏈接的變量和函數也具有語言鏈接,這使得鏈接用不同編程語言編寫的翻譯單元成為可能。
在命名空間范圍內聲明的以下任何名稱都具有外部鏈接,除非命名空間未命名或包含在未命名命名空間中 (C++11 起):
- 上面未列出的變量和函數(即未聲明為靜態的函數、未聲明為靜態的命名空間范圍的非常量變量以及任何聲明為 extern 的變量);
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.