CY's Blog

All work is preparing yourself for the accident waiting to happen.

今天想要來介紹一本對軟體工程師來說很棒的書,叫做軟技能:代碼之外的生存指南 (Soft Skills : The software developer’s life manual) 。最一開始會關注到這本書是因為有人介紹說這本書堪稱是軟體界的「原則」。由於對「原則」有還不錯的評價,所以就有了閱讀的興趣。這本書號稱是給軟體工程師看的書,但是裡面卻沒有任何一行程式碼,作者希望告訴讀者的是要成好的軟體工程師,不能只專注在專業上,要把重點放在「整個人」上。書中分享了如何找工作、自我營銷、自我學習等等,甚至還有理財、健身和愛情。同樣身為軟體工程師,我認為可以從前人身上觀察和學習他的經驗應該是蠻有幫助的,因此下面會分享幾個從書中學到比較重要的概念。

第一個是就算是受聘於公司,也應該要當作自己是在經營生意。只有把「我」當成是一家公司看待,才能做出好的商業決策。在一般公司朝九晚六(如果沒加班的話)的生活,很容易把個人的思維限制住,認為就是把固定事情做好然後領固定的薪水。然而,如果從經濟學的角度來看,其實我們都是在販賣服務並且獲取報酬,只是販賣的對象是固定某個客戶而已。當可以用比較宏觀的角度看待工作的時候,就會發現可以選擇的策略比想像中多很多,例如開始評估自己所提供的服務和價錢跟當前市場狀況是否有吻合,不吻合的話就會進一步精進服務的內容(提升能力),或是開拓其他可能的客戶(找其他公司)。這種心態上的轉變會讓我們不會只是被動接受現況,而會有積極的思維去影響現實,獲得更好的結果。

再來關於自我行銷的方面作者也提到很多,自我行銷其實跟前面相呼應,如果要把工作當作在經營生意,怎麼讓潛在客戶知道自己就很重要。找工作最容易的方法是讓工作來找你,當別人有求於你時,就更容易得到比較好的條件。對軟體工程師來說,最好的行銷方式就是寫blog,有許多有價值的文章,自然知名度就會打開。書中強調了好幾次經營blog的重要性,然後還提到最重要的就是毅力,只要持之以恆地撰寫文章,就已經勝過大多數的同行了。雖然我本身已經有意識地在經營blog了,但是常常都只是想說留個紀錄供自己未來參考。然而作者反對這種做法,他認為如果要能吸引別人最重要的是出發點是對他人有益,如果能夠對他人產生價值,就會受到關注,因此文章不是自己寫開心就好。這對我過去寫文章的方式是一種震撼,現在開始會思考我的記錄事情的角度是否可以解決讀者遇到的問題,是否容易閱讀及理解。

在變動很快的科技業中,如何自我學習是非常重要的,特別是軟體業,沒多久就有新的framework或程式語言出現,這些技術大概很難透過學校老師教導,只能靠著自己尋找資源去學習。書中作者介紹了十步學習法,不過我不打算在這裡細部講解這個方法,取而代之,我想分享作者提到的四個自學方式循環:學習、實踐、掌握、教授。我們不應該期待自己把某個領域學完後再開始應用,要盡量在最短時間內找到必須要學會的內容,然後就去實踐,從實踐過程中一定會遇到問題,這時再回去翻資料掌握這些問題,當有一定程度的理解後,嘗試跟他人分享,確認是否真的理解。透過這樣的循環,可以幫助自己更快掌握該領域知識。其實這套方法跟之前我看過的有效自學方式很類似,例如最小必要知識架構術費曼法,很明顯這些方法已經成為主流了。我想,不應該被傳統的學習概念(從基礎知識一步步慢慢學習)所限制,而是從應用面來學習,也就是知道自己要做到什麼,反推回去需要學習哪些知識,不但有效率,而且也更符合這個社會緊湊的腳步。

關於時間管理的部分,作者推薦用使用番茄鐘(通常代表的是工作25min後休息5min),如果不知道番茄鐘可以參考wiki的介紹。這一年下來,其實我都是用番茄鐘來管理我的時間,目前也覺得透過番茄鐘,確實可以幫助專注力,然而我卻從書中發現自己並沒有善用番茄鐘最大的威力。番茄鐘並不只是用來幫助自己提升效率的工具,更重要的是可以用來幫助時間規劃。在時間管理上,最常遇到的問題就是不知道每項工作到底要花多少時間做完,而在固定時間內,到底可以做完多少事情。這兩個問題番茄鐘都幫忙解決了,透過把一項大任務切割成番茄鐘的長度,代表的是強迫自己分割大任務變成可估算完成時間的小任務,而每個人每天可以完成多少的番茄鐘是有數量限制的,也代表我們會很清楚每天可以做多少事情,這樣規劃方式可以加強預測工作進度的準確度。另外番茄鐘帶來的另一個好處是可以更安心地進行休閒活動,大家應該會有經驗如果去玩樂會有種罪惡感想逼自己去工作,如果清楚每日能做到的番茄鐘數量,那休息享樂時就不會感到內疚,因為每天該完成的工作都已經達成了!

最後一點是關於自我弱點的方面,從小到大我們所受的教育都是要把缺點彌補起來,然而這在專業上其實是說不通的,大家應該常聽過樣樣通,樣樣鬆。與其告訴他人自己會很多東西,不如專精在比較小的領域(或是大領域中的某種應用),也許在市場上並沒有那麼多的需求,但是錄取機率則會大幅增加。不過也是有要去彌補弱點的情況,那就是該弱點會大幅影響效率時。當不知道某項技術其實可以很容易達成某件事前,可能都會很排斥去學習,但其實只要花幾個小時就能獲得很大的效益,也就是常聽到的CP值很高。然而要怎麼找到這些CP值高的技術呢?我們可以記錄每個自己沒聽過技術的遇到頻率,當遇到頻率高於一定值時,那就代表有學習的價值。舉個例子,其實我有時候看經濟新聞都會聽到A輪、B輪等等名詞,但是都一直沒動力去搞懂,結果對新聞內容都一知半解。在看本書的理財部份時,作者有做一個簡單的解釋,結果花不到半小時,就對這些常見名詞有初步認識了,也變相了加深我對經濟新聞理解,像是這種知識就很值得花時間投資。

當然這本書還有很多很有價值的內容,不過受限於篇幅,無法每個都說。如果對上面的分享心有戚戚焉的話,我想這本書應該蠻適合你。進入科技業也不過快三年而已,未來應該還有很長的職業生涯,如何好好經營是很大的課題。目前還是以多多參考前輩們的經驗以及不斷自我反省為主,找出真正適合自己的道路。

簡介

這篇我們想要來探討 Linux 是怎麼和程式互動的,這邊包括兩個部分:Linux 如何執行程式以及程式如何讓 Linux 做系統操作。

程式如何執行main

一般要呼叫程式來執行的,我們知道的是只要在 shell 下類似./a.out的指令,程式就會執行我們程式中的 main,但是這其中的原理是什麼呢?讓我們看看到執行 main 前做了哪些事。

下面例子我們以Kernel v4.17為例

  1. 首先 shell 會 fork 一個 process,然後再呼叫 exec 系列函數把該 process 置換成指定的程式
  2. execve 會呼叫 do_execve ,然後再呼叫 do_execveat_common,可參考fs/exec.c的1856行
    1
    2
    3
    4
    5
    6
    7
    8
    int do_execve(struct filename *filename,
    const char __user *const __user *__argv,
    const char __user *const __user *__envp)
    {
    struct user_arg_ptr argv = { .ptr.native = __argv };
    struct user_arg_ptr envp = { .ptr.native = __envp };
    return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
    }
  3. 接著do_execveat_common會讀取struct linux_binprm,並且根據檔案格式尋找適合的binary header
    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
    static int do_execveat_common(int fd, struct filename *filename,
    struct user_arg_ptr argv,
    struct user_arg_ptr envp,
    int flags)
    {
    ...
    // 重要的structure,保留執行檔的相關訊息
    struct linux_binprm *bprm;
    ...
    // 打開要執行的ELF檔
    file = do_open_execat(fd, filename, flags);
    ...
    // 生成mm_struct,供執行檔使用
    retval = bprm_mm_init(bprm);
    if (retval)
    goto out_unmark;
    // 計算帶入的參數
    bprm->argc = count(argv, MAX_ARG_STRINGS);
    if ((retval = bprm->argc) < 0)
    goto out;
    ...
    // 讀取 header
    retval = prepare_binprm(bprm);
    ...
    // 裡面會呼叫 search_binary_handler,根據檔案格式呼叫適合的binary_handler
    retval = exec_binprm(bprm);
    ...
    }
  4. ELF的binary handler位在fs/binfmt_elf.c的690行,做了header確認後會load program header和設定並執行elf_interpreter
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    static int load_elf_binary(struct linux_binprm *bprm)
    {
    ...
    // 讀取program header
    elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
    ...
    // 讀取elf_interpreter
    retval = kernel_read(bprm->file, elf_interpreter,
    elf_ppnt->p_filesz, &pos);
    // 把當前程式資訊清除並換上新的程式
    retval = flush_old_exec(bprm);
    ...
    current->mm->end_code = end_code;
    current->mm->start_code = start_code;
    current->mm->start_data = start_data;
    current->mm->end_data = end_data;
    current->mm->start_stack = bprm->p;
    ...
    // 執行elf_interpreter
    start_thread(regs, elf_entry, bprm->p);
    ...
    }
  5. 經過Context Switch後,應該會從elf_interpreter執行,通常應該會是/lib/ld-x.x.so。ld-x.x.so的進入點是_start,最後會連結到glibc/elf/rtld.c的_dl_start,針對環境變數做處理。
  • 我們常見的LD_PRELOAD也是在這邊進行處理的
  1. 當上述工作都做完後,就會進入 ELF binary 的_start,其中會呼叫 glibc 的__libc_start_main進行初始設定,最後就會呼叫main()
    1
    result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);

使用 system call

通常AP在Linux要跟kernel層互動大概只能透過system call,然而system call的使用大多數已經被包裝起來,所以幾乎不會看到,這邊我們來探討一下要怎麼在Linux直接呼叫system call。以下範例皆來自BINARY HACKS:駭客秘傳技巧一百招

syscall

最簡單的呼叫system call方法是syscall。

syscall.c

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
int ret;
ret = syscall(__NR_getpid);
printf("ret=%d pid=%d\n", ret, getpid());
return 0;
}

執行結果如下

1
2
3
$ make syscall
$ ./syscall
ret=18 pid=18

看起來是很順利取得PID。我們可以把__NR_getpid換成其他的system call數字,也可以達到同樣效果。

int 0x80

當然我們也可以用int 0x80來做到同樣的事情,但是要注意的是這樣的效率不會比較好,可參考What is better “int 0x80” or “syscall”?

另外這個做法在x64的架構是無法被使用的,可參考What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code?

syscall2.c

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>

int main(void)
{
int ret;
asm volatile ("int $0x80":"=a"(ret):"0"(__NR_getpid));
printf("ret=%d pid=%d\n", ret, getpid());
return 0;
}

sysenter

這部分也是只能在x86的平台上使用,會出現這個機制的理由是int 0x80的效率實在太差了。這邊的使用方式有點複雜,就不列出來了。

比較

這三種方式的比較簡單統整一下

syscall:現在主流,能在x64運行
int 0x80:只能在x86,效率差,已被捨棄
sysenter:只能在x86,用來替代int 0x80

詳情可以參考Linux系统调用机制int 0x80、sysenter/sysexit、syscall/sysret的原理与代码分析,寫得非常詳細。

參考

簡介

GNU gcc其實在編譯時也可以帶許多特殊功能,讓程式更佳的彈性,並帶來優化或更好debug的效益。這邊我們主要介紹兩個功能,內建函式和屬性__attribute__

內建函式

要特別注意的是,這些內建函數是跟CPU架構息息相關,所以並不是每個平台都可以順利使用。另外就是編譯的時候不能帶上 -fno-builtin選項,通常-fno-builtin是為了幫助我們確保程式的結果是如同我們所想像的樣子呈現,而不會被一些最佳化改變樣子,方便設定breakpoint和debug。

找呼叫者

首先我們先來談談找呼叫者這件事,我想大家應該都有經驗曾經發現程式死在某一行,但是卻不知道是誰呼叫的,這時候只能痛苦地去從stack反推return address。但是其實gcc內是有特殊內建函式可以幫助我們的,這邊介紹下面兩個好用函式。

  • void *builtin_return_address(unsigned int LEVEL):找到函式的return address是什麼,參數的LEVEL代表要往上找幾層,填0的話代表呼叫當前函式者的下一個執行指令。
  • void *builtin_frame_address(unsigned int LEVEL):找到函式的frame pointer,參數的LEVEL代表要往上找幾層,填0的話代表呼叫當前函式者的frame pointer。

要注意的是LEVEL不能填變數,也就是編譯時必須確定該數字。

範例

我們還是透過一個簡單的例子來說明一下

test.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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>

void test3(void)
{
void *ret_addr, *frame_addr;
ret_addr = __builtin_return_address(0);
frame_addr = __builtin_frame_address(0);
printf("0: ");
printf("ret_addr=0x%x frame_addr=0x%x\n", ret_addr, frame_addr);
ret_addr = __builtin_return_address(1);
frame_addr = __builtin_frame_address(1);
printf("1: ");
printf("ret_addr=0x%x frame_addr=0x%x\n", ret_addr, frame_addr);
ret_addr = __builtin_return_address(2);
frame_addr = __builtin_frame_address(2);
printf("2: ");
printf("ret_addr=0x%x frame_addr=0x%x\n", ret_addr, frame_addr);
ret_addr = __builtin_return_address(3);
frame_addr = __builtin_frame_address(3);
printf("3: ");
printf("ret_addr=0x%x frame_addr=0x%x\n", ret_addr, frame_addr);
printf("test3\n");
}

void test2(void)
{
void *ret_addr, *frame_addr;
ret_addr = __builtin_return_address(0);
frame_addr = __builtin_frame_address(0);
printf("0: ");
printf("ret_addr=0x%x frame_addr=0x%x\n", ret_addr, frame_addr);
ret_addr = __builtin_return_address(1);
frame_addr = __builtin_frame_address(1);
printf("1: ");
printf("ret_addr=0x%x frame_addr=0x%x\n", ret_addr, frame_addr);
ret_addr = __builtin_return_address(2);
frame_addr = __builtin_frame_address(2);
printf("2: ");
printf("ret_addr=0x%x frame_addr=0x%x\n", ret_addr, frame_addr);
printf("test2\n");
test3();
}

void test1(void)
{
void *ret_addr, *frame_addr;
ret_addr = __builtin_return_address(0);
frame_addr = __builtin_frame_address(0);
printf("0: ");
printf("ret_addr=0x%x frame_addr=0x%x\n", ret_addr, frame_addr);
ret_addr = __builtin_return_address(1);
frame_addr = __builtin_frame_address(1);
printf("1: ");
printf("ret_addr=0x%x frame_addr=0x%x\n", ret_addr, frame_addr);
printf("test1\n");
test2();
}

void test(void)
{
void *ret_addr, *frame_addr;
ret_addr = __builtin_return_address(0);
frame_addr = __builtin_frame_address(0);
printf("0: ");
printf("ret_addr=0x%x frame_addr=0x%x\n", ret_addr, frame_addr);
printf("test\n");
test1();
}

int main()
{
test();
return 0;
}

好,那我們來編譯並執行看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ make test
$ ./test
0: ret_addr=0x4007c8 frame_addr=0x2bba8ba0
test
0: ret_addr=0x4007bc frame_addr=0x2bba8b80
1: ret_addr=0x4007c8 frame_addr=0x2bba8ba0
test1
0: ret_addr=0x40076d frame_addr=0x2bba8b60
1: ret_addr=0x4007bc frame_addr=0x2bba8b80
2: ret_addr=0x4007c8 frame_addr=0x2bba8ba0
test2
0: ret_addr=0x4006e1 frame_addr=0x2bba8b40
1: ret_addr=0x40076d frame_addr=0x2bba8b60
2: ret_addr=0x4007bc frame_addr=0x2bba8b80
3: ret_addr=0x4007c8 frame_addr=0x2bba8ba0
test3

可以看到每層function所對應的return address和frame address都被列出來,但是要怎麼驗證是否真的是這樣呢?我們把程式逆向一下看位置。這邊我們鎖定test1()的return address,也就是0x4007bc,應該是test()函式的呼叫test1()的下一行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ objdump -d test
...
0000000000400770 <test>:
400770: 55 push %rbp
400771: 48 89 e5 mov %rsp,%rbp
...
4007b2: e8 59 fc ff ff callq 400410 <puts@plt>
4007b7: e8 28 ff ff ff callq 4006e4 <test1>
4007bc: 90 nop
4007bd: c9 leaveq
4007be: c3 retq

00000000004007bf <main>:
...

的確,下一行nop的位置就是就是4007bc,符合我們的想法。

其他有用的builtin函式

除了上面的例子,其實還有其他有用的builtin函式,這邊就只是列出來提供參考:

  • int __builtin_types_compatible_p(TYPE1, TYPE2):檢查TYPE1和TYPE2是否是相同type,相同回傳1,否則為0。注意這邊const和非const會視為同種類型。
  • TYPE __builtin_choose_expr(CONST_EXP, EXP1, EXP2):同CONST_EXP?EXP1:EXP2的概念,但是這個寫法會在編譯時就決定結果。常用方式是在寫macro時可以搭配__builtin_types_compatible_p當作CONST_EXP,選擇要呼叫什麼函式。
  • int __builtin_constant_p(EXP):判斷EXP是否是常數。
  • long __builtin_expect(long EXP, long C):預先知道EXP的值很大機率會是C,藉此做最佳化,kernel的likely和unlikely也是靠這個實現的。
  • void __builtin_prefetch(const void *ADDR, int RW, int LOCALITY):把ADDR預先載入快取使用。
    • RW:1代表會寫入資料,0代表只會讀取
    • LOCALITY:範圍是0~3,0代表用了馬上就不用(不用關心time locality)、3代表之後還會常用到
  • int __builtin_ffs (int X):回傳X中從最小位數開始計算第一個1的位置,例如__builtin_ffs(0xc)=3,當X是0時,回傳0。
  • int __builtin_popcount (unsigned int X):在X中1的個數
  • int __builtin_ctz (unsigned int X):X末尾的0個數,X=0時undefined。
  • int __builtin_clz (unsigned int X):X前面的0個數,X=0時undefined。
  • int __builtin_parity (unsigned int x):X值的parity。

__attribute__

weak & alias

測試是否支援某function

通常會使用__attribute__(weak)是為了避免有函式衝突的狀況,我們看個例子
a.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

extern void printf_test(void) __attribute__((weak));

int main()
{
printf("This is main function\n");
if(printf_test)
{
printf("Here is printf_test result: \n");
printf_test();
}
else
printf("We don't support printf_test\n");
return 0;
}
1
2
3
4
$ make a
$ ./a
This is main function
We don't support printf_test

雖然我們沒有printf_test,但是直接編譯是會通過的,因為printf_test被視為weak,假設在連結時找不到,是會被填0的。

那如果有printf_test的情況呢?我們加上b.c重新編譯看看

1
2
3
4
5
6
#include <stdio.h>

void printf_test(void)
{
printf("This is b function.\n");
}
1
2
3
4
5
$ gcc a.c b.c
$ ./a.out
This is main function
Here is printf_test result:
This is b function.

看起來就會執行printf_test了。這樣的功能對我們要動態看有無支援函式幫助很大。

為函式加上default值

這邊我們會用到alias的attribute,alias的話通常會跟weak一起使用,最常被用到的是幫不確定有無支援的函式加上default值。

a.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

void print_default(void)
{
printf("Not support this function.\n");
}

void print_foo(void) __attribute__((weak, alias("print_default")));
void print_bar(void) __attribute__((weak, alias("print_default")));

int main()
{
printf("This is main function\n");
print_foo();
print_bar();
return 0;
}

b.c

1
2
3
4
5
6
#include <stdio.h>

void print_foo(void)
{
printf("foo function.\n");
}
1
2
3
4
5
$ gcc a.c b.c
$ ./a.out
This is main function
foo function.
Not support this function.

可以看到因為print_bar並沒有被宣告,所以最後會執行alias的print_default。

在main前後執行程式

有時候會想要在main的執行前後可以做些事,這時候就會用到下面兩個attribute

  • constructor:main前做事
  • destructor:main之後做事

讓我們看個範例

test.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

__attribute__((constructor))
void before(void)
{
printf("before main\n");
}

__attribute__((destructor))
void after(void)
{
printf("after main\n");
}

int main()
{
printf("This is main function\n");
return 0;
}
1
2
3
4
5
$ make test
$ ./test
before main
This is main function
after main

結果的確如我們所料。另外這邊有點要注意,跟前面不一樣的是,__attribute__((constructor))__attribute__((destructor))必須放在函式前面,不然會有error: attributes should be specified before the declarator in a function definition的錯誤。

其他attribute

剩下還有一些有機會會用到的attribute,這邊就不多談,只列出來參考。

  • __attribute__((section("section_name"))):代表要把這個symbol放到section_name
  • __attribute__((used)):不管有沒有被引用,這個symbol都不會被優化掉
  • __attribute__((unused)):沒有被引用到的時候也不會跳出警告
  • __attribute__((deprecated)):用到的時候會跳出警告,用來警示使用者這個函式將要廢棄
  • __attribute__((stdcall)):從右到左把參數放入stack,由callee(被呼叫者)把stack恢復正常
  • __attribute__((cdecl)):C語言預設的作法,從右到左把參數放入stack,由caller把stack恢復正常
  • __attribute__((fastcall)):頭兩個參數是用register來存放,剩下一樣放入stack

參考

簡介

最近由於工作常常會用到,所以打算來談談如何來撰寫 linker script,也可以當作未來自己參考用途。

linker的作用就是把輸入檔(object file)的 section 整理到輸出檔的 section。除此之外也會定下每個object file 中尚未確定的符號位址,所以如果有 object file 用到不存在的symbol,就會出現常看到的 undefined reference error

而 linker script 就是提供給 linker 參考的文件,它告訴 linker 我想要怎麼擺放這些 section,甚至也可以定義程式的起始點在哪邊。

簡單範例

最簡單的 linker script 是用SECTIONS指令去定義 section 的分佈。

test.ld

1
2
3
4
5
6
7
8
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}

在上例,.被稱作 location counter,代表的是指向現在的位址,我們可以讀取或是移動它 (我覺得可以想像成我們在打電腦文件時的游標,代表現在要處理這個位置)。

這段 script 主要做的事是,先把 location counter 移到 0x10000,在這裡寫入所有輸入檔的.text section後,再來移到0x8000000放所有輸入檔的.data section.bss section

當然,最重要的還是去嘗試,所以讓我們來試試看,結果是不是真的像我們所想的。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
void test(void);

int global_bss;
int global_data = 123;

int main()
{
global_bss = 0;
test();
global_data++;
return 0;
}

test.c

1
2
3
4
5
6
void test(void)
{
int i;
// do nothing.
for (i = 0; i < 10000; i++);
}

嘗試編譯並看結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ gcc -c main.c test.c
$ ld -T test.ld main.o test.o
$ objdump -h a.out

a.out: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000046 0000000000010000 0000000000010000 00010000 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .eh_frame 00000058 0000000000010048 0000000000010048 00010048 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 00000004 0000000008000000 0000000008000000 00200000 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .bss 00000004 0000000008000004 0000000008000004 00200004 2**2
ALLOC
4 .comment 00000011 0000000000000000 0000000000000000 00200004 2**0
CONTENTS, READONLY

我們可以看到在VMA和LMA的地方,text是從0x10000開始,data和bss則是從0x8000000開始放,跟我們所安排的結果一樣。

這邊說明一下,一定會有人覺得奇怪,為什麼編譯出來的檔案無法執行,這個是因為我們並沒有符合 Linux 可執行的格式來 link,如果你想要知道一般我們下 gcc 是使用什麼 linker script 的話,可以使用如下方式:

1
gcc -Wl,-verbose main.c test.c

這樣就可以看到所使用的 linker script 了。

常用的功能

接著我們來談談在linker script中常見到的功能,這邊我們可以參考 jserv 帶領成大同學開發的 rtenv 中的 linker script

那我們就一一了解每個符號的意義吧!

ENTRY

用 ENTRY 可以指定程式進入點的符號,不設定的話 linker 會試圖用預設.text的起始點,或者用位址0的地方。

以 x86 為例,預設進入點是ENTRY(_start),而 rtenv 則是設定為 ENTRY(main)

MEMORY

Linker 預設會取用全部的記憶體,我們可以用 MEMORY 指令指定記憶體大小,在 rtenv 的例子中,指定了 FLASH 跟 RAM 兩種的輸出位置與大小

ORIGIN代表起始位置,LENGTH為長度

1
2
3
4
5
MEMORY
{
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 128K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

接下來SECTION部分,就能用 > 符號把資料寫到指定的位置

1
2
3
4
5
.bss : {
_sbss = .;
*(.bss) /* Zero-filled run time allocate data memory */
_ebss = .;
} >RAM

KEEP

KEEP 指令保留某個符號不要被 garbage collection ,例如我們不希望 ARM 的 ISR vector 會被優化掉。

1
2
3
4
5
.text :
{
KEEP(*(.isr_vector))
...
}

section 的本體

section 的指定方式是 linker script 中的重點,其中也有許多設定。

我們可以參考官方文件先對 section 的功能做一個快速了解。

1
2
3
4
5
6
7
8
9
10
section [address] [(type)] :
[AT(lma)]
[ALIGN(section_align) | ALIGN_WITH_INPUT]
[SUBALIGN(subsection_align)]
[constraint]
{
output-section-command
output-section-command
...
} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]

output-section-command 代表的就是我們要怎麼擺放每個 section。

在這個例子裡可以看到有許多 LMA,除了 LMA 外,其實還有 VMA,它們兩個究竟有什麼不同呢?

LMA/VMA 的概念

這裡大概是最重要的部分,也是之前我一直搞不清楚的地方。

link script 中設計了兩種位址:VMA 和 LMA

LMA (Load Memory Address) VMA (Virtual Memory Address)
位置 ROM/Flash RAM
意義 程式碼保存的位置 程式碼執行的位址

也就是 LMA 是 output file 的位置,VMA 是載入 section 到 RAM 時的位置,但是在大多數情況下兩者會是一樣的。

我們再看看上例是怎如何指定 LMA 和 VMA 的

  • LMA 是用ATAT>來決定位址,為可選,沒指定就用VMA當LMA
    • AT(LMA):告訴 linker 這個 section 應該要去哪個 LMA 載入資料到 VMA,要填 address
    • AT>lma_region:為 LMA 所在區域,需事先定義
  • >region:為 VMA 所在區域,region需事先定義
  • 在 linker script 的寫法基本上是這個架構[VMA] : [AT(LMA)]

繼續以 rtenv 為例,當指定了_sidata的 symbol 位置後,AT 就是要求載入到 FLASH 時要在.text的後面,換句話說.data的 LMA 要在.text

1
2
3
4
5
6
7
8
/* Initialized data will initially be loaded in FLASH at the end of the .text section. */
.data : AT (_sidata)
{
_sdata = .;
*(.data) /* Initialized data */
*(.data*)
_edata = .;
} >RAM

取得 section 的位置

在程式中,有時候可能還是會需要取得每個 section 的所在位址,我們可以用如下的方式取得

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text :
{
KEEP(*(.isr_vector))
*(.text)
*(.text.*)
*(.rodata)
*(.rodata*)
_smodule = .;
*(.module)
_emodule = .;
_sprogram = .;
*(.program)
_eprogram = .;
_sromdev = .;
*(.rom.*)
_eromdev = .;
_sidata = .;
} >FLASH

上面的7個 symbol 分別代表開始和結束,例如_smodule代表 module 的開始,而_emodule則代表 module 的結束。

這樣的好處是 symbol 的部分我們可以在主程式這樣使用

1
2
3
4
5
6
7
8
extern uint32_t _sidata;
extern uint32_t _sdata;
extern uint32_t _edata;

uint32_t *idata_begin = &_sidata;
uint32_t *data_begin = &_sdata;
uint32_t *data_end = &_edata;
while (data_begin < data_end) *data_begin++ = *idata_begin++;

值得注意的是,如果 C 已經有用到該變數_sidata,那就要用PROVIDE(_sdata = .)來避免 linker 出現重複定義的錯誤

Stack 的位址

通常 stack 位址我們都會放在 RAM 的最下方讓他往上長,所以我們可以用下面表示方式:

1
_estack = ORIGIN(RAM) + LENGTH(RAM);

代表 stack 的放置位址是在 RAM 的最下方。

常見問題

如果section重複被使用,會發生什麼事?

每個輸入檔的 section 只能在出現在 SECTIONS 中出現一次。什麼意思呢?讓我們看個例子

1
2
3
4
SECTIONS {
.data : { *(.data) }
.data1 : { data.o(.data) }
}

我們可以看到data.o中的.data section應該在第一個 OUTPUT-SECTION-COMMAND (也就是.data : { *(.data) })被用掉了,所以在.data1 : { data.o(.data) }將不會再次出現,代表的就是.data1 section會是空的。

如果只想要把某個library的.o放入的話

可用*xxx.a:*yyy.o (.bss*)的方式,舉例來說:

1
2
3
4
5
6
7
.bss_RAM2 : ALIGN(4)
{
*libmytest.a:*.o (.bss*)
*(.bss.$RAM2*)
*(.bss.$RamLoc64*)
. = ALIGN(4) ;
} > RamLoc64

如果我不想要把特定檔案的section放入

可以使用EXCLUDE_FILE,例如我想放除了 foo.o、bar.o 外,所有的.bss section,可以這麼做:

1
(*(EXCLUDE_FILE (*foo.o *bar.o) .bss))

詳細可參考下方連結

參考

我們很常聽到這些例子:有人會因為缺錢去借高利貸,結果不得翻身。有些人因為太忙於事業,忽略了家人,到了後來才後悔。雖然很常聽到,但是我們都很難想像這些人為什麼會去做這樣的決策,畢竟理智上告訴我們高利貸是不能去碰觸的,因為利息很可怕、家人比起事業更為重要,要多陪家人。「 稀缺:我們是如何陷入貧窮與忙碌的 」這本書為我們做了解答,故事中的主角都是因為稀缺導致了無法做長遠思考。

稀缺,也是代表了資源有限,所以我們大腦會告訴我們要節省,不要浪費,使用的時候就會再三權衡。這個也是我們演化的機制,像是老祖先可能缺乏食物,就必須要對食物存量極度小心,確保自己的生存。這樣最大好處當然就是我們可以確保稀有資源有被最大化使用。我們在日常生活中其實也有相關經驗,當我們在死線最後一刻,效率會非常高,因為這時時間是稀缺資源,會排除掉其他不重要的事情只專注在當前需要被完成的事,這也就是作者所提到的「專注紅利」。

甚至,這樣的專注力也可以讓我們表現更好。我們知道經濟學總是假設人類是理性的,會跟著需求供給曲線走,但是現實生活卻不是如此,人常常會被感覺所誤導。舉個例子,如果有兩家店有一段距離,我們在A看到20塊錢的東西,但是知道B有賣10元的同樣商品,大多數人願意跑去B買。但是如果現在換成比較貴的就不一樣了,A賣3000,B賣2990,大部分的人寧可在A買一買就好。因為人類有這樣不合理性的行為,所以才有行為經濟學這門學科崛起。這些不合理的概念其實都來自我們對金錢比較沒有實感,我們很難估計省下的10元到底有什麼價值,這點就連經濟學家可能也會犯錯。但是貧窮的人在使用金錢上會更有概念,不會被誤導,因為他們很清楚省下的錢要用來做什麼。我們也可以這麼說,貧窮會讓人更加理性,更接近經濟學裡面理性個體的假設。

儘管稀缺擁有專注的好處,但是它帶來的壞處更多。首先,我們可能會因為過度專注在稀缺資源,所以有了「管窺」現象,只看到自己所關注的,忽略掉其他事情。舉個例子,當我們在忙碌時,可能對其他外界的打擾就會很敏感,甚至脾氣會很差。除了管窺,我們用來處理事情的「帶寬」也變少了,因為心裡心心念念想著稀缺的資源,做其他事情時會很難專心,效率變差。而且因為資源稀少,我們也必須花更多精力去「權衡」怎麼使用資源,作者用行李箱的例子來具體描述這個部分。如果我們行李箱很大,我們可以什麼都不用想,把所有可能用到的東西一股腦塞進去,但是如果行李箱很小,那就必須要仔細思考到底什麼東西會用到,什麼應該用不到。這個「權衡」的行為也是消耗精力(或說帶寬)的主要來源。

因為管窺、帶寬減少、權衡,我們會過度放大眼前的問題,無法好好的考慮未來並且做計畫。這時就很可能出現了「借用」的狀況。我們前面提到的會去借高利貸就是因為貧窮者為了解決眼前的問題,跟未來借錢(未來要還高利貸的利息),而且因為沒辦法客觀評估未來,所以高估自己未來還錢的能力。不斷地跟未來借用,就會陷入稀缺陷阱中,因為現在的稀缺,導致未來的稀缺,讓人無法逃出這個輪迴。這個就像是我們一天的行事曆每項任務緊接下一項任務,假設第一個事情delay了,就會影響到後面的每個行程,陷入自己需要需要不斷趕下一個行程的輪迴中。

很明顯的,要解決稀缺陷阱的問題,就是要保留餘裕。以前面趕行程的例子,如果我們有留一個緩衝時間,那就能利用緩衝的資源把拖延到的時間補上。但是這個緩衝機制絕對不是在問題發生時才做,我們需要在資源還充足的時候就做好這些規劃。想像你自己已經都很忙了,怎麼可能還能夠想到要留一個緩衝時間呢?除此之外,我們還有其他手段可以應付稀缺陷阱,像是我們可以用預設取代需要主動操作,一次性的任務不要分多次去做、減少自己需要權衡的機會、多個milestone取代單一deadline。這邊我就不細多提了,但是我認為這邊最主要的概念是當自己已經陷入「稀缺」中,我們要減少需要消耗精力去做的事,因為稀缺必然會導致自己去處理其他事情的精力減少,進而增加出錯的機會。一旦出錯,這個就可能會變成下個你要去救火的點。這個概念其實跟之前讀到 精力管理 還蠻像:我們該管理的不是時間,而是精力,因為能讓你把事情做好做滿的是你的精力。減少自己的精力消耗,就可以讓你更能夠專注於處理重要的事情。

本書是2013所出,離現在已經有點時間。我原本也想說書中的概念大概早就過時了,但是沒想到卻出乎意料,讓我重新理解大家常掛在嘴上的「沒錢、沒時間」。了解了稀缺帶給我們的影響,才能知道要怎麼去面對它,避免落入稀缺陷阱,讓自己總是忙於救火。我想,人生在世大概都會遇到某項資源缺少的問題,所以了解「稀缺」應該是我們不得不去面對的課題。

簡介

有時候我們會在C的程式碼內看到asm{...}的結構,這代表的是行內組譯的概念,也就是在C語言中為了效率等目的直接要求compiler加入我們所指定組合語言。

舉個最簡單的範例,如果我們要求加入nop的指令,那就會變成如下:

1
2
3
4
5
6
/* 一個nop指令 */
asm("nop");

/* 多行要用\n隔開 */
__asm__("nop\n"
"nop\n");

不管是asm還是__asm__都是合法的,只要不要跟自己的symbol有衝突即可。

聰明的你可能發覺一件事,剛剛的例子只有指令而已,那如果假設我們要跟自己設定的變數互動那要怎麼辦呢?這時候就要用比較複雜的格式

1
2
3
4
5
asm ( assembler template               /* 組合語言內容 */
: output operands /* 輸出的參數 */
: input operands /* 輸入的參數 */
: list of clobbered registers /* 組合語言執行後會改變的項目 */
);

範例

我們還是直接來看看程式比較有感覺

範例一

我們寫一個簡單的test.c,只負責做加法。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main()
{
int sum, num1, num2;
num1 = 1;
num2 = 2;
sum = num1 + num2;
printf("sum=%d\r\n", sum);
return 0;
}

編譯並且看一下組語的內容

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
$ gcc test.c -s test.s
$ cat test.s
.file "test.c"
.text
.section .rodata
.LC0:
.string "sum=%d\r\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $1, -4(%rbp)
movl $2, -8(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
movl %eax, -12(%rbp)
movl -12(%rbp), %eax
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 8.1.0"
.section .note.GNU-stack,"",@progbits

先不管其他細節,可以看到中間有兩行addl %edx, %eaxmovl %eax, -12(%rbp),對應的也就是sum = num1 + num2;,那我們來改寫一下吧!

test.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main()
{
int sum, num1, num2;
num1 = 1;
num2 = 2;
sum = num1 + num2;
asm(
"addl %%edx, %%eax\n"
:"=a"(sum)
:"a"(num1), "d"(num2)
);
printf("sum=%d\r\n", sum);
return 0;
}

編譯並執行後就會發現結果是一樣的。不過到這邊我想大部分的人心中一定充滿了三個小朋友,所以還是在稍微解釋一下。

如前面所提,我們最主要執行的是addl %%edx, %%eax\n,這邊跟前面不一樣的是%另有用途(後面會提),所以要表示暫存器%eax時,我們要用%%來取代%字元。
然後第二行的"=a"(sum)中,=代表執行結束後我們要把某個值填到某個變數內(這邊指的就是括號中的sum),可是某個值又是怎麼決定的呢?這個就是a的概念,也就是「規範條件」,要求編譯器只能對應到符合條件的register。

如果以x86的架構為例(這邊要注意每個CPU架構的規範條件都不同):

規範條件 Register(s)
a %eax, %ax, %al
b %ebx, %bx, %bl
c %ecx, %cx, %cl
d %edx, %dx, %dl
S %esi, %si
D %edi, %di
f fp

由此可知就是要把%eax的結果填入sum中。同理,第三行的input部分"a"(num1), "d"(num2)分別也代表在執行組合語言前為num1和num2選擇register(這邊的例子是num1填入%eax、num2填入%edx)。

回頭看一下如果編成組合語言會是什麼樣子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
movl $1, -4(%rbp)
movl $2, -8(%rbp)
movl -4(%rbp), %eax
movl -8(%rbp), %edx
#APP
# 8 "test.c" 1
addl %edx, %eax

# 0 "" 2
#NO_APP
movl %eax, -12(%rbp)
movl -12(%rbp), %eax
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
....

在#APP和#NO_APP間就是我們的組語部分,看起來蠻符合我們的預期。

範例二

可是我們難道都一定要自行決定register嗎?我們想要交由compiler決定。這時候其實可以用比較寬鬆的限制條件。一樣是x86的架構才能用:

規範條件 Register(s)
r %eax, %ebx, %ecx, %edx, %esi, %edi
q %eax, %ebx, %ecx, %edx
0,1,2.. %0, %1, %2…(代表第幾個參數)

那就修改程式吧!

test.c

1
2
3
4
5
6
7
...
asm(
"addl %2, %0\n"
:"=r"(sum)
:"0"(num1), "r"(num2)
);
...

在這裡,我們input使用sum和num2使用r,代表交由compiler決定要用哪個register。但是num1為什麼是0呢?這個意思是我們要num1的值所放入的register要跟sum同樣。
0,1,2分別代表我們所決定的register順序,也就是%0=>之後要輸出到sum的register,%1=>num1放入的register,%2=>num2放入的register。

當然最後執行結果也會和範例一一樣。

參考

安裝openssl

MAC

1
$ brew install openssl

MAC上如果要使用library有點麻煩,需要先找到對應的路徑

1
2
3
4
5
6
7
8
$ find /usr/local/Cellar/ -name "libssl.*"  # 找到library的路徑
/usr/local/Cellar//openssl/1.0.2o_1/lib/pkgconfig/libssl.pc
/usr/local/Cellar//openssl/1.0.2o_1/lib/libssl.dylib
...
$ find /usr/local/Cellar/ -name "ssl.h" # 找到header的路徑
/usr/local/Cellar//node/8.4.0/include/node/openssl/ssl.h
/usr/local/Cellar//openssl/1.0.2o_1/include/openssl/ssl.h
...

看起來路徑是在/usr/local/Cellar/openssl/1.0.2o_1/我們先記起來,後面編譯時會用到。

創造憑證

openssl本身就有提供很多好用的工具,我們最常用到的大概就是用來產生憑證吧!

這邊介紹產生兩種常見憑證(RSA,ECC)的方法。

產生RSA憑證

1
2
3
4
5
6
# 產生2048長度的key
$ openssl genrsa -out server.key 2048
# 用key產生CSR,指定用sha384簽CSR
$ openssl req -new -sha384 -key server.key -out server.csr
# 產生自簽名證書
$ openssl x509 -req -sha1 -days 3650 -signkey server.key -in server.csr -out server.crt

產生ECC憑證

1
2
3
4
5
6
# 產生ECC key
$ openssl ecparam -genkey -name secp384r1 -out ecc.key
# 用key產生CSR,指定用sha384簽CSR
$ openssl req -new -sha384 -key ecc.key -out ecc.csr
# 產生自簽名證書
$ openssl x509 -req -sha1 -days 3650 -signkey ecc.key -in ecc.csr -out ecc.crt

使用openssl內建的連線工具

有時候我們只是想要測試ssl連線而已,還要自己寫程式有點麻煩,還好我們可以使用openssl提供的連線工具

client和server都有提供,非常方便的!

client

  • -msg:看細節(hex格式)
  • -cipher:決定要用哪種cipher連線
  • -showcerts:把cert的chain也列出來
  • -curves:指定要用的橢圓算法,client hello的extension中的elliptic_curves
  • -sigalgs:指定交換key要用的簽名方式,client hello的extension中的signature_algorithms
  • -no_tls1 -no_ssl3:加上後就可以只用tls1.2連線了
    1
    2
    3
    4
    5
    6
    7
    8
    # 最基本連線
    $ openssl s_client -connect [IP]:[port]
    # 看連線細節
    $ openssl s_client -msg -connect [IP]:[port]
    # 指定連線方式
    $ openssl s_client -connect sslanalyzer.comodoca.com:443 -cipher ECDHE-RSA-AES128-GCM-SHA256 -curves secp384r1 -sigals RSA+SHA512
    # 限制只能用TLS1.2連線
    $ openssl s_client -no_tls1 -no_ssl3 -connect [IP]:[port]

server

1
2
# server開啟5678 port並且用server.key當private key,cert用server.pem
$ openssl s_server -accept 5678 -key server.key -cert server.pem

其他

1
2
# 如果要看有哪些連線方式,可使用如下指令
$ openssl ciphers ALL

函式庫使用

我們來介紹openssl的函式庫最基本的使用方式。

基本範例 - client & server

這邊寫了兩個client和server的基本範例當作參考,大家可以基於這兩者來拓展自己的程式。

程式碼

ssl_client.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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>

#define RECV_SIZE 256

SSL_CTX *create_sslcontext()
{
const SSL_METHOD *method;
SSL_CTX *ctx;
// Support only TLSv1.2
method = TLSv1_2_client_method();
// Create context
ctx = SSL_CTX_new(method);
if (!ctx)
return NULL;
return ctx;
}

int create_socket(char *ip, int port)
{
int fd;
struct sockaddr_in addr;

// New socket
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0 )
return -1;
// TCP connect to server
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
return -1;

return fd;
}

// Usage: ./ssl_client.out [IP] [port]
int main(int argc, char *argv[])
{
int fd;
SSL_CTX *ctx;
int port;
int len;
SSL *ssl;
char buf[RECV_SIZE];

if (argc != 3)
return -1;
// Parse parameter
port = atoi(argv[2]);
printf("Connect to %s:%d\n", argv[1], port);

// SSL init
OpenSSL_add_ssl_algorithms();
// Create SSL_CTX
if ((ctx = create_sslcontext()) == NULL)
return -1;
// Create socket
if ((fd = create_socket(argv[1], port)) < 0)
return -1;
// Start to build ssl connection
ssl = SSL_new(ctx);
SSL_set_fd(ssl, fd);
if (SSL_connect(ssl) <= 0)
return -1;
// SSL write/read
do {
printf("Write data to server (q for quit): ");
memset(buf, 0, sizeof(buf));
gets(buf);
if (strcmp("q", buf) == 0)
break;
if (SSL_write(ssl, buf, strlen(buf)) < 0)
break;
memset(buf, 0, sizeof(buf));
len = SSL_read(ssl, buf, RECV_SIZE);
if (len < 0)
break;
else
printf("Recv %d bytes: %s\n", len, buf);
} while(1);
// SSL close
SSL_shutdown(ssl);
// Free resource
SSL_free(ssl);
close(fd);
SSL_CTX_free(ctx);
EVP_cleanup();
return 0;
}

ssl_server.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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>

#define SSL_CERT "server.crt"
#define SSL_KEY "server.key"

#define BUF_LEN 256

SSL_CTX *create_sslcontext()
{
const SSL_METHOD *method;
SSL_CTX *ctx;
// Support only TLSv1.2
method = TLSv1_2_server_method();
// Create context
ctx = SSL_CTX_new(method);
if (!ctx)
return NULL;
return ctx;
}

int configure_sslcertkey_file(SSL_CTX *ctx)
{
SSL_CTX_set_ecdh_auto(ctx, 1);
// Load certificate file
if (SSL_CTX_use_certificate_file(ctx, SSL_CERT, SSL_FILETYPE_PEM) <= 0)
return -1;
// Load private key file
if (SSL_CTX_use_PrivateKey_file(ctx, SSL_KEY, SSL_FILETYPE_PEM) <= 0 )
return -1;
return 0;
}

int create_socket(int port)
{
int fd;
struct sockaddr_in addr;

addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);

if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0 )
return -1;
if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
return -1;
if (listen(fd, 1) < 0)
return -1;

return fd;
}

// Usage: ./ssl_server.out [port]
int main(int argc, char *argv[])
{
int server_fd, client_fd;
SSL_CTX *ctx;
SSL *ssl;
struct sockaddr_in addr;
uint len = sizeof(addr);
int port;
char buf[BUF_LEN];

if (argc != 2)
return -1;
port = atoi(argv[1]);
printf("Listen port: %d\n", port);

// SSL init
OpenSSL_add_ssl_algorithms();
// Create SSL_CTX
if ((ctx = create_sslcontext()) == NULL)
return -1;
// Configure cert and key
if (configure_sslcertkey_file(ctx) < 0)
return -1;
// Create socket
if ((server_fd = create_socket(port)) < 0)
return -1;
// Accept connection
if ((client_fd = accept(server_fd, (struct sockaddr*)&addr, &len)) < 0)
return -1;
// Build SSL connection
ssl = SSL_new(ctx);
SSL_set_fd(ssl, client_fd);
if (SSL_accept(ssl) <= 0)
return -1;
// SSL read/write
while(1)
{
memset(buf, 0, sizeof(buf));
len = SSL_read(ssl, buf, BUF_LEN);
if (len <= 0)
break;
else
SSL_write(ssl, buf, strlen(buf));
}
// Close client
SSL_free(ssl);
close(client_fd);
// Close server and relase resource
close(server_fd);
SSL_CTX_free(ctx);
EVP_cleanup();
return 0;
}

編譯與執行

接下來寫個簡單的Makefile,這時候就要用到前面所找到的路徑了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SSL_PATH=/usr/local/Cellar/openssl/1.0.2o_1/
CFLAGS=-I$(SSL_PATH)include -L$(SSL_PATH)lib/ -lcrypto -lssl
CC=gcc
BIN=ssl_server ssl_client

all: $(BIN)

ssl_server: ssl_server.c
$(CC) $^ -o $@.out $(CFLAGS)

ssl_client: ssl_client.c
$(CC) $^ -o $@.out $(CFLAGS)

clean:
-rm *.o
-rm *.out

這邊要特別記住-lcrypto -lssl要放最後面,不然有些平台會有error

然後就可以執行看看了

1
2
3
$ make
$ ./ssl_server 2222
Listen port: 2222

這時候另一邊再來執行client

1
2
3
4
./ssl_client.out 127.0.0.1 2222
Connect to 127.0.0.1:2222
Write data to server (q for quit): abcd
Recv 4 bytes: abcd

可以順利收送資料了!

參考

比較shared/static library

程式在執行的時候,大部分都會需要引用函式庫(library),library有分shared和static,兩者代表不同的引用方式。

static library shared library
優點 不需要考慮執行環境的相依性問題 使用空間小(檔案和記憶體)、更換library不用重build
缺點 執行檔極大、更換library需重build 在異地執行可能會因為相依性無法執行

動態函式庫

在開始前,先確定幾個名詞

  • soname:代表特定library的名稱,如libmylib.so.1,最後面的1是version
  • real name:實際放有library程式的檔案名稱,名稱會包含三個版號,分別為version, minor和release,如libmylib.so.1.0.0
    • version代表原介面有移除或改變,與舊版本不相容
    • minor代表新增介面,舊介面沒改
    • release代表程式修正,介面沒改
  • linker name:用於連結時的名稱,不含版號的soname,如libmylib。通常會link到實際的real name。

如何編譯

首先我們先把.c編譯成.o,這邊要加上-fPIC的參數

這個原因是要產生Position Independent code,確保code segment在動態連結時不用花時間重新定位,而且重新定位會造成無法和其他process共享.text區段。

事實上,如果不加-fPIC也是可以產生library,但是產生的執行檔就需要另外存有重新定位的資訊(.rel.dyn區段),而且會有上述的問題。

1
$ gcc -c -fPIC hello.c world.c

接下來就是產生shared library了,解釋一下參數的意思

  • -shared:代表要編成shared library
  • -Wl:是用來傳遞參數給linker,讓-soname和libmylib.so.1傳給linker處理
  • -soname:用來指名soname為libmylib.so.1
  • -o:最後library會被輸出成libmylib.so.1.0.0
    1
    $ gcc -shared -Wl,-soname,libmylib.so.1 -o libmylib.so.1.0.0 hello.o world.o
    soname很重要,就如同前面所提,可以讓開發者和應用程式表示兼容標準,可以用objdump確認soname
    1
    2
    $ objdump -p libmylib.so.1.0.0 | grep SONAME
    SONAME libmylib.so.1
    完成後再用ln建立soname和linker name兩個檔案
    1
    2
    $ ln -s libmylib.so.1.0.0 libmylib.so
    $ ln -s libmylib.so.1.0.0 libmylib.so.1

如何使用

如果有人要使用的話,下列兩種方式都可以。不過要注意目錄下如果同時有static和shared會使用shared為主,如果要static就要加上-static編靜態函式庫

1
2
$ gcc main.c libmylib.so -o a.out
$ gcc main.c -L. -lmylib -o a.out

但是shared library執行的時候還是需要有library才能執行,所以要把.so安裝到系統中,有三種方法:

  1. 把libmylib.so.1 放到系統常見的library目錄,如/lib, /usr/lib
  2. 設定/etc/ld.so.conf ,加入一個新的library搜尋目錄,並執行ldconfig更新/etc/ld.so.cache
  3. 設定LD_LIBRARY_PATH 環境變數來搜尋library,如LD_LIBRARY_PATH=. ./a.out

這邊提一下一般而言找library的順序

  1. LD_LIBRARY_PATHLD_AOUT_LIBRARY_PATH環境變數所指的路徑
  2. ld.so.cache的記錄來找shared library。
  3. /lib,/usr/lib內的檔案

查看shared library的關係 - ldd

我們要怎麼知道某個執行檔有使用到哪些library呢?這時候就要用到ldd這個指令了。

ldd其實是一個shell script,它會把檔案所用到library一一列出,包括library會用到的library。

舉例來說,如果我們不用ldd,其實是可以從ELF的Dynamic Section獲得shared library資訊

1
2
3
4
5
6
7
8
$ readelf -d /bin/cat

Dynamic section at offset 0x7dd8 contains 26 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x15e8
0x000000000000000d (FINI) 0x5a4c
...

我們看到NEEDED就是需要的dynamic library,但是這個library可能也需要其他library。

1
2
3
4
5
6
7
8
$ readelf -d /lib/x86_64-linux-gnu/libc.so.6

Dynamic section at offset 0x198ba0 contains 26 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [ld-linux-x86-64.so.2]
0x000000000000000e (SONAME) Library soname: [libc.so.6]
0x000000000000000c (INIT) 0x20050
...

因此我們知道/bin/cat需要libc.so.6,而libc.so.6還需要ld-linux-x86-64.so.2。這樣尋找實在太麻煩了,其實我們可以直接用ldd

1
2
3
4
$ ldd /bin/cat
linux-vdso.so.1 (0x00007fff8613c000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f654a3bf000)
/lib64/ld-linux-x86-64.so.2 (0x00007f654a967000)

看,是不是很輕鬆呢?

靜態函式庫

會有static library的概念是,如果我有很多.o檔,那每次要引用其實都不是很方便,所以最好的方法還是可以打包起來,也就是使用ar指令。

如何編譯

static libary建立方式很簡單,一樣要先建立.o

1
$ gcc -c test1.c test2.c

接下來開始打包,參數意義如下

  • r:代表加入新檔案或取代現有檔案
  • c:.a檔不存在時不會跳錯誤訊息
  • u:根據timestamp保留檔案
  • s:建立索引,加快連結速度
    1
    $ ar rcs libtest.a test1.o test2.o
    如果要顯示函式庫 libstack.a 的內容
    1
    2
    3
    $ ar -tv libtest.a
    rw-r--r-- 0/0 1464 Jan 1 00:00 1970 test1.o
    rw-r--r-- 0/0 1464 Jan 1 00:00 1970 test2.o
    如果要從libtest.a中取出test1.o
    1
    ar -x libtest.a test1.o

如何使用

編譯方法一樣很簡單,有兩種

1
2
3
gcc main.c libtest.a
# 也可以使用gcc的-l,-L代表要搜尋的目錄位置,-l會捨去library的lib開頭
gcc main.c -L. -ltest

symbol衝突

假設我們在創建library時遇到symbol衝突會發生什麼事呢?這邊我們分三種情況探討

首先先創三個檔案

hello.c

1
2
3
4
void test()
{
printf("hello\n");
}

world.c

1
2
3
4
void test()
{
printf("world\n");
}

main.c

1
2
3
4
5
6
void test();
int main()
{
test();
return 0;
}

shared library連結時,object file有衝突

嘗試編譯與連結

1
2
3
4
5
6
$ gcc -c -fPIC hello.c world.c
$ gcc -shared -o libmylib.so hello.o world.o
world.o: In function `test':
world.c:(.text+0x0): multiple definition of `test'
hello.o:hello.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

會發現出現錯誤,原因是動態連結跟一般編譯一樣會檢查symbol是否重複

static library打包時,object file有衝突

那如果是用static library呢?

1
2
3
4
5
$ gcc -c hello.c world.c
$ ar crs libhello.a hello.o
$ ar crs libworld.a world.o
$ gcc -o main.out main.c libhello.a libworld.a
hello

發現居然沒事,這個原因是因為ar只有打包功能不負責檢查。可是問題來了,到底是執行哪個呢?答案是看順序。

1
2
3
4
5
6
$ gcc -o main.out main.c libhello.a libworld.a
$ ./main.out
hello
$ gcc -o main.out main.c libworld.a libhello.a
$ ./main.out
world

使用shared library時,不同library有衝突

那如果是兩個shared library彼此間有函數衝突的現象呢?

1
2
3
$ gcc -fPIC -shared -o libhello.so  hello.c
$ gcc -fPIC -shared -o libworld.so world.c
$ gcc -o main.out libhello.so libworld.so main.c

結果一樣沒有錯誤,原因是在動態連結時會使用最先看到的symbol,所以順序不同就有不同結果

1
2
3
4
5
6
$ gcc -o main.out libhello.so libworld.so main.c 
$ LD_LIBRARY_PATH=. ./main.out
hello
$ gcc -o main.out libworld.so libhello.so main.c
$ LD_LIBRARY_PATH=. ./main.out
world

這個特性也跟LD_PRELOAD有關,我們可以用LD_PRELOAD來抽換shared library就是因為連結時會先使用先看到的symbol。當然這也曾經造成了一些危害,例如goahead的CVE-2017-17562

執行中載入library

除了執行開始時載入library外,我們也可以用程式來載入

1
2
3
4
5
6
7
8
9
10
// 動態載入所需的header
#include <dlfcn.h>
// 載入指定library
void *dlopen(const char *filename, int flag);
// 透過symbol name取得symbol在library的記憶體位址
void *dlsym(void *handle, const char *symbol);
// 關閉dlopen開啟的handler
int dlclose(void *handle);
// 傳回錯誤訊息。
char *dlerror(void);

範例:dltest.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
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

int main() {
void *handle;
void (*f)();
char *error;
/* 開啟之前所撰寫的libmylib.so 程式庫 */
handle = dlopen("./libmylib.so", RTLD_LAZY);
if( !handle ) {
fputs( dlerror(), stderr);
exit(1);
}
/* 取得hello function 的address */
f = dlsym(handle, "hello");
if(( error=dlerror())!=NULL) {
fputs(error, stderr);
exit(1);
}
/* 呼叫function */
f();
/* 結束handler */
dlclose(handle);
return 0;
}

記得編譯時要連結dl library

1
2
$ gcc dltest.c -ldl
$ LD_LIBRARY_PATH=. ./a.out

library公開symbols管理

有時候我們並不希望所提供的library會把所有symbol都洩漏出去,這時候大部分的人都會使用static限制外部呼叫。但是當這個函式在library中其他檔案會引用到,那就沒辦法設為static了。

那該怎麼辦呢?這邊有兩個方法:

使用 version script

首先我們先創兩個檔案當範例

test.c

1
2
3
4
void test()
{
printf("test\n");
}

func.c

1
2
3
4
5
6
void test();
void func()
{
printf("func\n");
test();
}

然後我們編成shared library,並且看看symbol

1
2
3
4
5
6
$ gcc -fPIC -c test.c func.c
$ gcc -shared -o libmylib.so test.o func.o
$ nm -D libmylib.so | grep -v '_' # -D 代表顯示dynmaic部分,-v 代表反向選擇
00000000000005e8 T func
U puts
00000000000005d5 T test

可以看到test還是被暴露出來了,但是明明test應該只想要在library中被使用而已。

這時候我們可以試試GNU linker的version script。
libmylib.map

1
2
3
4
{
global: func;
local: *;
};

這個意思是只要顯示func,其他function都要隱藏。然後我們link的時候加上version script試看看:

1
2
3
4
$ gcc -shared -o libmylib.so test.o func.o -Wl,--version-script,libmylib.map
$ nm -D libmylib.so | grep -v '_'
00000000000004e8 T func
U puts

成功隱藏test了!

使用__attribute__語法

除了使用version script以外,也可以用gcc特有的語法,__attribute__((visibility("default")))

首先我們先改寫要公開的函式,代表我們只要暴露func()給外界看到

func.c

1
2
3
4
5
6
void test();
__attribute__((visibility("default"))) void func()
{
printf("func\n");
test();
}

然後在編譯成.o時要記得加上-fvisibility=hidden,把其他function都隱藏起來。

1
2
3
4
5
$ gcc -c -fPIC test.c func.c -fvisibility=hidden
$ gcc -shared -o libmylib.so test.o func.o
$ nm -D libmylib.so | grep -v '_'
00000000000005a8 T func
U puts

達到的效果和version script一樣!

用version script控制版本

這邊我們再多談談version script其他的用法,其實他除了管理要暴露出來的symbol外,我們也可以依照版本控制library要暴露出來的function。

首先我們先出第一版程式
libtest.c

1
2
3
4
5
#include <stdio.h>
void func(int num)
{
printf("num=%d\n", num);
}

libtest1.h

1
void func(int num);

version1.c

1
2
3
4
5
6
7
#include <stdio.h>
#include "libtest1.h"
int main()
{
func(1);
return 0;
}

然後正常編譯執行

1
2
3
4
5
$ gcc -fPIC -c libtest.c
$ gcc -shared -o libtest.so libtest.o
$ gcc -L. -ltest -o version1.out version1.c
$ LD_LIBRARY_PATH=. ./version1.out
num=1

很順利正常執行,那我們假設現在要出第二個版本可以怎麼做

libtest2.c

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
void func_1(int num)
{
printf("num=%d\n", num);
}

void func_2(int num1, int num2)
{
printf("num1=%d, num2=%d\n", num1, num2);
}
__asm__(".symver func_1,func@LIBTEST_1.0");
__asm__(".symver func_2,func@@LIBTEST_2.0");

稍微解釋一下,首先先實作兩個function,然後再用後面兩個__asm__symver來把同樣symbol加上版號,至於第二行@@的意思代表為預設版本。

接下來的部分就一樣撰寫新的程式

libtest2.h

1
void func(int num1, int num2);

version2.c

1
2
3
4
5
6
7
#include <stdio.h>
#include "libtest2.h"
int main()
{
func(1,2);
return 0;
}

然後這時候就要出動version script了

libtest2.map

1
2
3
4
5
6
7
LIBTEST_1.0 {
global: func;
local: *;
};
LIBTEST_2.0 {
global: func;
}LIBTEST_1.0;

然後我們編譯並執行看看

1
2
3
4
5
6
7
$ gcc -fPIC -c libtest2.c
$ gcc -shared -o libtest.so libtest2.o -Wl,--version-script,libtest2.map
$ gcc -L. -ltest -o version2.out version2.c
$ LD_LIBRARY_PATH=. ./version1.out
num=1
$ LD_LIBRARY_PATH=. ./version2.out
num1=1, num2=2

可以看到兩者執行結果不同,為什麼會這樣呢?我們先看一下他們連結到的symbol

1
2
3
4
5
6
7
8
$ readelf -a version1.out  | grep func
000000601018 000500000007 R_X86_64_JUMP_SLO 0000000000000000 func + 0
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND func
51: 0000000000000000 0 FUNC GLOBAL DEFAULT UND func
$ readelf -a version2.out | grep func
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 func@LIBTEST_2.0 + 0
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND func@LIBTEST_2.0 (2)
46: 0000000000000000 0 FUNC GLOBAL DEFAULT UND func@@LIBTEST_2.0

可以看到version1.out是使用func,而version2.out的symbol就是func@@LIBTEST_2.0。那同樣是引用相同library,到底是怎麼知道要呼叫哪個func呢?在呼叫func的情況下,會自動找到最初的版本也就是func@LIBTEST_1.0。而之後的程式編譯時link library則會去找default的版本,也就是有兩個@的func@@LIBTEST_2.0,所以就不會有搞混的情況發生了。

這個方法在要維持兼容性的情況下非常好用,可以在不影響舊版的情況下改變函式規格。

參考

Installation

MAC

  • 安裝
    以前安裝時需要安裝docker和boot2docker,但現在只要到官網下載DOCKER COMMUNITY EDITION (CE)就可以了。

boot2docker是MAC下輕量的Linux VM,專門用來執行docker daemon

然後以前使用都會用kitematic這個GUI的操作介面,現在docker官方也已經整進去了,我們可以直接透過docker的應用程式下載kitematic(在上方工具列的選項裡)

安裝詳細流程可以參考如何在 macOS 上安裝 Docker CE,寫得非常清楚。

ubuntu

Ubuntu的安裝方式也跟以前不一樣了,可參考官網的作法,Get Docker CE for Ubuntu

Windows

Windows的安裝教學連結在此,值得注意的是只有Windows10才有支援Hyper-V,如果是其他版本就必須要安裝使用Virtualbox的Docker Toolbox來取代了。

常用指令

可以用一張圖職階概括大部分常用docker的指令,圖片來自Docker —— 從入門到實踐 附錄一:命令查詢

images

  • 尋找images
    1
    docker search XXX
  • 把images抓下來
    1
    docker pull XXX
  • 看目前有哪些images
    1
    docker images
  • 刪除某images
    1
    docker rmi XXX

container

  • 看目前有哪些container正在跑
    1
    docker ps
  • 看包括所有停止的container
    1
    docker ps -a
  • 讓某個container開始/停止
    1
    docker start/stop XXX
  • 刪除某container
    1
    docker rm XXX
  • 看某個container資訊
    1
    docker inspect XXX

RUN

執行部分其實可以加上很多參數:

  • -d: 代表以daemon執行(背景執行)

  • -p port:port: 代表port映射,例如-p 8080:80就是把 port 8080 對應到image的 port 80

  • -v dir:dir: 代表映射目錄,例如-v /home/share:/var/www:rw就是把/home/share對應到image的/var/www,且權限為rw。路徑需要為絕對路徑。

  • --rm:當有container存在時自動移除

  • -i:互動模式

  • -t:允許TTY

  • -w path:設定進入container的工作路徑

  • -e key=value:帶入環境變數

  • 跑images

    1
    2
    docker run --rm -i -t -p 8080:80 nginx
    docker run -i -t ubuntu /bin/bash
  • 背景執行

    1
    docker run -d -p 8080:80 -v shared_dir:/var/www:rw nginx

COMMIT

  • 看有甚麼改變
    1
    docker diff XXX
  • 提交成新的images
    1
    docker commit -m="註解" -a="author" XXX repo_name
  • 看歷史
    1
    docker history XXX

Dockerfile

我們也可以用Dockerfile產生image,可參考使用Dockerfile建置

下面是個範例

1
2
3
4
5
6
7
8
9
10
11
12
# base image
FROM ubuntu:14.04

# 執行的command
RUN apt-get update
RUN apt-get install -y nginx

# 要開的port,注意在run的時候還是要加上-p才能真正讓外部連接該port
EXPOSE 80

# 環境變數
ENV PATH $PATH:/home/bin

建立image

1
docker build -t repo_name:tag_name .

範例

看完command可能還是不清楚怎麼用,這邊用安裝nginx的docker image來說明

取得image

首先我們先搜尋nginx

1
2
3
4
5
6
$ docker search nginx
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
nginx Official build of Nginx. 8564 [OK]
jwilder/nginx-proxy Automated Nginx reverse proxy for docker con… 1331 [OK]
richarvey/nginx-php-fpm Container running Nginx + PHP-FPM capable of… 547
....

我們先抓officical的images

1
2
3
4
5
6
7
8
$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
f2aa67a397c4: Pull complete
3c091c23e29d: Pull complete
4a99993b8636: Pull complete
Digest: sha256:0fb320e2a1b1620b4905facb3447e3d84ad36da0b2c8aa8fe3a5a81d1187b884
Status: Downloaded newer image for nginx:latest

現在local端就有nginx的image了

1
2
3
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest ae513a47849c 11 days ago 109MB

運行container

開始運行container,並且讓port 8080對應到nginx container的port 80,工作路徑為/home,然後執行bash

1
$ docker run --rm -i -t -p 8080:80 -w /home nginx bash

我們也可以選擇背景執行,並且把shared_dir對應到/var/www

1
$ docker run -d -p 8080:80 -v shared_dir:/var/www:rw nginx

一定有人會問這樣的情況下怎麼控制bash呢?我們可以用exec command

1
$ docker exec -i -t 78fc bash

操作運行中的container

看一下當前有的container

1
2
3
4
5
6
7
8
9
10
11
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e2cf9ea13bb4 nginx "nginx -g 'daemon of…" 2 minutes ago Up 2 minutes 0.0.0.0:8080->80/tcp priceless_murdock
$ docker inspect e2cf9ea13bb4
[
{
"Id": "e2cf9ea13bb477e49f1c0ff75a683555d1a75ef953529087375c83ee1a88b65f",
"Created": "2018-05-12T06:17:14.979076095Z",
"Path": "nginx",
"Args": [
...

我們可以隨時中斷或啟動該container

1
2
$ docker stop e2cf9ea13bb4
$ docker start e2cf9ea13bb4

提交改變成為新的image

看看該container有什麼改變

1
2
3
4
5
6
7
8
9
10
11
$ docker diff e2cf9ea13bb4
C /run
A /run/nginx.pid
C /var
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
A /var/www

commit我們所做的改變變成新的image

1
2
3
4
5
6
$ docker commit -m "New nginx" -a "evshary" e2cf new_nginx
sha256:ed66214b3e3a510a7cc47e341f64f6596560164d6f06a22f93dca8d05ecac081
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
new_nginx latest ed66214b3e3a 17 seconds ago 109MB
nginx latest ae513a47849c 11 days ago 109MB

可以從history看我們所做改變歷史

1
2
3
4
5
$ docker history new_nginx
IMAGE CREATED CREATED BY SIZE COMMENT
ed66214b3e3a About a minute ago nginx -g daemon off; 2B New nginx
ae513a47849c 11 days ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
<missing> 11 days ago /bin/sh -c #(nop) STOPSIGNAL [SIGTERM] 0B

刪除container/images

玩膩了,可以刪除images,記得要先刪掉container才行刪images喔

1
2
3
4
5
6
7
8
9
$ docker rm e2cf9ea13bb4
$ docker rmi new_nginx
Untagged: new_nginx:latest
Deleted: sha256:ed66214b3e3a510a7cc47e341f64f6596560164d6f06a22f93dca8d05ecac081
$ docker rmi nginx
Deleted: sha256:ae513a47849c895a155ddfb868d6ba247f60240ec8495482eca74c4a2c13a881
Deleted: sha256:160a8bd939a9421818f499ba4fbfaca3dd5c86ad7a6b97b6889149fd39bd91dd
Deleted: sha256:f246685cc80c2faa655ba1ec9f0a35d44e52b6f83863dc16f46c5bca149bfefc
Deleted: sha256:d626a8ad97a1f9c1f2c4db3814751ada64f60aed927764a3f994fcd88363b659

參考

簡介

程式設計師很大的機率是脫離不了Linux,而如果我們要在Linux上compile,大概一定會接觸到ELF這個格式。底下來簡單介紹一下ELF的格式是什麼,我們要怎麼從它獲得資訊。

ELF全名是Executable and Linking Format,在Linux中是編譯後的binary、object檔規範,也就是說我們從source code編譯後產生的檔案格式就是ELF了。

ELF的格式可以從兩種角度來看,第一種是Link的時候,第二種是執行的時候。兩者都一樣會有ELF header,但是底下的組成概念就完全不一樣。

Link的時候:

ELF header
Program Header Table(Optional)
Section 1
Section 2
Section N
Section Header Table

執行的時候:

ELF header
Program Header Table
Segment 1
Segment 2
Segment N
Section Header Table(Optional)

兩者最大的差異是Link的時候是以Section為觀點,用Section Header Table來當索引,指向各個Section。執行的時候則是用Segment為觀點,一個Segment可能是多個Section所組成,然後再用Program Header Table指向各個Segment。

觀察ELF的方法

那要如何觀察ELF呢?如果你嘗試用記事本打開應該只會看到一團不知所云的亂碼,所以我們底下會透過各種工具的使用教學來解釋ELF格式。

查看執行檔 - od

首先我們可以試著使用od這個指令來看檔案內容。od全名是octal dump,顧名思義就是用八進制來印內容,但他並不僅僅如此而已。

od指令的格式:od -t [顯示格式] -A [偏移量使用的基數] [filename]

  • -t:後面可接型態(d, o, x…)、一次顯示的byte數(數字)、是否顯示ASCII code(z)
  • -A:偏移量有(d, o, x, n),n代表不顯示偏移量
  • -v:不省略重複的內容

我們最常用格式:

  • od -t x1 -A x [filename]:代表用16進制來顯示檔案,偏移量是16的倍數
  • od -t x1z -A x [filename]:同上,但是多加上顯示ASCII code

那我們來看看ELF檔長什麼樣子,這邊以大家最常用的ls為例

1
2
3
4
5
$ od -t x1z -A x /bin/ls | less
000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 >.ELF............<
000010 02 00 3e 00 01 00 00 00 90 48 40 00 00 00 00 00 >..>......H@.....<
000020 40 00 00 00 00 00 00 00 00 a7 01 00 00 00 00 00 >@...............<
....

可以看到前面有個7f 45 4c 46開頭,ASCII是.ELF(.代表非可見字元,這邊是0x7f也就是\177),這個就是傳說中的ELF magic code了。不過這邊先停一下,如果我們要繼續用hex來看其實有點累,所以先換個工具來試試吧!

使用readelf來觀察ELF資訊

readelf很明顯就是觀察ELF檔案的專門工具,使用方式如下

  • 格式:readelf [選項] [filename]
  • 讀取標頭選項
    • -h:印 ELF header
    • -l:印 Program Header Table
    • -S:印 Section Header Table
    • -e:三者都印
  • 讀取資訊選項
    • -s:符號表
    • -r:重定位資訊
  • 特別用法:
    • -a:所有標頭資訊全部印出
    • -xn:先用-S看要查看的Section數字,然後n填上該數字就可以hexdump那個section

那我們來看看ls的ELF header長什麼樣。從下面可以看到,除了剛剛看到的Magic code外,還有版本、適用哪個OS/ABI、在哪個機器平台運行、entry point adddress等等。

值得注意的是這邊有紀錄Program Header、Section Header的開始位址、大小、數量,所以我們可以用這個資訊找到Program/Section Header。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ readelf -h /bin/ls
ELF 檔頭:
魔術位元組: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
類別: ELF64
資料: 2 的補數,小尾序(little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
類型: EXEC (可執行檔案)
系統架構: Advanced Micro Devices X86-64
版本: 0x1
進入點位址: 0x404890
程式標頭起點: 64 (檔案內之位元組)
區段標頭起點: 108288 (檔案內之位元組)
旗標: 0x0
此標頭的大小: 64 (位元組)
程式標頭大小: 56 (位元組)
Number of program headers: 9
區段標頭大小: 64 (位元組)
區段標頭數量: 28
字串表索引區段標頭: 27

而Program Header的部分,我們可以看到有9個Segement,以及實際的位址在哪。另外有個「區段到節區映射中」(Section to Segment mapping),這就是多個Section如何組成一個Segment的對應。

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
$ readelf -l /bin/ls
Elf 檔案類型為 EXEC (可執行檔案)
進入點 0x404890
共有 9 個程式標頭,開始於偏移量 64

程式標頭:
類型 偏移量 虛擬位址 實體位址
檔案大小 記憶大小 旗標 對齊
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000019d44 0x0000000000019d44 R E 200000
LOAD 0x0000000000019df0 0x0000000000619df0 0x0000000000619df0
0x0000000000000804 0x0000000000001570 RW 200000
DYNAMIC 0x0000000000019e08 0x0000000000619e08 0x0000000000619e08
0x00000000000001f0 0x00000000000001f0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x000000000001701c 0x000000000041701c 0x000000000041701c
0x000000000000072c 0x000000000000072c R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000019df0 0x0000000000619df0 0x0000000000619df0
0x0000000000000210 0x0000000000000210 R 1

區段到節區映射中:
節區段...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gn u.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_ frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got

Section Header的話會仔細列出這個ELF所包含的所有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
29
30
$ readelf -S /bin/ls

共有 28 個區段標頭,從偏移量 0x1a700 開始:

區段標頭:
[號] 名稱 類型 位址 偏移量
大小 全體大小 旗標 連結 資訊 對齊
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
0000000000000068 0000000000000000 A 5 0 8
...
[24] .data PROGBITS 000000000061a3a0 0001a3a0
0000000000000254 0000000000000000 WA 0 0 32
[25] .bss NOBITS 000000000061a600 0001a5f4
0000000000000d60 0000000000000000 WA 0 0 32
[26] .gnu_debuglink PROGBITS 0000000000000000 0001a5f4
0000000000000008 0000000000000000 0 0 1
[27] .shstrtab STRTAB 0000000000000000 0001a5fc
00000000000000fe 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

objdump取得ELF內容

除了看ELF內的資訊外,我們可以進一步得到更細的資訊,包括dump內容和反組譯程式,這時候就要用objdump了

  • objdump -s -j [section] [filename]:把特定section完整dump出來
  • objdump -h [filename]:看有哪些section,跟readelf功用類似
  • objdump -x [filename]:把所有section都顯示出來
  • objdump -d [filename]:反組譯程式
  • objdump -d -S [filename]:反組譯程式加上行數
  • objdump -d -l [filename]:反組譯程式加上source code

同樣以ls為例,可以看到我們把text section的內容印出來了

1
2
3
4
5
6
7
8
9
$ objdump -s -j .text /bin/ls

/bin/ls: 檔案格式 elf64-x86-64

Contents of section .text:
4028a0 50b9882c 4100baa6 0e0000be 36374100 P..,A.......67A.
4028b0 bf983c41 00e896fb ffff660f 1f440000 ..<A......f..D..
4028c0 41574156 41554154 554889f5 5389fb48 AWAVAUATUH..S..H
....

objcopy/strip修改ELF檔案

objcopy最主要的功能就是可以把文件作轉換,一部份或全部的內容copy另一個文件中

  • objcopy -S -R .comment -R .note [input filename] [output filename]:把編譯出來的symbol移除不必要的section(-S代表去掉symbol, relocation的訊息)
  • objcopy -O binary -j [section] [input filename] [output filename]:也可以把某個section拿出來

關於移除不必要的section部分,其實strip就可以做到了,只要用strip [filename]即可。

objcopy進階用法

objcopy可以做到把檔案變成ELF格式,提供給我們linking,這樣我們就可以避免檔案的讀取。

這邊用個簡單的範例,假設我們想要把某個文字檔包在程式內部(其實可以用圖片比較有感覺,只是我不想寫太複雜的程式)

先創立text.txt

1
This is test txt.

然後把text.txt變成object file

1
objcopy -I binary -O elf64-x86-64 -B i386:x86-64 text.txt text.o

如果這時候show object資訊的話

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ objdump -x text.o

text.o: 檔案格式 elf64-x86-64
text.o
系統架構:i386:x86-64, 旗標 0x00000010:
HAS_SYMS
起始位址 0x0000000000000000

區段:
索引名稱 大小 VMA LMA 檔案關閉 對齊
0 .data 00000012 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 g .data 0000000000000000 _binary_test_txt_start
0000000000000012 g .data 0000000000000000 _binary_test_txt_end
0000000000000012 g *ABS* 0000000000000000 _binary_test_txt_size

symsymbola把下面那些symbol放入test.c內,即可使用

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdlib.h>

extern char _binary_text_txt_start[];
extern char _binary_text_txt_end[];
extern char _binary_text_txt_size[];

int main()
{
char *ptr = _binary_text_txt_start;
printf("text.txt=%s\r\n", ptr);
return 0;
}

編譯並執行

1
2
3
4
$ gcc test.o test.o -o a.out
% ./a.out
text.txt=This is test txt.

nm觀察symbol

剛剛提了那麼多都是以ELF內的各種section為主,但是我們實際開發程式其實還是比較重視symbol,那我們有簡單方式可以看symbol嗎?這時候就要用到nm了。

  • nm [filename]:可以顯示symbol的數值、型態、名稱
  • nm --size-sort -r -S [filename]:由大到小顯示symbol的數值、大小、型態、名稱

舉個例子,我們可以看到下面執行結果symbol由大到小排序

1
2
3
4
5
6
7
$ nm --size-sort -r -S test
00008464 00000064 T __libc_csu_init
00008444 00000020 T main
000084c8 00000004 T __libc_csu_fini
000084d4 00000004 R _IO_stdin_used
00011028 00000001 b completed.9228

關於常見型態的部分可參考下表:

Section 類型(大寫代表global、小寫是local)
text section T/t
data section D/d
Read only R/r
BSS B/b
未定義(如extern) U

addr2line從位址轉成symbol

有時候我們執行程式會只知道位址,但是想要從位址得到到底是在程式哪行掛掉

  • addr2line -f -e [filename] [address]:-f代表要顯示是哪個function,-e代表address是來自該執行檔

總結

本篇文章主要簡單介紹ELF的結構,然後我們可以用 od、readelf、objdump、objcopy/strip、nm, addr2line 幾個工具觀察ELF的格式。如果想要有進一步的認識,建議可以研究參考的連結。

參考

0%