# 2018q3 Homework1 contributed by <`ian910297`> ###### tags: `note` 實驗環境:使用 google cloud platform 服務,免費 300 美金額度,環境如下 ``` Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 1 On-line CPU(s) list: 0 Thread(s) per core: 1 Core(s) per socket: 1 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 62 Model name: Intel(R) Xeon(R) CPU @ 2.50GHz Stepping: 4 CPU MHz: 2500.000 BogoMIPS: 5000.00 Hypervisor vendor: KVM Virtualization type: full L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 30720K NUMA node0 CPU(s): 0 Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid pni pclmulqdq ssse3 cx16 pcid sse4_1 sse4_2 x2apic popcnt aes xsave avx f16c rdrand hypervisor lahf_lm pti fsgsbase tsc_adjust smep erms xsaveopt ``` 如果實驗有需要好的配備,本機端也已經灌好 linux 系統,只是礙於平常要打電動,不便切換系統 # 指標篇 * 參考資料 - [ ] [指標篇共筆](https://hackmd.io/s/HyBPr9WGl) - [x] [直播錄影(上)](https://www.youtube.com/watch?v=G7vERppua9o&feature=youtu.be) - [x] [直播錄影(下)](https://www.youtube.com/watch?v=Owxols1RTAg) * [頭腦體操](https://stackoverflow.com/questions/8208021/how-to-increment-a-pointer-address-and-pointers-value/8208106#8208106) operator 優先權如下 1. `()` 2. `++` 3. `*` 我先是照做頭腦體操上面的程式碼,其中只有最後兩個部分,執行起來沒什麼問題,不太能理解為啥會發生 segfault ```clike= #include <stdio.h> #include <stdlib.h> #include <string.h> int main() { char *ptr, *head; head = ptr = malloc(sizeof(char) * 100); strcpy(ptr, "Hello,World!"); printf("%c\n", *++ptr); printf("%c\n", *(++ptr)); free(head); return 0; } ``` >你有 指標篇 (上) 對應的共筆嗎? >[name=課程助教][color=red] >應該是第一筆參考資料吧,只是我前幾天懶得寫筆記做實驗......,剛剛才發現連結貼錯 >[name=ian910297][color=blue] * Understanding Declarations 取自 [C Traps and Pitfalls](http://www.literateprogramming.com/ctraps.pdf) ```clike= (*(void(*)())0)(); ``` 一開始完全看不懂,只好先從 [C Traps and Pitfalls](http://www.literateprogramming.com/ctraps.pdf) 看一下他的解說,先解釋了幾個概念 ```clike= int main() { int *g(), (*h)(); retrun 0; } ``` * g is a function return pointer to int * h is a pointer to function return int :::warning :question: 這邊有個點讓我很疑惑,在 main 裡面是可以宣告 function 的嗎?於是就查了一些資料 [Declaration of a function inside main()](http://qa.geeksforgeeks.org/2839/qa.geeksforgeeks.org/2839/declaration-of-a-function-inside-main.html)。反正,是可以宣告 function 的 :::danger 去 C99/C11 規格書找出對應的描述 :notes: jserv ::: 那就可以來解析這條式子了 1. `void (*)()` means `a pointer to function returning void` 2. `(*0)()` means `a pointer to function returning void` ,這個部份不太能理解怎麼把 0 轉型過去的,反正他就是這個意思 最後可以使用 `typedef` 將他簡化為下列的式子 ```clike= typedef void (*funcptr)(); (*(funcptr)0)(); ``` 有一題額外的練習題可以練習解構 [How to read this prototype?](https://stackoverflow.com/questions/15739500/how-to-read-this-prototype) ```clike= void ( *signal(int sig, void (*handler)(int)) ) (int); ``` * signal is a pointer to function that takes two parameters * a integer * a pointer to function takes one parameter * a integer * return a void * return a function pointer takes one parameter * a interger * return a void * Go 語言也有指標 這個部份我現在就懶得看,因為還沒有要寫 golang * 已經知道的部分 [C語言: 超好懂的指標](https://kopu.chat/2017/05/15/c%E8%AA%9E%E8%A8%80-%E8%B6%85%E5%A5%BD%E6%87%82%E7%9A%84%E6%8C%87%E6%A8%99%EF%BC%8C%E5%88%9D%E5%AD%B8%E8%80%85%E8%AB%8B%E9%80%B2%EF%BD%9E/) 老師在直播的時候有提到,當你宣告變數的時候,編譯器未必會把變數照順序放在記憶體位置上。使用 gdb 在觀察時發現,若有下 `-O` 相關的參數時,會完全找不到變數位置,我並不確定產生出來的程式碼是有什麼差別,以下使用 [godbolt](https://gcc.godbolt.org/) 做一些觀察 ```clike= int func() { int a = 10; int b = 100; return b; } ``` 沒有參數時 ```clike= func(): push rbp mov rbp, rsp mov DWORD PTR [rbp-4], 10 mov DWORD PTR [rbp-8], 100 mov eax, DWORD PTR [rbp-8] pop rbp ret ``` 參數為 `-O1` ```clike= func(): mov eax, 100 ret ``` 當有最佳化的時候,先不要管變數放在哪了,其實編譯器根本沒有把變數儲存到記憶體上,可能是直接塞一個 immediate value 到 register * c 語言的一些想法 * 編譯器可以用 c 寫,不用使用別的語言。 * c 語言開發者的想法 * 開發 Unix 作業系統 * 充分掌握硬體、記憶體 * 代表的文化是 Unix * [c99規格書](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf) 部分章節 * C99 [3.14] object * region of data storage in the execution environment, the contents of which can represent values * 記憶體位置 * The pronunciation of `&` is `address of` * call-by-value * C99 [6.2.5] types * * `void *` `void *` 需要強制轉型,去做 alignment ```clike= int main() { char *X; void *Y; char c = *X; char d = * (char *) Y; } ``` * alignment * pointer to pointer 為什麼要這樣設計? 因為 c 都是以 call by value 的形式在傳遞,這樣的設計可以讓我們在 function 裡面修改變數,透過 pointer to pointer ,可以規避 lifetime 一到 function 裡面宣告的變數就被清空 :::info 不見得會「被清空」,這與編譯器實作有關 參見 [2018q1 第 11 週測驗題](https://hackmd.io/s/r1zL9gnaf) 測驗 2,試著作答並解釋 clang 和 gcc 行為的落差。 :notes: jserv ::: 題目是解釋這段程式碼在 clang 和 gcc 行為的落差 ```clike= int main(int argc, char *argv[]) { char *p, *q; uintptr_t pv, qv; { char a = 3; p = &a; pv = (uintptr_t) p; } { char b = 4; q = &b; qv = (uintptr_t) q; } if (p != q) { printf("%p is different from %p\n", (void *) p, (void *) q); printf("%" PRIxPTR " is not the same as %" PRIxPTR "\n", pv, qv); } else { printf("Surprise!\n"); } return 0; } ``` 首先我們可以看到要比較的兩個變數 `pv`, `qv` ,他們的型態是 `uintptr_t`,這是一個會根據現在是什麼位元的系統,去做相對應的型態 使用 [godbolt](https://gcc.godbolt.org/) 做一些觀察 * clang6.0.0 ```clike= main: # @main push rbp mov rbp, rsp sub rsp, 64 lea rax, [rbp - 50] lea rcx, [rbp - 49] mov dword ptr [rbp - 4], 0 mov dword ptr [rbp - 8], edi mov qword ptr [rbp - 16], rsi mov byte ptr [rbp - 49], 3 /* char a = 3; */ mov qword ptr [rbp - 24], rcx /* p = &a; */ mov rcx, qword ptr [rbp - 24] /* pv = (uintptr_t) p; */ mov qword ptr [rbp - 40], rcx /* pv = (uintptr_t) p; */ mov byte ptr [rbp - 50], 4 /* char b = 4; */ mov qword ptr [rbp - 32], rax /* q = &b; */ mov rax, qword ptr [rbp - 32] /* qv = (uintptr_t) q; */ mov qword ptr [rbp - 48], rax /* qv = (uintptr_t) q; */ mov rax, qword ptr [rbp - 24] /* if (p != q) { */ cmp rax, qword ptr [rbp - 32] /* if (p != q) { */ je .LBB0_2 /* if (p != q) { */ ``` * gcc7.2.0 ```clike= main: push rbp mov rbp, rsp sub rsp, 64 mov DWORD PTR [rbp-52], edi mov QWORD PTR [rbp-64], rsi mov BYTE PTR [rbp-33], 3 /* char a = 3; */ lea rax, [rbp-33] /* p = &a; */ mov QWORD PTR [rbp-8], rax /* p = &a; */ mov rax, QWORD PTR [rbp-8] /* pv = (uintptr_t) p; */ mov QWORD PTR [rbp-16], rax /* pv = (uintptr_t) p; */ mov BYTE PTR [rbp-34], 4 /* char b = 4; */ lea rax, [rbp-34] /* q = &b; */ mov QWORD PTR [rbp-24], rax /* q = &b; */ mov rax, QWORD PTR [rbp-24] /* qv = (uintptr_t) q; */ mov QWORD PTR [rbp-32], rax /* qv = (uintptr_t) q; */ mov rax, QWORD PTR [rbp-8] /* if (p != q) { */ cmp rax, QWORD PTR [rbp-24] /* if (p != q) { */ je .L2 /* if (p != q) { */ ``` 兩者主要差異是在做 `&`(address of) 時,讓我們鎖定有使用到的 register rax * clang 的實作是 mov * mov 會直接改變數值 * value of p is qword ptr [rbp - 24] * value of q is qword ptr [rbp - 32] * rax 的地址從頭到尾都在 [rbp - 50] 上 * gcc 的實作是 lea, mov * lea 是間接改變數值,他會先載入那個位置的地址 * value of p is QWORD PTR [rbp - 8] * value of q is QWORD PTR [rbp - 24] * rax 的地址一直在改變,從 [rbp - 33] 到 [rbp - 34] 答案我認為是 (d) gcc 的實作與規格書不符,他不應該可以操作 [rbp - 34] ,照規格書來看 lifetime 已經結束 * 找 *** 的 project :::info 提示: 數學運算相關專案很容易看到 :notes: jserv ::: 在 github 上翻了半天,數學運算相關的專案,不知道為啥就是沒看到,不過看到一些奇怪的地方有用到 <s>triple pointer</s> the pointer to the pointer to the pointer ,大多是在開啟檔案讀資料的時候。 :::danger 不要濫用詞彙,務必依據 C 語言規格書來描述 :notes: jserv ::: >好的,我會更加注意 >[name=ian910297][color=blue] 在 github 下載專案後,我都是使用 grep 來搜尋在哪邊有使用到 ```shell grep -F "***" --include=\*.c -r ./ ``` 以下是在 [jakogut/tinyvm](https://github.com/jakogut/tinyvm.git) 專案裡面,有發現到 the pointer to the pointer to the pointer 的應用 檔案 [libtvm/tvm_lexer.c](https://github.com/jakogut/tinyvm/blob/master/libtvm/tvm_lexer.c) ```clike=52 /* NULL terminate the array to make iteration later easier*/ lexer->source_lines[i] = NULL; /* Split the source into individual tokens */ for (i = 0; lexer->source_lines[i]; i++) { lexer->tokens = (char ***)realloc( lexer->tokens, sizeof(char **) * (i + 2)); lexer->tokens[i] = (char **)calloc(MAX_TOKENS, sizeof(char *)); tokp = strtok(lexer->source_lines[i], " \t,"); for (j = 0; (tokp && j < MAX_TOKENS); j++) { char *token = tvm_htab_find_ref(defines, tokp); token = token ? token : tokp; lexer->tokens[i][j] = calloc(1, (strlen(token) + 1)); strcpy(lexer->tokens[i][j], token); tokp = strtok(NULL, " \t,"); } } ``` lexer 顧名思義是用來解析文字,將其切成一個一個 token 的程式,讓我們看一下現在 lexer 這個物件是什麼 檔案 [include/tvm/tvm_lexer.h](https://github.com/jakogut/tinyvm/blob/master/include/tvm/tvm_lexer.h) ```clike=9 struct tvm_lexer_ctx { char **source_lines; char ***tokens; }; ``` `lexer->tokens` 的部分,在每次遞迴時,都不斷重複<s>調用</s> 呼叫 `realloc()` ,因為已經執行過 `calloc()` ,只需要重新調整空間即可,再來就不斷的把 `token` 塞進 `lexer->tokens[i]` 裡面 :::success call 和 invoke 在繁體中文的翻譯是「呼叫」 :notes: jserv ::: 這邊 *** 主要應用是根據檔案有多少行(lines)來儲存相對應的 `token` * forward declaration 這個部份我有一個疑問,記得以前學過 function prototype ,這個東西聽起來跟他很像,先查閱 [c99規格書](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf) * C99 [6.2.1] Scope of identifiers * A function prototype is a declaration of a function that declares the type of its parameters * array v.s. pointer 不能變更為 pointer ```clike= // In declaration extern char x[]; char a[10]; ``` 以前在寫字串的時候,覺得這樣操作是合法的,沒有思考過 pointer 本質上就是只有一個 word 長度的變數( pointer 的 size 會依據不同位元的作業系統來做一些改變),是透過儲存地址來指向目標 ```clike= int main() { char a[10]; strcpy(a, "abcdefghij"); printf("%s\n", a); printf("%c\n", *a); printf("%c\n", *(a+1)); return 0; } ``` 可以變更為 pointer ```clike= // In declaration func(char x[]); func(char *x); // In expression int main() { int x[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; printf("%d %d %d %d\n", x[4], *(x + 4), *(4 + x), 4[x]); } ``` c 沒有真正的陣列,只有 C99 [6.5.2.1] <b>Array subscripting</b> * 使用 gdb 做實驗 ```clike= struct tags { int a; double b[3]; char token[20]; }; int main() { struct tags var[17]; int a[10][10]; return 0; } ``` 最基礎的指令 ```shell= # print p var # eval p var.a = 10 ``` :::info GDB 的 pretty print 很好用,參見: * [GDB Manual](https://sourceware.org/gdb/onlinedocs/gdb/Pretty_002dPrinter-Commands.html) * [不為人知的gdb 使用方式系統-gdb pretty printer](https://yodalee.blogspot.com/2016/09/gdb-gdb-pretty-printer.html) :notes: jserv ::: 在觀察 structure 時,可以先做一些設定以便閱讀 ```shell= (gdb) p var[0] $1 = {a = -10176, b = {6.9533488729516863e-310, 0, 6.9533490664387614e-310}, token = "\340\333\377\377\001\000\000\000\312\a\000\000\000\000\000\000\006\000\000\000\000\000\000\000@\330\377\377\377\177\000\000\240b\000\000\003\000\000\000@\341\377\367\377\177\000\000`\355\377\367\377\177\000\000\000\000\000\000\000\000\000\000\001\b\000\000\000\000\000\000\312\a\000\000\000\000\000\000\001\000\000\000\000\000\000\000\355\201\000\000\000\000\000\000\000\000\000"} (gdb) set print pretty (gdb) p var[0] $2 = { a = -10176, b = {6.9533488729516863e-310, 0, 6.9533490664387614e-310}, token = "\340\333\377\377\001\000\000\000\312\a\000\000\000\000\000\000\006\000\000\000\000\000\000\000@\330\377\377\377\177\000\000\240b\000\000\003\000\000\000@\341\377\367\377\177\000\000`\355\377\367\377\177\000\000\000\000\000\000\000\000\000\000\001\b\000\000\000\000\000\000\312\a\000\000\000\000\000\000\001\000\000\000\000\000\000\000\355\201\000\000\000\000\000\000\000\000\000" } ``` 相關設定,我們可以寫在 `~/.gdbinit` 檔案裏面,避免每次都要重新輸入 ```shell= # print pretty structure set print pretty # 保存歷史命令 set history filename ./.gdb_history set history save on ``` 也可以在裡面定義屬於自己的格式,在 gdb 定義自己的 command * [User-defined commands](ftp://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_188.html) ```clike= define printhello print "Hello, World!" end ``` 可以透過下列指令來查看已定義的 command ```shell= show user show user printhello ``` 介紹了這些,再來就是做個簡單的練習,自定義一個專屬的格式,來印出我的 structure ,目標是不要印出數字 ```clike= define printtags set $i = 0 set $len = strlen($tags_token) printf "(tags token) %s, (length) %d\n", $tags_token, $len while $i < $len set $c = $tags_token[$i++] if $c <48 || $c > 57 printf "%c", $c end end printf "\n" end ``` 需要做一點前置處理,因為這是 User-defined command 不是 function or macro ,不能傳遞參數進去,必須要先指定好要用的變數是什麼 ```shell= Reading symbols from a.out...done. (gdb) b 12 Breakpoint 1 at 0x400560: file var.c, line 12. (gdb) r Starting program: /home/ian910297/c/a.out Breakpoint 1, main () at var.c:13 13 return 0; (gdb) p strcpy(var[0].token, "Hello, 1234World!") $1 = -8304 (gdb) set $tags_token = var[0].token (gdb) printtags (tags token) Hello, 1234World!, (length) 17 Hello, World! ``` gdb 還有一個東西叫做 [pretty printer](https://sourceware.org/gdb/onlinedocs/gdb/Pretty-Printing.html#Pretty-Printing) ,可以讓我們更輕易達到目的,不一定要使用 User-defined command 而且在 gdb v7 之後開始支援 python ,所以我們可以使用 python 來撰寫 pretty-printer 先來一個簡單的範例程式,依據 [23.2.2.5 Pretty Printing API](https://sourceware.org/gdb/onlinedocs/gdb/Pretty-Printing-API.html#Pretty-Printing-API) 裡面有提到,我們必須宣告一個 class 可以有四個 Function * to_string * necessnary * * display_hint * optional * 只有三種回傳值 * array * map * string * children * optional * * default_visualizer ```python= import gdb.printing class Tags(object): def __init__(self, val): self.val = val def to_string(self): return "I'm tags!" pp = gdb.printing.RegexpCollectionPrettyPrinter('TagsPrettyPrinter') pp.add_printer('Tags', '^tags', Tags) gdb.printing.register_pretty_printer(gdb.current_objfile(), pp) ``` 原來要在 gdb 裡面載入 python 程式,直接做使用,我一開始在嘗試使用 pip 安裝 gdb package ,然後發現完全找不到 ```shell= Reading symbols from a.out...done. # load pretty printer (gdb) source tags.py # list pretty printer (gdb) info pretty-printer global pretty-printers: TagsPrettyPrinter Tags builtin mpx_bound128 (gdb) b 12 Breakpoint 1 at 0x400560: file var.c, line 12. (gdb) r Starting program: /home/ian910297/c/a.out Breakpoint 1, main () at var.c:13 13 return 0; (gdb) p var $1 = {I'm tags, I'm tags, I'm tags, I'm tags, I'm tags, I'm tags, I'm tags, I'm tags, I'm tags, I'm tags, I'm tags, I'm tags, I'm tags, I'm tags, I'm tags, I'm tags, I'm tags} (gdb) ``` 如果要有跟上面 User-defined command 一樣的效果, to_string() 要做一些改寫 ```python= addr = self.val.address token = self.val['token'] strlen = len(token) i = 0 content = [] while i < strlen: c = token[i] if c <48 or c>57: content.append(chr(c)) i = i + 1 return '(token)%s (length=%d): "%s"' % (addr, strlen, "".join(content)) ``` 不知道為什麼一直執行失敗,顯示 `Python Exception <class 'NotImplementedError'> Invalid operation on gdb.Value.:` 發現是不能呼叫 len() ,猜測可能是 char 型態的問題,導致呼叫 len() 失敗,但我實在懶得理他,反正把字串長度寫死就好了 ```shell= Reading symbols from a.out...done. (gdb) b 12 Breakpoint 1 at 0x400560: file var.c, line 12. (gdb) r Starting program: /home/ian910297/c/a.out Breakpoint 1, main () at var.c:13 13 return 0; (gdb) source tags.py (gdb) p strcpy(var[0].token, "Hello, 1234World!") $1 = -8304 (gdb) p var[0] $2 = (token)Hello, 1234World! (pretty printer)Hello, World! ``` 查了一堆 GDB 的資料,學到不少用法,還是覺得稍嫌無聊,記得以前看過一個專案 [CS:APP二进制炸弹开篇](https://blog.csdn.net/astrotycoon/article/details/73201149)([檔案下載](https://github.com/astrotycoon/CS-APP-binary-bomb)),是搭配課本第三章使用的 LAB ,感覺是一個蠻好玩的 GDB 練習,但難度有點高 * 字串操作 ```clike= char *r = malloc(strlen(s) + strlen(t) + 1); // use sbrk; change progrram break if (!r) exit(1); /* print some error and exit */ strcpy(r, s); strcat(r, t); ``` 可能錯誤 * 呼叫 `strcpy`, `strcat` 時,可能會因為字串過長,超出原本配置的記憶體空間,導致錯誤 * `malloc` 也可能失敗 解決方法 * 每次檢查 `malloc` 的回傳結果 * 多給一點記憶體 :::warning :question: 1. 實際應用時,會有這樣記憶體配置相關的錯誤,但比如說 c++ 就有 `vector` 的型態可以使用,不知道這又是怎麼實作,可能用 c 實作出 `vector` 的效果嗎? 2. 找一個 strdup 的 invalid example ::: 在 github 上找到一個專案 [eteran/c-vector](https://github.com/eteran/c-vector) ,使用 c 實作 `std::vector` ```clike= ``` * function pointer