# 2024q1 Homework1 (lab0) contributed by < `syuan1017` > ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 44 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 16 On-line CPU(s) list: 0-15 Vendor ID: AuthenticAMD Model name: AMD Ryzen 7 4800H with Radeon Graphics CPU family: 23 Model: 96 Thread(s) per core: 2 Core(s) per socket: 8 Socket(s): 1 Stepping: 1 Frequency boost: enabled CPU max MHz: 2900.0000 CPU min MHz: 1400.0000 BogoMIPS: 5789.61 Virtualization features: Virtualization: AMD-V Caches (sum of all): L1d: 256 KiB (8 instances) L1i: 256 KiB (8 instances) L2: 4 MiB (8 instances) L3: 8 MiB (2 instances) NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-15 ``` ### 安裝必要開發套件 ```shell $ sudo apt install build-essential git-core valgrind $ sudo apt install cppcheck clang-format aspell colordiff ``` | 套件 | 描述 | | ------ | ----------- | | build-essential | meta-package | | git-core | version control system package | | valgrind | dynamic Binary Instrumentation (DBI) framwork| | cppcheck | static analysis tool for C/C++ code | | clang-format | automatically formatting C, C++, Objective-C, and Objective-C++ code | | aspell | spell Checking | | colordiff | enhances the output of the diff tool | ## 研讀過程 :::warning 補上對應的超連結 ::: 根據 [The Linux Kernel API - List Management Functionse](https://www.kernel.org/doc/html/latest/core-api/kernel-api.html),list_del_init_careful() 與 list_empty_careful 能夠保證記憶體操作順序,但對於其中 smp_store_release() 的操作不了解 ```c static inline void list_del_init_careful(struct list_head *entry) { __list_del_entry(entry); WRITE_ONCE(entry->prev, entry); smp_store_release(&entry->next, entry); } ``` ## 指定佇列操作的實作 ### `q_new` 根據 C99 規格書,malloc 函式的返回值有可能是 null pointer,因此必須先檢查返回值是否為 null pointer 在進行後續操作 * C99 [7.20.3.3-3] The malloc function returns either a null pointer or a pointer to the allocated space. ### `q_free` 在執行 make check 過程中出現以下錯誤訊息 > Segmentation fault occurred. You dereferenced a NULL or invalid pointermake: *** [Makefile:57: check] Aborted (core dumped) 嘗試開啟 SANITIZER 做進一部分析錯誤原因,並找出錯誤的程式碼位置,最後透過 gdb 觀察發現 `element_t *ele` 所指向的位置已經執行 free,因此 `cur->next` 就會存取非法記憶體位置,此處修改為取得當前節點的結構體位置後,就將指標 `cur` 指向前一個節點 > SUMMARY: AddressSanitizer: heap-use-after-free /home/alan/linux2024/lab0-c/queue.c:33 in q_free ```diff void q_free(struct list_head *head) { struct list_head *cur; if (!head) return; list_for_each(cur, head) { element_t *ele; list_del(cur); ele = list_entry(cur, element_t, list); + cur = cur->prev; free(ele->value); free(ele); } free(head); } ``` :::danger 使用 `git diff` 或 `diff -u` 命令來產生程式碼之間的變更列表,避免憑感覺填入,注意位移量。 ::: 參考 `list.h` 後發現巨集 `list_for_each_safe(node, safe, head)` 透過預先紀錄下一個節點,來達成在走訪鏈結串列的過程中允許刪除當前節點,因此將程式碼進行修正 ```diff void q_free(struct list_head *head) { - struct list_head *cur; + struct list_head *cur, *next; if (!head) return; - list_for_each (cur, head) { - element_t *ele; + list_for_each_safe (cur, next, head) { + element_t *ele = list_entry(cur, element_t, list); list_del(cur); - ele = list_entry(cur, element_t, list); - cur = cur->prev; free(ele->value); free(ele); } ``` ### `q_insert_head` \ `q_insert_tail` 在實作此兩函式是中透過 strdup 來將輸入的字串複製到節點內 ```c bool q_insert_head(struct list_head *head, char *s) { element_t *ele = malloc(sizeof(element_t)); if (!ele) return false; /* Duplicate of the string s */ ele->value = strdup(s); if (!ele->value) { free(ele); return false; } list_add(&ele->list, head); return true; } ``` 根據 man page 的 `strdup(3)` 中說明: > The `strdup() `function returns a pointer to a new string which is a duplicate of the string s. Memory for the new string is obtained with malloc(3), and can be freed with free(3). 其中是透過 `malloc(3)` 來取得新的空間,因此必須先檢查返回值是否為 null pointer 在進行後續操作 ### `q_remove_head` \ `q_remove_tail` 分別透過巨集 `list_first_entry` 和 `list_last_entry` 來取得欲移除的節點,根據 C99 規格書說明,若陣列 s2 長度小於 n,會以 null 字元將剩餘的長度填滿,因此此處不再額外判斷節點內所儲存的字串長度 (C99 [7.21.2.4-3]) > If the array pointed to by s2 is a string that is shorter than n characters, null characters are appended to the copy in the array pointed to by s1, until n characters in all have been written. ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *ele; ele = list_first_entry(head, element_t, list); if (sp) strncpy(sp, ele->value, bufsize - 1); list_del(&ele->list); return ele; } ``` 原本預期透過 `strncpy` 來複製字串,若`ele->value` 的長度小於 `bufsize - 1` 會自動補滿空字元,但透過 make test 發現會有錯誤,會將整個包括後面的 'x' 全部印出來,因此修正為複製完字串後再加上空字元 ```diff - if (sp) - strncpy(sp, ele->value, bufsize - 1); + if (sp) { + size_t copy_size = + strlen(ele->value) > bufsize - 1 ? bufsize - 1 : strlen(ele->value); + strncpy(sp, ele->value, copy_size); + sp[copy_size] = '\0'; + } ``` ### `q_size` 觀察到 `q_size()` 會被 `do_size()`, `do_sort()`, `do_ascend()`, `do_descend()` 等函式所呼叫,而在上述函式中就會檢查該佇列是否有建立過,因此不再重複檢查,此處透過巨集 `list_for_each` 走訪整個佇列並計算節點總量。 ### `q_delete_mid` 參考 [LeetCode 2095. Delete the Middle Node of a Linked List](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/description/) ,此處採用快慢指標來取得 middle node > TODO: 思考是否有效率更佳的作法 ```c bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) return false; struct list_head *fast = head->next; struct list_head *slow = head->next; while ((fast != head) && (fast->next != head)) { fast = fast->next->next; slow = slow->next; } list_del(slow); element_t *ele = list_entry(slow, element_t, list); free(ele->value); free(ele); return true; } ``` ### `q_delete_dup` > [commit 6576b49](https://github.com/sysprog21/lab0-c/commit/6576b493a54f424bfef38602a5bbc67eaf31e40a) 此題對應到 [LeetCode 82. Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/description/) ,主要差別在於 LeetCode 是使用單向鏈結串列實作,而本作業是採用環狀雙向鏈結串列實作,因此不須額外紀錄前一個節點,並搭配 `list_for_each_entry_safe` 來走訪整個佇列,並透過 `list_del` 將節點從鏈結串列中移除並在釋放記憶體空間 ### `q_swap` 此題對應到 [LeetCode 24. Swap Nodes in Pairs](https://leetcode.com/problems/swap-nodes-in-pairs/description/) ,透過 while 迴圈逐一檢查是否有一對節點可以進行置換,並透過 `list_move` 將當前節點移除並插入到下一個節點之後 ```c void q_swap(struct list_head *head) { if (!head) return; struct list_head *cur = head->next; while ((cur != head) && (cur->next != head)) { list_move(cur, cur->next); cur = cur->next; } } ``` ### `q_reverse` > [commit b4aa1b7](https://github.com/sysprog21/lab0-c/commit/b4aa1b73af52058da5844ee2de0259cb2f51bd36) 此題與 [LeetCode 206. Reverse Linked List](https://leetcode.com/problems/reverse-linked-list/description/) 風格相似,而此處實作採用 `list_for_each_safe` 走訪除了最後一個節點外的每個節點,依序將當前節點從鏈結串列中移除,接著再新增到最後一個節點之後 ### `q_reverseK` 此題對應到 [LeetCode 25. Reverse Nodes in k-Group](https://leetcode.com/problems/reverse-nodes-in-k-group/description/),此處透過兩個暫時的鏈結串列 `tmp_head` 與 `revs_head`,在走訪鏈結串列的過程中一次將 K 個節點透過 `list_cut_position` 搬移到 `revs_head` 並反轉鏈結串列,接著在透過 `list_splice_tail` 搬移到 `tmp_head`,剩餘原本鏈結串列中不足 K 個的節點則不作任何動作,最後在透過 `list_splice` 將節點全不搬移回到原本的轉鏈結串 `head` 中 >TODO: >1. 本實作是透過 `list_for_each_safe` 來走訪鏈結串列,而巨集說明有告知當前節點是允許被移除的,但對鏈結串列的其他修改會造成不預期的行為,因此此處須再做確認 ```c void q_reverseK(struct list_head *head, int k) { if (!head) return; LIST_HEAD(tmp_head); LIST_HEAD(revs_head); struct list_head *cur, *next; size_t i = 0; list_for_each_safe (cur, next, head) { if (++i == k) { list_cut_position(&revs_head, head, cur); q_reverse(&revs_head); list_splice_tail(&revs_head, &tmp_head); i = 0; } } list_splice(&tmp_head, head); } ``` ### `q_sort` ### `q_ascend` \ `q_descend` > [commit ac74909](https://github.com/syuan1017/lab0-c/commit/ac7490931ade595c02a99b354d1b931e6917adf6) 此題對應到 [LeetCode 2487. Remove Nodes From Linked List](https://leetcode.com/problems/remove-nodes-from-linked-list/),本實作在走訪鏈結串列的過程中,透過 `ele_r` 和 `ele_l` 來作走訪過程中的比較並刪除對應的節點 ### `q_merge` ## 研讀 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 並嘗試引入到 lab0-c 中