--- tags: linux2023 --- # 2023q1 Homework1 (lab0) contributed by <`DokiDokiPB` > ### 開發環境 ```shell $ gcc --version gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 ``` ```shell $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 4 On-line CPU(s) list: 0-3 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i5-4250U CPU @ 1.30GHz ``` 使用主機: Macbook Air 2014 A1466 (Ubuntu Linux) <!-- 不過很意外作業可以在 MacOS 編譯 --> ## 開發過程 在 ```queue.c``` 中,使用 ```list.h``` 的 ```list_head``` 串接資料,```list.h``` 中具有幾個好用函式與巨集 - 初始化佇列開頭:```INIT_LIST_HEAD``` - 新增單一節點:```list_add```, ```list_add_tail``` - 轉移多個節點至新的 ```head```:```list_splice``` - `node`:表示具有元素( ```element_t``` )的節點 - `list`:具有多個節點的佇列開頭,或是<s>複數串接的節點</s> ??? - `head`:表示佇列開頭 :::warning 改進漢語表達。 :notes: jserv ::: ### q_new ```c struct list_head *q_new() { struct list_head *head = malloc(sizeof(struct list_head)); if (!head) { return NULL; } INIT_LIST_HEAD(head); return head; } ``` 透過 `malloc` 配置 ```head```,依據 ```list_head```,皆為 ```head->next```, ```head->prev``` 指向 ```heaed``` ### q_free ```c void q_free(struct list_head *l) { if (!l) { return; } element_t *e, *safe; list_for_each_entry_safe (e, safe, l, list) { free(e->value); free(e); } free(l); } ``` 在 ```list_for_each_entry_safe``` 中,利用 ```*safe``` 指標保留下個節點的位址,就能將現在操作的節點釋放 ### q_insert_head, q_insert_tail 將節點加入至 ```head``` , ```q_insert_head``` 透過 ```list_add``` 加為首個節點;```q_insert_tail``` 透過 ``` list_add_tail``` 加為末個節點 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head || !s) { return false; } element_t *e = malloc(sizeof(element_t)); char *dup = strdup(s); if (!e) { return false; } if (!dup) { free(e); return false; } e->value = dup; list_add(&e->list, head); return true; } bool q_insert_tail(struct list_head *head, char *s) { // ... list_add_tail(&e->list, head); return true; } ``` ### q_remove_head, q_remove_tail 將指定節點的元素從 ```head``` 移除 ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || !bufsize) { return NULL; } if (list_empty(head)) { return NULL; } struct list_head *node = head->next; element_t *e = container_of(node, element_t, list); size_t len = strlen(e->value); memset(sp, 0, bufsize); if (len > bufsize) { strncpy(sp, e->value, bufsize); sp[bufsize - 1] = '\n'; } else { strncpy(sp, e->value, len); } list_del(node); return e; } element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { // ... struct list_head *node = head->prev; // ... return e; } ``` 依據在 ```queue.h``` 中函式的說明,```bufsize``` 為 ```*sp``` 的陣列長度 - 若 ```bufsize``` 小於元素中的字串長度(變數 ```len``` ),則擷取部分元素字串。 - 若 ```bufsize``` 大於 ```len```,則整段複製到 ```*sp``` :::warning 思考如何精簡程式碼,善用前置處理器。 :notes: jserv ::: ### q_size ```c int q_size(struct list_head *head) { if (!head) { return 0; } int c = 0; struct list_head *node; list_for_each (node, head) { c++; } return c; } ``` 利用 ```list_for_each``` 走過整個佇列,取得元素數量 ### q_delete_mid ```c= bool q_delete_mid(struct list_head *head) { // if (!head) { return false; } if (list_empty(head)) { return false; } struct list_head *slow, *fast; slow = head->next; fast = head->next; while (fast != head && fast->next != head) { slow = slow->next; fast = fast->next->next; } list_del(slow); element_t *e = container_of(slow, element_t, list); free(e->value); free(e); return true; } ``` 依據 [LeetCode 2095. Delete the Middle Node of a Linked List](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/) 以及其圖片的說明 - 當節點數量為奇數時,中間節點為正中間(若有 7 個節點,取第 4 個) ![](https://assets.leetcode.com/uploads/2021/11/16/eg1drawio.png) - 當節點為偶數時,中間節點為中間取後一個(若有 4 個節點,取第 3 個) ![](https://assets.leetcode.com/uploads/2021/11/16/eg2drawio.png) 在第 15 ~ 17 行中,透過一次移動一個節點的 ```slow``` 指標與一次移動兩個節點的 ```fast``` 指標,當 ```fast``` 回到 ```head``` 指標時,```slow``` 指標即在中間節點的位址上。 :::warning 改進程式碼,使其更精簡的程式碼。 :notes: jserv ::: ### q_reverse ```c= void q_reverse(struct list_head *head) { if (!head) { return; } head->prev->next = NULL; struct list_head *node = head->next; struct list_head *cur = head; while (node) { struct list_head *tmp = node->next; cur->prev = node; node->next = cur; node->prev = tmp; cur = node; node = tmp; } cur->prev = head; head->next = cur; } ``` ```反向 = reverse``` :::warning 需要詞彙精準的定義。 :notes: jserv ::: 在第 6 行中,先將 doubly-linked list 尾端元素 ```head->prev->next``` 指派為 ```NULL```,使得在 9 行中能夠脫離 ```while``` 迴圈,表示到尾端處理完畢 - 使用 ```node``` 變數走過每個節點,並將每個節點反向。 - 使用 ```cur ``` 表示 ```node``` 的前一個節點;在第 18 行中表示反向後的第一個節點,串接至 ```head``` <!-- 改天來畫圖 --> ### q_reverseK ```diff void q_reverseK(struct list_head *head, int k) { // https://leetcode.com/problems/reverse-nodes-in-k-group/ if (k <= 1) return; struct list_head *safe, *node, *start = head->next; int c = 0; list_for_each_safe (node, safe, head) { c++; if (c == k) { c = 0; struct list_head *prev = start->prev; LIST_HEAD(sub_head); - sub_head.next = start; - start->prev = &sub_head; - sub_head.prev = node; - node->next = &sub_head; + list_cut_position(&sub_head, start, node); q_reverse(&sub_head); + list_splice(&sub_head, prev); - prev->next = sub_head.next; - sub_head.next->prev = prev; - sub_head.prev->next = safe; - safe->prev = sub_head.prev; start = safe; } } } ``` 每 ```k``` 個節點,將該複數節點加入到 ```sub_head``` 中,透過既有的函式 ```q_reverse``` 處理;再將處理好的 ```sub_head``` 中的節點加回 ```head``` 中 :::warning 看<s>其他同學的實作</s>,傾向直接操作節點。我的實作還是以一個 ```sub_head``` 包裝想要操作的節點。 TODO: 撰寫更精簡的程式碼。參照他人的程式碼時,應標註出處。 :notes: jserv ::: 原本的實作考量 ```list_cut_postition``` 的函式參數名稱都為 ```head_from```, ```head_to```,可能僅適用完成的佇列而非節點。之後,觀摩 [yanjiew](https://hackmd.io/@yanjiew/linux2023q1-lab0#q_reverseK) 後,我發現可以使用 ```list_cut_position``` 去切割 ```head``` 的多個節點,再使用 ```list_splice``` 接回原本的 ```head``` ### q_swap ```c void q_swap(struct list_head *head) { // https://leetcode.com/problems/swap-nodes-in-pairs/ q_reverseK(head, 2); } ``` 在作業說明中, ```q_swap``` 是一種 ```q_reverseK(head, 2);``` 的特例。 ### q_sort :::danger 避免張貼完整程式碼,你應該闡述自己的想法、考量因素,還有相關的策略。程式碼列表僅為輔助性質。 :notes: jserv ::: ```diff void q_sort(struct list_head *head) { // ... // split with middle. struct list_head *slow = head->next, *fast = head->next->next; while (fast != head && fast->next != head) { slow = slow->next; fast = fast->next->next; } LIST_HEAD(left_head); LIST_HEAD(right_head); - struct list_head *right_node = slow->next; - left_head.next = head->next; - left_head.next->prev = &left_head; - left_head.prev = slow; - left_head.prev->next = &left_head; - right_head.next = right_node; - right_head.next->prev = &right_head; - right_head.prev = head->prev; - right_head.prev->next = &right_head; - INIT_LIST_HEAD(head); + list_cut_position(&left_head, head, slow); + list_splice_init(head, &right_head); q_sort(&left_head); q_sort(&right_head); merge_two_lists(&left_head, &right_head, head); } ``` 這裡採用 merge sort,程式碼主體可分為 divide, conquer and merge - Divide: 將 ```head``` 切為左半邊 ```left_head``` 與右半邊 ```right_head``` ,在程式碼中,切割的中間的節點為 ```slow``` - Conquer: 將 ```left_head``` 與 ```right_head``` 分別用 ```q_sort``` 函式處理 - Merge: 合併兩個佇列(以 merge_two_lists 函式實作) 將較小元素先加入至 ```head``` 中,若其中一方沒有元素,即把另一方整個加入至 ```head``` 尾端中 在實作的過程中,可以利用 ```list_cut_position``` 切割左半與右半佇列 ### q_descend ```c int q_descend(struct list_head *head) { int ret = 0; if (!head) return ret; if (list_empty(head)) return ret; ret = 1; LIST_HEAD(less); struct list_head *cur = head->prev, *node = head->prev->prev; while (node != head) { struct list_head *prev = node->prev; element_t *e1 = list_entry(cur, element_t, list); element_t *e2 = list_entry(node, element_t, list); int c = strcmp(e2->value, e1->value); if (c >= 0) { cur = node; ret++; } else { list_move(node, &less); } node = prev; } element_t *e, *safe; list_for_each_entry_safe (e, safe, &less, list) { free(e->value); free(e); } return ret; } ``` 在 [LeetCode 2487. Remove Nodes From Linked List](https://leetcode.com/problems/remove-nodes-from-linked-list/) 以及圖片的說明中 ![](https://assets.leetcode.com/uploads/2022/10/02/drawio.png) ``` 5 -> 2 -> 13 -> 3 -> 8 -->> 13 -> 8 ``` 可以視為由後往前來數的嚴格遞增的佇列。在實作上,也是從後往前的方式移除較小的元素。 - ```less``` 表示要被刪除的節點,先將欲刪除的節點預先加入至 ```less``` 佇列中 - ```cur``` 表示當前最大數值的節點 - ```node``` 表示當前比較的節點,若大於 ```cur``` 的字串,則 ```cur``` 變為 ```node```,```ret``` 累計加 1 ### q_delete_dup ```c bool q_delete_dup(struct list_head *head) { // https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/ if (!head) return false; if (list_is_singular(head)) return true; LIST_HEAD(delete_head); int move_count = 1; struct list_head *cur = head->next; while (cur != head) { struct list_head *node = cur->next; bool isdup = false; while (node != head) { struct list_head *next = node->next; element_t *e1 = list_entry(node, element_t, list); element_t *e2 = list_entry(cur, element_t, list); int c = strcmp(e1->value, e2->value); if (c == 0) { isdup = true; list_move(node, &delete_head); } node = next; } if (isdup) { list_move(cur, &delete_head); } else { move_count += 1; } cur = head; for (int i = 0; i < move_count; i++) cur = cur->next; } element_t *e, *safe; list_for_each_entry_safe (e, safe, &delete_head, list) { free(e->value); free(e); } return true; } ``` 雖然有提供 [LeetCode 23. Merge k Sorted Lists](https://leetcode.com/problems/merge-k-sorted-lists/),根據函式註解說明,並無特別強調排序。 ![](https://assets.leetcode.com/uploads/2021/01/04/linkedlist1.jpg) 在沒有排序的佇列中,每次比較一個節點是否有重複,皆必須走過所有節點確認 - ```cur``` 表示當前的節點 - ```node``` 表示被比較的節點,如果與 ```cur``` 的元素相同,則加入到 ```delete_head``` 中 - 透過 ```delete_head``` 重複的節點刪除。 此程式碼移除重複的節點,憑肉眼觀察也是,不過卻有 ```ERROR: Duplicate strings are in queue or distinct strings are not in queue```,即使使用 ```q_sort``` 也會出現。 🤔🤔🤔 問題待排除 ``` $ ./qtest cmd> new l = [] cmd> it 5 l = [5] cmd> it 4 l = [5 4] cmd> it 3 l = [5 4 3] cmd> it 2 l = [5 4 3 2] cmd> it 1 l = [5 4 3 2 1] cmd> it 5 l = [5 4 3 2 1 5] cmd> it 4 l = [5 4 3 2 1 5 4] cmd> dedup ERROR: Duplicate strings are in queue or distinct strings are not in queue l = [3 2 1] cmd> quit ``` ### q_merge ```c int q_merge(struct list_head *head) { // https://leetcode.com/problems/merge-k-sorted-lists/ return 0; } ``` ~~如果是兩個佇列合併,則只需要 ```merge_two_lists``` 函式,若是多個佇列合併,則需要考量使用 priority queue 紀錄最小值~~ --- ## 排除錯誤以及使用 Valgrind 排除錯誤 #### 錯誤一:在兩個以上的佇列,在命令列使用 `free` 會造成程式進入無窮迴圈 :::danger 避免使用「卡住」這樣不精準的詞彙。 :notes: jserv ::: 遇到了一個錯誤,步驟如下 1. ```./qtest``` 啟動程式 2. ```new``` 兩次,會有兩個不同 ID 的 queue 3. ```free``` 一次會造成程式碼卡住 - 經過測試之後,並不是 ```q_free``` 造成的無限迴圈 - 推測是在某個地方造成無限迴圈 解決思路: - 使用 GDB 命令 ```gdb qtest``` 重現錯誤 - 使用 ```Ctrl+C``` 中斷執行,查看無限迴圈的行數 - 使用 ```where``` 查看 call stack - 使用 ```s``` 逐行檢查卡住的部份 - 發現卡在 ```qtest.c``` 的第 851 ~ 853 行 - 使用 ```p cur```, ```p currnet->q```, ```p currnet->q->next``` 檢查記憶體位址 ``` Reading symbols from qtest... (gdb) run Starting program: /home/g111056119/Documents/lab0-c_2023/qtest [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". cmd> new l = [] cmd> new l = [] cmd> free ^C Program received signal SIGINT, Interrupt. is_circular () at qtest.c:853 853 cur = cur->next; (gdb) where #0 is_circular () at qtest.c:853 #1 q_show (vlevel=vlevel@entry=3) at qtest.c:877 #2 0x0000555555558ab5 in do_free (argc=<optimized out>, argv=<optimized out>) at qtest.c:123 #3 0x0000555555559dfb in interpret_cmda (argc=argc@entry=1, argv=argv@entry=0x555555569070) at console.c:181 #4 0x000055555555a3b0 in interpret_cmd (cmdline=cmdline@entry=0x555555568ae0 "free") at console.c:201 #5 0x000055555555b01a in run_console (infile_name=infile_name@entry=0x0) at console.c:691 #6 0x00005555555591ed in main (argc=1, argv=<optimized out>) at qtest.c:1223 (gdb) p current->q $18 = (struct list_head *) 0x5555555654b8 <current> (gdb) p current->q->next $19 = (struct list_head *) 0x555555568f78 (gdb) p current->q->next->next $20 = (struct list_head *) 0x5555555654c0 <chain> (gdb) p current->q->next->next->next $21 = (struct list_head *) 0x555555568f78 (gdb) p current->q->next->next->next->next $22 = (struct list_head *) 0x5555555654c0 <chain> ``` ```c=847 static bool is_circular() { struct list_head *cur = current->q->next; while (cur != current->q) { if (!cur) return false; cur = cur->next; } // ... } ``` 透過比較 ```do_free``` 與 ```do_prev``` 函式內容,發現多打 ```&``` 符號 - ```do_free``` 函式 ```c if (chain.size > 1) { qnext = ((uintptr_t) &current->chain.next == (uintptr_t) &chain.head) ? chain.head.next : current->chain.next; } ``` - ```do_prev``` 函式 ```c if (chain.size > 1) { next = ((uintptr_t) chain.head.prev == (uintptr_t) &current->chain) ? chain.head.next : current->chain.next; current = next ? list_entry(next, queue_contex_t, chain) : NULL; } ``` 因此在 ```do_free``` 函式中移除 ```&``` 即可 ```diff if (chain.size > 1) { - qnext = ((uintptr_t) &current->chain.next == (uintptr_t) &chain.head) + qnext = ((uintptr_t) current->chain.next == (uintptr_t) &chain.head) ? chain.head.next : current->chain.next; } ``` 後來發現 github/lab0-c 已經修正該錯誤,因此只要 ``` git``` 操作即可 - ```git remote add lab0-c https://github.com/sysprog21/lab0-c.git``` - ```gir remote update``` - ```git merge lab0-c/master``` #### 錯誤二 使用指令測試單獨的檔案 ``` valgrind ./qtest -f ./traces/trace-01-ops.cmd ``` valgrind 會回報以下問題: ``` # Test of insert_head and remove_head l = [] l = [gerbil] l = [bear gerbil] l = [dolphin bear gerbil] Removed dolphin from queue l = [bear gerbil] Removed bear from queue l = [gerbil] Removed gerbil from queue l = [] ==8952== 32 bytes in 1 blocks are still reachable in loss record 1 of 2 ==8952== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==8952== by 0x10CBCE: do_new (qtest.c:145) ==8952== by 0x10DDFA: interpret_cmda (console.c:181) ==8952== by 0x10E3AF: interpret_cmd (console.c:201) ==8952== by 0x10E7B0: cmd_select (console.c:610) ==8952== by 0x10F09C: run_console (console.c:705) ==8952== by 0x10D1EC: main (qtest.c:1223) ==8952== ==8952== 56 bytes in 1 blocks are still reachable in loss record 2 of 2 ==8952== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==8952== by 0x10F110: test_malloc (harness.c:133) ==8952== by 0x10F515: q_new (queue.c:17) ==8952== by 0x10CC07: do_new (qtest.c:149) ==8952== by 0x10DDFA: interpret_cmda (console.c:181) ==8952== by 0x10E3AF: interpret_cmd (console.c:201) ==8952== by 0x10E7B0: cmd_select (console.c:610) ==8952== by 0x10F09C: run_console (console.c:705) ==8952== by 0x10D1EC: main (qtest.c:1223) ==8952== ``` 後來發現 [git commit c7d31497c5845bc0af308bf4601e7636c18f0153](https://github.com/sysprog21/lab0-c/commit/c7d31497c5845bc0af308bf4601e7636c18f0153) 修正此問題 --- :::warning command 的翻譯是「命令」,instruction 的翻譯是「指令」 :notes: jserv ::: ## 在 qtest 提供新的命令 shuffle 參考其他巨集的實作,新增一個 `shuffle` 命令 ```diff ADD_COMMAND(new, "Create new queue", ""); ADD_COMMAND(free, "Delete queue", ""); + ADD_COMMAND(shuffle, "Shuffle the current queue.", ""); ``` 由於在 git 設定中,不能變更 ```queue.h``` 與 ```list.h```,因此實作只能在 ```q_test.c``` 進行 ```c static bool do_shuffle(int argc, char *argv[]) { if (argc != 1) { report(1, "%s takes no arguments", argv[0]); return false; } if (!current) { report(3, "Warning: Try to operate null queue"); return false; } struct list_head *q = current->q; if (!q) return false; if (list_empty(q) || list_is_singular(q)) return true; const int n = q_size(q); for (int i = n; i > 1; i--) { int r = rand() % i; struct list_head *tmp = q->next; while (r--) tmp = tmp->next; list_move_tail(tmp, q); } return q_show(3); } ``` --- ### 研讀論文 〈[Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf)〉 台北大學歐士田教授的[假設檢定的介紹](https://web.ntpu.edu.tw/~stou/class/ntpu/CH11-Keller-in-Chinese-2011.pdf),閱讀之後對於論文的理解非常有幫助,使用了基本的統計學在驗證 $H_0$ - 論文摘要中,闡述一種資訊安全的攻擊方式:Timing attacks,一種 side-channel attacks (旁路攻擊),例如函式驗證敏感資訊時,若是錯誤的資訊就提早回傳結果,導致攻擊者可以利用處理時間,來破解驗證機制。 - 論文在不知道硬體實作的情況下,透過統計方式檢驗是否程式碼為固定時間執行 (constant time) - 論文介紹中,主要闡述其貢獻:```TIMING LEAKAGE DETECTION``` - 使用兩組資料測試,利用統計的方式去比較這兩者的執行時間 - 第一組為固定的常態分佈的測試資料(固定的資料有固定的結果) - 第二組為隨機的資料 - 由於沒學過統計,這裡我理解為使用兩個統計結果,使用其標準差跟平均數去計算兩者統計結果差異,利用 Welcch's t-test 跟 two-tails test 計算出某個數值,以驗證或推翻虛無假說。 ### 修正 Probably not constant time or wrong implementation 使用測試案例 ```trace-17-complexity.cmd``` 會獲得 ```Probably not constant time or wrong implementation```,觀察其測試內容 ``` # Test if time complexity of q_insert_tail, q_insert_head, q_remove_tail, and q_remove_head is constant option simulation 1 it ih rh rt option simulation 0 ``` 根據[作業說明提示:亂數+論文閱讀](https://hackmd.io/@sysprog/linux2023-lab0/%2F%40sysprog%2Flinux2023-lab0-d#-%E8%AB%96%E6%96%87%E3%80%88Dude-is-my-code-constant-time%E3%80%89%E9%87%8D%E9%BB%9E%E6%8F%90%E7%A4%BA) 計算 approximate of df(degree of freedom)公式,參考 [Welch (Unpooled Variance) t Tests and Confidence Intervals: Introduction](https://youtu.be/2-ecXltt2vI?t=229) 提及的公式: - $S_1$ 與 $S_2$ 表示變異數 - $n$ 表示樣本數量 $$ df = \frac{({\frac{S_1^2}{n_1} + \frac{S_2^2}{n_2}})^2}{{\frac{1}{n_1 - 1}}({\frac{S_1^2}{n_1}})^2 + {\frac{1}{n_2 - 1}}({\frac{S_2^2}{n_2}})^2} $$ 參考 [RinHizakura 的筆記](https://hackmd.io/@RinHizakura/ByuY_q6AU#dudect)中的描述,在 ```fixture.c```的```report```函式中 ```c static bool report(void) { double max_t = fabs(t_compute(t)); double number_traces_max_t = t->n[0] + t->n[1]; double max_tau = max_t / sqrt(number_traces_max_t); // ... // ... /* Definitely not constant time */ if (max_t > t_threshold_bananas) return false; /* Probably not constant time. */ if (max_t > t_threshold_moderate) return false; /* For the moment, maybe constant time. */ return true; } ``` - ```max_t``` 表示 t value - 利用 t value 查看在 df 產生的分佈中位置 - 計算區域下的積分面積,此為 p value - 如果 p value 超過小於某個指定數值 $\alpha$,即表示可以推翻 $H_0$ 並支持 $H_1$,在本作業中 - $H_0$ 表示 Probably Not Constant Time 的虛無假設 - $H_1$ 表示對立假設 新增以下程式碼: ```clike double s1 = t->m2[0] / (t->n[0] - 1); s1 = s1 * s1; double s2 = t->m2[1] / (t->n[1] - 1); s2 = s2 * s2; double num = s1 / t->n[0] + s2 / t->n[1]; num = num * num; double d1 = s1 / t->n[0]; d1 = (d1 * d1) / (t->n[0] - 1); double d2 = s2 / t->n[1]; d2 = (d2 * d2) / (t->n[1] - 1); double den = d1 + d2; double df = num / den; ``` 附註: - 目前的程式碼計算僅僅算到 df。尚未有想法去計算積分下的數值 <!-- 在 oreparaz/dudect 的程式碼具備 percentile 的處理,但在 lab0-c 則無。 https://github.com/oreparaz/dudect --> --- ## 研讀 Linux 核心原始程式碼的 lib/list_sort.c 並嘗試引入到 lab0-c 專案