# 2024q1 Homework1 (lab0) contributed by < `rain20010126` > :::danger 詳閱作業說明,指定的格式中有空白字元,留意各式細節,唯有重視小處並步步為營,方可挑戰原始程式碼規模超越三千七百萬行的 Linux 核心 > 了解,感謝老師指正! ::: ### Reviewed by `Shawn531` * 實作前先釐清每個函式的用處與形式,譬如在`queue.h`有說明每個函式要達成甚麼目的,這樣可以加速開發。 * 可以多表達自己的第一手想法。 * 善用`diff`表達程式碼的差異。 ### Reviewed by `ShawnXuanc` 在提交 `commit` 時可以增加對功能的說明 可以將相同的程式碼再利用,像是 `q_insert`, `q_remove` 中有需多重複的程式碼 可以使用美式英文當作程式碼的註解 ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 13.2.0-4ubuntu3) 13.2.0 $ lscpu Architecture: aarch64 CPU op-mode(s): 64-bit Byte Order: Little Endian CPU(s): 8 On-line CPU(s) list: 0-7 Vendor ID: Apple Model name: Blizzard-M2 Model: 0 Thread(s) per core: 1 Core(s) per socket: 4 Socket(s): 1 Stepping: 0x1 Frequency boost: enabled CPU(s) scaling MHz: 38% CPU max MHz: 2424.0000 CPU min MHz: 600.0000 BogoMIPS: 48.00 ``` :::warning 注意標示的方法。 ::: ### GCC 編譯器 ``` $ gcc -o stack -g -no-pie stack.c ``` `-o` : 指定特定輸出的檔案名稱 `-g` : 可以配合 `gdb` 做除錯如`$ gdb -q stack`,其中`-q` 的目的為讓他安靜一點 `-no-pie`: 抑制 PIE,簡單來說就是將原本執行檔是把一段位置寫進去(直接的地址),使用 PIE 的話就是改成相對位置,來避免針對內存位置的攻擊,所以使用 PIE 後需要對執行檔做追蹤的話會比較麻煩 ==我在調整風格時遇到以下問題==,有嘗試做 `sudo apt upgrade gdb` 仍然無法解決此問題 ``` (gdb) set disassembly-flavor intel No symbol "disassembly" in current context. ``` ==另外的問題是我在使用 `disas main` 的輸出的結果如下==, 不知道要怎麼和老師在 [你所不知道的 C 語言:函式呼叫篇](https://hackmd.io/@sysprog/c-function) 中的 `rbp` 和 `rsp` 做對應 ```c (gdb) disas main Dump of assembler code for function main: 0x0000000000400638 <+0>: stp x29, x30, [sp, #-32]! 0x000000000040063c <+4>: mov x29, sp 0x0000000000400640 <+8>: mov w0, #0x1 // #1 0x0000000000400644 <+12>: bl 0x40061c <funcA> 0x0000000000400648 <+16>: str w0, [sp, #28] 0x000000000040064c <+20>: mov w0, #0x0 // #0 0x0000000000400650 <+24>: ldp x29, x30, [sp], #32 0x0000000000400654 <+28>: ret End of assembler dump. ``` ### 你所不知道的 C 語言 : 函示呼叫篇 #### malloc & calloc 比較: `malloc` 盡可能用已經用過並且釋放掉的記憶體空間; `calloc` 則是配置記憶體空間並將內容**歸零**(通常會使用沒有被使用過的記憶體空間或是 `calloc` 過後但沒有做進一步修改的記憶體空間),如果要配置歸零後的記憶體空間使用 `malloc` ,相比於使用 `malloc + memset` 的速度來的快,如果沒有歸零的需求,先使用 `malloc` ,因為 `malloc` 的記憶體配置成功率較高 #### Parameter & Argument : `Parameter` 為形式, `Argument` 為實際值 #### stack : 概念為後入先出,使用區域變數會配置在 `stack` 中。 `stack frame` 為一層的區域 - rsp(stack pointer) : 指向 stack 頂端 - rbp(base pointer) : 指向 stack 底部 #### heap(heap allocator) : 概念為**分堆**,會分成不同大小,例如由小到大為 `first bit` `small bit` `big bit` 等等,會有不同的配置策略。 使用 `malloc`, `free` 等配置記憶體或是釋放記憶體都有關係,以下為 `heap` 和 `stack` 的範例 ```c long *arr = malloc((n + 1) * sizeof(*arr)); // heap long arr[n+1]; // stack ``` ### 你所不知道的 C 語言:遞迴呼叫篇 #### 遞迴 VS 迭代 遞迴使用 function,在 function 中呼叫相同的一個 function (直接遞迴),或是 `function A` 會呼叫 `function B` ,而 `function B` 會呼叫 `function A` (間接遞迴) 遞迴與非遞迴比較 - 遞迴優點 : 較精簡且易理解、變數較少、函式記憶體空間較省 - 非遞迴優點 : 通常效率較高、不須額外 stack 空間 `&&` (Logical AND) operator 與 `||` (Logical OR) operator ## 針對佇列操作的程式碼實作 :::danger 無論標題和內文中,中文和英文字元之間要有空白字元 (對排版和文字搜尋有利) ::: 在安裝完 Ubuntu Linux,熟悉鏈結串列和 git 的操作後我開始嘗試寫作業,順序即是每個函式的順序,因為一開始不知道怎麼下手,就先參考其他同學的格式並思考如何下手,首先看到 [Jackiempty](https://hackmd.io/@Jackiempty/HJv_2m7np) `q_new` 的部份後,發現我也不知道有 `INIT_LIST_HEAD(list_head)` 的函式存在,以及同學其他呼叫的函式都不太熟悉,在了解我到不是很熟悉下列網站後 (https://github.com/torvalds/linux/blob/master/include/linux/list.h#L35) ,所以我寫作業的方法主要是先參考同學的寫法,了解大概有什麼函式是需要用的,理解完需要用到的函式後,再自己下手。 :::danger 1. 避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景。 2. 改進你的漢語描述。 ::: ### `q_new` - 在了解到是要建立一個空佇列後,考慮到可能會有記憶體空間配置失敗的問題,因此簡單加上條件判斷是否有成功配置記憶體空間並搭配 `INIT_LIST_HEAD` 即順利完成此函式的建立。 ### `q_free` :::danger 你不該花太多時間在「查找資料」,閱讀、思考,然後闡述自己的洞見,才是課程在意的議題。 > 好的,謝謝老師。 ::: 在理解 ` list_for_each_entry_safe` 時遇到了不小的困難,後來在<s>查找資料</s> 時,發現我的理解大有問題,首先 `#define` 的意思是一個巨集指令,和一般定義一個函式的方式有些不同,我原先以為他的意思和定義函數是一樣的意思,再來參照 [aa860630](https://hackmd.io/@qOvjgDvTQrGZGAlv5oHqsA/linux2024-homework1) 及 [nelson0720j](https://hackmd.io/@woLves-1822/linux2024-homework1),將巨集的輸入設定成 `entry` 和 `safe` 後,了解到此巨集主要的作用是在刪除當前節點時,能同時知道下一個節點的位置,有這樣的理解後,完成了 `q_free` 的建立。 ### `q_insert_head` :::danger 本該如此,有何好說? 自己嘗試! ::: <s>這次我是自己從頭嘗試看看</s>,但配置新的節點後,我遇到了一個問題是該怎麼知道配置 `node->value` 的記憶體空間,於是我詢問 chatgpt 來嘗試自己解決問題 ,結果他給我一段範例程式碼,所以在我閱讀後,了解到可以使用 `strlen` 來知道字串的長度,同時加 1 是為了<s>存儲字符串結束符</s> (\0),以及需要考慮到記憶體配置失敗的問題,下一個問題是我要怎麼把輸入的字串複製過去 ,之後在閱讀 [nelson0720j](https://hackmd.io/@woLves-1822/linux2024-homework1) 的說明後了解到可以使用 `strncpy` ,於是以下是我的初步程式碼。 :::danger 注意詞彙: * string 是字串 * character 是字元 * store 是儲存 ::: ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *node = malloc(sizeof(element_t)); if (!node) return false; // 獲得字串字數並使用strncpy將字串複製過去配置好的記憶體空間 node -> value = malloc(strlen(s) + 1); strncpy(node->value, s, strlen(s) + 1); if (!node -> value) return false; list_add(&node->list, head); return true; } ``` 接著我使用 `make test` 進行測試後發現函式有誤,同時有內存洩漏的問題,於是我參考了 [csotaku0926](https://hackmd.io/@csotaku0926/linux2024-homework1) 的作法,檢查我出錯的地方,發現我少考慮到了單個字體的大小,以及在 `return false` 前應該要把配置失敗的記憶體釋放掉,接著就成功了~~~ ### `q_insert_tail` :::danger 避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。 ::: 有了 `q_insert_head` 的經驗,於是我就將 `q_insert_head` 的最後一行程式碼從 `list_add(&node->list, head);` 修改成 `list_add_tail(&node->list, head);` 。 另外有看到 [csotaku0926](https://hackmd.io/@csotaku0926/linux2024-homework1) 的作法,覺得他的作法聰明許多,他是直接利用先前寫的 `q_insert_head` 來完成函式,即將函式輸入部份從原先的 `*head` 改成 `head->prev` 就完成了,這種作法就可以省去重複的程式碼。 ### `q_remove_head` 在一開始,我初步對這個函式的理解是'移除' `head` 中和 `*sp` 指標指向相同的元素開始後的 `bufsize` 個元素,因為不確定我的想法是否正確,所以我查看了 [csotaku0926](https://hackmd.io/@csotaku0926/linux2024-homework1) 的程式碼,發現我的想法大錯特錯,了解到 `bufsize` 是用來存取 `head->value` 的大小的,同時 `sp` 是用來儲存 `head->value` 的原先值的,並搭配 `list_del_init` 斷開head的連結,因此在有這些了解後完成了程式碼。 我比較疑惑的部份是為什麼 `remove` 需要刪除節點內的 `value` 另外一開始在撰寫程式時沒注意到 queue empty 的情況 ### `q_remove_tail` 因為有先前 `q_insert_head` 和 `q_insert_tail` 的經驗,所以也嘗試看能不能用 `q_remove_head` 的方式撰寫本函式,但是失敗了,於是我參考了 [csotaku0926](https://hackmd.io/@csotaku0926/linux2024-homework1) 的程式碼,發現是要使用 `head->prev->prev` 但依然遇到了以下問題 ``` ERROR: Removed value meerkat_panda_squirrel_vulture_wolf != expected value aardvark_bear_dolphin_gerbil_jaguar ``` :::danger 不理解就說不理解,不要裝可憐。理工人員說要精準。 ::: 已解決,將 `list_del_init` 修改成 `list_del` ,但是<s>有點不太了解其中原因</s>,有查看 [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-Leetcode-2095-Delete-the-Middle-Node-of-a-Linked-List) 的說明,但不懂兩者的差別為何會造成一個編譯成功一個編譯失敗的問題 ```c static inline void list_del(struct list_head *entry) { __list_del_entry(entry); entry->next = LIST_POISON1; entry->prev = LIST_POISON2; } static inline void list_del_init(struct list_head *entry) { __list_del_entry(entry); INIT_LIST_HEAD(entry); } ``` :::danger 1. 減少非必要的項目縮排 (即 `* `),使用清晰、明確,且流暢的漢語書寫 2. 注意程式碼的標示和張貼方式,避免非必要的縮排 ::: ### `q_size` 參考自 [牛刀小試](https://hackmd.io/@sysprog/linux2024-lab0/%2F%40sysprog%2Flinux2024-lab0-a) ,透過 `list_for_each` 走訪每個節點,同時經過一個節點時 `len` 加一 ### `q_delete_mid` 在閱讀完[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-Leetcode-2095-Delete-the-Middle-Node-of-a-Linked-List)後,我有一個初步的了解是可以使用快慢指標來找到中間的節點,以下是我第一次完成的程式碼 ```c bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) return false; struct list_head *fast = head, *slow = head; while (fast!=tail && fast->next!=tail) { fast = fast->next->next; slow = slow->next; } slow = slow-> next; // 這裡有誤 list_del(slow); q_release_element(list_entry(slow, element_t, list)); return true; } ``` 但是遇到了以下問題 ``` ERROR: Attempted to free unallocated block. Address = 0xdeadbeef Segmentation fault occurred. You dereferenced a NULL or invalid pointer ``` 後來在參考 [nosba0957](https://hackmd.io/@chucknos/linux2024-homework1) ,發現迴圈結束後不需要再做一次 `slow = slow-> next;` ,但修改後仍然出現同的問題,最後在解決前面 `q_remove_tail` 的問題後就沒有出現上述問題了 另外在 `git commit` 時收到了將 `tail` 設定成常數的建議 :::danger HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」,不是用來「假裝自己有付出」 ::: ### q_delete_dup 了解函式的目標是在已經排序的狀況,移走佇列中具備重複內容的節點,我初步的想法是重複的元素會相鄰,我就從頭一個一個檢查看節點的內容,ㄖ,並了解可以使用 `strcmp` ,若是兩者字串相等的話會回傳0,並不使用 `node = node->next` 以便再重新檢查和下一個元素是否也出現重複得情況,若兩字串不相等,則使用 `node = node->next` ,對下一個節點進行檢查,以下是我初步的程式碼 ```c bool q_delete_dup(struct list_head *head) { if (!head || list_empty(head)) return false; element_t *current, *next; struct list_head *node = head->next, *delete; while(node!=head){ current = container_of(node, element_t, list); next = container_of(node->next, element_t, list); if (!strcmp(current->value, next->value)){ delete = node->next; list_del(delete); q_release_element(container_of(delete, element_t, list)); } else node = node->next; } return true; } ``` 接著我在使用 `git commit` 後收到了針對 current 變數的警告,要在使用這個變數的區域內聲明它,以減少其作用範圍,增加程式碼的可讀性和維護性。 另外在 `q_test` 出現了以下問題 ``` Segmentation fault occurred. You dereferenced a NULL or invalid pointerAborted ``` 接著我查看了 [csotaku0926](https://hackmd.io/@csotaku0926/linux2024-homework1) 後發現我沒有正確理解題目的意思,重複的每個節點都需要刪除,所以我新增了以下程式碼如下,同時使用 `remove_cur`判斷是否有刪除節點 ,但是在 `q_test` 依然出現了相同的問題。 ```c element_t *del = NULL; if (remove_cur){ del = node->prev; list_del_init(del); //這邊有誤 q_release_element(del); } ``` 因為找不到問題所在,同時我發現我先前的程式碼沒有考慮到有重複多次的情形,所以我將迴圈改成 `while` 的寫法,以下是我的最終版本,我參考了 [csotaku0926](https://hackmd.io/@csotaku0926/linux2024-homework1) 的作法,改進了我先前沒有繼續檢查的問題。 ```c bool q_delete_dup(struct list_head *head) { // https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/ if (!head || list_empty(head)) return false; struct list_head *current = head->next, *next = current->next; bool remove_cur; while (current != head && current->next != head) { next = current->next; remove_cur = false; element_t *entry; // 此處條件有誤 while (!strcmp(list_entry(next, element_t, list)->value, list_entry(current, element_t, list)->value)){ remove_cur = true; list_del(next); entry = container_of(next, element_t, list); q_release_element(entry); next = current->next; } current = current->next; if (remove_cur){ struct list_head *del = current->prev; list_del(del); entry = container_of(del, element_t, list); q_release_element(entry); } } return true; } ``` 再來我在執行 `qtest` 再有多目標需要刪除時會出現以下問題,經過了解後,我發現需要在以上 while 迴圈有誤處考慮 `next` 為 null 的情況,因次在條件部份補上 `&& next` 即完成程式碼 ``` l = [jiji kiki lili mimi mimi bibi bibi] cmd> dedup Segmentation fault occurred. You dereferenced a NULL or invalid pointerAborted ``` :::danger 避免非必要的項目縮排 (即 `* `),以清晰、明確,且流暢的漢語書寫。 ::: ### `q_swap` 一開始我沒有很理解題目的意思,但在查看完 [nosba0957](https://hackmd.io/@chucknos/linux2024-homework1#q_delete_mid) 的文字說明後,就了解題目是兩兩節點進行交換,交換完後再到下兩個節點進行交換,並參考該同學的程式碼,使用 `list_move` 對節點進行移除再插入。 ### `q_reverse` 我的初步想法是把 `head->next` 也就是 `first` 插入到 `tail` 後,再重新設定 `first`,以下是我初步的程式碼。 ```c void q_reverse(struct list_head *head) { struct list_head *first = head->next; struct list_head *tail = head->prev; while(first!=head){ list_move(first, tail); first = head->next; } } ``` 但出現以下錯誤 ``` l = [yiyi iyiy gigi igig] cmd> reverse ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient ``` 後來發現我條件部份沒設定好,會出現無窮迴圈的情況,將條件重新設定成 `while(tail->prev!=head)` 即完成函式的建立。 ### `q_reverseK` 我的想法是把tail設定到需要反轉部份的尾端,然後再用先前 `q_reverse` 的方法完成,以下為我的程式碼。 ```c void q_reverseK(struct list_head *head, int k) { struct list_head *first = head->next, *tail = head; for (int i = 0; i < k; i++) { tail = tail->next; // 找到需要反轉區間的尾端 } while (tail->prev != head) { list_move(first, tail); first = head->next; } } ``` 後來同學在查看我的程式碼後指出我理解錯題目的要求了,題目要求是將 k 個為一組的元素進行反轉,所以我使用了 `size` 來判別剩下的元素還有沒有 k 個,沒有的話即跳出迴圈,接著利用前面 `reverse` 的方法,不斷更新 `head` 和 `tail` ,並利用 `this_term_head` 來存取此次循環的 `head` 作為 while 迴圈判斷使用。 ```c void q_reverseK(struct list_head *head, int k) { if (!head || list_empty(head)) return; int size = q_size(head); struct list_head *tail = head; struct list_head *this_term_head = head; struct list_head *next_term_head = head; while (size >= k) { size = size - k; for (int i = 0; i < k&& tail->next; i++) { tail = tail->next; } struct list_head *first = this_term_head->next; next_term_head = first; while (tail->prev != this_term_head) { list_move(first, tail); first = this_term_head->next; } tail = next_term_head; this_term_head = next_term_head; } } ``` ### `q_sort` 在觀看 [ Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) ,並了解不同作法的原理後,我選擇以時間複雜度較低的方法: `Merge Sort` ,也就是先找到佇列的中點,慢慢拆分到每個佇列只包含一個元素,再兩兩合併成完整的一個佇列,於是我開始實做。 在實做過程中,首先我遇到的困難是我該如何儲存每個斷開節點的位置,於是我查看了 [WangHanChi](https://hackmd.io/@wanghanchi/linux2023-lab0) 的程式碼和說明 但實做的第一次結果遇到了以下問題,所以我慢慢針對不同的函式下去做修正 ``` l = [yiyi hihi jiji kiki ioio] cmd> sort Segmentation fault occurred. You dereferenced a NULL or invalid pointerAborted (core dumped) ``` 以下為我 `merge_two_nodes`的程式碼 ```c struct list_head *merge_two_nodes(struct list_head *L, struct list_head *R) { if (!L && !R) { return NULL; } struct list_head *head = NULL; while(L && R){ if (strcmp(list_entry(L, element_t, list)->value, list_entry(R, element_t, list)->value) <= 0){ head = L; head = head->next; L = L->next; } else{ head = R; head = head->next; R = R->next; } } if (L){ head->next = L; } if{ head->next = R; } return head; } ``` :::danger 什麼是「虛擬」的節點? ::: 後來發現我回傳的數值有問題,需要再額外存取 `head` 的位置再做回傳,後來我了解到可以使用<s>虛擬節點</s> ??? (改正:可以使用額外的一個 h 來初始化 head,這樣可以讓 head 在迴圈外部始終指向正確的位置,也不用特別處理第一個元素) 方式更好的解決此問題,也就是以下 ```c struct list_head *merge_two_nodes(struct list_head *L, struct list_head *R) { if (!L && !R) { return NULL; } struct list_head h; struct list_head *head = &h; while (L && R) { if (strcmp(list_entry(L, element_t, list)->value, list_entry(R, element_t, list)->value) <= 0) { head->next = L; L = L->next; } else { head->next = R; R = R->next; } head = head->next; } // Link the remaining elements to the head if (L) { head->next = L; } if (R) { head->next = R; } return h.next; } ``` :::danger 程式碼的註解都該用美式英語書寫,不要出現漢字。 你如何確保測試的過程已涵蓋排序演算法的最差狀況? ::: ### `q_descend` 了解到題目是要移除當該節點右側有大於該節點的節點,則刪除該節點,初步想法就是按照順序一個一個檢查,但想一想覺得這個方法會重複確認很多次,於是我就參考了 [csotaku0926](https://hackmd.io/@csotaku0926/linux2024-homework1) 的說明,了解到可以從尾端往前檢查,看有沒有比自己小的節點做刪除。 :::danger 清楚標註你參照的 GitHub 帳號名稱 ::: 另外看到函式需要回傳一個數值,查看<s>同學</s> [csotaku0926](https://hackmd.io/@csotaku0926/linux2024-homework1)的程式碼了解是要回傳佇列的節點數量,於是完成我以下的程式碼 ```c int q_descend(struct list_head *head) { // https://leetcode.com/problems/remove-nodes-from-linked-list/ if (!head || list_empty(head)) return 0; struct list_head *ptr = head->prev; while (ptr != head && ptr->prev != head) { int compare = strcmp(list_entry(ptr, element_t, list)->value, list_entry(ptr->prev, element_t, list)->value); if (compare >= 0) { element_t *entry = container_of(ptr->prev, element_t, list); list_del(ptr->prev); q_release_element(entry); } else ptr = ptr->prev; } return q_size(head); } ``` ### `q_merge` 我採用了比較直觀的做法,同樣先把環形串列修改成非環形的,並將第一個串列使用 `merge_two_nodes` 不斷與後續的進行連接,最後與 `q_sort` 使用相同的作法將串列修改回雙向環狀的 ## Valgrind + 自動測試程式 目前使用 `make valgrind` 在程式碼中沒有找出記憶體的相關問題,但使用 `make test` 時無法通過第17項的測試 ## 實作 shuffle ### Fisher–Yates shuffle 此方法是透過一個迴圈去迭代,從最後一個元素開始,與前面的隨機一個元素進行交換,此作法的好處是,可以確保交換過得位置不會再被交換,且時間複雜度為 *O(n)* ,機率分佈也是均勻的 新增的方法參考 [willwillhi](https://hackmd.io/@willwillhi/lab0-2023) ,目前 `qtest` 中的 `do_shuffle` 沒有檢查可能的錯誤 在實作 `q_shuffle` 時,遇到了以下問題 ``` l = [1 2 3 4] cmd> shuffle ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient ``` 在檢查完程式碼後,發現第 16 行部份有誤,在與前面交換後,原本 `temp` 所在的位置變為 `pick` 所在,因此往前更新應該修改為 `temp = pick->prev` ```clike= void q_shuffle(struct list_head *head){ if (!head) return; int size = q_size(head); struct list_head *temp = head->prev; while(size>=1){ int index = rand() % size; struct list_head *pick = temp; for(;index>0;index--){ pick = pick->prev; } swap(temp,pick); temp = temp->prev; // error here size--; } } ``` 但在進行腳本測試時的結果如下 ``` Expectation: 41666 Observation: {'1234': 0, '1243': 0, '1324': 0, '1342': 0, '1423': 0, '1432': 0, '2134': 0, '2143': 0, '2314': 0, '2341': 0, '2413': 0, '2431': 0, '3124': 0, '3142': 0, '3214': 0, '3241': 0, '3412': 0, '3421': 0, '4123': 0, '4132': 0, '4213': 0, '4231': 0, '4312': 0, '4321': 0} chi square sum: 999984.0 ``` 於是重新利用 `qtest` 進行檢查,發現在執行第二次 shuffle 時會出現以下問題 ``` l = [1 2 3 4] cmd> shuffle l = [3 1 4 2] cmd> shuffle ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient Segmentation fault occurred. You dereferenced a NULL or invalid pointerAborted (core dumped) ``` 接著我比較了自己的程式碼與 [willwillhi](https://hackmd.io/@willwillhi/lab0-2023) 的差別,發現我的 `swap` 函式需要考慮當交換元素是相鄰的情況,只需做一次 `list_del` 與 `list_add` 即可,最後我的 `q_shuffle` 的結果如下 ``` Expectation: 41666 Observation: {'1234': 41472, '1243': 41759, '1324': 41898, '1342': 41787, '1423': 41762, '1432': 41896, '2134': 41434, '2143': 41604, '2314': 41916, '2341': 41627, '2413': 41577, '2431': 41962, '3124': 41547, '3142': 41829, '3214': 41259, '3241': 41485, '3412': 41506, '3421': 41420, '4123': 41392, '4132': 41662, '4213': 41752, '4231': 41858, '4312': 42075, '4321': 41521} chi square sum: 24.648538376614027 ``` ### 亂數 Entropy 定義是某個隨機事件平均每個結果發生時所能傳達出的資訊量,公式如下 $$ S = - \sum_{i=1}^{n} P(x_i) log_bP(x_i) = - \sum_{i=1}^{n} P(x_i) I(x_i) $$其中 $$ I(x_i) = \log_b\left(\frac{1}{P(x_i)}\right) $$ $I(x_i)$為期望值,由上式可知,當一個事件的機率越低,所獲得的資訊量則越多 ## 論文〈Dude, is my code constant time?〉 ### INTRODUCTION 面對時序攻擊,舉例來說就是根據密碼的執行時間來推斷密碼,這時判斷執行時間是否是常數時間$O(1)$就十分重,因此本篇論文開發出一種工具 `dudect` ,一個透過統計分析的程式判斷程式是否是以常數時間執行,解決了底層硬體的問題,也就是無法獲得 CPU 運作訊息的問題 ### APPROACH `dudect` 所使用的方法是 `timing leakeage detection tests` ,首先測量兩種不同輸入數據類型的執行時間,然後檢查這兩種時間分佈是否在統計上有顯著差異,運用 fix-vs-random 產生兩種測試資料,第一種資料是常數,第二種資料是亂數,而第一種資料通常會給定一個較特別的值,來了解極端狀況 在統計分析前會先進行後處理如 Cropping: 從一組測量值中刪除或捨棄那些偏離正常值或超出 threshold 的測量值,減少不符合正常情況數據的影響,或是其他高階處理 接著是應用統計測試來嘗試反駁 null hypothesis: `the two timing distributions are equal` ,若是成功反駁此假說,則說明執行時間非常數時間,在本論文中使用 Welch’s t-test 來檢測兩個分佈是否相同 **以下為 Welch’s t-test 在本文應用的說明** 在進行 Welch’s t-test 時, $t$ 越接近 0 代表兩組數據的差異越小,在論文中 $t$ 的 threshold 為 4.5 , t 的定義如下 $$ t = \frac{\bar{X}_1 - \bar{X}_2}{\sqrt{\frac{\sigma_1^2}{n_1} + \frac{\sigma_2^2}{n_2}}} $$ - $\bar{X}_1$ 為 class1 採樣的執行時間的平均值 - $\bar{X}_2$ 為 class2 採樣的執行時間的平均值 - $n_1$ 為 class1 採樣的大小 - $n_2$ 為 class2 採樣的大小 - $\sigma_1$ 為 class1 採樣的執行時間的標準差 - $\sigma_2$ 為 class2 採樣的執行時間的標準差 ### 實作 在作業要求中有提到在 [oreparaz/dudect](https://github.com/oreparaz/dudect/blob/master/src/dudect.h) 的程式碼具備 percentile 的處理,但在 [lab0-c](https://github.com/sysprog21/lab0-c) 則無,因此實作目標為在 `lab0-c/dudect` 中新增此項功能,根據 [dudect](https://github.com/oreparaz/dudect/blob/master/src/dudect.h) 可以了解 `percentile` 的作用為設定閾值,也就是先前所說的後處理的技巧 `Cropping` ## 研讀 Linux 核心原始程式碼 list_sort.c