contributed by < ihost1002
>
$ 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 phy
sical, 48 b
its virtual
Byte Order: Little Endi
an
CPU(s): 6
On-line CPU(s) list: 0-5
Vendor ID: GenuineInte
l
Model name: Intel(R) Co
re(TM) i5-8
400 CPU @ 2
.80GHz
CPU family: 6
Model: 158
Thread(s) per core: 1
Core(s) per socket: 6
Socket(s): 1
Stepping: 10
CPU(s) scaling MHz: 20%
CPU max MHz: 4000.0000
CPU min MHz: 800.0000
BogoMIPS: 5599.85
註: Ubuntu使用的是 24.04.2-LTS
C99 ( ISO/IEC 9899:1999
):
pdf 版本: N1256
網頁版本: N1256
註: pdf 版本來自你所不知道的C語言:開發工具和規格標準 ,打開後右上角顯示 ISO/IEC 9899:TC3
, 所以網頁版也是相同版本,由 google 搜尋關鍵字 n1256 html draft
。
分支
命名如下:
以 wip/
開頭後接 6
個分支名稱: alloc
,insert
,size
,delete
,sort
,queue
。
wip/alloc
:
此分支
存放 q_new()
, q_free()
兩個函式的 commit
。
wip/insert
:
此分支
存放 q_insert_head()
, q_insert_tail()
,q_remove_head()
, q_remove_tail()
四個函式的 commit
。
wip/size
:
此分支
存放 q_size()
一個函式的 commit
。
wip/delete
:
此分支
存放 q_delete_mid()
, q_delete_dup()
兩個函式的 commit
。
wip/sort
:
此分支
存放 q_sort()
, q_ascend()
,q_descend()
, q_swap()
, q_reverse()
, q_reverseK()
,q_merge()
七個函式的 commit
。
wip/queue
:
此分支
合併上述所有的 commit
。等到 16
個函式都完成且經測試沒問題,再合併到 master
。
另外下面程式碼實作對應的 commit 來自 wip/queue。 由於前述分支是透過其他分支 rebase 再 merge 而得,因此會在對應的原始分支看到相同的 commit 但可能是 相同/不同的 hash。個人理解是除了 wip/queue 以外的分支都是個人整理用,只對內給自己看,好處是可以馬上知道這個分支做了哪些修改。一旦確認實作正確,隨時可合併出新的 wip/queue 分支
概述: 此作業延續自 2024q1 Homework1 (lab0),由於自身基礎不足及私人原因,無法完成,這次再次嘗試撰寫,目標先訂為完成所有 make test
測試,即執行結果為 100/100
。目前結果為 95/95
, trace-017
不符要求: q_insert_head
、 q_insert_tail
、 q_remove_head
、 q_remove_tail
要求執行後為 constant time
,細節及個人想法在對應實作內容 q_insert_head
裡。另外所有的程式碼可能很醜,請老師及同學見諒。
q_size
計算目前佇列的節點總量。去年的紀錄: commit 2c3dcd6,這次用 list_for_each 重寫
+ /* Return zero if queue is NULL or empty */
- const struct list_head *count = head;
- while (count->next != head) {
- count = count->next;
+ const struct list_head *node = NULL;
+ list_for_each(node, head)
comit fd560e4
q_new
建立新的「空」佇列。去年的紀錄: commit 7eb7ee4、commit 97b841f、commit 498c33b。這次重寫,用到的內建函式用註解的方式說明,並說明 malloc 被 test_malloc 取代, commit message 只寫出應完成的結果, commit 86d2439
q_insert_head
在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則)。舊的紀錄: commit 360627d, 這次用輔助函式 q_insert 把實作內容寫在前者,並使用 function pointer: insert 依據 position 來判斷呼叫 list_add / list_add_tail ,
/*
* Define pointer to function of type
* void (*)(struct list_head *, struct list_head *)
* refer to list_add(), list_add_tail().
*/
+typedef void (*list_insert)(struct list_head *node, struct list_head *head);
+
/* Do q_insert* operation */
+static inline void insert_operation(list_insert operation,
+ struct list_head *node,
+ struct list_head *head)
+{
+ operation(node, head);
+}
/* Helper function of q_insert_* */
+static inline bool q_insert(struct list_head *head, char *s, bool position)
+{
+ ...
+ list_insert insert = position ? list_add : list_add_tail;
+ insert_operation(insert, &element->list, head);
+ ...
+}
bool q_insert_head(struct list_head *head, char *s)
{
- return true;
+ return q_insert(head, s, 1);
}
bool q_insert_tail(struct list_head *head, char *s)
{
- return true;
+ return q_insert(head, s, 1);
}
commit 29b3593 。前述實作無法通過 constant time 測試,當時第一個想到的是那篇論文 〈Dude, is my code constant time?〉,英文不好,所以我先參考了 Holy 同學整理的 論文閱讀:Dude, is my code constant time? 知道為 timing attack。 駭客會透過比較字串的時間長短而推論出正確的字串,我當時理解成這個想法:假設一個平行宇宙中,大家用某種餐點的時間都固定,然後一次只能吃一種餐點且用餐期間不會做其他事。大雄喜歡宜靜,想知道她喜歡吃什麼,接著宜靜走進某間餐館吃飯:假如咖喱飯 20 分鐘、 涼麵 15 分、其他餐點…等,那大雄在外面紀錄宜靜用餐的時間不就知道她喜歡吃什麼了嗎?那假如宜靜不想讓別人知道,她只要用餐完後做些其他的事情佔用時間,這樣大雄不就無法得知靜香到底吃了什麼! 回到實作內容,想法是讓每次複製字串的時間都相同,要做到這件事需要知道最大輸入的字串長度,經過測試 (2025/03/19) , qtest 上最大輸入的字串長度為 4092, 由於 4096 為 2 的倍數,所以我以前述數字為基準。實作時先用以下方法: s 字串複製到 element_t->value, 另外複製 4096 減去前述字串長度,完成複製總字元長度為至少 4096 個字元
+ /* Fake copy */
+ char fake_string[4096] = "1";
+ fake_string[4096 - length - 1] = '\0';
+ char fake_copy[4096] = {0};
+ strncpy(fake_copy, fake_string, 4096 - length);
commit 10b23d7,但仍無法通過 constant time 測試。後來想到了,可能是複製到 element_t->value 才行,改成
- char fake_string[4096] = "1";
- fake_string[4096 - length - 1] = '\0';
- char fake_copy[4096] = {0};
- strncpy(fake_copy, fake_string, 4096 - length);
+ const char *fake_string = "1";
+ int goal = (4096 - length) >> 1;
+ for (int i = 1; i < goal; i++)
+ strncpy(element->value, fake_string, 2);
commit b5c1e0b, 但是 option simulation 1 測試時出現以下錯誤 ERROR message: Corruption detected in block with address 0x???????????? when attempting to free it.
。 這是由於修改到 MAGICFOOTER 導致的錯誤,只把額外宣告的字元複製到 element_t->value 造成前者錯誤發生,這讓我產生疑惑,難道只能複製 s 的內容到前述 value 中?因此我重複複製 s 內容到 elemet_t->value,並確保至少複製總共 4096 個字元
- /* Fake copy */
- const char *fake_string = "1";
- int goal = (4096 - length) >> 1;
- for (int i = 1; i < goal; i++)
- strncpy(element->value, fake_string, 2);
+ /*
+ * Copy Strings to element_t->value and make sure to copy at least
+ * 4096 characters in length.
+ */
+ for (int i = 0; i <= 4096; i += length)
+ strncpy(element->value, s, length);
commit f0bb601,誤打誤撞通過 constant time 測試,後來以為成功就做成紀錄,結果 trace 17 之前的效能測試失敗了。我應確實了解 〈Dude, is my code constant time?〉及 commit b5c56e0 但沒做到。
q_insert_tail
在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則)。舊的紀錄: commit ee66c58,其餘參考 q_insert_head
q_free
釋放佇列所佔用的記憶體。舊的紀錄: commit f24e239 是錯的,修正後可執行
struct list_head *q = head->next;
- element_t *t = NULL;
while (q->next != head) {
struct list_head *r = q->next;
- free(q);
+ INIT_LIST_HEAD(q);
+ t = container_of(q, element_t, list);
+ free(t->value);
+ free(t);
q = r;
}
- free(q);
+ INIT_LIST_HEAD(q);
+ t = container_of(q, element_t, list);
+ free(t->value);
+ free(t);
free(head);
commit 4202d34 。這次用內建 list_for_each_safe 走訪所有節點 、 INIT_LIST_HEAD 斷開鏈結 、 q_release_element 釋放對應記憶體, commit d80404d
q_remove_head
在佇列開頭 (head) 移去 (remove) 給定的節點。舊的紀錄: commit 48ba2b7, 含 q_remove_tail 實作, 除了移除節點的是 頭 或 尾 其餘是重複的程式碼。後來跑 traces/trace-xx 的時候發現錯誤,修正後
- removed_e->value[bufsize - 1] = '\0';
+ size_t str_n = strlen(removed_e->value);
+ size_t size = str_n <= bufsize ? str_n + 1 : bufsize;
+ if (str_n > bufsize) {
+ removed_e->value[size - 1] = '\0';
+ }
- strncpy(sp, removed_e->value, bufsize);
+ strncpy(sp, removed_e->value, size);
- removed_e->value[bufsize - 1] = '\0';
+ size_t str_n = strlen(removed_e->value);
+ size_t size = str_n <= bufsize ? str_n + 1 : bufsize;
+ if (str_n > bufsize) {
+ removed_e->value[size - 1] = '\0';
+ }
- strncpy(sp, removed_e->value, bufsize);
+ strncpy(sp, removed_e->value, size);
commit 97fee47。
這次重寫,使用輔助函式 q_remove 避免重複相同的程式碼及 position 判斷是移出 頭 或 尾端節點
/* Helper function of q_remove_* */
+static inline element_t *q_remove(struct list_head *head,
+ char *sp,
+ size_t bufsize,
+ bool position)
+{
+ /* Set terminating char */
+ size_t string_length = strlen(element->value);
+ size_t size = string_length <= bufsize ? (string_length + 1) : bufsize;
+ if (string_length > bufsize)
+ element->value[size - 1] = '\0';
+ /* Copy string:value of element into sp */
+ strncpy(sp, element->value, size);
+}
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
- return NULL;
+ return q_remove(head, sp, bufsize, 1);
}
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
- return NULL;
+ return q_remove(head, sp, bufsize, 0);
}
commit 2034b7f
q_remove_tail
在佇列尾端 (tail) 移去 (remove) 給定的節點。參考 q_remove_head
q_delete_mid
移走佇列中間的節點。舊的紀錄: commit b2190a3。 這次參考你所不知道的 C 語言: linked list 和非連續記憶體 其中快慢指標技巧重寫, commit cdcf471。
q_delete_dup
在已經排序的狀況,移走佇列中具備重複內容的節點。舊紀錄: commit eb8ac29。 本次些微修改變數名稱及使用內建函式 q_release_element 重寫, commit ec3fa27。使用 indirect pointer 好處在移除節點時不用更新節點內容
q_swap
交換佇列中鄰近的節點。舊紀錄: commit e592eb6, 當初是想用 xor 來交換數值,但發現無法使用 unitptr_t, 所以就自建輔助程式 swap 來交換每個節點對應的 next 、 prev 的值,現在來看寫得很醜,可能會看不懂。這次重寫發現其功能等同呼叫 q_reverseK 其 k 為 2 ,後續參考 q_reverseK
q_reverse
以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點。與 q_reverseK 合併,功能等於前述函式帶入 k = 總節點數,參考 q_reverseK
q_reverseK
類似 q_reverse,但指定每 k 個節點為一組,進行反向順序排列。舊紀錄: commit 29763b4。也有使用 commit e592eb6 的 swap, 所以很醜。本次與 q_swap 、 q_reverse 合併,用 list_move 、 list_move_tail 完成節點交換, commit a1d7692。 使用 indirect pointer 好處在搬移節點時不用更新節點內容
q_sort
以遞增/遞減順序來排序鏈結串列的節點。 本來想自己想一個符合時間複雜度 O(nlogn) 的實作,但結果是怎樣想都只想出 O(n2) , commit f9001a0,不得已只好參考 你所不知道的 C 語言: linked list 和非連續記憶體 其中 遞迴方式 且 top-down 的 merge sort 實作,但沒有真懂。用到自建的輔助函式 merge_two_list 來合併兩個佇列, merge_sort_list 遞迴呼叫自己並在把佇列從中切兩半,後半用 mid 作佇列頭,再遞迴呼叫自己完成 merge sort 實作, commit af4ec1d
q_ascend
element_t
結構有兩個成員: value
、 list
, 每個節點都是前述結構中的 list
,而 value
儲存字串
。對每個節點 n1 ,其右邊節點 n2 對應的字串
與自身字串
相比為小
時刪除此節點 n1。舊紀錄: commit c58a2f4 含 q_descend 實作。本次自建輔助函式 q_delete_strictly 取代重複的程式碼,作法是從最後的節點往前找,帶入 descend 決定是 q_ascend 還是 q_descend, commit ff9b18d
q_descend
與 q_ascend 相似,比較結果為大
時刪除節點 n1。參考 q_ascend
q_merge
合併所有已排序的佇列,並合而為一個新的已排序佇列。目前是一個可執行的實作,但是當合併越多時越沒效率,commit c68ad41
進行中