--- tags: jserv-Linux --- # 2021q1 Homework1 (lab0) contributed by < `jw910731` > # 環境設定 我用的是Arch Linux,在這邊紀錄遇到的雷 1. aspell 沒有字典 - 需要額外安裝 `aspell-en` 來下載英文字典 2. clang-format 找不到套件 - 在 Arch Linux 裡面的 `clang-format` 指令是放在 `clang` 套件裡面,安裝即可 此外的套件名稱以我的印象都是一樣,不然就是名稱改動甚小,隨意搜尋馬上就可找到 # 基礎作業實作 - 基礎的Linked List based Queue ## 進度 - [x] new - [x] free - [x] insert_head - [x] Implement - [x] Test - [x] insert_tail - [x] Implement - [x] Test - [x] remove_head - [x] size - [x] reverse - [ ] sort - [x] Mental Implementation - [ ] Implementation - [ ] Test ## Queue struct ```diff /* Queue structure */ typedef struct { - list_ele_t *head; /* Linked list of elements */ + list_ele_t *head, *tail; /* Linked list of elements */ + int size; } queue_t; ``` ## Helper Functions 本人~~生性健忘~~,所以需要 Helper Function 統一重複操作 - ele_create ```cpp= /* * Create New Node * Return false if malloc failed * Otherwise return true */ static inline bool ele_create(list_ele_t **newh, char *s) { *newh = malloc(sizeof(list_ele_t)); // return false if malloc failed if (*newh == NULL) return false; // copy string content size_t len = strlen(s) + 1; // count in additional space for c string null terminator // malloc string space (*newh)->value = malloc(len); if ((*newh)->value == NULL) { // when malloc for string failed // prevent memory leak on failed memory allocation free(*newh); return false; } // copy string content strncpy((*newh)->value, s, len); // initialize fields (*newh)->next = NULL; return true; } ``` - ele_delete ```cpp= /* * Delete a node * Passed pointer is advance to next node automatically * Passed pointer must not be null nor pointed to NULL * Otherwise, the result is not guaranteed */ static inline void ele_delete(list_ele_t **del) { free((*del)->value); // free string list_ele_t *tmp = (*del)->next; // save pointer for next node free(*del); *del = tmp; } ``` ## new / free - **new** :::spoiler **Code** ```cpp= queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); // when allocation failed return NULL if (q == NULL) return NULL; q->head = NULL; q->tail = NULL; q->size = 0; return q; } ``` ::: - **free** :::spoiler **Code** ```cpp= void q_free(queue_t *q) { // free iterated through lists if (q != NULL) { for (list_ele_t *it = q->head; it != NULL;) { ele_delete(&it); } } /* Free queue structure */ free(q); } ``` ::: `new` 基本上只要把每個要使用的 properties 初始化過就好,特別注意 `malloc` 失敗時的狀況就好 `free` 稍微麻煩一些,但基本上要注意的不外乎是 1. 參數為NULL 特判掉 2. 走訪 Linked List 走過去就 `free` 掉 ## insert_head / insert_tail - **insert_head** :::spoiler **Code** ```cpp= bool q_insert_head(queue_t *q, char *s) { list_ele_t *newh; // when q is NULL return false if (q == NULL) return false; // handle node creation failed if (!ele_create(&newh, s)) { return false; } newh->next = q->head; q->head = newh; // correctly initialize tail when is NULL if (q->tail == NULL) q->tail = q->head; // maintain size ++(q->size); return true; } ``` ::: - **insert_tail** :::spoiler **Code** ```cpp= bool q_insert_tail(queue_t *q, char *s) { list_ele_t *newh; // when q is NULL return false if (q == NULL) return false; // if create node fail, return false if (!ele_create(&newh, s)) { return false; } // concat to existed list // prevent empty list if (q->tail != NULL) { // concat node to current list q->tail->next = newh; } else { // is the first element in list q->head = newh; } // set tail to new node q->tail = newh; // maintain size ++(q->size); return true; } ``` ::: 主要還是要注意參數為 `NULL` 的狀況與 `malloc` 爛掉的狀況 此外就是分配空件與使用 `strncpy` 時要考慮到 C String NULL Terminator 所佔據的空間 我因此直接將內部計算好的字串長度(實作為 `strlen` 的回傳值)直接加 1 ### insert_head 為了在 Queue 為空時可以好好維護 `tail` ,因此特判為空的狀況,有想過有沒有比較優雅的解,但好想睡所以之後再想辦法 ### insert_tail 基本上如法泡製 `insert_head` 的實作後,只有少數幾個點要注意。當整個 Queue 為空的時候,應該要在此時額外維護 `head` 的狀態。 :::danger 目前遇到自動檢測程式對於我的`insert_tail`的實作是不是常數時間複雜度的測試,給出的答案並不穩定,目前還沒什麼頭緒為什麼會爛掉 ::: ## reverse :::spoiler **Code** ```cpp= void q_reverse(queue_t *q) { // let the q_size do the null check if (q_size(q) <= 1) return; list_ele_t *prev = NULL, *tail = q->tail; q->tail = q->head; for (list_ele_t *it = q->head; it != NULL;) { // forward iterator temp list_ele_t *tmp = it->next; // reverse linkage it->next = prev; prev = it; // advance prev it = tmp; // advance iterator } q->head = tail; } ``` ::: 基本上是直觀的 in-place reverse 實作 額外要注意的是要好好更新 `head` 與 `tail` 指標,不然之後的存取操作可能出錯! ## sort 目前打算要以 Merge Sort 來實作,過程不用複製,且每個函數的區域變數極少,推估空間複雜度 ( 主要是 Stack 花費 ) 可到 $\mathcal{O}(\log n)$ **尚未實作,只完成精神實作而已** # 抓蟲 ## 使用 ASan 尋找傳說中的 Invalid Memory Access 先使用 `make SANITIZER=1` 召喚 ASan 現身,然後我就得到了 ``` ==28994==ERROR: AddressSanitizer: global-buffer-overflow on address 0x560f4b4bb6c0 at pc 0x560f4b4a50ca bp 0x7ffffe755390 sp 0x7ffffe755380 READ of size 4 at 0x560f4b4bb6c0 thread T0 #0 0x560f4b4a50c9 in do_help_cmd /lab0-c/console.c:307 #1 0x560f4b4a51dd in interpret_cmda /lab0-c/console.c:221 #2 0x560f4b4a59ab in interpret_cmd /lab0-c/console.c:244 #3 0x560f4b4a7082 in run_console /lab0-c/console.c:660 #4 0x560f4b4a3d82 in main /lab0-c/qtest.c:780 #5 0x7f6f2d5c8b24 in __libc_start_main (/usr/lib/libc.so.6+0x27b24) #6 0x560f4b4a15cd in _start (/lab0-c/qtest+0x85cd) 0x560f4b4bb6c1 is located 0 bytes to the right of global variable 'echo' defined in 'console.c:59:13' (0x560f4b4bb6c0) of size 1 SUMMARY: AddressSanitizer: global-buffer-overflow /lab0-c/console.c:307 in do_help_cmd ``` 然後來抓個關鍵字 "global buffer overflow" 告訴我們這個爛掉的記憶體存取是來自全域變數,且存取點是在 `console.c` 的第 307 行 然後 "READ of size 4" 告訴我他**可能**把他當 `int` 存取,但爛掉了! 然後找找 307 做了什麼 ```cpp=296 static bool do_help_cmd(int argc, char *argv[]) { cmd_ptr clist = cmd_list; report(1, "Commands:", argv[0]); while (clist) { report(1, "\t%s\t%s", clist->name, clist->documentation); clist = clist->next; } param_ptr plist = param_list; report(1, "Options:"); while (plist) { report(1, "\t%s\t%d\t%s", plist->name, *plist->valp, plist->documentation); plist = plist->next; } return true; } ``` 看來是 `*plist->valp` 存取爛掉了呢 (整行就他一個整數存取,或精確而言的 4 byte read access) 然後就回溯回去看是誰的錯吧! 使用神奇 CLion 幫我找到 `plist` 的寫入點,會找到 `add_param` 這個函數是唯一的寫入點,以下是節錄的函數 prototype : > 其實這個功能gdb也有, 名字叫做hardware watchpoint(也有人說這叫hardware breakpoint), 相關操作可以在[這裡](https://sourceware.org/gdb/onlinedocs/gdb/Set-Watchpoints.html#Set-Watchpoints)看看 > [name=asef18766] ```cpp=134 void add_param(char *name, int *valp, char *documentation, setter_function setter) { // 略 ele->valp = valp; // 依舊略 } ``` 再溯源到 `add_param` 的 invoker 會找到所有呼叫者都在 `init_cmd` 裡面,其中的 108 行就是我們要找的問題了! ```cpp=88 void init_cmd() { cmd_list = NULL; param_list = NULL; err_cnt = 0; quit_flag = false; add_cmd("help", do_help_cmd, " | Show documentation"); add_cmd("option", do_option_cmd, " [name val] | Display or set options"); add_cmd("quit", do_quit_cmd, " | Exit program"); add_cmd("source", do_source_cmd, " file | Read commands from source file"); add_cmd("log", do_log_cmd, " file | Copy output to file"); add_cmd("time", do_time_cmd, " cmd arg ... | Time command execution"); add_cmd("#", do_comment_cmd, " ... | Display comment"); add_param("simulation", (int *) &simulation, "Start/Stop simulation mode", NULL); add_param("verbose", &verblevel, "Verbosity level", NULL); add_param("error", &err_limit, "Number of errors until exit", NULL); add_param("echo", (int *) &echo, "Do/don't echo commands", NULL); init_in(); init_time(&last_time); first_time = last_time; } ``` 我們會發現 108 行裡面 ```cpp=108 add_param("echo", (int *) &echo, "Do/don't echo commands", NULL); ``` 一個 bool 變數被取指標後,指標被轉型成 `int`,但 `int` 的長度是 4 個 byte,但 bool 卻是 1 byte ( 在我的機器上是這樣,前面對行別長度的描述不是 portable 的 ) `int` 並不保證與 `bool` 型態等長,所以會導致對這個轉型後的指標 ( `(int *) &echo` ) 進行操作時導致記憶體存取錯誤! 要順便注意其實 `(int *) &simulation` 也有剛才上述的問題,也要記得一併清除,同時因為 `simulation` 是一個 exported symbol ,所以還要注意需要對 header 做對應的修改。