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

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

這種問題的解法之一是 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 = ∑
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

Design Pattern 提供某個情境下針對某問題的某種解決方法。這些解法通常是前人整理出來、經過驗證的。

  • 「情境」是某種不斷出現的狀況。
  • 「問題」是在某個情境下希望達到的目標,也可能是情境下的限制。
  • 「解決方法」希望是個 general 的設計,用來解決限制、達到目標。

常用來描述 design pattern 的方式是 GoF 書裡使用的格式。

使用 Design Pattern

能用簡單方法解決問題就用簡單方法。pattern 會為系統增加額外複雜度,只有需要的時候才使用。

使用 pattern 要注意是否有意義以及是否對系統其他部分造成影響,不要為了用而用。如果預期系統在未來會有實際的改變,也可以用 pattern 預先加上彈性。不過要注意,必須是實際的改變,而不是假定的改變。自己覺得可能會改變就用一堆 pattern 也不對,至於什麼叫做「未來實際會有的改變」又是另一個故事了……(喂

熟悉大部分 pattern 後,需要時大概會知道需要什麼 pattern,接著可以參考 pattern 的 motivation 確認想法對不對,再來要考量會不會對系統造成不良影響。設計上確認後,實作可以參考 UML 跟範例了解實作上的眉眉角角。

GoF 把 design pattern 分成三大類:creational、structural、behavioral patterns。有時候不知道確切需要哪個 pattern,可以先對問題分類再從類別中找適合的 pattern。

Design Pattern 分類

Creational pattern

處理「產生 object」。主要的目的有二,一是希望封裝 concrete class,二是想封裝「如何產生並結合 concrete class 的 instance」的過程。

  • Abstract Factory
  • Builder
  • Factory Method
  • Prototype
  • Singleton

Structural pattern

可以合成 class 跟 object 到更大的結構中。

Behavior pattern

重點在 class 與 object 間的互動,以及各自的責任。

Anti-pattern

anti-pattern 就是不好的 pattern,告訴大家什麼叫用不好的解決方式解決一個問題,好避免用到這些方法。一個好的 anti-pattern 除了說明不好的方法之外,會建議改用其他 pattern。不然就沒建設性啦

pattern 世界

除了 design pattern 之外,還有像特定領域如 concurrent 系統的 pattern、組織溝通上的 pattern、UI/UX 的 pattern 等等。pattern 是蠻 general 的概念,在蠻多領域都會出現的,共通的概念是「在面對某種重複出現的情境、狀況或問題時的應對方式」。

Ref

大名鼎鼎的 MVC(Model-View-Controller)是一種 compound pattern,要說是個架構也行,這篇主要用 design pattern 的角度去看 MVC。

compound pattern 是一堆 design pattern 被結合起來使用以解決一般性問題。

MVC 將 component 分成 model、view 以及 controller:

  • model 包含商業邏輯、資料、狀態等等,真正的功能跟做事的 code 會放在 model。
  • view 負責呈現各種資料跟畫面。
  • controller 接收來自 view 的 input,解讀後要 model 做事。依據使用的 model 不同,controller 對 input 的解讀會不同。

MVC 中的 design pattern

MVC 中有 Observer patternStrategy pattern 以及 Composite pattern

model 利用 observer pattern 通知 controller 及 view 狀態改變。如果 observer 使用 pull 的方式取得資料,model 會開 getter。如果用 push,model 會在 notify 時將 state 傳給 observer。view 一般只從 model 取得資料、不會改變 model。要 model 做事或改變 model 是 controller 的工作。有 controller 可以讓 model 跟 view 不要綁那麼緊。

view 跟 controller 使用 strategy pattern,controller 是 view 的 strategy。view 負責管理呈現,任何操作與動作都交給 controller 處理。strategy 讓 view 只需要更換 controller 就能有不同動作。

view 通常用 composite pattern 管理內部的顯示 component,例如 window、panel、widget、button 的階層式結構。

另外,如果需要轉換某個 model 的 interface 就輪到 Adapter pattern 上場啦。例如 db table 的 primary key 改了,新程式碼希望用新的 primary key 當參數又不想改舊 model,就可以用 adapter 轉換 interface。

Murmur

最早遇到 MVC 是在網頁 framework,所以一直理解 model 為「資料」。後來發現 model 好像不只是資料,有文件說 model 包含商業邏輯,但我還是說不上來到底是什麼。現在的理解是絕大多數的程式都是 model,只是把 view 拆出去並且引進 controller 作為類似中介的角色。