# 2020q3 Homework1 (lab0) contributed by < `JimmyLiu0530` > ###### tags: `進階電腦系統理論與實作` ## 工具簡介 - 學習 [GNU/Linux 開發工具](https://hackmd.io/@sysprog/gnu-linux-dev) - [Cppcheck](http://cppcheck.sourceforge.net/): 靜態程式碼分析工具 - [Valgrind](https://valgrind.org/): 動態程式分析工具 - 學習使用 Git 與 GitHub - [clang-format](https://clang.llvm.org/docs/ClangFormat.html): 確保一致的程式風格 - 使用方式: ```shell $ clang-format -i *.[ch] ``` - [Valgrind](https://valgrind.org/): 動態分析記憶體工具 - 使用方法: ```shell $ make valgrind ``` - [AddressSanitizer ](https://github.com/google/sanitizers/wiki/AddressSanitizer): 強化執行時期的記憶體檢測 - 開啟方式: ```shell $ make SANITIZER=1 ``` 詳細介紹請至[進階電腦系統理論與實作 lab0](https://hackmd.io/@sysprog/2020-lab0) 觀看 ## 作業要求 用鏈結串列(linked list)來實作佇列(queue),依據指示著手修改 `queue.[ch]` 和連帶的檔案 - `q_new`: 建立新的「空」佇列 - `q_free`: 釋放佇列所佔用的記憶體 - `q_insert_head`: 在佇列開頭 (head) 加入 (insert) 給定的新元素 (以 LIFO 準則) - `q_insert_tail`: 在佇列尾端 (tail) 加入 (insert) 給定的新元素 (以 FIFO 準則) - `q_remove_head`: 在佇列開頭 (head) 移除 (remove) 給定的元素 - `q_size`: 計算佇列中的元素數量 - `q_reverse`: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列元素,換言之,它只能重排已經存在的元素 - `q_sort`: 以==遞增順序==來排序鏈結串列的元素。可參閱 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 得知實作手法 ## 實作 ### Queue structure - `tail` 在`q_insert_tail`會提到它的用途 - `size` 在`q_size`會提到它的用途 ```c= typedef struct { list_ele_t *head; list_ele_t *tail; int size; } queue_t; ``` ### Linked list element ```c= typedef struct ELE { /* Pointer to array holding string. * This array needs to be explicitly allocated and freed */ char *value; struct ELE *next; } list_ele_t; ``` ### 各函式開發流程 - **`q_new`** - 建立一個空的佇列,若沒有足夠空間則回傳 NULL ```c= 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; } - `q_size` - 需在 O(1) 時間下,回傳此佇列的大小 - 最直觀的方法就是從頭到尾算一遍 -> O(n) - 思考: O(1) 代表`與佇列大小無關`,卻要算它的大小? - 只能在每`新增`一個 node 時,計數器`+1`,每`刪除`一個 node 時,計數器`-1`, 而這裡的計數器就是 `queue_t` 中的 `size` ```c= int q_size(queue_t *q) { if (!q || !q->head) return 0; return q->size; } ``` - **`q_free`** - 釋放佇列使用的所有記憶體空間 - `tmp` 用來記錄這次要釋放的空間 - 小心!!第 8、9 行順序不可顛倒 ```c= void q_free(queue_t *q) { if (!q) return; while (q->head) { list_ele_t *tmp = q->head; q->head = q->head->next; free(tmp->value); free(tmp); } free(q); } ``` - **`q_insert_head`** - 將新元素加到佇列的`前端` - 先檢查 `q` 是否為 NULL,再檢查是否有足夠空間可配置 - 若記憶體不足無法成功配置,則收回之前給 `new` 的空間 - **第11行** `+1`是因為字串尾端會有 NULL 當作結束字符 - [snprintf](http://www.cplusplus.com/reference/cstdio/snprintf/) 功用在於將 `s 所指的字串`最多存入`(length-1)`個字元到 `newh->value 所指的 buffer` 中,而且會自動先將 buffer 清空,`s 所指的字串`補"\0"後才寫入 (補充)[c語言字串複製函式介紹](https://medium.com/@cashbooktw/c-%E8%AA%9E%E8%A8%80%E7%9A%84%E5%AD%97%E4%B8%B2%E8%A4%87%E8%A3%BD-94da3884dc6e):這是有關 strcpy、strncpy、sprintf 跟 snprintf的介紹跟常見的問題 - **第 19 行** 記得 size 值+1 - **第 21-23 行** 考慮到加入一個原本為空的佇列,需改變`q->tail`,否則加到前端是不需要改動 `q->tail` ```c= bool q_insert_head(queue_t *q, char *s) { if (!q) return false; list_ele_t *newh; newh = malloc(sizeof(list_ele_t)); if (!newh) return false; int length = strlen(s) + 1; newh->value = (char *) malloc(length * sizeof(char)); if (!newh->value) { free(newh); return false; } snprintf(newh->value, length, "%s", s); newh->next = NULL; q->size++; /*newEle is the first element in queue*/ if (!q->tail) { q->tail = newh; } newh->next = q->head; q->head = newh; return true; } ``` - **`q_insert_tail`** - 思路與`q_insert_head`相似,但並非全抄!!! - **第21-22行** 考慮到加入一個原本為空的佇列,需改變`q->head`,否則加到尾端是不需要改動 `q->head` > 在跑 `make test` 時,這個function出現以下的錯誤提示: > Segmentation fault occurred. You dereferenced a NULL or invalid pointer > 我一直去跟上面 `q_insert_head` 函式對,明明都長一樣啊... 對!就是因為都長一樣才錯。後來再仔細畫圖跑過一次,原來是因為~~複製`q_insert_head`複製太高興~~,導致第24行 > ```c= > q->tail->next = newElej; > ``` > 忘記放到 else 中 ```c= bool q_insert_tail(queue_t *q, char *s) { if (!q) return false; list_ele_t *newEle; newEle = malloc(sizeof(list_ele_t)); if (!newEle) return false; int length = strlen(s) + 1; newEle->value = (char *) malloc(length * sizeof(char)); if (!newEle->value) { free(newEle); return false; } snprintf(newEle->value, length, "%s", s); newEle->next = NULL; q->size++; // newEle is the first element in queue if (!q->tail) { q->head = newEle; } else { q->tail->next = newEle; } q->tail = newEle; return true; } ``` - **`q_remove_head`** - 題目要求將刪除的字串複製到 `*sp`,最多複製 `bufsize-1` 個字元(**第6行**) - **第7-10行** 跟上面提到的`q_free`函式一樣釋放空間 - 記得要將 `q->size` -1 ```c= bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (!q || !q->head) return false; snprintf(sp, bufsize, "%s", q->head->value); list_ele_t *tmp = q->head; q->head = q->head->next; free(tmp->value); free(tmp); q->size--; return true; } ``` - **`q_reverse`** - 將佇列中的元素反向,又佇列是用`鏈結串列`實作出來的,所以這裡的反向其實就跟 [quiz1](https://hackmd.io/@JimmyLiu0530/Hkwvpkc4D/edit) 鏈結串列的 reverse 概念一樣 - **核心想法**: 將 `cursor` 視為指向 head 的前一個節點,`next` 視為指向 head 的後一個節點。不斷更新(往後挪)三個指標 cursor、head、next 直到 linked list 的尾端 - 與 `quiz1` 稍微不同的是**不用回傳**鏈結串列新的 head ,因為可以直接 `q->head = cursor` 作更新 ```c= void q_reverse(queue_t *q) { if (!q || !q->head) return; q->tail = q->head; list_ele_t *cursor = NULL; list_ele_t *head = q->head; while (head) { list_ele_t *next = head->next; head->next = cursor; cursor = head; head = next; } q->head = cursor; } ``` - **`q_sort`** > 題目說明 "Sort elements of queue in ascending order",一開始不太清楚題目的意思,不知道是要`比字串的字母順序`還是`比字串的長度`。後來詢問同學的看法,他的解讀是`前者`,因此我就往`比較字串的字母順序`方面去寫了 - 因為有測 `worst case` 的測資,所以我選擇了在 worst case 仍保持 O(nlog(n))的 `Merge Sort` - `Merge Sort` 核心觀念: - 用 `divide and conquer` 方法,不斷將鏈結串列切半,再將這兩半遞迴的呼叫 `mergeSort`,直到無法在切為止 - 之後在呼叫 `merge` 函式將兩個==已排序==的鏈結串列合併成一個排序好的鏈結串列 - 在 `q_sort` 函式中呼叫 `mergeSort`,會在下方介紹 - **第6行** 更新鏈結串列的開頭 - **第8-9行** 更新鏈結串列的尾端。這邊之所以不從q->head 開始找而是從 q->tail 是因為有機會能`節省更新時間`。因為經過排序後,在最壞的情況下 q->tail 指向鏈結串列的開頭,此時就等於從 q->head 找。也就是說,q->tail 最爛的情況跟 q->head 一樣,所以平均來看,從 q->tail 會比從 q->head 找還來的快! ```c= void q_sort(queue_t *q) { if (!q || !q->head || q->size == 1) return; q->head = mergeSort(q->head); // update q->tail while (q->tail->next) q->tail = q->tail->next; } ``` - **`mergeSort`** - **第3-4行** 遞迴的邊界條件,(i.e. 此鏈結串列只有一個或沒有元素時)。 - **第7-18行** 如同上面所說,Merge Sort 會將鏈結串列`對半切`,此段落就是在做這件事 > 我原本用最徒法煉鋼的方法找中間節點,後來在網路上看到這個如此`優美`、`有品味`的作法,因而改成現在你現在看到的這個 - 它的想法就是一人每次走兩步,另一人走一步。當走兩步的人到尾端,則走一步的人就會在中間 - **第21-22行** 切半之後,分別遞迴呼叫 `mergeSort` - **第24行** 最後在呼叫 `merge` 將兩已排序的鏈結串列做合併 ```c= list_ele_t *mergeSort(list_ele_t *head) { if (!head || !head->next) return head; // divide into two halves list_ele_t *twoStep = head->next; list_ele_t *oneStep = head; while (twoStep) { twoStep = twoStep->next; if (twoStep) { twoStep = twoStep->next; oneStep = oneStep->next; } } twoStep = oneStep->next; oneStep->next = NULL; // recursively call mergeSort list_ele_t *ll = mergeSort(head); list_ele_t *rl = mergeSort(twoStep); // merge two ordered lists return merge(ll, rl); } ``` - **`merge`** - 合併兩已排序的字串 - merge 有`遞迴`跟`迭代`兩種方法,因為如果用遞迴去寫,在 AddressSanitizer 分析下顯示 `stack-overflow`,因此後來乾脆改寫成迭代的方法 - **第7-12行** 當只剩`其中一邊`的鏈結串列還有元素,將它直接加入到 `res` - **第14-17行** 去比較兩字串的字母順序 (strcmp),再呼叫 `MoveNode` 函式將擁有字母順序較小的字串加到 `res` ```c= list_ele_t *merge(list_ele_t *ll, list_ele_t *rl) { list_ele_t *res = NULL; list_ele_t **indirect = &res; while (1) { if (!ll) { *indirect = rl; break; } else if (!rl) { *indirect = ll; break; } if (strcmp(ll->value, rl->value) <= 0) MoveNode(indirect, &ll); else MoveNode(indirect, &rl); indirect = &((*indirect)->next); } return res; } ``` ```c= void MoveNode(list_ele_t **dest, list_ele_t **src) { list_ele_t *tmp = *src; *src = tmp->next; // src sublist goes forward tmp->next = *dest; *dest = tmp; } ``` ## 執行自動評分程式 1. 先編譯程式碼,產生測試程式 `qtest` ```shell $ make ``` 2. 執行自動評分程式 ```shell $ make test ``` 也可以查看 `qtest` 所使用的例子 ```shell $ make check ``` ## 除錯(記憶體方面) - 使用 `make SANITIZER=1` 去跑 `./qtest -f traces/trace-17-complexity.cmd` 時,發現出現以下有關記憶體的錯誤訊息: ```shell ==13138==ERROR: AddressSanitizer: global-buffer-overflow on address 0x55bcd42fe9e0 at pc 0x55bcd42ed9e5 bp 0x7ffe90de2710 sp 0x7ffe90de2700 READ of size 4 at 0x55bcd42fe9e0 thread T0 #0 0x55bcd42ed9e4 in do_option_cmd /home/jimmy/linux2020/lab0-c/console.c:368 #1 0x55bcd42ec572 in interpret_cmda /home/jimmy/linux2020/lab0-c/console.c:220 #2 0x55bcd42ecfb9 in interpret_cmd /home/jimmy/linux2020/lab0-c/console.c:243 #3 0x55bcd42edc64 in cmd_select /home/jimmy/linux2020/lab0-c/console.c:569 #4 0x55bcd42ee032 in run_console /home/jimmy/linux2020/lab0-c/console.c:628 #5 0x55bcd42eb0ad in main /home/jimmy/linux2020/lab0-c/qtest.c:772 #6 0x7fb8881120b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2) #7 0x55bcd42e888d in _start (/home/jimmy/linux2020/lab0-c/qtest+0x788d)` 0x55bcd42fe9e1 is located 0 bytes to the right of global variable 'simulation' defined in 'console.c:20:6' (0x55bcd42fe9e0) of size 1 'simulation' is ascii string '' SUMMARY: AddressSanitizer: global-buffer-overflow /home/jimmy/linux2020/lab0-c/console.c:368 in do_option_cmd Shadow bytes around the buggy address: 0x0ab81a857ce0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0ab81a857cf0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0ab81a857d00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0ab81a857d10: f9 f9 f9 f9 00 f9 f9 f9 f9 f9 f9 f9 00 f9 f9 f9 0x0ab81a857d20: f9 f9 f9 f9 00 f9 f9 f9 f9 f9 f9 f9 00 f9 f9 f9 =>0x0ab81a857d30: f9 f9 f9 f9 00 f9 f9 f9 f9 f9 f9 f9[01]f9 f9 f9 0x0ab81a857d40: f9 f9 f9 f9 00 00 00 00 01 f9 f9 f9 f9 f9 f9 f9 0x0ab81a857d50: 04 f9 f9 f9 f9 f9 f9 f9 00 00 00 00 00 00 00 00 0x0ab81a857d60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0ab81a857d70: 00 f9 f9 f9 f9 f9 f9 f9 01 f9 f9 f9 f9 f9 f9 f9 0x0ab81a857d80: 01 f9 f9 f9 f9 f9 f9 f9 04 f9 f9 f9 f9 f9 f9 f9 Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb Shadow gap: cc ==13138==ABORTING ``` 根據它給的提示 `READ of size 4 at thread T0`,我去檢查 `console.c` 的第 368 行 ```c= int oldval = *plist->valp; ``` 因為提示給 `READ`,而這行會執行讀的動作就只有 deference plist->valp 這個指標而已,因此我就朝這個方向走。在`console.c` 的第 365 行 ```c= param_ptr plist = param_list; ``` `plist` 會指向 `param_list` 這個全域變數指的空間,所以也就是說,`param_list` 指的這個空間有問題。 接下來一一去看所有會改動到 `param_list` 所指的空間的函式。發現在`add_param` 函式中也有 `int *valp` 這個參數,而且只有 `init_cmd` 這個函式會呼叫 `add_param` 函式,因此我就在 `init_cmd` 這個函式中發現了下面的問題。 這是`add_param`函式的 prototype: ```c= void add_param(char *name, int *valp, char *doccumentation, setter_function setter); ``` 他的 valp 會指向一個 int 空間的位址,但在 `init_cmd` 函式中傳入的卻是 ```c= add_param("simulation", (int *) &simulation, "Start/Stop simulation mode",NULL); ``` 強制從 bool 轉型到 int 的 `(int *) &simulation`,所以才會造成 `*plist->valp` 會 overflow。同理,下面 echo 的傳入值 `(int *) &echo` 也會造成 overflow。最後,將 `simulation` 跟 `echo` 都改成 `int` 就可以解決了! ## Git相關 ### 說明 (有任何錯誤的地方還請指教) 這次作業讓我有機會去學習如何去使用 git。Git 是一個版本控制系統,簡單來說就是能紀錄檔案的修改內容、修改者、修改時間...等資訊,不管是讓自己或是其他合作的人能清楚了解檔案的來龍去脈,而 Github只是使用 Git 為核心基礎的雲端服務平台。 還有一些 GUI 工具來讓我們更了解或是更方便使用 git,像是 [SourceTree](https://www.sourcetreeapp.com/) 跟 [GitHub Desktop](https://desktop.github.com/) ![工作目錄、暫存區、儲存庫](https://gitbook.tw/images/tw/using-git/working-staging-and-repository/all-states.png) 說明 git add 跟 git commit 的作用 ### 指令 以下是我在這個作業使用到的 git 指令: - 把修改過的檔案從工作目錄加入到暫存區 ```shell $ git add [Filename] ``` - 把修改過的檔案從暫存區加入到儲存庫,並記錄 commit message ([如何寫一個 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/)) ```shell $ git commit -a ``` - 將本機 repository 和遠端 Github repository 同步 - 打開空的 repository,Github 會指示你如何將本機 repository 的 remote 設定為遠端的 repository 目錄,並輸入以下命令,即可連線並把目前的 repository 同步到 Github 上面了。 ```shell $ git remote add origin git@github.com:你的帳號名稱/你的專案名稱.git $ git push -u origin master ``` - 將 Commit 送出去 - 將本地端的 master branch 與遠端的 master branch 作 fast-forward 合併 - 如果出現 [rejected] 錯誤的話,表示你必須先作 pull。 ```shell $ git push ``` - 從遠端更新 - 先 git fetch 遠端的 branch,然後與本地端的 branch 做 merge,產生一個 merge commit 節點 ```shell $ git pull ``` - clone 其他使用者專案 ```shell $ git clone https://github.com/Username/repository.git ```