# 2020q3 Homework1 (lab0) contributed by < `MingRuey` > :::spoiler :warning: 請詳閱作業說明,務必從 GitHub fork 原有的 `lab0-c` repository,而非自行建立新的 repository,工程人員應當留意各式細節。 :notes: jserv > 已 Fork [lab0-c](https://github.com/sysprog21/lab0-c) 並修正 GitHub 連結至 [該 Repo](https://github.com/MingRuey/lab0-c) 詳閱作業要求,中英文字元之間用一個半形空白區隔。另外,`GitHub` 的 "G" 和 "H" 字元應大寫,符合該公司品牌書寫方式 :notes: jserv > 感謝,已修正 :warning: [作業區](https://hackmd.io/@sysprog/2020-homework1) 登記時用 GitHub 帳號 > 已經用 GitHub 帳號重新 pull 檔案,並更新連結 ::: ## 實作流程 --- ### **q_insert_head**, **q_insert_tail** 創造新的 ```lst_ele_t``` 幾個注意的地方: * val 的記憶體大小要考慮 trailing null byte ,```size_t s_length = strlen(s) + 1;``` strlen 的文件指出: > The strlen() function calculates the length of the string pointed to by s, excluding the terminating null byte ('\0'). * memcpy 取代 strcpy ,因為 > 依據 [CERN Common vulnerabilities guide for C programmers](https://security.web.cern.ch/recommendations/en/codetools/c.shtml) 建議而移除 gets / sprintf / strcpy 等不安全的函式; > Update: 我想到 q_remove_head 裡面應該也要考慮輸入參數 char *sp ,的長度,避免 buffer overflow 。 除了上述關於創造新節點的細節之外,此 ```q_insert_tail``` 函式要求的時間複雜度是 O(1) ,我選擇在 queue_t 的定義裡面加上一個紀錄尾巴所在位置的指標。 接著在 ```q_insert_head``` 在更新頭的指標之後,也要考慮 empty queue 的情況,尾巴的只要也要一併更新,反過來 ```q_insert_tail``` 的時候也要考慮 empty 的情況去對應更新頭。 ### **q_remove_head** 先取出頭的指標,令其為 target,接著更新 queue ,記得要處理 queue 只有一個 element 的情況。接著若 sp 不為 NULL ,要複製 ```target->value``` 到 sp 上,此時一樣要注意 strlen 的行為不包含 trailing null byte 。 ### **q_reverse** 我採用三指標的方式將 linked list 反向,首先排除小於兩個元素的 queue,接著用兩個指標指向 ```q->head``` 跟 ```q->head->next```,並且再用一個指標指向 ```q->head->next->next```,因為反向連節點的過程中要保留下一個節點的位置: ```graphviz digraph reverse{ rankdir=LR; node [shape=record]; 1 [label = "{<data>1 |<ref>}"]; 2 [label = "{<data>2 |<ref>}"]; 3 [label = "{<data>3 |<ref>}"]; 4 [label = "{<data>... |<ref>}"] 1:ref -> 2:data [color=black]; 2:ref -> 3:data [color=black]; 3:ref -> 4:data [color=black]; tmp->3; cur->1; next->2; 2:ref -> 1:data [color=red] } ``` 如上述圖表所示,用 tmp 先記好未來 next 要去的地方,接著把 next 所在的節點連回 cur ,最後兩個指標都往下走一步: ```graphviz digraph reverse{ rankdir=LR; node [shape=record]; 1 [label = "{<data>1 |<ref>}"]; 2 [label = "{<data>2 |<ref>}"]; 3 [label = "{<data>3 |<ref>}"]; 4 [label = "{<data>... |<ref>}"] 1:ref -> 2:data [color=black]; 2:ref -> 1:data [color=black]; 3:ref -> 4:data [color=black]; cur->2; next->3; } ``` 而起頭所在的節點,也就是上圖的 1 ,則在迴圈結束之後設為 NULL 就處理完畢了。 在第一次寫出 q_reverse 的時候立刻進行了測試,發現我得到了 **許多的 Segmentation fault** ,首先我在 qtest 中鍵入: ``` new free new ih first ``` 到 ```ih first``` 出現 ```Segmentation fault occurred. You dereferenced a NULL or invalid pointer```,發現我忘了把新創好的 ```char *``` assign 給 ```list_ele_t->value``` ... 處理完這個 bug ,測試的分數已經來到 59/100,扣掉還沒實作的 q_sort , trace-07, trace-11, trace-12 都還有 momery problem,正要 commit 的時候 Cppcheck 又幫我抓了一個 memory leak,我在建立節點的時候: ```c static list_ele_t *loc_element(char *s) { list_ele_t *new = malloc(sizeof(list_ele_t)); if (!new) { return NULL; } size_t s_length = strlen(s) + 1; char *val = malloc(sizeof(char) * s_length); if (!val) { return NULL; } ... } ``` #11 ```return NULL``` 忘了處理前面已經 malloc 好的 new ,解完這個 bug ,分數來到了 71/100 ,主要剩下 trace-07 的 memory problem ,以及 q_sort 至此先 commit。 --- 我鍵入 make valgrind 給了我比較細的錯誤訊息,我閱讀了 Valgrind 的教學,了解我的 Segmentation fault 發生在對 NULL queue 取 size: ``` ... +++ TESTING trace trace-07-robust: # Test operations on NULL queue ==85154== Invalid read of size 4 ==85154== at 0x10CD7A: q_size (queue.c:161) ==85154== by 0x109AC9: do_size (qtest.c:512) ==85154== by 0x10B992: interpret_cmda (console.c:220) ==85152== by 0x10BF06: interpret_cmd (console.c:243) ==85154== by 0x10C4D4: cmd_select (console.c:569) ==85154== by 0x10C71C: run_console (console.c:628) ==85154== by 0x10AE41: main (qtest.c:772) ==85154== Address 0x10 is not stack'd, malloc'd or (recently) free'd ==85154== Segmentation fault occurred. You dereferenced a NULL or invalid pointer ... ``` 這個 bug 也可以在 qtest 中對 NULL queue 執行 ```size``` 重現。 做一個小修改之後就搞定了。 --- ### **q_sort** 接著開始撰寫 q_sort ,這裡採用的演算法是 MergeSort,策略主要是用一個 pointer to pointer (head) 直接更新 recursive function 的輸入,並且利用 ```q->size``` 我們可以簡單地找到 queue 的中點來進行分割。 <details> <summary> 第一版 q_sort </summary> ```cpp static void recur_sort(list_ele_t **target, int length) { if (length <= 1) { return; } list_ele_t **left_head = target, **right_head = target; int middle = length / 2; for (int i = middle; i > 1; i--) { right_head = &(*right_head)->next; } // just right before the middle element list_ele_t *next = (*right_head)->next; (*right_head)->next = NULL; right_head = &next; recur_sort(left_head, middle); recur_sort(right_head, length - middle); list_ele_t *last = *target, *left = *left_head, *right = *right_head; bool flag = false; while (true) { if (!left) { last->next = right; break; } else if (!right) { last->next = left; break; } if (strcmp(left->value, right->value) <= 0) { if (!flag) { flag = true; *target = left; last = left; } else { last->next = left; last = left; } left = left->next; } else { if (!flag) { flag = true; *target = right; last = right; } else { last->next = right; last = right; } right = right->next; } } } ``` </details> 以上是我完成的第一版,結果測試得到 94/100 ,再次使用 Valgrind 進行記憶體除錯,訊息如下 (queue.c #262 是 ```right_head = &(*right_head)->next;```),讓我得知我在取中點的時候有 derefernce NULL pointer。 ``` ... ==103272== Invalid read of size 8 ==103272== at 0x10CC2E: recur_sort (queue.c:206) ==103272== by 0x10CF0F: q_sort (queue.c:262) ==103272== by 0x109D37: do_sort (qtest.c:552) ==103272== by 0x10B992: interpret_cmda (console.c:220) ==103272== by 0x10BF06: interpret_cmd (console.c:243) ==103272== by 0x10C4D4: cmd_select (console.c:569) ==103272== by 0x10C71C: run_console (console.c:628) ==103272== by 0x10AE41: main (qtest.c:772) ==103272== Address 0x8 is not stack'd, malloc'd or (recently) free'd ==103272== Segmentation fault occurred. You dereferenced a NULL or invalid pointer ... ``` Debug 第一版在 trace 16 的 segmentation fault 的途中,即便 valgrind 已經幫我找出哪行發生 derefernce null pointer ,但邏輯實在有點太亂了腦袋無法處理,因此改了第二版,將 Merge Sort 合併的部分的邏輯寫清楚而且令提取成一個函式: <details> <summary> 第二版程式 </summary> ``` /* * Merge two list and return the new head. */ static list_ele_t *merge(list_ele_t *head1, list_ele_t *head2) { list_ele_t *merged = NULL; list_ele_t *cursor = NULL; while (head1 && head2) { list_ele_t **head = strcmp(head1->value, head2->value) <= 0 ? &head1 : &head2; if (!cursor) { merged = *head; cursor = *head; } else { cursor->next = *head; cursor = cursor->next; } *head = (*head)->next; } if (head1) { cursor->next = head1; } else if (head2) { cursor->next = head2; } return merged; } /* * Sort the linked list with known length. */ static void recur_sort(list_ele_t **target, int length) { if (length <= 1) { return; } list_ele_t *lhead = *target, *rhead = *target; int halflen = length / 2; for (int i = halflen; i > 1; i--) { rhead = rhead->next; } list_ele_t *tmp = rhead; rhead = rhead->next; tmp->next = NULL; recur_sort(&lhead, halflen); recur_sort(&rhead, length - halflen); *target = merge(lhead, rhead); } /* * Sort elements of queue in ascending order * No effect if q is NULL or empty. In addition, if q has only one * element, do nothing. */ void q_sort(queue_t *q) { if (!q || !q->size || q->size == 1) { return; } recur_sort(&q->head, q->size); list_ele_t *ele = q->head; for (int l = q->size; l > 0; l--) { ele = ele->next; } q->tail = ele; } ``` </details> 接著繼續檢查 trace 16 的 segmentation fault ,在一陣兵荒馬亂之後,我發現我的 q->sort,trace 16 的內容是 sort, reverse 再 sort ,我觀察到我的函示在第一次 sort 之後直接 free ,也就是把 trace-16 改成下面這樣: ``` new ih RAND 10000 sort free ``` segmnetation fault 會消失,但是取而代之有 memory leak! 我在程式內埋點之後注意到,我的 q->tail 在 sort 完之後會變成 NULL ,隨即發現在 q_sort 的結尾重新 assign tail 的部分搞錯了, for loop 的中止條件應該從 ```for (int l = q->size; l > 0; l--)``` 變成 ```for (int l = q->size; l > 1; l--)```,修正完之後此 bug 就消失,也在測試項目拿到 100/100 。 ## Valgrind 排除 qtest 實作的記憶體錯誤 --- ``` make clean && make SANITIZER=1 && make test ``` 可以讓我們發現程式碼執行 trace 17 時的記憶體問題,發生 [Global buffer overflow](https://blog.gypsyengineer.com/en/security/global-buffer-overflows.html): ``` ==6054==ERROR: AddressSanitizer: global-buffer-overflow on address 0x560c01e2b9c0 at pc 0x560c01c1c86e bp 0x7ffcec1201c0 sp 0x7ffcec1201b0 READ of size 4 at 0x560c01e2b9c0 thread T0 #0 0x560c01c1c86d in do_option_cmd /home/mrchou/code/Guts_2020Fall/lab0-c/console.c:368 #1 0x560c01c1b48a in interpret_cmda /home/mrchou/code/Guts_2020Fall/lab0-c/console.c:220 #2 0x560c01c1be8e in interpret_cmd /home/mrchou/code/Guts_2020Fall/lab0-c/console.c:243 #3 0x560c01c1cb77 in cmd_select /home/mrchou/code/Guts_2020Fall/lab0-c/console.c:569 #4 0x560c01c1cf94 in run_console /home/mrchou/code/Guts_2020Fall/lab0-c/console.c:628 #5 0x560c01c1a0ad in main /home/mrchou/code/Guts_2020Fall/lab0-c/qtest.c:772 #6 0x7fe9232f8b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96) #7 0x560c01c17809 in _start (/home/mrchou/code/Guts_2020Fall/lab0-c/qtest+0x6809) ... ``` ### 閱讀程式碼 先檢視程式執行流程,從 Makefile 開始: 1. (Makefile) make test -> 執行 driver.py 2. (driver.py) 執行 driver.py -> Tracer.run() -> for loop 用 qtest 執行每一個 trace (qtest -f trace-xx-ops.cmd) 3. (qtest.c) main 函式依序執行 queue_init, init_cmd, console_init, ... -> main 執行 run_console 4. (console.c) run_console 會用 push_file 以 2. 當中的 trace 檔案創建一個 buffer ,存在全域的 buf_stack 5. (console.c) run_console 反覆呼叫 cmd_select 執行輸入的命令 第一次執行 cmd_select 的時候,不會進到 #567-571 的 while loop ,而是先過 cmd_select 其餘部分來讀取 cmd 檔案,並在 #605 把 cmd 的 header 輸出畫面上。 第二次以後的 cmd_select 則會消耗 cmd 檔案其餘的命令,直到 readline 回傳 NULL 為止。 從前面的錯誤訊息, global buffer overflow 發生在 cmd_select #569 ,interpret_cmda 執行 do_option_cmd 時在 #368 試圖存取 ```plist->valp``` 出錯。 add_param 會在 2. 中把預先定義好的 "malloc", "fail" 參數加進 param_list , #366 的 while loop 就是在 param_list 裡面尋找該參數的值。 ### 除錯 觀察到出錯的 trace-17 是唯一使用 simulation 這個參數的 trace ,懷疑是 simulation 參數加入的時候出了問題: ```c add_param("simulation", (int *) &simulation, "Start/Stop simulation mode", NULL); ``` 跟其他的參數比起來: console.c #104-105 ```c add_param("verbose", &verblevel, "Verbosity level", NULL); add_param("error", &err_limit, "Number of errors until exit", NULL); ``` qtest.c #101-105 ```c add_param("length", &string_length, "Maximum length of displayed string", NULL); add_param("malloc", &fail_probability, "Malloc failure probability percent", NULL); add_param("fail", &fail_limit, "Number of times allow queue operations to return false", NULL); ``` simulation 設計一個把 bool pointer 轉型成 int pointer 的過程, 我一時想不到怎麼驗證這個行為,參考了 [RinHizakura](https://hackmd.io/@RinHizakura/ByuY_q6AU#Address-Sanitizer), 把 pointer 後面 4 個 bytes 的內容 printf ,執行數次可以發現後三個 byte 的數值無法預期。 ```c #include <stdio.h> #include <stdbool.h> void print_bytes(void *ptr, int size) { unsigned char *p = ptr; int i; for (i=0; i<size; i++) { printf("%02hhX ", p[i]); } printf("\n"); } int main() { bool a = true; bool *ptr_b = &a; printf("&a: %p\n", ptr_b); printf("*(&a): %d\n", *ptr_b); print_bytes(ptr_b, sizeof(bool)); int *ptr_i = (int *) ptr_b; printf("(int *) &a: %p\n", ptr_i); printf("*(int *) &a: %d\n", *ptr_i); print_bytes(ptr_i, sizeof(int)); } ``` 也跑了 [YLowy](https://hackmd.io/@YLowy/BJyYjFuEP) 驗證的做法。 注意到 #106 的 echo 也有相同問題,處理這個 bug ,將 simulation 跟 echo 的型態都改成 int 可以解決 overflow 的問題。 :::info 我思考了一會兒如何兼顧 bool 跟 int 型態, 一種可能是 PELE 記錄下指標儲存內容的大小,在取值的時候要小心的解讀記憶體上的 bytes 但這樣似乎沒有直接改 bool 型態乾淨,而且記憶體用量因為儲存一個 size_t 而變大。 另一種可能是修改 add_param ,如果輸入的指標是 bool * ,則將進來的值先取出,轉成 int ,再取指標。 這樣的問題是當 simulation (or echo) 的值從外部被人更改之後, PELE 儲存的值不會改變,造成不預期的行為。 不知道有沒有其他更妥善的做法? ::: ## TODO: - Massif 視覺化 - 研讀 Dudect