# 2020q1 Homework1 (lab0) contributed by < `gagachang` > ###### tags: `linux2020` * [H01: lab0](https://hackmd.io/@sysprog/linux2020-lab0) ## 實作環境 * 作業系統:Ubuntu 18.04.3 LTS in VirtualBox VM * Kernel version:5.3.0-40-generic * gcc version:8.3.0 * RAM size:8GB ## 作業要求 * 在 GitHub 上 fork [lab0-c](https://github.com/sysprog21/lab0-c) * 參閱 [Git 教學和 GitHub 設定指引](https://hackmd.io/@sysprog/git-with-github) ^附教學影片^ * ==詳細閱讀 [C Programming Lab](http://www.cs.cmu.edu/~213/labs/cprogramminglab.pdf)== ,依據指示著手修改 `queue.[ch]` 和連帶的檔案,測試後用 Git 管理各項修改,記得也該實作 `q_sort` 函式。 * 在提交程式變更前,務必詳閱 [如何寫好 Git Commit Message](https://blog.louie.lu/2017/03/21/%E5%A6%82%E4%BD%95%E5%AF%AB%E4%B8%80%E5%80%8B-git-commit-message/) * 不用理會 [Autolab](http://www.autolabproject.com/) 和檔案下載的描述,這兩者都是 CMU 專屬 * 修改排序所用的比較函式,變更為 [natural sort](https://github.com/sourcefrog/natsort),在 "simulation" 也該做對應的修改,得以反映出 [natural sort](https://github.com/sourcefrog/natsort) 的使用。 * 除了修改程式,也要編輯「[作業區](https://hackmd.io/@sysprog/linux2020-homework1)」,增添開發紀錄和 GitHub 連結,除了提及你如何逐步達到自動評分程式的要求外,共筆也要涵蓋以下: * 運用 Valgrind 排除 `qtest` 實作的記憶體錯誤,並透過 Massif 視覺化 "simulation" 過程中的記憶體使用量,需要設計對應的實驗 * 研讀論文 [Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf),解釋本程式的 "simulation" 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 [Student's t-distribution](https://en.wikipedia.org/wiki/Student%27s_t-distribution) 及程式實作的原理; * 解釋 [select](http://man7.org/linux/man-pages/man2/select.2.html) 系統呼叫在本程式的使用方式,並分析 [console.c](https://github.com/sysprog21/lab0-c/blob/master/console.c) 的實作,說明其中運用 CS:APP [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 的原理和考量點。可對照參考 [CS:APP 第 10 章重點提示](https://hackmd.io/@sysprog/H1TtmVTTz) :arrow_forward: 為避免「舉燭」,請比照 `qtest` 實作,撰寫獨立的終端機命令解譯器 (可建立新的 GitHub repository) * 挑戰題 * 參照 [antirez/linenoise](https://github.com/antirez/linenoise),強化 `qtest` 命令列功能,使其支援單行或多行編輯、歷史命令處理、命令自動補完 (例如輸入 `h` 之後按下 Tab 按鍵,自動接續補上 `elp`,成為完整的 `help` 命令)。除了整合程式碼外,應當說明 [antirez/linenoise](https://github.com/antirez/linenoise) 的運作原理,注意到 [termios](http://man7.org/linux/man-pages/man3/termios.3.html) 的運用 * 思考如何預設開啟 AddressSanitizer,卻又能滿足效能測試所需 (可能需要兩種以上的 build target) * 指出現有程式的缺陷 (提示: 和 [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 有關),嘗試強化並提交 pull request * 截止日期: * Mar 2, 2020 (含) 之前 * 越早在 GitHub 上有動態、越早接受 code review,評分越高 ## 實作歷程 我主要照著 `driver.py` 吃測資的順序,來實作此份作業。 `q_new`, `q_insert_head`, `q_insert_tail`, `q_remove_head`, `q_reverse` 很快地就在一個下午內實作完成。 以下記錄實作時發生的一些問題: ### Fault 1 : test case No.15 and No.17 實作好 `q_insert_tail`,並執行 `make test` 時,發現無法通過第 15 及 17 筆測資。 :::danger 文字訊息不要用圖片展現 :notes: jserv ::: 此時的測試截圖如下: ![](https://i.imgur.com/r7qqIcN.png) 此時的 `q_insert_tail` 程式碼如下: ```cpp=100 bool q_insert_tail(queue_t *q, char *s) { list_ele_t *newt; size_t length; if (!q) { return false; } newt = malloc(sizeof(list_ele_t)); if (!newt) { return false; } length = strlen(s); newt->value = malloc(length + 1); if (!newt->value) { free(newt); return false; } memset(newt->value, 0, length + 1); // Clear the allocated memory to zero strncpy(newt->value, s, length); newt->next = NULL; q->tail->next = newt; q->tail = newt; q->size++; /* if there is only one element in queue, set the newt as q->head too */ if (q->size == 1) { q->head = newt; } return true; } ``` 因為執行 `make test` 不會印出問題所在,此時使用 valgrind 單獨測試第 17 筆測資,發現是 `queue.c` 第 125 行出錯,馬上前往程式碼查看 ![](https://i.imgur.com/i4e2mxN.png) ![](https://i.imgur.com/7ozOruT.png) 當場發現這行是個 BUG,假若 queue 一開始為空的,且馬上呼叫 `q_insert_tail`,因為此時的 `q->tail = NULL`,因此這行程式碼就會變成 `NULL->next`,才會產生 invalid write !! 後來更改正確的程式碼,檢查 `q->tail` 是否為 NULL,即解決此 BUG。 ### Fault 2 : valgrind --tool=massif error 使用 valgrind 並帶入 massif 工具參數進行測試 `qtest` 會發生下圖錯誤: ![](https://i.imgur.com/MJEc9Cf.png) 解決方法:將目錄下 `.valgrindrc` 檔案中的 `--show-leak-kinds=all` 移到 `--memcheck:leak-check=full` 前面。 ### Fault 3 : test case NO.15 最後只剩下第 15 個 test case 沒過,如下圖: ![](https://i.imgur.com/wwQ9Lu8.png) 決定先照著 `traces/trace-15-perf.cmd` 跑一遍測試,發現似乎是 `sort` 出問題: ![](https://i.imgur.com/pYkhiVT.png) 嘗試執行 `valgrind --tool=massif ./qtest`,並將第 15 筆測資餵進去分析,結果發現 massif 無法成功執行,如下圖: ![](https://i.imgur.com/4E5OS9F.png) 執行 `make valgrind` 分析記憶體行為,可發現有 Stack overflow 發生 ![](https://i.imgur.com/IzranB0.png) 看起來像是 Stack 空間滿了,發生在執行 `nat_isspace` 時,這是在排序中的比較函式 `strnatcmp` 中會呼叫到的函式,可見應該是在進行排序時讓 Stack 滿了,因為此時我實作的排序使用了[Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/)的 recursive 版本,因此勢必要將 recursive 改掉。 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 的另一版本為使用 pseudo node,但第 15 個測資無法 malloc 新的 element 使用,所以最後寫了最 naive 的版本 (程式碼紀錄在下節),成功滿分! ![](https://i.imgur.com/uKveeeY.png) :::info 有個奇怪的點是,執行 `make test` 有時候無法通過第 15 個測資,會只拿到 94 分,看起來跟程式效能有關連?猜測是因為執行分析而造成程式運作太久 alarm 到期 (不知是否為挑戰題第二部分);而執行 `make valgrind` 則可以通過拿到滿分,以下是截圖證明。 make test (執行多次有時候會滿分) ![](https://i.imgur.com/VXSNvLi.png) make valgrind ![](https://i.imgur.com/04AkAew.png) ::: ## 實作程式碼 打開 `queue.h`,發現裡面定義了我們要實作的資料結構以及函式。 ### 主要資料結構 * **list_ele_t** ```clike typedef struct ELE { char *value; struct ELE *next; } list_ele_t; ``` * **queue_t** 原先只有 head 這個指標變數,為了達成能在 $O(1)$ 的時間複雜度之下完成 `q_insert_tail` 和 `q_size`,新增了 tail 指標指向 queue 的尾端,以及一個 integer size,用來記錄目前 queue 的長度。 ```cpp typedef struct { list_ele_t *head; // Pointer to the head of queue list_ele_t *tail; // Pointer to the tail of queue int size; // The size of queue } queue_t; ``` ### **q_new** 宣告一個 queue 資料結構,並為其分配記憶體,若無法取得記憶體空間,則回傳 NULL;若成功取得記憶體,則初始化 q 的結構成員,並回傳 q 的位址。 ```cpp queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); if (!q) { return NULL; } q->head = NULL; q->tail = NULL; q->size = 0; return q; } ``` :::success 在研讀老師的講座與實作此份作業之前,我並不知道 `malloc` 也會有失敗的時候,以後我會養成檢查 `malloc` 是否有成功的好習慣! ::: ### **q_free** ```cpp void q_free(queue_t *q) { list_ele_t *temp; if (!q) { return; } while ((temp = q->head) != NULL) { if (temp->value) { free(temp->value); } q->head = q->head->next; free(temp); } free(q); } ``` ### **q_insert_head** ```cpp bool q_insert_head(queue_t *q, char *s) { list_ele_t *newh; size_t length; if (!q) { return false; } newh = malloc(sizeof(list_ele_t)); if (!newh) { return false; } length = strlen(s); newh->value = malloc(length + 1); if (!newh->value) { free(newh); newh = NULL; return false; } strncpy(newh->value, s, length); newh->value[length] = '\0'; newh->next = q->head; q->head = newh; q->size++; /* if there is only one element in queue, set the newh as q->tail too */ if (!q->tail) { q->tail = newh; } return true; } ``` ### **q_inset_tail** ```cpp bool q_insert_tail(queue_t *q, char *s) { list_ele_t *newt; size_t length; if (!q) { return false; } newt = malloc(sizeof(list_ele_t)); if (!newt) { return false; } length = strlen(s); newt->value = malloc(length + 1); if (!newt->value) { free(newt); newt = NULL; return false; } strncpy(newt->value, s, length); newt->value[length] = '\0'; newt->next = NULL; if (q->tail) { q->tail->next = newt; } q->tail = newt; q->size++; /* if there is only one element in queue, set the newt as q->head too */ if (q->size == 1) { q->head = newt; } return true; } ``` ### **q_remove_head** ```cpp bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { list_ele_t *original_head; if (!q || q->size == 0) { return false; } if (sp && q->head->value) { size_t length; length = strlen(q->head->value); length = length <= bufsize - 1 ? length : bufsize - 1; strncpy(sp, q->head->value, length); sp[length] = '\0'; } original_head = q->head; q->head = q->head->next; if (original_head->value) { free(original_head->value); } free(original_head); q->size--; /* if there is no element in queue, just set the q->tail to NULL too */ if (q->size == 0) { q->tail = NULL; } return true; } ``` ### **q_size** ```cpp int q_size(queue_t *q) { return q == NULL ? 0 : q->size; } ``` ### **q_reverse** 使用兩個指標進行 linked list 的反轉 ```cpp void q_reverse(queue_t *q) { if (!q || q->size == 0 || q->size == 1) { return; } list_ele_t *former, *latter; former = q->head; latter = NULL; q->tail = q->head; while (former) { q->head = q->head->next; former->next = latter; latter = former; former = q->head; } q->head = latter; } ``` ### **q_sort** 使用 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 的 merge sort 版本實作 sorting ```cpp void q_sort(queue_t *q) { if (!q || q->head == NULL || q->size < 2) { return; } q->head = merge_sort_list(q->head); // find the new q->tail list_ele_t *tail = q->head; while (tail->next) { tail = tail->next; } q->tail = tail; } ``` ```cpp list_ele_t *merge(list_ele_t *left, list_ele_t *right) { // merge with recursive if (!right) return left; if (!left) return right; list_ele_t *head, *temp; // find the head of the two splitted lists if (strnatcmp(left->value, right->value) < 0) { head = left; left = left->next; } else { head = right; right = right->next; } // keep sorting temp = head; while (left && right) { if (strnatcmp(left->value, right->value) < 0) { temp->next = left; left = left->next; } else { temp->next = right; right = right->next; } temp = temp->next; } // check if there is a rest element if (left) temp->next = left; if (right) temp->next = right; return head; } ``` ```cpp list_ele_t *merge_sort_list(list_ele_t *head) { if (!head || !head->next) return head; list_ele_t *fast = head->next; list_ele_t *slow = head; // split list while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } fast = slow->next; slow->next = NULL; // sort each list list_ele_t *left = merge_sort_list(head); list_ele_t *right = merge_sort_list(fast); // merge return merge(left, right); } ``` ## Massif 視覺化 “simulation” * 運用 Valgrind 排除 `qtest` 實作的記憶體錯誤,並透過 Massif 視覺化 "simulation" 過程中的記憶體使用量,需要設計對應的實驗 ## dudect 與 simulation 模式原理 * 研讀論文 [Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf),解釋本程式的 "simulation" 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 [Student's t-distribution](https://en.wikipedia.org/wiki/Student%27s_t-distribution) 及程式實作的原理; ## 解釋 select 原理 * 解釋 [select](http://man7.org/linux/man-pages/man2/select.2.html) 系統呼叫在本程式的使用方式,並分析 [console.c](https://github.com/sysprog21/lab0-c/blob/master/console.c) 的實作,說明其中運用 CS:APP [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 的原理和考量點。可對照參考 [CS:APP 第 10 章重點提示](https://hackmd.io/@sysprog/H1TtmVTTz) :arrow_forward: 為避免「舉燭」,請比照 `qtest` 實作,撰寫獨立的終端機命令解譯器 (可建立新的 GitHub repository) ## 挑戰題 * 參照 [antirez/linenoise](https://github.com/antirez/linenoise),強化 `qtest` 命令列功能,使其支援單行或多行編輯、歷史命令處理、命令自動補完 (例如輸入 `h` 之後按下 Tab 按鍵,自動接續補上 `elp`,成為完整的 `help` 命令)。除了整合程式碼外,應當說明 [antirez/linenoise](https://github.com/antirez/linenoise) 的運作原理,注意到 [termios](http://man7.org/linux/man-pages/man3/termios.3.html) 的運用 * 思考如何預設開啟 AddressSanitizer,卻又能滿足效能測試所需 (可能需要兩種以上的 build target) * 指出現有程式的缺陷 (提示: 和 [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 有關),嘗試強化並提交 pull request