# 2021q1 Homework1 (lab0) contributed by < `ChenBoSyun` > ###### tags: `linux2021` ## 作業要求 在 `queue.[h/c]` 完成以下要求 * `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`: 以==遞增順序==來排序鏈結串列的元素 除了實作上述的功能外,還要考慮以下記憶體檢測的議題 1. 先執行 qtest 再於命令提示列輸入 help 命令,會使 開啟 Address Sanitizer 觸發錯誤,應予以排除 2. 運用 Valgrind 排除 qtest 實作的記憶體錯誤,並透過 Massif 視覺化 “simulation” 過程中的記憶體使用量,需要設計對應的實驗 ## 實作細節 ### queue.h * 新增 `list_ele_t *tail` 與 `unsigned int size` ,為了讓 `q_insert_tail()` 與 `q_size()` 能在O(1) 時間內完成 ```c /* Queue structure */ typedef struct { list_ele_t *head; /* Linked list of elements */ list_ele_t *tail; unsigned int size; } queue_t; ``` ### queue.c #### q_new * 檢查是否有正常的配置記憶體 (`malloc` 回傳值不是 `NULL`) ```c queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); if (q == NULL) { return NULL; } q->head = NULL; q->tail = NULL; q->size = 0; return q; } ``` :::warning 可調整程式碼風格如下: ```cpp queue_t *q = malloc(sizeof(queue_t)); if (!q) return NULL ``` 與之相似,`if (q != NULL)` 可簡化為 `if (q)` :notes: jserv ::: #### q_free * 避免目前的 `list_ele_t *curr` 被 `free` 後,無法取得下一個 `list_ele_t`,記得先 將 `curr->next` 指派給 `tmp` ```c void q_free(queue_t *q) { if (q == NULL) { return; } list_ele_t *curr = q->head; list_ele_t *tmp; while (curr != NULL) { tmp = curr->next; free(curr->value); free(curr); curr = tmp; } free(q); return; } ``` :::info 問題: 是否需要指派 `NULL` 給 `queue_t*`,避免 dangling pointer ```c void q_free(queue_t **q) { if (*q == NULL) { return; } list_ele_t *curr = (*q)->head; list_ele_t *tmp; while (curr != NULL) { tmp = curr->next; free(curr->value); free(curr); curr = tmp; } free(*q); *q = NULL; return; } ``` > 你可用設計實驗並用 ASan 檢測,看會不會遇到 use-after-free 的警告 > :notes: jserv ::: #### q_insert_head * 當配置記憶體失敗時,在 `function return` 之前記得把已經配置過的記憶體 `free` 掉 * 配置記憶體給 string buffer 時,要注意 `strlen()` 在計算字串長度時不會包含空字元(`'\0'`),記得還要再加一個 byte 存放 `'\0'` > The strlen() function calculates the length of the string pointed to by s, excluding the terminating null byte ('\0'). - [man strlen](https://man7.org/linux/man-pages/man3/strlen.3.html) ```c bool q_insert_head(queue_t *q, char *s) { list_ele_t *newh; if (q == NULL) { return false; } newh = malloc(sizeof(list_ele_t)); if (newh == NULL) { return false; } newh->value = malloc(sizeof(char) * (strlen(s) + 1)); if (newh->value == NULL) { free(newh); return false; } memcpy(newh->value, s, strlen(s)); *(newh->value + strlen(s)) = '\0'; newh->next = q->head; q->head = newh; q->size++; if (q->size == 1) { q->tail = newh; } return true; } ``` #### q_insert_tail * 為了讓 `q_insert_tail` 在 O(1) 時間內完成,在 `queue_t` 內加上 `tail` 讓它指向佇列的最後一個元素 ```c bool q_insert_tail(queue_t *q, char *s) { if (q == NULL) { return false; } list_ele_t *newt; newt = malloc(sizeof(list_ele_t)); if (newt == NULL) { return false; } newt->value = malloc(sizeof(char) * (strlen(s) + 1)); if (newt->value == NULL) { free(newt); return false; } memcpy(newt->value, s, strlen(s)); *(newt->value + strlen(s)) = '\0'; newt->next = NULL; q->tail->next = newt; q->tail = newt; q->size++; return true; } ``` #### q_remove_head ```c bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (q == NULL || q->head == NULL) { return false; } char *str = q->head->value; if (sp != NULL) { if (strlen(str) > bufsize - 1) { memcpy(sp, str, bufsize - 1); *(sp + bufsize - 1) = '\0'; } else { memcpy(sp, str, strlen(str)); *(sp + strlen(str)) = '\0'; } } free(str); list_ele_t *tmp = q->head; q->head = q->head->next; free(tmp); q->size--; return true; } ``` #### q_size ```c int q_size(queue_t *q) { if (q == NULL || q->head == NULL) { return 0; } return q->size; } ``` #### q_reverse * 在反轉之前,先將 head 指派給 tail;因為反轉後的 tail 和 head 順序會顛倒 * 迴圈內,將 curr 指向 prev 時;避免找不到 next,要先將 next 保存下來 ``` void q_reverse(queue_t *q) { if (q == NULL || q->head == NULL) { return; } q->tail = q->head; list_ele_t *prev = NULL; list_ele_t *curr = q->head; list_ele_t *next; while (curr != NULL) { next = curr->next; curr->next = prev; prev = curr; curr = next; } q->head = prev; return; } ``` #### q_sort * 參考 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/),使用 O(nlogn) 的 merge sort * `merge()`: left 跟 right 是已排序狀態,因此只要將 left right 兩個佇列的元素兩兩 (strcmp) 比較再串接即可 ::: info 我原先沒搞清楚 `strcmp` 的回傳值代表的意思,從 man strcmp 的 DESCRIPTION 也沒有說明回傳值的細節 > strcmp() returns an integer indicating the result of the comparison, as follows: • 0, if the s1 and s2 are equal; • a negative value if s1 is less than s2; • a positive value if s1 is greater than s2. 參照 strcmp.c 原始碼,發現strcmp是照順序比較 str1 str2 的字元大小, 若兩個字元一樣,則繼續比較下一組字元。 ```c int STRCMP (const char *p1, const char *p2) { const unsigned char *s1 = (const unsigned char *) p1; const unsigned char *s2 = (const unsigned char *) p2; unsigned char c1, c2; do { c1 = (unsigned char) *s1++; c2 = (unsigned char) *s2++; if (c1 == '\0') return c1 - c2; } while (c1 == c2); return c1 - c2; } ``` 值得注意的是,我在看原始碼時注意到 ```c if (c1 == '\0') return c1 - c2; ``` 當下我覺得為何只針對 c1 檢查 `'\0'`,後來推測是避免當 str1 str2 所有字元一樣時,可能造成 buffer overflow 問題 (`*s1++` 超過字串的邊界),且程式與預期的結果會不符 這邊的檢查換成 `if (c2 == '\0')` 也是可以的 ::: :::info 一開始實作 merge two sorted linked list,使用的是遞迴的作法 ```cpp list_ele_t *merge(list_ele_t *left, list_ele_t *right) { if (left == NULL) { return right; } if (right == NULL) { return left; } if (strcmp(left->value, right->value) < 0) { left->next = merge(left->next, right); return left; } else { right->next = merge(left, right->next); return right; } } ``` 執行測資 trace-15-perf.cmd,會出現 segmentation fault。 參考 [RinHizakura](https://hackmd.io/@RinHizakura/ByuY_q6AU#Segmentation-fault) 的筆記,發現有可能是因為頻繁使用遞迴導致 stack overflow > 我使用 make SANITIZER=1,trace-15-perf.cmd 沒有輸出任何訊息 ::: * `merge_list()`: 實作將 linked list 分成 left 和 right,在 `merge_list()` 的最後會呼叫 `merge()`,因此 `merge_list()` 回傳的佇列是已排序的 ``` list_ele_t *left = merge_list(head); list_ele_t *right = merge_list(fast); ``` ```c list_ele_t *merge(list_ele_t *left, list_ele_t *right) { list_ele_t dummy; dummy.next = NULL; list_ele_t *curr = &dummy; while (left != NULL && right != NULL) { if (strcmp(left->value, right->value) < 0) { curr->next = left; curr = curr->next; left = left->next; } else { curr->next = right; curr = curr->next; right = right->next; } } if (left == NULL) { curr->next = right; } if (right == NULL) { curr->next = left; } return dummy.next; } list_ele_t *merge_list(list_ele_t *head) { if (head == NULL || head->next == NULL) { return head; } list_ele_t *fast = head->next; list_ele_t *slow = head; while (fast != NULL && fast->next != NULL) { slow = slow->next; fast = fast->next->next; } fast = slow->next; slow->next = NULL; list_ele_t *left = merge_list(head); list_ele_t *right = merge_list(fast); return merge(left, right); } /* * 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 == NULL || q->head == NULL) { return; } q->head = merge_list(q->head); list_ele_t *curr = q->head; while (curr && curr->next) { curr = curr->next; } q->tail = curr; return; } ``` ## 透過 Address Sanitizer 修正,呼叫 help 命令時所造成的記憶體錯誤 開啟 Address Sanitizer 後,執行 help 命令會出現以下錯誤訊息 ``` ==8599==ERROR: AddressSanitizer: global-buffer-overflow on address 0x560ff3ffa3c0 at pc 0x560ff3fe37bd bp 0x7ffdfe9b4850 sp 0x7ffdfe9b4840 READ of size 4 at 0x560ff3ffa3c0 thread T0 #0 0x560ff3fe37bc in do_help_cmd /home/old-cat/linux2021/lab0-c/console.c:307 #1 0x560ff3fe38d0 in interpret_cmda /home/old-cat/linux2021/lab0-c/console.c:221 #2 0x560ff3fe40b5 in interpret_cmd /home/old-cat/linux2021/lab0-c/console.c:244 #3 0x560ff3fe57f8 in run_console /home/old-cat/linux2021/lab0-c/console.c:660 #4 0x560ff3fe23e5 in main /home/old-cat/linux2021/lab0-c/qtest.c:780 #5 0x7f38d471b0b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2) #6 0x560ff3fdfb8d in _start (/home/old-cat/linux2021/lab0-c/qtest+0x8b8d) 0x560ff3ffa3c1 is located 0 bytes to the right of global variable 'echo' defined in 'console.c:59:13' (0x560ff3ffa3c0) of size 1 ``` 從錯誤訊息中得知該程式出現 global-buffer-overflow global-buffer-overflow 是因為存取全域變數時,超過系統配置給你的記憶體範圍時,所產生的記憶體錯誤 追蹤程式碼發現,宣告全域變數 echo 是 bool 型態 ```c static bool echo = 0; ``` 呼叫 `add_param` 時會將 `&echo` 轉型成 `(int *)` ,但這裡還不會出現問題,因為並沒有存取記憶體的動作 ```c add_param("echo", (int *) &echo, "Do/don't echo commands", NULL); ``` 在 `do_help_cmd` 會呼叫以下程式,`%d` 這個格式化輸出會讀取一個 int 的大小(4 bytes) 這樣會超過原本配置的(1 bytes)的記憶體空間 ```c report(1, "\t%s\t%d\t%s", plist->name, *plist->valp, plist->documentation); ``` 修正方法: 將 `echo` 宣告成 `int` 型態,同理 `simulation` 也是。