# 2025q1 Homework1 (lab0) contributed by < `davidshiao55` > {%hackmd NrmQUGbRQWemgwPfhzXj6g %} ## 開發環境 ```bash $ gcc --version gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0 $ 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): 12 On-line CPU(s) list: 0-11 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz CPU family: 6 Model: 158 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 1 Stepping: 10 CPU(s) scaling MHz: 19% CPU max MHz: 4500.0000 CPU min MHz: 800.0000 BogoMIPS: 5199.98 Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge m ca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 s s ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nons top_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer a es xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpu id_fault epb pti ssbd ibrs ibpb stibp tpr_shadow flexp riority ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida ara t pln pts hwp hwp_notify hwp_act_window hwp_epp vnmi m d_clear flush_l1d arch_capabilities Virtualization features: Virtualization: VT-x Caches (sum of all): L1d: 192 KiB (6 instances) L1i: 192 KiB (6 instances) L2: 1.5 MiB (6 instances) L3: 12 MiB (1 instance) NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-11 Vulnerabilities: Gather data sampling: Mitigation; Microcode Itlb multihit: KVM: Mitigation: VMX disabled L1tf: Mitigation; PTE Inversion; VMX conditional cache flush es, SMT vulnerable Mds: Mitigation; Clear CPU buffers; SMT vulnerable Meltdown: Mitigation; PTI Mmio stale data: Mitigation; Clear CPU buffers; SMT vulnerable Reg file data sampling: Not affected Retbleed: Mitigation; IBRS Spec rstack overflow: Not affected Spec store bypass: Mitigation; Speculative Store Bypass disabled via prct l Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointe r sanitization Spectre v2: Mitigation; IBRS; IBPB conditional; STIBP conditional; RSB filling; PBRSB-eIBRS Not affected; BHI Not affect ed Srbds: Mitigation; Microcode Tsx async abort: Not affected ``` ## 針對佇列操作的程式碼實作 ### q_free > commit: [81544d6](https://github.com/davidshiao55/lab0-c/commit/81544d6cf0fa1d182fccfd1783106f37ae6960bd) 實作 `q_free` 函式,以正確地釋放佇列所佔用的所有記憶體資源。透過使用 `list_for_each_entry_safe` 巨集,我們可以安全地遍歷鏈結串列,並在迴圈中呼叫 `q_release_element` 來釋放每個元素的記憶體。迴圈結束後,使用 `free` 函式釋放 `list_head` 結構體本身,確保沒有記憶體洩漏。 > commit: [f25c599](https://github.com/davidshiao55/lab0-c/commit/f25c59959cc4576d8205ed0100a27415bcf37c87) ```diff void q_free(struct list_head *head) { + if (!head) + return; + element_t *entry = NULL, *safe = NULL ``` 為 `q_free` 函式新增了對空指標的檢查,以避免對空指標執行 `free` 操作。具體修改如下: ### q_insert_head > commit: [54f1e93](https://github.com/sysprog21/lab0-c/commit/54f1e930fbb01b42e1eb062e5dec89e1cc5ef144) 首先,對 `head` 進行空指標檢查,以確保傳入的佇列有效。接著,分配記憶體空間給新的 `element`,若記憶體分配失敗則直接回傳 `false`。隨後,使用 `test_strdup` 分配記憶體並複製 `s` 到 `element` 結構體中的字串,確保新節點的內容正確。最後,使用 `list_add` 將新節點加入佇列的開頭,並回傳 `true`,表示插入成功。 > commit: [aec8cf5](https://github.com/davidshiao55/lab0-c/commit/aec8cf5188fd5cd1bb9eb8fa9c1a646f55f3149c) ```diff void q_free(struct list_head *head) /* Insert an element at head of queue */ bool q_insert_head(struct list_head *head, char *s) { - if (!head) + if (!head || !s) return false; element_t *e = malloc(sizeof(element_t)); if (!e) return false; - e->value = test_strdup(s); + size_t len = strlen(s) + 1; + e->value = malloc(len); + if (!e->value) { + free(e); + return false; + } + strlcpy(e->value, s, len); list_add(&e->list, head); return true; ``` 對 `q_insert_head` 函式進行了以下改進: 1. 加入對 `s` 的空指標檢查:在插入新元素時,首先檢查字串指標 `s` 是否為空,以避免對空指標進行操作,確保程式的穩定性。 2. 改用 `strlcpy` 取代 `test_strdup`:在 `harness.c` 中發現註解標註該檔案屬於測試支援程式碼(test support code)。在閱讀 CERN 相關文獻後,決定改用 `strlcpy` 來實作字串複製,以提高記憶體管理的安全性。 3. 避免記憶體洩漏:在檢測到 `e->value` 的記憶體分配失敗時,新增對 `e` 的記憶體釋放操作,以避免記憶體洩漏問題的發生。 ### q_insert_tail > commit: [7629631](https://github.com/davidshiao55/lab0-c/commit/7629631497c36190a4b3bd5bc41b3eb5970eb376) > commit: [aec8cf5](https://github.com/davidshiao55/lab0-c/commit/aec8cf5188fd5cd1bb9eb8fa9c1a646f55f3149c) 同 `q_insert_head` 的實作,但將 `list_add` 換成 `list_add_tail`。 ### q_remove_head > commit: [d2f60aa](https://github.com/davidshiao55/lab0-c/commit/d2f60aabc7811c9c8318df3c9d8ee5a8ebc32ec2) 首先,檢查 `head` 是否為空指標,或佇列是否為空,若是則直接回傳 `NULL`。接著,使用 `list_first_entry` 取得佇列的第一個元素 `e`,若提供了字串指標 `sp`,則使用 `strlcpy` 將元素的值複製到 `sp`,確保不超過 `bufsize`。然後,使用 `list_del` 將元素從佇列中移除,最後回傳指向已移除元素的指標 `e`。 ### q_remove_tail >commit: [7629631](https://github.com/davidshiao55/lab0-c/commit/7629631497c36190a4b3bd5bc41b3eb5970eb376) 同 `q_remove_head` 的實作,但將 `list_first_entry` 換成 `list_last_entry` 。 ### q_size > commit: [7bad176](https://github.com/davidshiao55/lab0-c/commit/7bad1769d72b135d9c869e88d599a3e0d1b2ada5) 首先,檢查 `head` 是否為空指標或佇列是否為空,若是則回傳 0。接著,使用 `list_for_each` 巨集遍歷佇列,計算元素數量並回傳。 ### q_delete_mid >commit: [30c06e1](https://github.com/davidshiao55/lab0-c/commit/30c06e179006558c68cb31c484226cc4e709c2b5) 在閱讀完[分析「快慢指標」](https://hackmd.io/@sysprog/ry8NwAMvT)後,使用快慢指標的方式實作,以更有效地利用時間局部性。首先,對 `head` 做空指標及空佇列檢查,若為空則回傳 `false`。接著,利用快慢指標法遍歷佇列,直到快指標到達佇列末尾,慢指標即指向中間節點。最後,使用 `list_del` 刪除該中間節點並釋放記憶體。 ### q_delete_dup >commit: [db08bab](https://github.com/davidshiao55/lab0-c/commit/db08bab12a38c0d022ecdb8b262503450841b336) 此題跟 leetcode 最大的差異是鏈結串列並非排序好的,在理解題目後考慮了兩種實作方法: 1. 使用雜湊表實作,時間複雜度:$O(n)$,空間複雜度:$O(n)$ 2. 使用巢狀迴圈實作,時間複雜度:$O(n^2)$,空間複雜度:$O(1)$ 考慮到 C 語言沒有原生雜湊表實作,選擇使用巢狀迴圈。考慮到刪除重複節點可能會刪除到下一個節點不適合使用 `list_for_each_safe` 巨集,改使用 `list_for_each` 巨集,並在內層迴圈中使用一個 `runner` 指標去刪除重複節點並檢測當前節點是否重複,若當前節點是重複節點則在外層迴圈再刪除此節點。 > commit: [bec3e6a](https://github.com/davidshiao55/lab0-c/commit/bec3e6aa775bb15262d190a338609a5acc044bc3) ```bash $./qtest cmd> new l = [] cmd> it a l = [a] cmd> it b l = [a b] cmd> it c l = [a b c] cmd> it a l = [a b c a] cmd> dedup ERROR: Duplicate strings are in queue or distinct strings are not in queue l = [b c] cmd> size ERROR: Computed queue size as 2, but correct value is 4 l = [b c] ``` 在發現以上測資會出錯後,閱讀 `qtest.c` 中 `do_dedup` 的實作,發現我錯誤理解題意,此題只需移除相鄰重複節點。時間複雜度:$O(n)$ ,空間複雜度 $O(1)$ ```diff bool q_delete_dup(struct list_head *head) if (!head || list_empty(head)) return false; - struct list_head *node = head->next; - - while (node != head) { + struct list_head *node; + list_for_each (node, head) { element_t *e = list_entry(node, element_t, list); bool dup = false; - struct list_head *safe = node->next; - // safe find the first non-duplicated value - while (safe != head) { - struct list_head *next = safe->next; - element_t *s = list_entry(safe, element_t, list); - if (!strcmp(s->value, e->value)) { + struct list_head *runner = node->next; + while (runner != head) { + struct list_head *next = runner->next; + + element_t *r = list_entry(runner, element_t, list); + if (!strcmp(e->value, r->value)) { dup = true; - list_del(&s->list); - q_release_element(s); - } else { - break; + list_del(&r->list); + q_release_element(r); } - safe = next; + runner = next; } if (dup) { + struct list_head *next = node->next; list_del(&e->list); q_release_element(e); + node = next; } - node = safe; } return true; } ``` 新的實作使用一個 `safe` 指標指向第一個與此節點結構體中字串相異的節點,若有找到相同節點則刪除相同節點和此節點。 ### q_reverse >commit: [9bbf5f2](https://github.com/davidshiao55/lab0-c/commit/9bbf5f29a8a6fdb7c768c82e9b1fb9d0134789a0) 首先,檢查 `head` 是否為空指標,或佇列是否為空,接著從 `head` 遍歷各節點,將個解點中 `next` 和 `prev` 指向的節點交換。 ### q_reverseK >commit: [c84a603](https://github.com/davidshiao55/lab0-c/commit/c84a6039fd0b4daa878b33cd078e595a98de661e) 首先,檢查 `head` 是否為空指標,佇列是否為空或佇列是否只有一個元素,若是則直接返回。然後,遍歷佇列,對每組 k 個節點進行反轉操作。對於每組節點,首先找到第 k 個節點,若不足 k 個則不進行反轉。接著,反轉這 k 個節點的指標方向。最後,調整前一組的最後一個節點與當前組的第一個和第 k 個節點,以及下一組的第一個節點之間的指標關係,以維持雙向鏈結串列的結構。 ### q_sort >commit: [0e31de7](https://github.com/davidshiao55/lab0-c/commit/0e31de76a317759e3d5b99494193596e588a76f3) 首先,將環狀鏈結串列轉換為非環狀的雙向鏈結串列。接著,使用快慢指標將串列分割為兩部分,並遞迴地對每部分進行合併排序。最後,將排序後的串列重新連接到頭節點。此實作中包含兩個輔助函式:`merge_sort_dlist` 使用快慢指標將串列分割為兩部分;`merge_two_sorted` 則為遞迴函式,用於合併兩個已排序的雙向鏈結串列。 `merge_sort_dlist` 中快慢指標偏移 `fast` 指標使得在偶數節點數的時候會選擇第一個中間節點,使得在節點數為二的時候叫容易處理分割。 ```c static struct list_head *merge_sort_dlist(struct list_head *head, bool descend) { ... struct list_head *slow = head, *fast = head->next; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; ... } ``` > commit : [309a9ba](https://github.com/davidshiao55/lab0-c/commit/309a9badee706b945ec3ad32414a094cc60b3399) 參照[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list)將 `merge_two_sorted` 函式從遞迴實作改為使用 indirect pointer ,類似於合併單向鏈結串列的方法,並添加額外的指標以維持雙向鏈結串列的結構。此更改有助於提高函式的效率,避免遞迴帶來的額外開銷。 ```c /* merges two sorted (non-circular) doubly linked lists */ static struct list_head *merge_two_sorted(struct list_head *left, struct list_head *right, bool descend) { struct list_head *head = NULL, **ptr = &head, **node, *prev = NULL; for (node = NULL; left && right; *node = (*node)->next) { const element_t *l = list_entry(left, element_t, list); const element_t *r = list_entry(right, element_t, list); bool cmp = (descend) ? (strcmp(l->value, r->value) >= 0) : (strcmp(l->value, r->value) <= 0); node = (cmp) ? &left : &right; *ptr = *node; (*node)->prev = prev; prev = *node; ptr = &(*ptr)->next; } *ptr = (struct list_head *) ((uintptr_t) left | (uintptr_t) right); (*ptr)->prev = prev; return head; } ``` >commit: [4dde27f](https://github.com/davidshiao55/lab0-c/commit/4dde27f8441870c1ec58689f4938a72f5a71caa3) 在實作 `q_merge` 後新增了兩個輔助函式,因此更精簡化了程式碼。 ```diff void q_sort(struct list_head *head, bool descend) return; // Break the the list into normal doubly-linked list - struct list_head *first = head->next; - struct list_head *last = head->prev; - first->prev = NULL; - last->next = NULL; + struct list_head *first = break_ring(head); struct list_head *sorted = merge_sort_dlist(first, descend); - struct list_head *tail = sorted; - while (tail->next) - tail = tail->next; - - // Link the head back - head->next = sorted; - head->prev = tail; - sorted->prev = head; - tail->next = head; + recirc(head, sorted); } ``` ### q_ascend >commit: [8d19670](https://github.com/davidshiao55/lab0-c/commit/8d1967051fb1683634172f21879b0f45d77e0616) 首先,檢查 `head` 是否為空指標、佇列是否為空或僅有一個元素,若是則回傳佇列大小。接著,從佇列尾部開始向前遍歷,追蹤目前的最小值,移除所有大於該最小值的節點,並在找到新的最小值時更新。此方法確保了剩餘的節點按遞增順序排列。 ### q_descend >commit: [83f6e33](https://github.com/davidshiao55/lab0-c/commit/83f6e334bce418afb9d26c3cf528f122ef69aab6) 同 `q_ascend` ,但將最小值換成最大值。 ### q_merge >commit: [526c2da](https://github.com/davidshiao55/lab0-c/commit/526c2dade43a633ce49638e768dbf95c505a3653) 包含三個輔助函式: 1. `break_ring`:將具有哨兵頭節點的環狀鏈結串列轉換為標準的雙向鏈結串列。 2. `recirc`:將標準的雙向鏈結串列重新構造成環狀鏈結串列,並加入 `head` 節點。 4. `merge_two_sorted`:從 `q_sort` 中提取出來的函式,用於合併兩個已排序的雙向鏈結串列。 在 `q_merge` 函式中遍歷節點,首先使用 `break_ring` 轉換環狀鏈結串列,接著透過 `merge_two_sorted` 合併該個和第一個已排序的佇列,最後使用 `recirc` 重新構造環狀鏈結串列。 ## 研讀 Linux 核心原始程式碼的 lib/list_sort.c `list_sort.c` 有別於傳統 top-down 的 merge sort,採取了 bottom-up 的策略,更有效率的利用快取。 ### 將 list_sort.c 引入 lab0 > commit: [824efeb](https://github.com/davidshiao55/lab0-c/commit/824efeb2641fa1aacee5298dbb8ee31eb93e97ff) 直接將 `list_sort.c` 引入 `queue.c` 中,再多加入 `cmp_func` 和 `sort_arg`,尚未比較效能。 ```diff +#define likely(x) __builtin_expect(!!(x), 1) +#define unlikely(x) __builtin_expect(!!(x), 0) + +struct sort_arg { + bool descend; +}; + +static int cmp_func(void *priv, + const struct list_head *a, + const struct list_head *b) +{ + const struct sort_arg *arg = priv; + const element_t *ea = list_entry(a, element_t, list); + const element_t *eb = list_entry(b, element_t, list); + + int cmp = strcmp(ea->value, eb->value); + if (!arg->descend) { + return cmp; // normal ascending + } else { + // just flip the sign + return -cmp; + } +} + +typedef int (*list_cmp_func_t)(void *, + const struct list_head *, + const struct list_head *); @@ -333,11 +591,16 @@ void q_sort(struct list_head *head, bool descend) if (!head || list_empty(head) || list_is_singular(head)) return; - // Break the the list into normal doubly-linked list - struct list_head *first = break_ring(head); + // Build an 'arg' struct with the chosen ordering + struct sort_arg arg = { + .descend = descend, + }; - struct list_head *sorted = merge_sort_dlist(first, descend); - recirc(head, sorted); + // Then just call list_sort. It will: + // 1) break the ring, + // 2) do a mergesort in place, + // 3) re-circularize the list. + list_sort(&arg, head, cmp_func); } ``` ## 實作q_shuffle 實驗1,000,000次 shuffle 實驗結果如下: ```bash $python3 scripts/shuffle.py Expectation: 41666 Observation: {'1234': 41450, '1243': 41577, '1324': 41709, '1342': 41571, '1423': 41283, '1432': 41835, '2134': 41623, '2143': 41725, '2314': 41279, '2341': 41439, '2413': 41830, '2431': 41689, '3124': 41762, '3142': 41821, '3214': 41754, '3241': 41692, '3412': 41610, '3421': 41708, '4123': 41979, '4132': 41775, '4213': 41825, '4231': 41522, '4312': 41699, '4321': 41843} chi square sum: 17.030672490759855 ``` ![image](https://hackmd.io/_uploads/rJpF9Dhjyg.png) 自由度為 4! - 1 = 23,P value = 0.7951,P value > apla (0.05),統計檢定的結果不拒絕虛無解說,不拒絕 shuffle 的結果遵循 Uniform distribution。 ## dudect 論文 這篇論文主要描述透過 fix-vs-random leakage detection 的方式不用透過硬體,依靠執行時間的統計分析判斷程式是否是常數執行時間。 [wurrrrrrrrrr的開發紀錄](https://hackmd.io/@Guanchun0803/linux2025-homework1#研讀論文%E3%80%88Dude-is-my-code-constant-time〉)提到他對在 `constant.c` 中對 fix class 的 constant 做變動得到的不同結果,在我自己實驗後也復現了同樣結果,在 constant 為 0 時會過不了 trace-17 但在 0xAA, 0xCC, 0xFF 都過得了。 我對這個實驗結果的解釋是當我們把 fix class 固定填入 0 時,`strlen` 這類遇到第一個 `\0` 就會停止的函式的的執行時間就會非常短,造成 False positive 的非常數執行時間,是 fix input 單方面造成的並沒有任何 secret-dependency,巫同學的實驗結果也看出這個現象。 ```diff if (classes[i] == 0) - memset(input_data + (size_t) i * CHUNK_SIZE, 0, CHUNK_SIZE); + memset(input_data + (size_t) i * CHUNK_SIZE, 0xff, CHUNK_SIZE); ```