Hello World Odyssey - Part 2. 關於符號的大小事

6/5/2021 ELFLinkerSymbol

# 一、Symbol table

# 1.1 Symbol table 簡介

  1. Symbol table 是一個資料結構,裡面存放關於這個程式的相關資訊物件。
  2. 在 compiler 的 lexical、syntactic analysis (語法分析)階段就被建立起來
  3. 用來幫助
    • Semantic analysis (文法分析) : Type checking 的工作,看看有無 type conflict。
    • Code generation : 在程式碼執行時,分配空間的工作。
    • Error handling: Variable A undefined 等錯誤訊息。
  4. 在整個編譯時間都會使用到

# 1.2 編譯期的變數繫結和兩階段的映射

在編譯期間,變數有兩階段的映射: environmentstate

environment 包含很多 name binding,而下例展示 environmentstate 的不同之處

A := B
1
1

A 的值變,但是記憶體位置不變,因此 state映射改變但是environment 映射改變,所以整體來說如同下表,經過兩階段映射可以得到該變數的值

name address value
A C1 1
B C2 3
C C3 5

# 1.3 變數的作用範圍

在編譯期間,編譯器會把我們的變數會依照作用範圍分為externelinternel,再來依照變數名稱的作用範圍,分配在不同的空間上。舉個例子

int a = 3;
void fun();
void fun2(int T)
{
      int k = 3; 
      while( i < T ){
	      int j = k + i;
	      i++;
	   }
}
int main()
{
      fun(); 
      fun2();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上段程式碼,我們可以將它簡單解析為樹的形式

再依照作用域的不同,分配在不同的區段

由上圖可以很清晰的理解,變數依照作用域的範圍編譯器會幫忙分配到各自的空間去,到這裡為止是在編譯期間做的事情。接下來要俯瞰整個流程😀

# 1.4 linker 的降臨

在 1.2, 1.3 小節,我們探討到編譯器幫我們做好映射、變數繫結以及分配區段的工作,但是 compiler 並不知道整個程式的情況,compiler 只能幫我們聚焦在單一檔案上,而無法俯瞰整個專案。也就是說,我們剛剛只是比較聚焦在局部而已,想當然爾,程式不可能只有一個檔案而已。我們需要 linker 將我們的符號做重定位以及解析,建立好符號彼此的相互關係、引用關係等,合成一個可執行檔。

我們需要 compiler 先幫我們區分 externelinternel,因為 internel 不同檔案彼此一定不會參照到對方,所以我們要交互參照的是 externel 的 symbol table,去檢查有沒有引用的問題。在符號解析完成後,我們也得到了可執行檔。

# 二、ELF Symbol table

# 2.1 似曾相似的 Symbol Table

我們再繼續延續上一章 (opens new window)的程式碼

// elf.c
unsigned long long data1 = 0xdddddddd11111111;
unsigned long long data2 = 0xdddddddd22222222;
void func1(){}
void func2(){}
1
2
3
4
5
1
2
3
4
5

編譯但是不連結,輸出 object file 的二進位檔案 elf.o

gcc -c elf.c -o elf.o
1
1

首先我們以一樣抽絲剝繭的方式, readelf 來查看我們的檔案內容

由 readelf 輸出,我們可以看到 symbol table 的一些資訊

  • 起始位置是 0xe8
  • 整個表格的大小是 0x120
  • 每一個元素大小為 0x18

同樣的,我們再用 readelf 來幫助我們看 .symtab 的 section

我們特別在意最後四項,因為和我們程式碼有關係,由輸出結果可以看到我們的變數位置,跟不同 Type 和 Bind 的類型。那我們再來看看能不能由二進位檔案找到我們的變數值😀,首先由上表,可以知道 data1 索引為 8 ,因此我們可以推出 data1 的起始位置

  • start = 0xe8;
  • ent_size = 0x18;
  • idx = 0x8; data1 的起始位置 = 0xe8 + 0x18 * 0x8 = 0x1a8,這裡我們使用 hexdump 跟 另外一個 xxd 的十六進位分析工具,因為 xxd 可以較方便又準確幫我們取出所需要的範圍。

嘿,我們得到原始 C code 的 data1, data2, func1, func2 在 symtab section 內的資料了

070000001100020000000000000000000800000000000000 // data1
0d0000001100020008000000000000000800000000000000 // data2
130000001200010000000000000000000700000000000000 // func1
190000001200010007000000000000000700000000000000 // func2
1
2
3
4
1
2
3
4

我們需要去分析這串資料,接下來我們從 elf.h 來看 symtab section 每個欄位的資料結構吧~

typedef struct
{
      Elf64_Word	st_name;		/* Symbol name (string tbl index) */
      unsigned char	st_info;		/* Symbol type and binding */
      unsigned char st_other;		/* Symbol visibility */
      Elf64_Section	st_shndx;		/* Section index */
      Elf64_Addr	st_value;		/* Symbol value */
      Elf64_Xword	st_size;		/* Symbol size */
} Elf64_Sym;
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

有了這個資料結構定義,我們就可以分析這個十六進位字串了😀

[data1]
st_name  = 07 00 00 00 // data1
st_info  = 11 
st_other = 00
st_shndx = 02 00 // .data section
st_value = 00 00 00 00 00 00 00 00 // .data section 的起始地址 從 base 的 offset
st_size  = 08 00 00 00 00 00 00 00 // 8 byte


[data2]
st_name  = 0d 00 00 00 // data2
st_info  = 11
st_other = 00
st_shndx = 02 00 // .data section
st_value = 08 00 00 00 00 00 00 00
st_size  = 08 00 00 00 00 00 00 00 // 8 bytes


[func1]
st_name  = 13 00 00 00 // func1
st_info  = 12 
st_other = 00
st_shndx = 01 00 // .text section
st_value = 00 00 00 00 00 00 00 00
st_size  = 07 00 00 00 00 00 00 00 // 7 bytes

[func2]
st_name  = 19 00 00 00 // func2
st_info  = 12 
st_other = 00
st_shndx = 01 00 // .text section
st_value = 07 00 00 00 00 00 00 00 
st_size  = 07 00 00 00 00 00 00 00 // 7 bytes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  1. st_name 是在 strtab section 的 偏移量 ,我們由這個偏移量去找我們變數的名稱,有沒有覺得很熟悉😄,沒錯,在之前的 section header table 每個元素都有個欄位 sh_name,我們由這個欄位的偏移量去 sh_strtab section 內找我們 section 的名稱,同理,我們 st_name 是去 strtab section 找對應的變數名稱,比較如下表
name name在哪裡 去哪裡找 找什麼
sh_name Section Header Table 的元素內 sh_strtab section 找 section 的名字
st_name Symbol Table 的元素內 strtab section 找變數的名字

那我們現在就用 st_name 來去 strtab section 找我們的變數名稱吧,一樣先用 readelf 找 strtab section 在哪裡,再利用 hexdump 或我們方才所提的 xxd 工具來分析。

  1. st_info 其中st_info的定義也是在 elf.h 內,st_info 是 unsigned char, 高四位元是 BIND, 低四位元是 TYPE,各值寫在 elf.h 內,可以看到是利用位元運算將兩個欄位獨立取出來。

因此我們可以得出

變數名稱 st_info Bind Type
data1 11 0x1 (STB_GLOBAL) 0x1 (STT_OBJECT)
data2 11 0x1 (STB_GLOBAL) 0x1 (STT_OBJECT)
func1 12 0x1 (STB_GLOBAL) 0x2 (STT_FUNC)
func2 12 0x1 (STB_GLOBAL) 0x2 (STT_FUNC)

這些我們之後會再介紹。

  1. st_shndx 這個就是對應到我們 section 的索引,這裡會依照的性質跟值,決定它要分配在什麼空間,例如
    1. 一般常數資料或是函數、指令,落在 .text section (read-only, index=1)
    2. 一般資料分配落在 .data section (index=2)
    3. 未初始化或是初始值為 0 的資料落在 .bss section (index=3) ...
    /* Special section indices.  */
    #define SHN_UNDEF	0		/* Undefined section */
    #define SHN_LORESERVE	0xff00		/* Start of reserved indices */
    #define SHN_LOPROC	0xff00		/* Start of processor-specific */
    #define SHN_BEFORE	0xff00		/* Order section before all others
                      (Solaris).  */
    #define SHN_AFTER	0xff01		/* Order section after all others
                      (Solaris).  */
    #define SHN_HIPROC	0xff1f		/* End of processor-specific */
    #define SHN_LOOS	0xff20		/* Start of OS-specific */
    #define SHN_HIOS	0xff3f		/* End of OS-specific */
    #define SHN_ABS		0xfff1		/* Associated symbol is absolute */
    #define SHN_COMMON	0xfff2		/* Associated symbol is common */
    #define SHN_XINDEX	0xffff		/* Index is in extra table.  */
    #define SHN_HIRESERVE	0xffff		/* End of reserved indices */
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    0xfff2 跟 0xffff 中間有gap,是因為方便未來可擴展,所以可能有些許空間上的浪費🤣

最後得到下表,記得要用 little-endian 方式取值

var name sh_name st_info st_other st_shndx st_value st_size
data1 07000000 11 00 0200 0000000000000000 0800000000000000
data2 0d000000 11 00 0200 0800000000000000 0800000000000000
func1 13000000 12 00 0100 0000000000000000 0700000000000000
func2 19000000 12 00 0100 0700000000000000 0700000000000000

所以你可以由上表發現 data1, data2st_shndx 值是 0x2,屬於 .data section 的 index,而另外兩個因為是 func 的符號,所以是 .rodata section 的 index。

那我們知道了

  • sh_name: 去 strtab 得到變數名稱
  • st_shndx: 知道我們的變數值在哪個 section
  • st_value: 知道變數在該 Section 的 偏移量(又是偏移量,你很熟了嗎 XD)
  • st_size: 知道我們的變數值在該節佔多大的 Bytes

所以我們的變數取值,寫成 C 語言

start:  Elf64_Shdr[ELF64_Sym.st_section].sh_offset + Elf64_Sym.st_value 
End: start + ELF64_Sym.size - 1
1
2
1
2

我們要取的地址範圍就是 ELF[start, End] (注意是封閉區間)

你是不是覺得有點 stackoverflow 了 🤣,這裡其實很簡單,和我們上一篇的查找方式雷同, 無非是三大步驟

  • 利用 readelf, hexdump (xxd, objdump) 來進行交叉觀察
  • name 都是偏移量,要去 sh_strtab, strtab 這兩個大字串 section 來以偏移量查找
  • 剩下就是看 elf.h 的資料結構定義,去切割 byte 賦予意義

你只是不適應看這種赤裸裸的機器語言,看久了你也會產生感情的😁,那我這裡在花一些時間好人做到底,結合前面的章節講的更清楚一點整個流程。

簡單來說,就是相互參照,算偏移量,一旦你知道起始位置、大小、偏移量,其實你大概就掌握了本質 ,大道至簡😉

# 2.2 Bind, Type, Section 的三角關係

接下來,就來講正式介紹剛剛輕輕帶過的 st_info

再複習一下,st_info的定義也是在 elf.h 內,st_info 是 unsigned char, 高四位元是 BIND, 低四位元是 TYPE,各值寫在 elf.h 內,可以看到是利用位元運算將兩個欄位獨立取出來。

因此我們可以得出

變數名稱 st_info Bind Type
data1 11 0x1 (STB_GLOBAL) 0x1 (STT_OBJECT)
data2 11 0x1 (STB_GLOBAL) 0x1 (STT_OBJECT)
func1 12 0x1 (STB_GLOBAL) 0x2 (STT_FUNC)
func2 12 0x1 (STB_GLOBAL) 0x2 (STT_FUNC)

以上是我們在 2-1 小節提到的部分。我們再用 readelf 來看看我們的符號表

不知道大家有沒有學過離散數學或是排列組合,每個符號我們將 賦予三種屬性: { ( Type, Bind, Ndx(Session)) }

  • bind set = { STB_LOCAL, STB_GLOBAL, STB_WEAK, .... }
  • type set = { STT_NOTYPE, STT_OBJECT, STT_FUNC, STB_NUM ...}
  • section set = { .text, .data, .bss....}
  • bind set x(叉積) type set x(叉積) section set = { (Bind, Type, Section) } 恰如一個樹,枚舉所有排列組和,有不合法的葉子,就剪除。

接下來我們就來做幾個實驗,來更理解符號的概念

# 2.2-1 Test 1 : Strong symbol

首先我們先在 s1.c, s2.c 輸入以下的值再連結

那接下來我們將 s1 的 a 設為 static,使其作用範圍只限 s1.c 的範圍

因此,Bind 簡單來說就是 全局的可見程度、覆蓋程度,所以不能 static 又是 extern。

# 2.2-2 Weak symbol

弱符號是什麼,我們的神書 CSAPP 提到以下規則,請看好喔😉

  1. 在編譯的時候,編譯器會將每個全局符號輸出給組譯器。其中,每個全局符號都有強弱之分

    • 函數和已經初始化了的全局變量是強符號
    • 未初始化的變量是弱符號
  2. 組譯器將這些符號的強弱訊息隱含的編碼在可重定位的目標文件的符號表裡

  3. 根據全局符號的強弱,鏈接器按以下規則處理多處定義的全局符號

    • 不允許有多個強符號
    • 如果有一個強符號和多個弱符號,則選擇強符號
    • 如果沒有強符號,有多個弱符號,則從這些弱符號中佔用空間最大的一個

簡單的說就是強者優先弱者平等的原則。

當然,這裡也引用弱符號的wiki (opens new window)

弱符號(Weak symbol)是連結器在生成ELF檔案的過程中使用的一種特殊屬性符號。預設情況下, ==如果沒有特別聲明,目的檔裡面的符號都是強符號。在連結過程中,一個強符號會優先於一個同名的弱符號。相比之下,兩個同名強符號一起連結會出現連結錯誤。當連結一個可執行檔,弱符號可以不定義。但對於強符號,如果沒有定義,鏈接器會產生一個「符號未定義」錯誤 (undefined symbol)。使用弱符號的目的是, 當不確定這個符號是否被定義的情況下,連結器也可以成功連結出ELF檔案,適用於某些模組還未實現的情況下,其他模組的先行除錯。 弱符號在C語言和C++語言的規範裡面沒有被提及,所以使用弱符號的代碼,移植性不是非常好。

因此,弱符號其實鮮少被使用,使用不當可能增加我們除錯的負擔。維基提供兩種方式宣告符號是弱符號。

  1. pragma
// function declaration
#pragma weak power2
int power2(int x);
1
2
3
1
2
3
  1. __attribute__((weak))
// function declaration
int __attribute__((weak)) power2(int x);
// or
int power2(int x) __attribute__((weak));

// variable declaration;
extern int __attribute__((weak)) global_var;
1
2
3
4
5
6
7
1
2
3
4
5
6
7

對弱符號有些了解後,可以知道

  • 弱符號不一定要被定義,但強符號一定要有定義。
  • 弱符號可以被定義或初始化,只是當遇到另外一個同名的強符號會被覆蓋掉

這裡同樣也用 readelf 來觀察

可以看到我們 add 被定義為弱符號,但我們的弱符號有定義。

再用稍微複雜一點的例子

前面所述,Bind 簡單來說就是 全局的可見程度、覆蓋程度,那在 C 語言關於 Bind (Local, Global, Weak) 的修飾如下表

Bind C語言關鍵字修飾
local static
global extern 或不修飾
weak __attribute__((weak))

# 2.2-3 大考時間

你能答對幾題呢 ?😉😉

這裡需要解釋一下其中的 d7,test.c 程式碼內是 int d7

因為不知道 d7 在未來連結時 別的 .o 檔有沒有定義,總共分為三個 Case

  1. test.c 有 int d7, s2.c 有 int d7 = 0 或 int d7,分配到 bss section
  2. test.c 有 int d7, s2.c 有 int d7 = 1,分配到 data section
  3. test.c 有 int d7, s2.c 沒有定義 d7 ,照編譯器預設值 0,分配到 bss section

因此,不知道到底要放在 .bss 或是 .data section, 是個 tentative defined, 不確定到底要放哪個 section, 因此就放入 common section, st_shndx = 0xfff2, st_value 就是按照型態去決定對齊(alignment),例如 int,st_size = 4 (byte), st_value = 4。但在未來,ld -r s*.o時產生的 EOF 檔案,因為全部都要確定好放哪個位置,所以 EOF 沒有 COMMON section.

還有另外一種方式可能也會把變數放在 COMMON section,如下

因為 b 不知道未來在連結時的情形,這種要分配到哪個section 相依於未來連結才會決定的狀況,一律先指定為 COM section

# 三、結論

這章內容有點艱深,但是概念真的不難,只要知道怎麼找符號的位置跟對應的值,多練幾次就熟啦😁。所以可以簡化為三個重點

  1. Symbol table 可以告訴我們符號的位置,以及怎麼去找到它的值。
  2. 每個符號都有一個三元素的 pair ( Type, Bind, Section ),組合多個符號的 pair 形成一個 symbol table,就可以輕鬆知道每個符號的特性,也是readelf -s xxx.o 的結果。
  3. 熟悉如何查看符號跟符號特性,對於Compiler 部分知識、未來會談論到的 dynamic link 時可以更有幫助。

按按旁邊的小火箭,開頭的那張圖表,你可以更了解了嗎 ? 😎

# 四、參考連結

  1. Linköping University Compiler Course (opens new window)
  2. 弱符號的wiki (opens new window)
  3. CSAPP關於符號解釋的問題? (opens new window)
Last Updated: Tue Jul 06 2021 19:28:11 GMT+0800