Linux ELF 詳解4 -- 深入 Symbol【轉】


轉自:https://blog.csdn.net/helowken2/article/details/113792555

Symbol 的分類
從鏈接器的角度看,Symbol 可以分為3類(這里的類別不同於 Symbol Type)

Global Symbol Def:定義在當前對象文件中,可以被其他對象文件引用。例如定義在當前對象文件中的非 static 的函數或者全局變量。
Global Symbol Ref:定義在其他對象文件中,被當前對象文件所引用。又被稱作 externals,例如定義在其他對象文件中的非 static 的函數或者全局變量。
Local Symbol:定義和引用都在當前對象文件中。例如 static 函數和 static 全局變量。這些 Symbol 對當前對象文件的任何地方都可見,但是不能被其他對象文件引用。
Local Symbol & 局部變量
Local Symbol 不是局部變量:

“.symtab” Section 不會包含任何局部變量
局部變量是運行期間在 Stack 上進行分配的
static 變量不會在 Stack 上進行分配,而是在編譯期間,由編譯器在 “.data” 或 “.bss” Section 中分配空間,然后在 “.symtab” Section 中創建 Symbol,這些 Symbol 名字都是唯一的。
局部變量的空間分配
下面查看局部變量 d(在 main 函數中)是如何在 stack 上進行分配的

$ cat program.c
...
extern int a;
...
int main() {
int d = function(100) + a;
...
}

# 反匯編后的代碼
$ objdump -d program.o
...
0000000000000000 <main>:
main():
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp # stack 上預留 16字節用於存儲局部變量 d(16字節是 x86-64 架構上的 ABI 規范)
8: bf 64 00 00 00 mov $0x64,%edi # 放入參數 100
d: e8 00 00 00 00 callq 12 <main+0x12> # 調用 function
12: 89 c2 mov %eax,%edx # 把 function 的返回值放到 edx
14: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 1a <main+0x1a> (把外部變量 a 的值放到 eax)
1a: 01 d0 add %edx,%eax # 計算 a + function 的返回值,計算結果放入 eax
1c: 89 45 fc mov %eax,-0x4(%rbp) # 把計算結果從 eax 放入 stack。[rbp - 4, rbp) 就是局部變量 d 在 stack 中所占的空間
...

static 變量的空間分配
// local_linker_symbol.c
int func1() {
static int a = 0;
return a;
}

int func2() {
static int a = 2;
return a;
}

$ gcc -c local_linker_symbol.c
$ readelf -sW local_linker_symbol.o
...
Num: Value Size Type Bind Vis Ndx Name
...
5: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 a.1832
6: 0000000000000000 4 OBJECT LOCAL DEFAULT 3 a.1835
...

由上可知

func1 中 a 的 Section Header index = 4,名字叫 “a.1832”
func2 中 a 的 Section Header index = 3,名字叫 “a.1835”
你可能會有疑問:怎么確定哪個 Symbol a 是 func1,哪個是 func2的?畢竟從名字上來看是完全看不出的,Symbol 順序也不一定跟程序的一致。

其實可以從它們的 Section Header index 來分辨。

$ readelf -SW local_linker_symbol.o
...
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
...
[ 3] .data PROGBITS 0000000000000000 000058 000004 00 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 00005c 000004 00 WA 0 0 4
...

func1 中 a 的初始值為0,所以它指向 “.bss” Section。而 func2 中 a 的初始值為2(不為0),所以它指向 “.data” Section。(當如果2個 a 的初始值都為0時,那么就沒法從這里分辨了,必須通過重定位表來進一步查看,這些是后面章節的內容。)

COMMON Symbol & “.bss” 的區別
在 program.c 中,有個全局變量 c 沒有初始化。

$ cat program.c
...
char c[10];
...

$ readelf -sW program.o
...
Num: Value Size Type Bind Vis Ndx Name
...
10: 0000000000000008 10 OBJECT GLOBAL DEFAULT COM c
...

為什么 c 的 Section Header index 是 SHN_COMMON(Ndx=COM),而不是指向 “.bss” Section Header index?另外 SHN_COMMON 的 Symbol 也表示關聯到一個未初始化的公共塊,那 COMMON 和 “.bss” 的區別是什么?

這里有個規范:

COMMON:對應沒初始化的全局變量。
“.bss”:對應沒初始化的 static 變量,初始值為 0 的 static 變量,初始值為 0 的全局變量。
看個例子:

// symbols.c
int a;
int a2 = 0;
int a3 = 3;
static int a4;
static int a5 = 0;
static int a6 = 4;

$ gcc -c symbols.c
$ readelf -sW symbols.o
...
Num: Value Size Type Bind Vis Ndx Name
...
5: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 a4
6: 0000000000000008 4 OBJECT LOCAL DEFAULT 3 a5
7: 0000000000000004 4 OBJECT LOCAL DEFAULT 2 a6
...
10: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM a
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 a2
12: 0000000000000000 4 OBJECT GLOBAL DEFAULT 2 a3

# 查看 Ndx = 2,3 時的 Section Header
$ readelf -SW symbols.o
...
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
...
[ 2] .data PROGBITS 0000000000000000 000040 000008 00 WA 0 0 4
[ 3] .bss NOBITS 0000000000000000 000048 00000c 00 WA 0 0 4
...

由上可知:

a 是未初始化的全局變量,所以是個 COMMON Symbol(規范1)
a2 是初始值為0的全局變量,所以分配在 “.bss” (規范2)
a3 是初始值為3(不為0)的全局變量,所以分配在 “.data”
a4 是未初始化的 static 變量,所以分配在 “.bss”(規范2)
a5 是初始值為0的 static 變量,所以分配在 “.bss”(規范2)
a6 是初始值為4(不為0)的 static 變量,所以分配在 “.data”
這個規范源於鏈接器做 Symbol 解析時的方式。

Symbol 解析
鏈接器通過關聯對 Symbol 的引用和它的定義來完成 Symbol 的解析。

這對於 Local Symbol 而言比較簡單,因為編譯器會保證 Local Symbol 的定義在同一對象文件中只有一份,否則編譯就會報錯。而 static 變量,只要不在同一作用域內出現重復定義(否則編譯報錯),那么就算在同一對象文件中出現多次,編譯器也會為它們生成不同的 Symbol(上面已經提及)。

但是對於 Global Symbol,這個情況就有點復雜了。當編譯器遇到一個 Symbol,卻又沒法在當前對象文件中找到它的定義,那么編譯器就認為這個 Symbol 的定義存放在其他對象文件中,於是編譯器就為這個 Symbol 在 Symbol Table 中插入一個對應的記錄,然后留給鏈接器去處理。

那么,現在問題來了:如果鏈接器解析 Symbol 時,發現有多個定義可以跟它匹配(名字一樣,但定義可以完全不同,譬如一個 int,一個是 char),那該如何選擇?

以下是 Linux 編譯系統所采取的策略:

編譯器在給匯編器導出 Global Symbol 時,會給每個 Global Symbol 都附帶上一個信息:strong 或者 weak;
匯編器再把這些信息編碼到 Relocatable file 的 Symbol Table 中;
函數和已經初始化的全局變量都屬於 strong Symbol,而沒有初始化的全局變量則屬於 weak Symbol。
查看 symbols.c 編譯后的匯編文件:

$ gcc -S symbols.c
$ cat symbols.s
.file "symbols.c"
.comm a,4,4 ; .comm 表示 a 是個未初始化的 Global Symbol(weak)
# -----------------------------------
.globl a2 ;.globl + .bss 表示 a2 是個初始值為0的 Global Symbol(strong),分配在 .bss
.bss
.align 4
.type a2, @object
.size a2, 4
a2:
.zero 4
# -----------------------------------
.globl a3 ; .globl + .data 表示 a3 是個初始值不為0的 Global Symbol(strong),分配在 .data
.data
.align 4
.type a3, @object
.size a3, 4
a3:
.long 3
# -----------------------------------
.local a4 ; .local + .comm 表示 a4 是個未初始化或初始值為0的 Local Symbol,分配在 .bss
.comm a4,4,4
# -----------------------------------
.local a5 ; .local + .comm 表示 a5 是個未初始化或初始值為0的 Local Symbol,分配在 .bss
.comm a5,4,4
# -----------------------------------
.align 4
.type a6, @object ; 表示 a6 是個初始值不為0的 Local Symbol,分配在 .data
.size a6, 4
a6:
.long 4
# -----------------------------------
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits

匯編器就是根據這些信息對 symbols.o 中的 Symbol Table 進行編碼。

有了 strong 和 weak 的概念后,當鏈接器遇到多個同名 Symbol 時,將使用以下規則進行處理:

多個 strong Symbol => 報錯
1個 strong Symbol + 多個 weak Symbol => 使用 strong Symbol 的定義
沒有 strong Symbol,只有多個 weak Symbol => 使用其中 size 最大的那個 weak Symbol 的定義
舉例說明

多個 strong Symbol
例子1
// func.c
int func() {
return 0;
}

void main() {
func();
}


// func2.c
char func() {
return 'a';
}

$ gcc -o func func.c func2.c
/tmp/cct8WbLt.o: In function `func':
func2.c:(.text+0x0): multiple definition of `func'
/tmp/ccxdAjJx.o:func.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

func 在兩個對象文件中都是函數,都是 strong Symbol,因此鏈接期間會報錯。

例子2
// fSym3.c
int func = 3;

$ gcc -o func func.c fSym3.c
/tmp/cc7LGOuc.o:(.data+0x0): multiple definition of `func'
/tmp/ccO9f6UR.o:func.c:(.text+0x0): first defined here
/usr/bin/ld: Warning: size of symbol `func' changed from 11 in /tmp/ccO9f6UR.o to 4 in /tmp/cc7LGOuc.o
/usr/bin/ld: Warning: type of symbol `func' changed from 2 to 1 in /tmp/cc7LGOuc.o
collect2: error: ld returned 1 exit status

func 在 func.c 是函數,在 fSym3.c 是初始化了的全局變量,都是 strong Symbol,因此鏈接期間會報錯。

例子3
// global_var.c
int a = 3;

void main() {
a = 4;
}

// global_var2.c
char a = 'a';

$ gcc -o gv global_var.c global_var2.c
/tmp/ccqVwxwi.o:(.data+0x0): multiple definition of `a'
/tmp/cchohOvY.o:(.data+0x0): first defined here
/usr/bin/ld: Warning: size of symbol `a' changed from 4 in /tmp/cchohOvY.o to 1 in /tmp/ccqVwxwi.o
collect2: error: ld returned 1 exit status

同理,2個初始化了的同名全局變量,鏈接期間也會報錯。

1 strong Symbol + 多個 weak Symbol
例子1
// f1.c
#include <stdio.h>

short a;

void main() {
printf("a: 0x%04x\n", a); // 打印 a 的16進制
}

// f2.c
char a = 0x01;

$ gcc -o f f1.c f2.c && ./f
/usr/bin/ld: Warning: alignment 1 of symbol `a' in /tmp/cc0bLEUk.o is smaller than 2 in /tmp/ccJKP7Jw.o
/usr/bin/ld: Warning: size of symbol `a' changed from 2 in /tmp/ccJKP7Jw.o to 1 in /tmp/cc0bLEUk.o
a: 0x0001

Warning 的意思是,Symbol a 在 f1.o 中是2字節,鏈接后找到的定義是1字節。這其實會引發奇怪的問題,下面會講到。

例子2
// f3.c
short a = 0x0201;

$ gcc -o f f1.c f3.c && ./f
a: 0x0201

運行正常,沒有 warning。

例子3
// f4.c
int a = 0x10000;

$ gcc -o f f1.c f4.c && ./f
/usr/bin/ld: Warning: size of symbol `a' changed from 2 in /tmp/ccHqp1Bm.o to 4 in /tmp/cc3gcmpj.o
a: 0x0000

Warning 告訴我們 Symbol a 在 f1.o 中是作為2字節使用的,但鏈接后實際占有了4字節的空間。f4.o 中 a 的值為 0x10000,對應到4個字節(地址從低往高)分別是:“00 00 01 00”(little endian),但 f1.o 還是只提取了最低地址的2個字節,也就是 “00 00”。

例子4
// f5.c
char a = 0x01;
char b = 0x02;

$ gcc -o f f1.c f5.c && ./f
/usr/bin/ld: Warning: alignment 1 of symbol `a' in /tmp/cc5QqKXe.o is smaller than 2 in /tmp/cc3CUiVm.o
/usr/bin/ld: Warning: size of symbol `a' changed from 2 in /tmp/cc3CUiVm.o to 1 in /tmp/cc5QqKXe.o
a: 0x0201

Warning 告訴我們,Symbol a 在 f1.o 中作為2字節使用,但是鏈接后 a 實際只占有1字節的空間,這時問題就來了。

$ objdump -d f1.o
...
0000000000000000 <main>:
...
4: 0f b7 05 00 00 00 00 movzwl 0x0(%rip),%eax # b <main+0xb>
...

從 f1.o 的匯編代碼中可以看到用於讀取 a 的指令是:movzwl。它的意思是:讀取1個 word(w, 2 bytes),然后0擴展(z)成4字節(l),因此就算現在 a 實際只占有1字節的空間,指令還是照樣讀取2字節,而除 a 之外的那個字節,在內存中是緊跟在 a 之后。

$ readelf -SW f5.o
...
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
...
[ 2] .data PROGBITS 0000000000000000 000040 000002 00 WA 0 0 1

$ hexdump -C -s0x40 -n2 f5.o
00000040 01 02 |..|
00000042

於是,movzwl 指令實際讀取到的是 “01 02” 2個字節,由於是 litte endian,所以最終解析出來的數值就是 0x0201。

例子5
// ff1.c
#include <stdio.h>

char a = 0x01;
char b = 0x02;

void f(void);

void main() {
f();
printf("a: 0x%02x\n", a);
printf("b: 0x%02x\n", b);
}

// ff2.c
short a;

void f() {
a = 0x0304;
}

Warning 告訴我們, Symbol a 在 ff1.o 中只占1字節,但是在 ff2.o 中是作為2字節使用的。當 ff2.o 中的 f 函數運行時,就會錯誤地改寫了除 a 之外的字節,這個字節緊跟在 a 之后。

# 可以看到 b 緊跟着 a
$ readelf -sW ff1.o
...
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
...
9: 0000000000000000 1 OBJECT GLOBAL DEFAULT 3 a
10: 0000000000000001 1 OBJECT GLOBAL DEFAULT 3 b
...


$ objdump -d ff2.o
...
0000000000000000 <f>:
...
4: 66 c7 05 00 00 00 00 movw $0x304,0x0(%rip) # d <f+0xd>
...

f 函數中的指令 “movw $0x304,0x0(%rip)”,意思把 “0x04 0x03”(little endian) 寫入從 a 開始連續的2個字節。從上圖可以看出 b 緊跟在 a 之后,於是 b 也被改寫了。

$ gcc -o ff ff1.c ff2.c && ./ff
/usr/bin/ld: Warning: alignment 1 of symbol `a' in /tmp/ccZ3iN36.o is smaller than 2 in /tmp/ccni1l3E.o
a: 0x04
b: 0x03

由此可見,weak Symbol 帶來的問題是不少的,而且很容易引發 bug。
如果想避免,可以加上 -fno-common:

$ gcc -fno-common -o f f1.c f5.c
/tmp/ccQWsBi2.o:(.data+0x0): multiple definition of `a'
/tmp/cch62Cgr.o:(.bss+0x0): first defined here
/usr/bin/ld: Warning: size of symbol `a' changed from 2 in /tmp/cch62Cgr.o to 1 in /tmp/ccQWsBi2.o
collect2: error: ld returned 1 exit status

那么鏈接的時候就會報錯。

0 strong Symbol + 多個 weak Symbol
當全部都是 weak Symbol 時,選擇 size 最大的那個定義。

例子1
// c1.c
int aaaaa;

void main() {
}

// c2.c
char* aaaaa;

// c3.c
short aaaaa;

// c4.c
long aaaaa;

# int 比 char* 要小,所以選 char*
$ gcc -o c c1.c c2.c && readelf -sW c | grep aaaaa
53: 0000000000601038 8 OBJECT GLOBAL DEFAULT 26 aaaaa

# int 比 short 要大,所以選 int
$ gcc -o c c1.c c3.c && readelf -sW c | grep aaaaa
53: 0000000000601034 4 OBJECT GLOBAL DEFAULT 26 aaaaa

# int 比 long 要小,所以選 long
$ gcc -o c c1.c c4.c && readelf -sW c | grep aaaaa
53: 0000000000601038 8 OBJECT GLOBAL DEFAULT 26 aaaaa

從上面的論述可知,weak Symbol 和 strong Symbol 都是 Global Symbol。weak Symbol 其實就是 COMMON Symbol(st_shndx = SHN_COMMON);而 strong Symbol 則是一開始就分配好的(要么在 “.data”,要么在 “.bss”)。

Symbol Binding: STB_WEAK
從前面的章節已知 Symbol Binding 主要有 STB_LOCAL,STB_GLOBAL 和 STB_WEAK。而 STB_LOCAL 和 STB_GLOBAL 我們已經論述過了,接下來要講的是 STB_WEAK。

STB_WEAK Symbol 跟上面說的 weak Symbol 很容易混淆,但其實它們完全不是一回事。為了清楚描述,下面用 COMMON Symbol 來代替 weak Symbol。

STB_WEAK Symbol 的作用是提供默認實現,當鏈接到其他包含有同名 strong Symbol 的對象文件時,STB_WEAK Symbol 則會被 strong Symbol 所替代。例如有些庫外露了一些接口,同時提供了它的默認實現。開發者可以提供遵循這個接口的專有實現,在運行的時候就能替換掉默認實現。如果沒有提供額外的實現,運行時也能使用默認的實現而不會報錯。

以下是 STB_WEAK Symbol 的一些特定規則:

STB_WEAK Symbol 遇上 strong Symbol,選擇 strong Symbol;
STB_WEAK Symbol 遇上 COMMON Symbol,選擇 COMMON Symbol;
只有 STB_WEAK Symbol,沒有其他 Symbol,選擇首次出現的 WEAK Symbol;
STB_WEAK Symbol 和 strong Symbol 一樣,都是一開始就分配好的(在 “.data” 或 “.bss”);
如果 STB_WEAK Symbol 未初始化,則值為0。
例子1
// default.c
#include <stdio.h>

__attribute__((weak)) int a;
__attribute__((weak)) int a2 = 0;
__attribute__((weak)) int a3 = 1;
int a4;

__attribute__((weak)) void f() {
printf("weak func, a=%d, a2=%d, a3=%d, a4=%d\n", a, a2, a3, a4);
}

void main() {
f();
}

$ gcc -o ft default.c && ./ft
weak func, a=0, a2=0, a3=1, a4=0


$ readelf -sW default.o
...
Num: Value Size Type Bind Vis Ndx Name
...
9: 0000000000000000 4 OBJECT WEAK DEFAULT 4 a
10: 0000000000000004 4 OBJECT WEAK DEFAULT 4 a2
11: 0000000000000000 4 OBJECT WEAK DEFAULT 3 a3
12: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM a4
...


$ readelf -SW default.o
...
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
...
[ 3] .data PROGBITS 0000000000000000 000084 000004 00 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 000088 000008 00 WA 0 0 4
...

由上可知,a, a2, a3 都是 STB_WEAK Symbol,a4 是 COMMON Symbol。a 和 a2 都是分配在 “.bss”,a3 分配在 “.data”。(規則3,4,5)

例子2
// custom_func.c
#include <stdio.h>

void f() {
printf("custom func.\n");
}

$ gcc -o ft default.c custom_func.c && ./ft
custom func.

custom_func.o 中的 f 函數是個 strong Symbol,所以替代了 default.o 中的默認實現。(規則1)

例子3
// custom_var.c
int a = 100;
int a2 = 200;
int a3 = 300;
int a4 = 400;

$ gcc -o ft default.c custom_var.c && ./ft
weak func, a=100, a2=200, a3=300, a4=400

由上可知,custom_var.o 中的 a, a2, a3 和 a4 都是 strong Symbol,替代了 default.o 中的默認實現以及 COMMON Symbol。(規則1)

例子4
// weak.c
__attribute__((weak)) int a4 = 111;

$ gcc -o ft default.c weak.c && ./ft
weak func, a=0, a2=0, a3=1, a4=0

a4 在 default.o 中是個 COMMON Symbol,所以選擇 COMMON Symbol。(規則2)

例子5
// weak2.c
__attribute__((weak)) int a3 = 333;

$ gcc -o ft default.c weak2.c && ./ft
weak func, a=0, a2=0, a3=1, a4=0

$ gcc -o ft weak2.c default.c && ./ft
weak func, a=0, a2=0, a3=333, a4=0

a3 在 default.o 和 weak2.o 中都是 STB_WEAK Symbol,所以哪個先出現就用哪個。(規則3)

小結
Symbol Table 中的 Symbol 主要有:

LOCAL Symbol,對應 static 函數和 static 變量,分配在 “.data” 和 “.bss”
GLOBAL Symbol,分成兩類:
strong Symbol,對應函數和初始化了的全局變量,分配在 “.data” 和 “.bss”
COMMON symbol(weak Symbol),對應未初始化的全局變量
STB_WEAK Symbol,對應附帶了 “attribute((weak))” 的函數和全局變量,分配在 “.data” 和 “.bss”
下一篇章,我們將講解與 Symbol 密切相關的重定位。
————————————————
版權聲明:本文為CSDN博主「懶惰的勞模」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/helowken2/article/details/113792555


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM