Load process 在 dynamic linking 的差別是 load 完執行檔後 OS 會先看 ELF 的 .interp section 知道要 load 哪個 dynamic linker,OS load dynamic linker 並將控制權先交給它,在 linux 下是 /lib/x86_64-linux-gnu/ld-2.19.so(很明顯也是個 shared library)。

dynamic linker 做的事情分成三步:

  1. 啟動
  2. load 所有需要的 shared library
  3. relocation & initialization

dynamic linker 本身也是個 shared library,但它有一些限制,例如不能有依賴的 shared library、不能使用 global 變數、不能 call function 等等。在啟動階段,dynamic linker 會先找到自己的 GOT,而 GOT 第一個 entry 存的是 .dynamic section 的 offset。.dynamic section 保存了 dynamic linker 需要的資訊,例如依賴哪些 shared library,dynamic symbol table、dynamic relocation table 的位置等等。readelf -d 可以查看 .dynamic section。

透過 .dynamic 裡的資訊,linker 可以知道自己的 relocation table 以及 symbol table,能對自己進行 relocation。linker 做完針對自己的 relocation 後,才能開始使用 global 變數、call function 等等。

啟動完成後,dynamic linker 將自己的 symbol 以及 executable file 的 symbol 合併到 global symbol table。接著開始從 executable file 的 .dynamic 得知依賴哪些 shared library,一一打開 shared library 並將之 load 到 memory 中、建立 mapping,並且將 shared library 的 symbol 合併到 global symbol table,再搜尋 shared library 的 dependency。如此 traverse 整棵 shared library dependency tree 之後,即 load 完所需的 shared library。通常會以 BFS traverse dependency tree。

symbol 的部份,ELF 以 dynamic symbol table 保存模組間 import 及 export symbol 的關係,位於 .dynsym,作用以及記錄的資訊跟 .symtab 差不多,但只記錄給其他模組使用的 symbol 以及使用其他模組的 symbol。

traverse dependency tree 的順序跟 symbol 的優先度有關──當兩個 shared library 有相同的 symbol 時要使用誰的?在 Linux 中會優先使用先 load 的 symbol。也就是說,shared library 的 load 順序會影響 symbol 的優先度,而 link 時指定的 library 順序會影響 load 的順序。

接著,linker 開始 traverse executable file 以及 shared library 的 relocation table,使用 global symbol table 來修正 GOT 及 PLT 內的 address。dynamic linking 中的 relocation table .rela.dyn 用來修正 .got 及 data section,.rela.plt 則修正 .got.plt section 內的 address。relocation 完成後,如果 shared library 擁有 .init section,dynamic linker 會執行它以進行對 shared library 的初始化。

最後,dynamic linker 將控制權交給程式的入口開始執行。

Relocation example

雖然簡單來說 dynamic linker 會在 load shared library 時進行 relocation,但如Dynamic Linking PIC所說的,實際上 ELF 是以 lazy binding 來 bind symbol。用個簡單例子看看怎麼做的:

foo.h
1
2
3
4
#ifndef __FOO_H
#define __FOO_H
int foo(int x, int y);
#endif
foo.c
1
2
3
4
int foo(int x, int y)
{
return (x + y);
}
bar.h
1
2
3
4
#ifndef __BAR_H
#define __BAR_H
void bar(int a, int b, int n);
#endif
bar.c
1
2
3
4
5
6
#include "foo.h"
void bar(int a, int b, int n)
{
foo(a, b);
foo(b, n);
}
main.c
1
2
3
4
5
6
7
#include "bar.h"

int main()
{
bar(2, 3, 10);
return 0;
}
1
2
3
$ gcc -g -shared -fPIC foo.c -o libfoo.so
$ gcc -g -shared -fPIC bar.c -o libbar.so
$ gcc -g main.c ./libfoo.so ./libbar.so -o main

最後編 executable file 時指定的 library 順序會影響 load library 的順序。

main()bar()foo() 設中斷點,停在 main() 之後觀察 foo 的 relocation 資訊:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
$ gdb ./main
Reading symbols from ./main...done.
(gdb) b main
Breakpoint 1 at 0x40067a: file main.c, line 5.
(gdb) b bar
Breakpoint 2 at 0x400550
(gdb) b foo
Function "foo" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 3 (foo) pending.
(gdb) r
Starting program: /home/cjw/source/dynamic-link/relocate/main

Breakpoint 1, main () at main.c:5
5 bar(2, 3, 10);

$ cat /proc/`pgrep main`/maps
00400000-00401000 r-xp 00000000 08:24 8259464 /home/cjw/source/dynamic-link/relocate/main
00600000-00601000 rw-p 00000000 08:24 8259464 /home/cjw/source/dynamic-link/relocate/main
7ffff762f000-7ffff77d0000 r-xp 00000000 08:22 1572872 /lib/x86_64-linux-gnu/libc-2.19.so
7ffff77d0000-7ffff79d0000 ---p 001a1000 08:22 1572872 /lib/x86_64-linux-gnu/libc-2.19.so
7ffff79d0000-7ffff79d4000 r--p 001a1000 08:22 1572872 /lib/x86_64-linux-gnu/libc-2.19.so
7ffff79d4000-7ffff79d6000 rw-p 001a5000 08:22 1572872 /lib/x86_64-linux-gnu/libc-2.19.so
7ffff79d6000-7ffff79da000 rw-p 00000000 00:00 0
7ffff79da000-7ffff79db000 r-xp 00000000 08:24 8259463 /home/cjw/source/dynamic-link/relocate/libbar.so
7ffff79db000-7ffff7bda000 ---p 00001000 08:24 8259463 /home/cjw/source/dynamic-link/relocate/libbar.so
7ffff7bda000-7ffff7bdb000 rw-p 00000000 08:24 8259463 /home/cjw/source/dynamic-link/relocate/libbar.so
7ffff7bdb000-7ffff7bdc000 r-xp 00000000 08:24 8259445 /home/cjw/source/dynamic-link/relocate/libfoo.so
7ffff7bdc000-7ffff7ddb000 ---p 00001000 08:24 8259445 /home/cjw/source/dynamic-link/relocate/libfoo.so
7ffff7ddb000-7ffff7ddc000 rw-p 00000000 08:24 8259445 /home/cjw/source/dynamic-link/relocate/libfoo.so
7ffff7ddc000-7ffff7dfc000 r-xp 00000000 08:22 1572869 /lib/x86_64-linux-gnu/ld-2.19.so
7ffff7fd5000-7ffff7fd8000 rw-p 00000000 00:00 0
7ffff7ff6000-7ffff7ff8000 rw-p 00000000 00:00 0
7ffff7ff8000-7ffff7ffa000 r-xp 00000000 00:00 0 [vdso]
7ffff7ffa000-7ffff7ffc000 r--p 00000000 00:00 0 [vvar]
7ffff7ffc000-7ffff7ffd000 r--p 00020000 08:22 1572869 /lib/x86_64-linux-gnu/ld-2.19.so
7ffff7ffd000-7ffff7ffe000 rw-p 00021000 08:22 1572869 /lib/x86_64-linux-gnu/ld-2.19.so
7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0
7ffffffdd000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

$ readelf -r libbar.so

Relocation section '.rela.dyn' at offset 0x430 contains 8 entries:
Offset Info Type Sym. Value Sym. Name + Addend
...

Relocation section '.rela.plt' at offset 0x4f0 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000200980 000300000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0
000000200988 000400000007 R_X86_64_JUMP_SLO 0000000000000000 foo + 0
000000200990 000700000007 R_X86_64_JUMP_SLO 0000000000000000 __cxa_finalize + 0

(gdb) x/1xbg 0x7ffff79da000+0x000000200988
0x7ffff7bda988: 0x00007ffff79da586

(gdb) info address foo
Symbol "foo" is a function at address 0x7ffff7bdb660.

libbar.so 裡需要修正 address 的位置是 0x7ffff79da000 + 0x000000200988,因為 library 要到 load 的時候才能知道起始位置,所以必須用 offset 算出要修改的地方,因為還沒用過 foo,這個 address 的內容還不是 foo 的 address。

1
2
3
4
5
$ readelf -S libbar.so
...
[20] .got.plt PROGBITS 0000000000200968 00000968
0000000000000030 0000000000000008 WA 0 0 8
...

從 relocation entry 來看,relocate 的 offset 是 0x000000200988,在 .got.plt 裡。0x7ffff7bda988 這個位置在 load libbar.so 的第三個 segment,屬性是 rw-p,是可以修改的。

繼續跑看第一次 call foo()

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
(gdb) c
Continuing.

Breakpoint 2, bar (a=2, b=3, n=10) at bar.c:7
7 foo(a, b);
(gdb) x/10i $pc
=> 0x7ffff79da6b1 <bar+17>: mov -0x8(%rbp),%edx
0x7ffff79da6b4 <bar+20>: mov -0x4(%rbp),%eax
0x7ffff79da6b7 <bar+23>: mov %edx,%esi
0x7ffff79da6b9 <bar+25>: mov %eax,%edi
0x7ffff79da6bb <bar+27>: callq 0x7ffff79da580 <foo@plt>
0x7ffff79da6c0 <bar+32>: mov -0xc(%rbp),%edx
0x7ffff79da6c3 <bar+35>: mov -0x8(%rbp),%eax
0x7ffff79da6c6 <bar+38>: mov %edx,%esi
0x7ffff79da6c8 <bar+40>: mov %eax,%edi
0x7ffff79da6ca <bar+42>: callq 0x7ffff79da580 <foo@plt>
(gdb) b *0x7ffff79da6bb
(gdb) c
(gdb) x/1i $pc
=> 0x7ffff79da6bb <bar+27>: callq 0x7ffff79da580 <foo@plt>
(gdb) si
0x00007ffff79da580 in foo@plt () from ./libbar.so
(gdb) x/3i $pc
=> 0x7ffff79da580 <foo@plt>: jmpq *0x200402(%rip) # 0x7ffff7bda988
0x7ffff79da586 <foo@plt+6>: pushq $0x1
0x7ffff79da58b <foo@plt+11>: jmpq 0x7ffff79da560
(gdb) si
0x00007ffff79da586 in foo@plt () from ./libbar.so
2: x/i $pc
=> 0x7ffff79da586 <foo@plt+6>: pushq $0x1
(gdb)
0x00007ffff79da58b in foo@plt () from ./libbar.so
2: x/i $pc
=> 0x7ffff79da58b <foo@plt+11>: jmpq 0x7ffff79da560
(gdb)
0x00007ffff79da560 in ?? () from ./libbar.so
2: x/i $pc
=> 0x7ffff79da560: pushq 0x20040a(%rip) # 0x7ffff7bda970
(gdb)
0x00007ffff79da566 in ?? () from ./libbar.so
2: x/i $pc
=> 0x7ffff79da566: jmpq *0x20040c(%rip) # 0x7ffff7bda978
(gdb) x/3i $pc
=> 0x7ffff79da566: jmpq *0x20040c(%rip) # 0x7ffff7bda978
0x7ffff79da56c: nopl 0x0(%rax)
0x7ffff79da570 <__gmon_start__@plt>: jmpq *0x20040a(%rip) # 0x7ffff7bda980
(gdb) si
_dl_runtime_resolve () at ../sysdeps/x86_64/dl-trampoline.S:34
34 ../sysdeps/x86_64/dl-trampoline.S: No such file or directory.
2: x/i $pc
=> 0x7ffff7df02b0 <_dl_runtime_resolve>: sub $0x38,%rsp
(gdb) x/1xbg 0x7ffff7bda978
0x7ffff7bda978: 0x00007ffff7df02b0

0x7ffff79da580foo 在 PLT 裡的位置,call foo() 的時候會跳過去。這個位置位於 load libbar.so 的第一個 segment,屬性 r-xp,可執行,同時也代表 PLT 是被放在可執行的 segment 裡。到 foo@plt 後會再跳到 foo.got.plt 的 entry 所指的 address,也就是前面看到的 0x00007ffff79da586,同時也是 foo@plt 的下一個指令,最後在 0x7ffff79da566: jmpq *0x20040c(%rip) # 0x7ffff7bda978 會跳到 0x7ffff7bda978 記錄的位置 0x00007ffff7df02b0,即 _dl_runtime_resolve 的入口,到這邊當然是要 resolve symbol 啦,至於中間 push 的動作跟給 _dl_runtime_resolve 參數有關。

設中斷點在 call 完 foo() 之後,看看 resolve 完 foo.got.plt 裡對應的 entry 會如何:

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
(gdb) b *0x7ffff79da6c0
Breakpoint 5 at 0x7ffff79da6c0: file bar.c, line 8.
(gdb) c
Continuing.

Breakpoint 3, foo (x=2, y=3) at foo.c:3
3 return (x + y);
2: x/i $pc
=> 0x7ffff7bdb66a <foo+10>: mov -0x4(%rbp),%edx
1: $pc = (void (*)()) 0x7ffff7bdb66a <foo+10>
(gdb) c
Continuing.

Breakpoint 5, bar (a=2, b=3, n=10) at bar.c:8
8 foo(b, n);
2: x/i $pc
=> 0x7ffff79da6c0 <bar+32>: mov -0xc(%rbp),%edx
1: $pc = (void (*)()) 0x7ffff79da6c0 <bar+32>
(gdb) x/7i $pc
=> 0x7ffff79da6c0 <bar+32>: mov -0xc(%rbp),%edx
0x7ffff79da6c3 <bar+35>: mov -0x8(%rbp),%eax
0x7ffff79da6c6 <bar+38>: mov %edx,%esi
0x7ffff79da6c8 <bar+40>: mov %eax,%edi
0x7ffff79da6ca <bar+42>: callq 0x7ffff79da580 <foo@plt>
0x7ffff79da6cf <bar+47>: leaveq
0x7ffff79da6d0 <bar+48>: retq
(gdb) x/1xbg 0x7ffff79da000+0x000000200988
0x7ffff7bda988: 0x00007ffff7bdb660
(gdb) info address foo
Symbol "foo" is a function at address 0x7ffff7bdb660.

出現 foo 的 address 啦!

再看第二次 call foo() 會怎樣:

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
(gdb) b *0x7ffff79da6ca
Breakpoint 6 at 0x7ffff79da6ca: file bar.c, line 8.
(gdb) c
Continuing.

Breakpoint 6, 0x00007ffff79da6ca in bar (a=2, b=3, n=10) at bar.c:8
8 foo(b, n);
2: x/i $pc
=> 0x7ffff79da6ca <bar+42>: callq 0x7ffff79da580 <foo@plt>
(gdb) si
0x00007ffff79da580 in foo@plt () from ./libbar.so
2: x/i $pc
=> 0x7ffff79da580 <foo@plt>: jmpq *0x200402(%rip) # 0x7ffff7bda988
1: $pc = (void (*)()) 0x7ffff79da580 <foo@plt>
(gdb) x/5i $pc
=> 0x7ffff79da580 <foo@plt>: jmpq *0x200402(%rip) # 0x7ffff7bda988
0x7ffff79da586 <foo@plt+6>: pushq $0x1
0x7ffff79da58b <foo@plt+11>: jmpq 0x7ffff79da560
0x7ffff79da590 <__cxa_finalize@plt>: jmpq *0x2003fa(%rip) # 0x7ffff7bda990
0x7ffff79da596 <__cxa_finalize@plt+6>: pushq $0x2
(gdb) si
foo (x=2, y=3) at foo.c:2
2 {
2: x/i $pc
=> 0x7ffff7bdb660 <foo>: push %rbp
1: $pc = (void (*)()) 0x7ffff7bdb660 <foo>

一樣跳到 foo@plt,接著會跳到 foo.got.plt 所記錄的 address,現在是 foo() 的 address 了所以就會進 foo() 了!

總結上面的 lazy binding 行為,call function 時會跳到 PLT 裡執行,進到 PLT 的第一個指令是跳到 symbol 於 GOT 裡所指向的位置。第一次 call 時 GOT 記錄的 address 會是 PLT 對應的下一個指令,相當於繼續執行 PLT 的 instruction,接著一路 call 到 resolve symbol 的 function 找 symbol 以及將 address 填進 GOT。之後再 call 到相同 function 一樣會先到 PLT,但第一個指令就會從 GOT 得到正確 address、跳去該 function 執行。

觀察 main 裡的 bar 也會有類似的情況,只是 main 因為是 executable file,relocation entry 的 offset 已經是 load 進 memory 的 address,可以不用像 shared library 那樣要用 load 的 memory 開頭算 offset。

Initialization

除了一般會進入 shared library 的 .init section 進行初始化外,GCC 還提供了 shared library 的 constructor 及 destructor 擴充語法,可以讓 function 在 load shared library 時執行以做更多初始化的動作,宣告語法如下:

1
2
void __attribute__((constructor)) init();
void __attribute__((destructor)) fini();

使用這種 constructor 跟 destructor 必須使用系統預設的 standard runtime library 以及啟動檔案,不可以用 -nostartfiles 以及 -nostdlib 參數。constructor 會在 load shared library 時執行(執行 main 之前),而 destructor 則會在執行 exit() 時執行。如果是 Explicit Runtime Linking,constructor 會在 dlopen() 時執行,destructor 則在 dlclose() 時執行。

Murmur & 省略的細節

上面這樣順順列下來看起來好簡單,但其實我找 GOT 跟 PLT 到底在做什麼找~很~久~一直錯把 GOT 當 PLT,就搞不懂是在幹嘛……

PIC 跟 GOT/PLT 還有不少細節,例如 ELF 怎麼處理 library 內跟跨 library 的 function call?變數的 relocation 怎麼做的?resolve function 怎麼知道要找哪個 symbol?這些先暫時略過,不然我永遠寫不完……Orz

用 Linux 桌面最麻煩的地方之一是中文介面。我已經放棄設語系為中文,畢竟 terminal 各種 message 顯示中文實在很奇怪,但還是需要輸入法跟能看一點的字型啊啊啊啊啊……

  • distribution:Debian 8
  • 桌面環境:KDE

輸入法

我先試了 scim,但用起來好像有點 bug,導致操作有點卡。後來換 hime,它的各種切換快捷鍵、符號輸入等等操作跟 windows 的中文輸入法差不多,就用它啦~

裝完、修改完設定檔 ~/.xsessionrc 讓 KDE 去用它,改完 relogin。

1
2
3
4
5
$ sudo apt-get install hime hime-chewing
$ cat ~/.xsessionrc
export GTK_IM_MODULE=hime
export XMODIFIERS=@im=hime
export QT_IM_MODULE=hime

中文字型

比較喜歡沒襯線、像麥克筆寫的字體。

目前覺得 Noto 系列跟 WenQuanYi Micro Hei 系列不錯。一般字體就挑順眼的,等寬字體是用 WenQuanYi Micro Hei Mono。WenQuanYi 還有正黑體的版本,但我覺得微米黑(Micro Hei)比較好看。

1
$ sudo apt-get install fonts-noto fonts-wqy-microhei fonts-wqy-zenhei

Ref:http://wiki.ubuntu.hk/w/%E4%B8%AD%E6%96%87%E5%AD%97%E5%9E%8B

想不到標題只好亂寫。跟這篇有點相關,但講點別的。

因為某些邏輯有點複雜,修改程式很容易會改壞東西,而且改的那個人也不見得會測到所有該測的條件。

這種問題的解法之一是 unit test,用充分的測試保護那個 module,就能確保之後的修改比較不會改壞原本好的東西。但是那個 project 還沒裝 unit test framework,我也不熟,而且有點時間壓力,我認為先做出一個堪用的版本比架起 unit test framework 重要。

不過我也真的覺得要是之後改了一點 code,就可能會山崩,阿不是,是壞一堆沒想到的地方啊啊啊。光想到可能會 bug 相連到天邊、牽一髮動全身就覺得可怕。

後來我用比較彈性的做法,讓之後可以用新增 class 或一小段 code 而不修改原本 code 的方式加新的邏輯。咦?好眼熟喔!阿不就OCP?真是突然體會了為什麼那些 principle 那麼討厭「修改原有 code」,因為改原本的 code 就有風險、有可能造成意想不掉的 bug。這種事就是工程師最討厭的──我明明沒動那裡為什麼那邊會壞。

修改意味著可能有 bug,而且蠻高的機率需要修改,來不及架測試安全網,至少以遵守 OCP 的方式避免之後生 bug,也是一個方法。無法降低風險至零,但應該要能降低到能夠掌控。

設計原則也好,自動測試也好,它們只是方法不同,最終的目標是一樣的──完成一個有品質的軟體、系統。漸漸覺得,好像是看情況使用,而不見得非得如何。當然,有好設計以及自動測試的軟體是更好的。

Dynamic linking 遇到的問題

Dynamic linking 想解決的一大問題是 memory 浪費。直覺想法是如果能讓不同 process 都會使用到的 library 在 memory 只有一份就能節省空間。對不同 process 來說 library 的內容必須是相同的才能共用。library 主要是 instruction 以及 data (executable file 都是這樣辣),data 不可能在 process 間共用,因為每個 process 都需要它自己的 data,不然會互相干擾(好像有古代系統是共用的…),因此能共用的主要是 instruction。

excutable file、object file 以及 library 等 binary file 中 instruction 會以不同定址方式 access symbol,絕對定址模式會將 symbol 的 virtual address 寫進 instruction,相對定址模式則跟 instruction 及 data 之間的相對位置有關。也就是說,無論是 executable file 還是要共用的 library,instruction 都可能涉及 symbol 的 address 資訊。

不像 executable file,library 在 compile time 無法知道會被 load 到哪,因為系統裡會有多個 library,如果各自指定要 load 到哪可能會撞到,所以得等到 runtime 由系統決定,其中 symbol 位置也要到 runtime 才能決定。這使得 compile time 無法修改 instruction 內的 address 資訊,也就是 static linking 做的事。

另一方面,即使在 runtime 修改 library 的 instruction,也會造成不同 process 實際上有不同的 library instruction 而無法共用。例如 library A 使用某個外部 symbol foo,process 1 跟 process 2 都有使用 library A,但它們分別以 library B 跟 library C 來提供 symbol foo 給 library A。此時,process 1 的 foo 的 address 在 library B,process 2 則在 library C,runtime 修改 library A instruction 會有兩種版本。

Position-independent Code(PIC)

上面的問題基本上是因為 instruction 裡含有 symbol address 相關資訊,就出現 Position-independent Code(PIC)「與位置無關的程式碼」來解決。

由於 process 有各自的 data section 而且可以修改裡面的值,PIC 將 library 會被修改的部分(instruction 中的 address 相關資訊)放到 data section,讓 instruction 跟 address 無關而能共用。

library 的 address reference 可分為 library 內與跨 library,各自又再分成 reference 到資料或 instruction(function call 或 jump),處理方式主要依據 library 內或跨 library 而不同。

library 內

同一 library 內的 instruction 跟資料間相對位置是固定的,所以可以用相對位置來 access 資料、call function 或 jump。

跨 library

ELF 在 data section 放一個指向其他 library 的 symbol 的 pointer array,稱為 Global Offset Table(GOT)。instruction 可以從 GOT 找到對應的 element 進行間接 reference。先找到 GOT(不同平台有不同作法,可以用相對定址也可以有特殊 register 記錄),再從 GOT 以及 instruction 所知道的「該 symbol 在 GOT 裡的 offset」得到 element,最後得到 symbol address。

GOT 由 linker 載入 library 時填填內容,同樣使用 relocation table 的 entry 標示需要修改的位置及如何修改。relocation table 不會管 offset 指向的位置是什麼,改那個地方的內容就對了,放 GOT element 就會改 GOT。至於變數與 call function 的差別在 GOT element 存的是變數還是 function 的 address,不過實際上 ELF 有區分變數跟 function,這部份下一篇再說。

雖然 GOT 可以達到 PIC,但代價是 access symbol 的速度會變慢,因為要先找到 GOT 再間接定址。

Example

foo.c
1
2
3
4
5
6
7
8
extern int sum;

int foo(int a, int b)
{
static int* p = (int*)123; // avoid to be placed in .bss
p = &sum;
return a + b;
}
1
2
$ gcc -c foo.c
$ gcc -fPIC -c foo.c -o foo.o.pic

-fPIC 跟沒有的差別:

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
34
35
36
37
38
39
40
$ objdump -d foo.o

foo.o: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <foo>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 89 7d fc mov %edi,-0x4(%rbp)
7: 89 75 f8 mov %esi,-0x8(%rbp)
a: 48 c7 05 00 00 00 00 movq $0x0,0x0(%rip) # 15 <foo+0x15>
11: 00 00 00 00
15: 8b 55 fc mov -0x4(%rbp),%edx
18: 8b 45 f8 mov -0x8(%rbp),%eax
1b: 01 d0 add %edx,%eax
1d: 5d pop %rbp
1e: c3 retq


$ objdump -d foo.o.pic

foo.o.pic: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <foo>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 89 7d fc mov %edi,-0x4(%rbp)
7: 89 75 f8 mov %esi,-0x8(%rbp)
a: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # 11 <foo+0x11>
11: 48 89 05 00 00 00 00 mov %rax,0x0(%rip) # 18 <foo+0x18>
18: 8b 55 fc mov -0x4(%rbp),%edx
1b: 8b 45 f8 mov -0x8(%rbp),%eax
1e: 01 d0 add %edx,%eax
20: 5d pop %rbp
21: c3 retq

結果是用 -fPIC compile 出來跟沒有用的 object file 不一樣(好像廢話),而且沒有 -fPIC 無法 link 成 shared object。

1
2
$ gcc -shared foo.o -o foo.so
/usr/bin/ld: foo.o: relocation R_X86_64_32S against `sum' can not be used when making a shared object; recompile with -fPIC

relocation table 如果有 R_X86_64_32S 定址無法變成 DSO。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ readelf -r foo.o

Relocation section '.rela.text' at offset 0x240 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000d 000300000002 R_X86_64_PC32 0000000000000000 .data - 8
000000000011 000a0000000b R_X86_64_32S 0000000000000000 sum + 0

Relocation section '.rela.eh_frame' at offset 0x270 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0

$ readelf -r foo.o.pic

Relocation section '.rela.text' at offset 0x278 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000d 000b00000009 R_X86_64_GOTPCREL 0000000000000000 sum - 4
000000000014 000300000002 R_X86_64_PC32 0000000000000000 .data - 4

Relocation section '.rela.eh_frame' at offset 0x2a8 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0

再看看 shared library 的 section 們,只列出比較相關的部份。

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
$ gcc -fPIC -shared -o foo.so foo.c
$ readelf -S foo.so
There are 27 section headers, starting at offset 0x1170:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
...
[ 3] .dynsym DYNSYM 00000000000001f8 000001f8
0000000000000150 0000000000000018 A 4 2 8
[ 4] .dynstr STRTAB 0000000000000348 00000348
00000000000000ab 0000000000000000 A 0 0 1
...
[ 7] .rela.dyn RELA 0000000000000430 00000430
00000000000000d8 0000000000000018 A 3 0 8
[ 8] .rela.plt RELA 0000000000000508 00000508
0000000000000030 0000000000000018 AI 3 10 8
...
[10] .plt PROGBITS 0000000000000560 00000560
0000000000000030 0000000000000010 AX 0 0 16
...
[18] .dynamic DYNAMIC 0000000000200760 00000760
00000000000001c0 0000000000000010 WA 4 0 8
[19] .got PROGBITS 0000000000200920 00000920
0000000000000030 0000000000000008 WA 0 0 8
[20] .got.plt PROGBITS 0000000000200950 00000950
0000000000000028 0000000000000008 WA 0 0 8
...

Lazy Binding

dynamic linking 以犧牲一點效能達到模組使用的靈活度。效能降低發生在兩個地方:程式開始執行時的 linking 工作以及 GOT 帶來的間接定址。

程式裡可能有很多 function 在執行過程中不會或很少被用到,例如錯誤處理跟少用的功能。一開始執行就 link 所有 library 裡的 function 顯然有點浪費,畢竟可能花時間 link 了卻沒用到。如果等到 function 第一次被使用時才 bind symbol(找 symbol、relocate 等等)可以加快程式啟動的速度,這個方法稱為 lazy binding。

ELF 用 Procedure Linkage Table(PLT)來實作 lazy binding。在沒有 lazy binding 前,會藉由 GOT 進行間接跳轉來 access 另一個模組的 function 。有了 lazy binding 表示一開始 load 模組時不會把 GOT 填完,所以使用 GOT 跳轉前要多一層 PLT 的處理:如果 GOT element 沒有值,會先由 dynamic linker 找到該 function 的 address,填入 GOT 後跳去該 function 執行。之後再使用到同一個 function,由於 GOT 裡已經有值,可以直接進行間接跳轉。

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

裝 Linux 桌面系統來用用,為了避免以後可能重灌還要重找一次,記下些安裝過程。

雖然有 open source 版的顯卡 driver,但我還是覺得裝 Nvidia 的比較好,而且不另外裝 driver 根本調不動螢幕解析度……

原本想裝 LMDE 2 然後用這篇的方法裝 Nvidia 的 driver,但不知道為什麼就是無法進文字模式。設開機進文字模式或不裝桌面開機會卡在半路開不了,直接關掉桌面系統還是停在無法操作的狀態。

後來發現 Debian 蠻好裝 Nvidia driver 的,而且 Mint 不只顯卡需要額外 driver 連 audio 都要搞到我有點崩潰,就乾脆換用 Debian 8 Jessie了。

Nvidia driver installation under Debian 8

雖然 Debian wiki 上都有,還是記錄一下。

修改 /etc/apt/sources.list,增加:

1
2
# jessie-backports
deb http://httpredir.debian.org/debian jessie-backports main contrib non-free

安裝 linux headers

1
# apt-get install linux-headers-$(uname -r|sed 's,[^-]*-[^-]*-,,')

安裝 Nvidia drier

1
2
# apt-get update
# apt-get install -t jessie-backports nvidia-driver

最後重開機,完成!

這個人又來胡言亂語了,不是什麼正經筆記

最近寫某個邏輯或定義常常不清楚或者 ambiguous 的東西(因為資料來源是給人看而且也沒整理過),但程式需要清楚的邏輯,感覺在試圖把模糊不清的東西轉成邏輯清晰的程式。似乎,在原本是人工作業而且沒有特別統一或整理過邏輯或步驟的事情上可能就有這種現象。

工程師很喜歡「找 general 的方法」跟「確認出清楚的邏輯」,畢竟這樣程式比較好寫。但是這東西不只原本的邏輯可能不清楚,還「一定會變動但無法預期會如何變動」,以至於後來我放棄找 general 的方法,而是把問題切得更細再組起來,其實就是 divide and conquer。 甚至稍微放寬對清晰邏輯的要求,改用擴充性比較好的做法。只要變動的時候可以用比較低風險──不會影響到其他部分──的方式因應就好,如此,無論是現在邏輯可能再修正或者之後增加目前沒有的東西都能處理。

這讓我更能體會為什麼很多設計都希望增加程式擴充性跟降低修改風險,這讓工程師在面對模糊或高度變動的狀況可以好過一點,不然會改到崩潰

至於,不能邏輯清楚了再寫嗎?不是不行,但有時候等回應實在太慢,要搞清楚完整邏輯需要花很多時間,而且仍然無法避免「找不到 general 方法」,那就弄個可以微調的辦法之後再慢慢調就好啦!

另外,最近看了一篇文章讓我覺得相對於機器世界的邏輯分明,人類世界本來就沒有那麼清晰跟確定,而工程師剛好是兩者的翻譯官。

Cargo basic

Cargo 是 Rust 的 build & package management system,通常會用 Cargo 管理 Rust project。安裝 Rust 會一起裝 Cargo。

這跟 npm、bower、composer 等等 package manager 很像,不過為 Rust 需要 compile,所以 Cargo 除了 package management 還有 build system。package manager 是為了解決各種 library 或 package dependency 的問題,例如像 C/C++ 得自己處理如何 build 跟 link library(還會有只剩 binary 檔不知道 source code 從哪來的 library)、維持 library 版本以及升級 library 等問題。

Cargo 裡稱 package 或 library 為 crate。

Cargo 基本指令:

1
2
3
4
$ cargo build
$ cargo run
$ cargo clean
$ cargo update # 升級 crate

Start new project

$ cargo new <project name> --bin

產生基本 project 的 directory,內含 src/Cargo.toml 並且建立 git repository。加 --bin 是為了 build 出可執行檔,不加會是 library。建立 project 後修改 Cargo.toml 即可,Cargo.toml sample:

1
2
3
4
5
6
[package]
name = "rust-hello-cargo"
version = "0.1.0"
authors = ["cjwind <cwentsai@gmail.com>"]

[dependencies]

Directory structure

1
2
3
4
src/    # source code directory
xxx.rs
Cargo.toml # Cargo configuration
Cargo.lock # Cargo uses the Cargo.lock file to keep track of dependencies

Cargo 會自己維護 Cargo.lock,不須手動修改。

Cargo.toml

Cargo 用 Cargo.toml 來 maintain dependency──需要什麼 crate 以及 crate 版本。這跟 nodejs、PHP 之類的 package management 差不多,只是各自用不同格式而已。

修改 Cargo.toml 之後再 build,Cargo 會去抓需要的 crate 跟處理 dependency──如果 crate 還有 depend 是 project 沒有的也會去抓──然後 compile 它們。

Cargo.lock

假設 project 依賴 crate A 1.0.12,crate A 有新版本 1.0.13 時會怎樣?

如果 Cargo 自動更新 crate 版本,萬一新版 crate A 反而讓我們的程式爛掉怎麼辦?畢竟總是可能有 bug 的,又或者某些介面或功能被修改了、行為跟原本不一樣也可能造成問題。

Cargo 對此的解決方式跟其他 package manager 差不多──使用 Cargo.lock

第一次 build project 的時候(應該是 dependency 改動過後的第一次),Cargo 會在 Cargo.lock 記下當下 crate 的版本並且認定這些版本是 ok 的。之後即使 crate 有新版本,Cargo 也不會主動去更新而是以 Cargo.lock 記錄的為準,直到我們要求升級 crate。

cargo update 預設只會升最後一碼版號,比較大版本的升級要手動改 Cargo.toml。因為大版本的更新可能會有 interface 的改動,需要我們確認相應修改沒問題才能更新。

延伸閱讀

explicit runtime linking 讓程式可以在執行過程中控制 load 及 unload 模組。這種方式讓載入模組變得更靈活,可以在程式需要時才載入某個模組,載入什麼模組也可以依執行狀況決定。因為不需要在一開始載入所有模組,所以可以減少啟動時間跟記憶體用量。另外,因為可以在執行期才載入,也就能在不關閉程式的情況下新增、刪除及更新模組。

在程式碼裡指定了什麼時候要 load 哪個模組,對程式來說它就「知道」要有載入模組的動作。如果是以 dynamic linker 來 load 跟 link,模組跟 library 對程式是透明的,它不知道它用了什麼。兩者的差別即在手動或自動 load shared library,以及load 的時機。

另一方面,如果需要知道一個程式依賴哪些模組,例如移植時要將所有相關模組移植到其他平台以確保所有功能正常。這時以 explicit runtime linking 載入的模組可能增加檢查 dependency 的難度,因為無法用 ldd 得知依賴的模組,可能要等到用到某個功能時才當掉或無法使用。當然這問題並不是太大的缺點,可以透過適當管理 dependency library 以及以一致的方式撰寫 explicit linking 程式碼來避免。

linux 以四個 function 來達到 explicit runtime linking,分別是 dlopen()dlsym()dlerror() 以及 dlclose(),它們宣告在 <dlfcn.h>,link 時須 -ldl

dlopen()

dlopen() 開啟一個 dynamic library,將其 load 到 process 的 adddress space,並完成初始化。

void *dlopen(const char *filename, int flag);

第一個參數 filename 很簡單,就是 dynamic library 的路徑。如果是絕對路徑就能直接打開 dynamic library,如果是相對路徑則會依照某個順序尋找 library,詳細的順序可參考 man dlopen

flag 必須指定 RTLD_LAZYRTLD_NOW 之一,RTLD_LAZY 表示 lazy binding,也就是 symbol 在第一次被用到的時候才 resolve。lazy binding 只用於 function,variable 會在 library load 時就 bind 好。RTLD_NOW 當然就是在 load library 的時候就會把所有 symbol 都 resolve。除了 RTLD_LAZYRTLD_NOW 之外,還有 RTLD_GLOBAL 可以跟他們一起使用,表示之後載入的 library 都可以使用這個 library 的 symbol。其他更多 flag 就參考 manual 囉。

dlopen() return 一個 handle,之後的操作要傳這個 handle 給其他 function。如果 load 失敗,會 return NULL。如果要 load 的 library 之前已經 load 過,會 return 跟之前相同的 handle。

如果 load 進來的 library 依賴其他 shared library(link 時指定,非 explicit runtime linking),dlopen() 會自動 load 那些 shared library。

dlsym()

讓我們可以找到 library 的 symbol。

void *dlsym(void *handle, const char *symbol);

handle 是剛剛 dlopen() return 的 handle。

symbol 是要找的 symbol 的名字,如果找不到 symbol 會 return NULL。找到 symbol 會 return 該 symbol 的 address。根據 manual,return 值是 NULL 無法當作找不到 symbol 的判斷,因為 return 值有可能真的是 NULL(不過我想不到這種狀況的例子…),須配合 dlerror() 才能確認是不是有找到 symbol:首先 call dlerror() 清掉前面的 error,接著 call dlsym() 找 symbol,再 call dlerror() 看是否有 error 來判斷是否找到 symbol。

如果多個 library 有相同的 symbol 會用哪個?dlsym() 會以 BFS 找 library 的 dependency tree,直到找到 symbol 或完全找不到為止。也就是說 load 順序會決定 symbol 同名時使用到誰,先 load 會先用。

dlerror()

call dlopen()dlsym()dlclose() 之後可以 call dlerror() 看上次呼叫有沒有成功。如果成功 dlerror() 會 return NULL,反之則 return error 相關資訊。

char *dlerror(void);

dlclose()

用來 unload library。

int dlclose(void *handle);

系統會用 counter 紀錄一個 library load 的次數,也就是 dlopen() 時加一,dlclose() 則減一,當 counter 變成 0 時 library 才會真的被 unload 掉。unload 的順序跟 load 相反,會先執行 .finit section 的程式碼,接著從 symbol table 中移除相關 symbol,取消 process address space 跟 library 的 memory mapping,最後關閉 library 檔案。

成功 return 0,失敗 return 非 0 值。

Example

簡單用個例子實作跟驗證。以下共有 libfoo.solibbar.solibkar.so 三個 library,主程式 main.c 以 explicit runtime linking load libfoo.so,而 libfoo.so 依賴 libbar.so 以及 libkar.so,後兩者有相同名稱的 function。

bar.c
1
2
#include <stdio.h>
void bar() { printf("bar() in bar.c\n"); }
kar.c
1
2
#include <stdio.h>
void bar() { printf("bar() in kar.c\n"); }

compile with

1
2
$ gcc -g -fPIC -shared -o libbar.so bar.c
$ gcc -g -fPIC -shared -o libkar.so kar.c
foo.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int zoo = 1;
const int coo = 5566;

int foo(int a, int b)
{
printf("foo = %x, &zoo = %x, &coo = %x\n", foo, &zoo, &coo);
return a + b;
}

static int koo(int n)
{
return n + coo;
}

compile with gcc -g -fPIC -shared -o libfoo.so foo.c ./libbar.so ./libkar.so

main.c
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <dlfcn.h>

int main()
{
void* handle = NULL;

handle = dlopen("./libfoo.so", RTLD_LAZY);

if (handle == NULL)
{
printf("Load library failed. error: %s\n", dlerror());
return -1;
}

int (*fn)(int, int) = NULL;
dlerror(); // clear error condition
fn = dlsym(handle, "foo");
// fn = dlsym(handle, "koo"); // fail

char* error = dlerror();
if (error != NULL)
{
printf("Load symbol failed. error: %s\n", error);
return -1;
}

printf("fn = %x\n", fn);

int ret = fn(3, 5);
printf("ret = %d\n", ret);

// doesn't check error
int* p = NULL;
p = dlsym(handle, "zoo");
printf("p = %x\n", p);

int* c = NULL;
c = dlsym(handle, "coo");
printf("c = %x, *c = %d\n", c, *c);

void (*bar)() = NULL;
bar = dlsym(handle, "bar");
bar();

getchar(); // just pause process

if (dlclose(handle) != 0)
{
printf("Close library fail. error: %s\n", dlerror());
return -1;
}

return 0;
}

compile with gcc -g -o main main.c -ldl

執行結果顯示拿到主程式 libfoo.so 裡 symbol 的 address,以及 load 到 libbar.so 裡的 bar()

1
2
3
4
5
6
7
$ ./main 
fn = ef40f740
foo = ef40f740, &zoo = ef60faf0, &coo = ef40f7a8
ret = 8
p = ef60faf0
c = ef40f7a8, *c = 5566
bar() in bar.c

process 的 address space:

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
00400000-00401000 r-xp 00000000 08:01 11409326                           /home/cjw/source/explicit-load/main
00600000-00601000 rw-p 00000000 08:01 11409326 /home/cjw/source/explicit-load/main
02552000-02573000 rw-p 00000000 00:00 0 [heap]
7fe9bee48000-7fe9bee49000 r-xp 00000000 08:01 11409316 /home/cjw/source/explicit-load/libkar.so
7fe9bee49000-7fe9bf048000 ---p 00001000 08:01 11409316 /home/cjw/source/explicit-load/libkar.so
7fe9bf048000-7fe9bf049000 rw-p 00000000 08:01 11409316 /home/cjw/source/explicit-load/libkar.so
7fe9bf049000-7fe9bf04a000 r-xp 00000000 08:01 11409324 /home/cjw/source/explicit-load/libbar.so
7fe9bf04a000-7fe9bf249000 ---p 00001000 08:01 11409324 /home/cjw/source/explicit-load/libbar.so
7fe9bf249000-7fe9bf24a000 rw-p 00000000 08:01 11409324 /home/cjw/source/explicit-load/libbar.so
7fe9bf24a000-7fe9bf24b000 r-xp 00000000 08:01 11409329 /home/cjw/source/explicit-load/libfoo.so
7fe9bf24b000-7fe9bf44a000 ---p 00001000 08:01 11409329 /home/cjw/source/explicit-load/libfoo.so
7fe9bf44a000-7fe9bf44b000 rw-p 00000000 08:01 11409329 /home/cjw/source/explicit-load/libfoo.so
7fe9bf44b000-7fe9bf5ec000 r-xp 00000000 08:01 7864382 /lib/x86_64-linux-gnu/libc-2.19.so
7fe9bf5ec000-7fe9bf7ec000 ---p 001a1000 08:01 7864382 /lib/x86_64-linux-gnu/libc-2.19.so
7fe9bf7ec000-7fe9bf7f0000 r--p 001a1000 08:01 7864382 /lib/x86_64-linux-gnu/libc-2.19.so
7fe9bf7f0000-7fe9bf7f2000 rw-p 001a5000 08:01 7864382 /lib/x86_64-linux-gnu/libc-2.19.so
7fe9bf7f2000-7fe9bf7f6000 rw-p 00000000 00:00 0
7fe9bf7f6000-7fe9bf7f9000 r-xp 00000000 08:01 7864385 /lib/x86_64-linux-gnu/libdl-2.19.so
7fe9bf7f9000-7fe9bf9f8000 ---p 00003000 08:01 7864385 /lib/x86_64-linux-gnu/libdl-2.19.so
7fe9bf9f8000-7fe9bf9f9000 r--p 00002000 08:01 7864385 /lib/x86_64-linux-gnu/libdl-2.19.so
7fe9bf9f9000-7fe9bf9fa000 rw-p 00003000 08:01 7864385 /lib/x86_64-linux-gnu/libdl-2.19.so
7fe9bf9fa000-7fe9bfa1a000 r-xp 00000000 08:01 7864371 /lib/x86_64-linux-gnu/ld-2.19.so
7fe9bfbf9000-7fe9bfbfc000 rw-p 00000000 00:00 0
7fe9bfc16000-7fe9bfc1a000 rw-p 00000000 00:00 0
7fe9bfc1a000-7fe9bfc1b000 r--p 00020000 08:01 7864371 /lib/x86_64-linux-gnu/ld-2.19.so
7fe9bfc1b000-7fe9bfc1c000 rw-p 00021000 08:01 7864371 /lib/x86_64-linux-gnu/ld-2.19.so
7fe9bfc1c000-7fe9bfc1d000 rw-p 00000000 00:00 0
7ffdd05ea000-7ffdd060b000 rw-p 00000000 00:00 0 [stack]
7ffdd0755000-7ffdd0757000 r-xp 00000000 00:00 0 [vdso]
7ffdd0757000-7ffdd0759000 r--p 00000000 00:00 0 [vvar]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

三個 library 都 load 進來啦~

用 gdb 在 _init_fini 設 break point 可以看到在 dlopen()dlclose() 時會分別執行 libfoo.so 及其他 library 的 _init_fini。同樣適用 GCC 提供的 shared library constructor 及 destructor,Ref

可以用 lddlibfoo.so 的 dependency:

1
2
3
4
5
6
$ ldd libfoo.so 
linux-vdso.so.1 (0x00007fffde785000)
./libbar.so (0x00007fdf77158000)
./libkar.so (0x00007fdf76f57000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdf76bac000)
/lib64/ld-linux-x86-64.so.2 (0x00007fdf7755a000)

如果 compile libfoo.so 時改變 link 的 shared library 順序,變成 gcc -g -fPIC -shared -o libfoo.so foo.c ./libkar.so ./libbar.so,load 到的 bar() 會變成是 libkar.so 的。

1
2
3
4
5
6
7
$ ./main
fn = a259c740
foo = a259c740, &zoo = a279caf0, &coo = a259c7a8
ret = 8
p = a279caf0
c = a259c7a8, *c = 5566
bar() in kar.c

ldd 看到的順序也會變:

1
2
3
4
5
$ ldd libfoo.so 
linux-vdso.so.1 (0x00007fff4db40000)
./libkar.so (0x00007f5b4ddb5000)
./libbar.so (0x00007f5b4dbb4000)
...

build 好 servo 之後執行 ./mach run -d tests/html/about-mozilla.html 視窗只出現白白的畫面。這怎麼看都不正常啊……ˊ(´;ω;`)

環境

  • LMDE 2 Cinnamon 64 bits
  • NVIDIA GeForce GTS 250
  • kernel 3.16.0-4-amd64

各種測試

build release 版後 ./mach run -r tests/html/about-mozilla.html 跟從 https://download.servo.org/ 抓 pre-built 版都白白的。

用 gdb 跑跑看

1
2
3
./mcah run -d --debug
gdb> r tests/html/about-mozilla.html
...blahblah

所以有在動啊……

./mach run -d tests/html/about-mozilla.html -o output.png 輸出圖檔,圖片也是一片白。

update 系統再 try 也不行。

用 VM LMDE 跑 pre-built 出現其他 error 0:1(10): error: GLSL 1.50 is not supported. Supported versions are 1.10, 1.20, 1.30, 1.00 ES, and 3.00 ES可能跟 VM 沒 support OpenGL 有關,有點岔題所以不管。

另一台電腦 Ubuntu 跑 pre-built,正常。

猜猜樂

pre-built 不會動表示不是我 build 的有問題。猜是 render 有問題,測輸出圖檔也不行,應該八九不離十是 render 問題。google 看不出所以然,Ubuntu 是好的但應該不至於 Ubuntu 可以 LMDE 卻不能用。

畫不出來,不然更新看看 driver。

先說結果,猜中了。(′‧ω‧‵)

檢查顯卡型號 & driver

1
2
$ lspci | grep VGA
01:00.0 VGA compatible controller: NVIDIA Corporation G92 [GeForce GTS 250] (rev a2)

看顯卡型號。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ glxinfo | OpenGL
OpenGL vendor string: NVIDIA Corporation
OpenGL renderer string: GeForce GTS 250/PCIe/SSE2
OpenGL core profile version string: 3.3.0 NVIDIA 340.101
OpenGL core profile shading language version string: 3.30 NVIDIA via Cg compiler
OpenGL core profile context flags: (none)
OpenGL core profile profile mask: core profile
OpenGL core profile extensions:
OpenGL version string: 3.3.0 NVIDIA 340.101
OpenGL shading language version string: 3.30 NVIDIA via Cg compiler
OpenGL context flags: (none)
OpenGL profile mask: (none)
OpenGL extensions:
OpenGL ES profile version string: OpenGL ES 2.0 NVIDIA 340.101 340.101
OpenGL ES profile shading language version string: OpenGL ES GLSL ES 1.00
OpenGL ES profile extensions:

看顯卡 driver,這是已經換成 NVIDIA driver 的結果。如果 OpenGL renderer string 出現 Mesa 之類的表示不是用 NVIDIA 的 driver。

更新顯卡 driver

為了更新顯卡搞了半天……總結如下。

抓 NVIDIA driver

修改 /etc/default/grubGRUB_CMDLINE_LINUX_DEFAULT="nouveau.blacklist=1 quiet splash text",之後

1
2
3
$ sudo update-grub
$ sudo update-initramfs
$ sudo reboot

nouveau.blacklist=1 是 disable nouveau,nouveau 是 open source 社群做的 NVIDIA 顯卡的 driver。

text 則是開機不進 GUI,因為得關掉 X server 才能裝 driver,乾脆直接進文字模式裝。我以為是改 run level,但是 google 到一堆都這樣搞就這樣了。有人說用 recovery mode 也可以裝 driver,不過我沒試。

從 NVIDIA 抓下 NVIDIA-Linux-x86_64-340.101.run,是個 script,要用 root 權限跑。

1
2
# export CC=gcc-4.8
# sh NVIDIA-Linux-x86_64-340.101.run

需要設 CC 是因為我 gcc 預設版本是 4.9,但是 compile driver 需要 4.8。

安裝過程是 console 的互動介面(忘記這叫什麼了啦)就不細寫了。中間有問要不要 build 成什麼 module 之類的,選 yes 會 fail,懶得研究那是什麼東西,用 no 就安裝成功了。

最後,修改 /etc/default/grub 以 GUI 開機:GRUB_CMDLINE_LINUX_DEFAULT="nouveau.blacklist=1 quiet splash" 再 update grub 跟 reboot。

開 servo 終於正常了…\T_T/

Ref