Dynamic Linking Basic

Static Linking 的問題

static link 拆分了可執行檔,讓不同人或組織可以開發自己的 module,最後再 link 成執行檔。隨著系統變複雜,OS 裡有多個 process 在執行,當多個 process 以 static linking 連結相同的 library 時,例如擁有 printf() 的 standard library,memory 會有多份類似的 library 程式碼,造成浪費。

另一個問題是 static linking 不易更新 module。例如 App 這個程式用到 Lib.oLib.o 更新時(Lib.o 可能是別人提供的 module,App 開發者無法控制其版本),App 的開發者必須拿 App.oLib.o 再 link 成新版 App,再給發佈新版 App。

Dynamic Linking 基本概念

為了解決 static link 在 memory 浪費以及不易更新的問題,最簡單的做法是將程式拆成多個 module,不在 compile 時期 link library,等到執行時才 link。如此一來就能只放一份共用 module 在 memory,需要該 module 的 executable file 就去 link。

不過現實世界總是不那麼美好,dynamic linking 還是會遇到其他問題,例如如果 program A 需要 1.0 版的 module、program B 需要 2.0 版的 module,或者一個程式更新共用 module 之後,其他程式因為 interface 相依性問題就壞了。

Linux 稱 ELF 動態連結檔為 Dynamic Shared Objects(DSO),簡稱 shared object,通常以 .so 為副檔名。Windows 則稱為 Dynamical Linking Library,以 .dll 為副檔名。這系列筆記會混用 DSO、shared object、shared library、module 等詞,皆指動態連結檔。

Usage in Linux

先用簡單範例看看 Linux 上如何使用 dynamic link。

source code

foo.h
1
2
3
4
#ifndef _LIB_H
#define _LIB_H
int foo(int a, int b);
#endif
foo.c
1
2
3
4
int foo(int a, int b)
{
return (a + b);
}
main.c
1
2
3
4
5
6
7
#include "lib.h"

int main()
{
foo(3, 4);
return 0;
}
1
2
$ gcc -fPIC -shared -o lib.so lib.c
$ gcc -o main.dyn main.c ./lib.so

-shared 表示要編出 shared object。link 出 main.dyn 時,linker 會看 foo() 是定義在其他 static object file 還是在 DSO 裡,如果在 DSO 裡就會標示這個 symbol 為 dynamic linking,等到 load 才進行 symbol relocation。上面第二行要寫 ./lib.so 是為了讓 linker 知道 foo() 是在 DSO 裡,而且 main.dyn 也會記錄 lib.so 的路徑。

1
2
3
4
5
$ ldd main.dyn 
linux-vdso.so.1 (0x00007fff29986000)
./lib.so (0x00007febdc1b6000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007febdbe0b000)
/lib64/ld-linux-x86-64.so.2 (0x00007febdc3b7000)

比較常用的寫法是 -l 指定 library、-L 指定找 library 的 path,例如 -lfoo -L./ 會找 libfoo.so 來 link,尋找順序則是先找目前目錄再找系統預設目錄。這種寫法在 main.dyn 裡只會記錄 lib.so、不會有路徑,執行時會從系統以及相關參數所設的路徑找 library,如果找不到會跳 error。

1
2
$ readelf -h lib.so | grep Type
Type: DYN (Shared object file)
1
2
3
4
5
6
7
8
9
10
11
12
$ readelf -s lib.so

Symbol table '.dynsym' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
...
8: 0000000000000660 20 FUNC GLOBAL DEFAULT 11 foo
...
Symbol table '.symtab' contains 53 entries:
Num: Value Size Type Bind Vis Ndx Name
...
46: 0000000000000660 20 FUNC GLOBAL DEFAULT 11 foo
...

lib.so 的 symbol table 有給 dynamic link 用的 .dynsym section 跟一般的 .symtab section。

1
2
3
4
5
6
7
8
9
10
$ readelf -s main.dyn

Symbol table '.dynsym' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
...
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND foo
Symbol table '.symtab' contains 65 entries:
Num: Value Size Type Bind Vis Ndx Name
...
56: 0000000000000000 0 FUNC GLOBAL DEFAULT UND foo

重點來啦,main.dyn 是 dynamic link lib.so 的可執行檔,它的 symbol table 標示了 foo 仍然是 undefined,而且在 .dynsym 裡也有這個 symbol。.dynsym section 不會出現在其他 object file 裡的 symbol。

virtual memory space

編出 static linking 版本的執行檔:ld -e main main.o lib.o -o main.st 。執行後 cat /proc/<pid>/maps 可以看到 process 的 virtual memory space:

1
2
3
4
5
00400000-00401000 r-xp 00000000 08:01 11409244                           /home/cjw/source/dynamic-link/main.st
7ffeaa7bb000-7ffeaa7dc000 rw-p 00000000 00:00 0 [stack]
7ffeaa7ed000-7ffeaa7ef000 r-xp 00000000 00:00 0 [vdso]
7ffeaa7ef000-7ffeaa7f1000 r--p 00000000 00:00 0 [vvar]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

簡單明瞭 load 進 main.st,比較 dynamic linking 的 main.dyn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
00400000-00401000 r-xp 00000000 08:01 11409246                           /home/cjw/source/dynamic-link/main.dyn
00600000-00601000 rw-p 00000000 08:01 11409246 /home/cjw/source/dynamic-link/main.dyn
7fdee17b3000-7fdee1954000 r-xp 00000000 08:01 7864382 /lib/x86_64-linux-gnu/libc-2.19.so
7fdee1954000-7fdee1b54000 ---p 001a1000 08:01 7864382 /lib/x86_64-linux-gnu/libc-2.19.so
7fdee1b54000-7fdee1b58000 r--p 001a1000 08:01 7864382 /lib/x86_64-linux-gnu/libc-2.19.so
7fdee1b58000-7fdee1b5a000 rw-p 001a5000 08:01 7864382 /lib/x86_64-linux-gnu/libc-2.19.so
7fdee1b5a000-7fdee1b5e000 rw-p 00000000 00:00 0
7fdee1b5e000-7fdee1b5f000 r-xp 00000000 08:01 11409245 /home/cjw/source/dynamic-link/lib.so
7fdee1b5f000-7fdee1d5e000 ---p 00001000 08:01 11409245 /home/cjw/source/dynamic-link/lib.so
7fdee1d5e000-7fdee1d5f000 rw-p 00000000 08:01 11409245 /home/cjw/source/dynamic-link/lib.so
7fdee1d5f000-7fdee1d7f000 r-xp 00000000 08:01 7864371 /lib/x86_64-linux-gnu/ld-2.19.so
7fdee1f5e000-7fdee1f61000 rw-p 00000000 00:00 0
7fdee1f7d000-7fdee1f7f000 rw-p 00000000 00:00 0
7fdee1f7f000-7fdee1f80000 r--p 00020000 08:01 7864371 /lib/x86_64-linux-gnu/ld-2.19.so
7fdee1f80000-7fdee1f81000 rw-p 00021000 08:01 7864371 /lib/x86_64-linux-gnu/ld-2.19.so
7fdee1f81000-7fdee1f82000 rw-p 00000000 00:00 0
7fffbbf58000-7fffbbf79000 rw-p 00000000 00:00 0 [stack]
7fffbbfce000-7fffbbfd0000 r-xp 00000000 00:00 0 [vdso]
7fffbbfd0000-7fffbbfd2000 r--p 00000000 00:00 0 [vvar]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

除了 main.dynlib.so 之外也 load 進 C Runtime /lib/x86_64-linux-gnu/libc-2.19.so 以及動態連結器 /lib/x86_64-linux-gnu/ld-2.19.so

那麼 DSO 會 load 到哪呢?從下面 lib.so 的 segment 來看,它會被 load 到的 address 是 0x00000000,但顯然這個 address 不合法。compile time 無法知道 DSO 會被 load 去哪,是在 runtime 才決定(最簡單的想法是 OS 找到一塊放得下 DSO 的 memory 放)。這也是可以想像的,如果 DSO 跟 executable file 一樣編譯時就決定自己要 load 到某個 address,多個 DSO 在系統裡可能會互撞。kill 掉原本的 main.dyn 再跑一次會發現 load lib.so 的 address 不一樣。

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
$ readelf -l lib.so

Elf file type is DYN (Shared object file)
Entry point 0x560
There are 6 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000704 0x0000000000000704 R E 200000
LOAD 0x0000000000000708 0x0000000000200708 0x0000000000200708
0x0000000000000230 0x0000000000000238 RW 200000
DYNAMIC 0x0000000000000720 0x0000000000200720 0x0000000000200720
0x00000000000001c0 0x00000000000001c0 RW 8
NOTE 0x0000000000000190 0x0000000000000190 0x0000000000000190
0x0000000000000024 0x0000000000000024 R 4
GNU_EH_FRAME 0x0000000000000680 0x0000000000000680 0x0000000000000680
0x000000000000001c 0x000000000000001c R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10

Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .eh_frame_hdr .eh_frame
01 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
02 .dynamic
03 .note.gnu.build-id
04 .eh_frame_hdr
05