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 作為類似中介的角色。

讓某個物件有個替身,藉以控制外界對物件的接觸。就是個經紀人的概念。(欸)

Proxy pattern 主要目的在於「控制存取」,被代理的物件可以是遠端主機上的物件、建立成本高(例如需要時間下載)的物件、或者需要做存取權限管控的物件。

UML

Proxy Pattern

很簡單的 UML,client 透過 interface Subject 使用物件,所以它不用知道使用的是真正有功能的物件 RealSubject 還是只是代理人 Proxy。Proxy 依據型態不同會用不同方式取得 RealSubject、使用 RealSubject 的功能,再將結果丟回給 client。

除了操作 interface 外,要讓 client 使用 proxy 而非真正的 object,有個方法是用 factory 產生物件、傳回給 client,不然 client 自己 new 了個 XXXProxy,看也知道不是真正的 object 啊 XD。不過,是不是真有必要再加一層 factory 隱藏使用 proxy 的事實倒是依情況而定,簡單的情境下讓 client 自己去生 XXXProxy 也不會怎麼樣,不見得非得用 factory 增加複雜度。

Proxy 與它的變種們

Remote Proxy

代理另一台電腦上的物件或 service 等等的代理人。使用 proxy 的 client 基本上不會知道 proxy 多做了網路連線、傳遞資料等等的事情,例如 Java 的 RMI(Remote Method Invocation)(這東西應該有點年代了,就當作是個簡單的例子就好)。

Virtual Proxy

代理建立成本很高的 object,所謂成本很高可能是需要比較多時間等等,例如 video object 需要從網路抓影片、回應很慢的 API 等等。等待過程中但又不希望卡到 UI 操作,這時候就可以透過 virtual proxy 先回一個 loading 畫面,等到 video 抓好或 API 回來之後,就由真正的 object 做事。

Protection Proxy

顧名思義,保護真正 object 的 proxy,它會根據權限決定外部可不可以存取真正的 object。

Dynamic Proxy

Java 把事情搞得更複雜,弄了個 dynamic proxy 出來,在 package java.lang.reflect

簡單來說,Java 讓 programmer 連 proxy 都不用寫了,只要寫個 InvocationHandler 讓 proxy object call,runtime 才依據要代理的 object 生出 proxy object。除了讓人更懶之外不太懂為什麼要搞這個出來…

其他奇奇怪怪的 Proxy 們

放關鍵字,需要用再查。

Firewall proxy、Caching proxy、Synchronization proxy、Smart Reference proxy、Complexity Hiding proxy、Copy-On-Wrtie proxy。

網路上常說的 proxy server,就有 cache 跟控制網頁存取的功能。

Proxy 與其他 pattern 的不同

Decorator

proxy 跟 decorator 都是對一個物件包裝,有點像,但是兩者的意圖不同。proxy 的目的是控制存取,decorator 則是增加功能。而且 proxy 通常不會一直包一直包、像洋蔥一樣好多層,decorator 會依據想要的功能一直包。

Adapter

proxy 看到一半,我就想,啊這跟 adapter 不是一樣嗎!不都是在使用的 client 跟真正的 objec 之間加一層!!!(施主冷靜)

還是不一樣啦。adapter 會改變 interface,例如 function 參數不一樣,proxy 會 implement 同樣的 interface。

將狀態封裝成獨立的類別,並把動作 delegate 到目前的狀態物件,讓物件行為隨著內在狀態改變而改變。

使用情境

系統中有些狀態以及操作,而操作會依據狀態不同而不同,通常這時候能畫出 state machine。

依據 state machine,我們可能就會寫出這樣的 code:

1
2
3
4
5
6
7
8
9
10
// pseudo code
function operation() {
if (state == STATE_A) {
// do sth.
}
else if (state == STATE_B) {
// do other thing
}
// ...
}

operation() 通常是 state machine 裡的動作。

不只 operation() 裡有很多 if,還可能有很多像 operation() 的 function。如果其中有 state 需求變動,或者要加入新的 state,改這段 code 頭就痛了……

State Pattern 是將 state 封裝成一個個 class(封裝會變動的部份)。將在不同 state 要做的操作,分別放到每個 state class 中,並透過將操作 delegate 給 state object 做,來消除原本 code 裡一堆的 if。

來點例子

用《深入淺出設計模式》的例子,糖果機與狀態 class 們的 code 擷取如下:

1
2
3
4
5
6
public interface State {
public void insertQuater();
public void ejectQuater();
public void turnCrank();
public void dispense();
}
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
public class NoQuarterState implements State {
GumballMachine gumballMachine;

public NoQuarterState(GumballMachine machine) {
this.gumballMachine = machine; // 擁有糖果機的參考
}

public void insertQuater() {
// do sth.
// 改變糖果機的 state
gumballMachine.setState(gumballMachine.getHasQuarterState());
}

public void ejectQuater() {
// do sth.
}

public void turnCrank() {
// do sth.
}

public void dispense() {
// do sth.
}
}

其他的 state class 就不一一列舉啦~依此推類。

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
public class GumballMachine {
State soldOutState;
State noQuarterState;
State hasQuarterState;
State soldState;

// 不需要用表示 state 的 const 來紀錄狀態,直接用 state object 就可以了
State state = soldOutState;
int count = 0;

public GumballMachine(int numberGumballs) {
// 先把所有 state object 生出來
soldOutState = new SoldOutState(this);
noQuarterState = new NoQuarterState(this);
hasQuarterState = new HasQuarterState(this);
soldState = new SoldState(this);

// 初始化 member
this.count = numberGumballs;
if (numberGumballs > 0) {
state = noQuarterState;
}
}

public void insertQuarter() {
// 將操作 delegate 給 state object
state.insertQuarter();
}

public void ejectQuarter() {
state.ejectQuarter();
}

// other operations

public State getHasQuarterState() {
return this.hasQuarterState;
}

// other getters for State
}

糖果機將操作 delegate 給 state object 做,state object 會做事並且改變糖果機的狀態。

UML

Context::request() call state.operation1(),將事情 delegate 給 state object 做。在糖果機的例子裡,GumballMachine 就是 Context

由誰處理狀態轉換?

狀態轉換可以在 state object 做,也可以在 context 做,糖果機的例子是由 state object 做狀態轉換。

一般原則是狀態轉換是固定的時候,適合在 context 做,而轉換會在 runtime 因為條件不同而有不同時適合在 state object 做。

在 state object 轉換狀態的缺點是會讓 state object 們互相依賴,解決這問題的方式之一是讓 state object 可以透過 context 取得其他 state object,也就是糖果機的 state getters。

與 Strategy Pattern 比較

State pattern 跟 Strategy pattern 的 UML 根本長得一樣。兩個 pattern 的差異在於「意圖」,也可以說是出發點、想達到的目的。

State pattern 將行為封裝在一堆 state object 中,context 隨著狀態不同將動作 delegate 給其中一個 state object。Strategy pattern 則是有很多 algorithm 可以選,通常由使用 algorithm 的使用者(使用 strategy 的 code)決定用哪一個。Strategy 提供了 algotithm 選擇上的彈性但由使用者主導。State 則以狀態為主,狀態改變會導致行為改變。