contributed by <Huadesuwa
>
$ uname -a
Linux hua-GV72-8RD 6.11.0-17-generic #17~24.04.2-Ubuntu SMP PREEMPT_DYNAMIC Mon Jan 20 22:48:29 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
$ gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ 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-8750H CPU @ 2.20GHz
CPU family: 6
Model: 158
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
Stepping: 10
CPU(s) scaling MHz: 33%
CPU max MHz: 4100.0000
CPU min MHz: 800.0000
BogoMIPS: 4399.99
commit: 5e8635d
目的
提供建立空佇列的統一介面
head
大小的記憶體空見(透過 malloc
)NULL
INIT_LIST_HEAD
初始化 struct list_head
記錄!:用Graphviz畫個list_head包裹的示意圖
commit: 5e8635d
目的
釋放佇列已配置的所有元素 ( element_t
) 記憶體空間,釋放的順序依序為字串陣列 ( value
指向的地址) 與佇列節點 ( element_t
) ,以及佇列 ( head
)
ERROR
原先的程式碼只會釋放 ( head
) 的記憶體空間,然而 head
就 但這是錯誤的,導致執行後仍有尚未釋放的記憶體空間被提醒錯誤:
There is no queue, but 4 blocks are still allocated.
改進
commit: aad9894
使用 Linux 核心 API list_for_each_safe
逐步走訪整個鏈結串列,並連帶釋放其記憶體空間:
+ if (!head)
+ return;
+ if (list_empty(head)) {
+ free(head);
+ return;
+ }
+ element_t *entry, *safe;
+ list_for_each_safe (entry, safe, head, list)
+ q_release_element(entry);
free(head);
commit: 0a7aebb
目的
element
) ,將給定的字符串複製至 value
element
) 插入至 head
與 head->next
之間head
與 head->prev
之間佇列使用的鏈結串列為雙向環狀鏈結串列
特殊狀況
malloc
申請配置失敗時: 返回falseNULL
: 返回 false
實作流程
value
指向字元陣列。ERROR
進行測試時,發現在佇列中新增兩個 element
後,當要釋放出去時,產生了錯誤,與記憶體分配有關。
ERROR
:Corruption detected in block with address 0x6271a022e290 when attempting to free it.
使用 Valgrind 來進一步調查,問題如下:
$ make valgrind
# Test of malloc failure on insert_head
==12268== Invalid write of size 1
==12268== at 0x10FE6F: q_insert_head (queue.c:58)
==12268== by 0x10CEDB: queue_insert (qtest.c:234)
==12268== by 0x10D0AC: do_ih (qtest.c:282)
==12268== by 0x10E74D: interpret_cmda (console.c:181)
==12268== by 0x10ED12: interpret_cmd (console.c:201)
==12268== by 0x10EFA1: cmd_select (console.c:593)
==12268== by 0x10F87F: run_console (console.c:673)
==12268== by 0x10DB3C: main (qtest.c:1444)
==12268== Address 0x0 is not stack'd, malloc'd or (recently) free'd
問題出現在使用 malloc
配置的記憶體空間是錯誤的,依照題目敘述,應當分配的是輸入 string
大小的記憶體空間才對。
調整為正確的記憶體分配:
commit: 7f06702
- char *str_copy = (char *) malloc(sizeof(char));
+ node->value = malloc(strlen(s) + 1);
commit: afefac4
目的
sp
中,能夠允許的最大長度為 buffsize - 1
並確保字串結尾總是 /0
。ERROR
執行 make test
發現的錯誤:
$ make test
CC queue.o
LD qtest
scripts/driver.py -c
--- Trace Points
+++ TESTING trace trace-01-ops:
# Test of insert_head and remove_head
--- trace-01-ops 5/5
+++ TESTING trace trace-02-ops:
# Test of insert_head, insert_tail, remove_head, remove_tail, and delete_mid
`ERROR`: Removed value gerbil != expected value bear
`ERROR`: Removed value meerkat != expected value gerbil
`ERROR`: Removed value bear != expected value meerkat
--- trace-02-ops 0/6
在測試 Remove
操作時,發現實際移除的節點與預期要移除的節點不一致。從 qtest
可見,rh
和 rt
命令的後續參數 str
是可選的。由此可推斷,錯誤的原因在於 rh
命令從佇列頭部移除節點時,結果與預期值不符。
在對 traces/trace-02-ops.cmd 中的命令進行圖形化呈現時,發現執行錯誤的原因是 delete_middle
功能尚未實作,導致整段命令無法正常運行。
只有正確刪除中間的節點時,第9行後的命令在執行時才會正確
insert of head & tail
remove tail - tiger
delete mid twice
insert tail - meerkat
rh dolphin、bear、bear、gerbil、meerkat
commit: e3ea447
目的
逐步走訪整個佇列,以統計目前佇列的元素數量
特殊狀況
若 head
指向 NULL
: 返回 0
實作流程
透過 list_for_each
逐步走訪整個佇列中的所有元素的,每當其與 head
地址不同,就將佇列大小加上 1
,直到再次碰見 head
。
commit: c424903
目的
刪除(含記憶體)佇列中第
特殊狀況
head
是 NULL
或空佇列:返回 false
實作流程
fast
、 slow
) 指向 head->next
、 head->next>next
作為起點,朝前移動。fast
)跑完一圈時,慢指標( slow
)正好會在中間(記錄!:以圖來表達快慢指標的前進方式
commit: aeb064c
目的
比較佇列元素與兩側的字串是否重複,如果重複就刪除。
特殊狀況
head
指向 NULL
或空佇列: 返回 false
list
就是 head
: 判斷字串是否重複,會用到下個元素的成員 value 。實作流程
list_for_each_entry_safe
的停止條件是當當前元素的 list
與 head
地址相同時。list_for_each_entry_safe (entry, safe, head, list) {
bool cur = (&safe->list != head && !strcmp(entry->value, safe->value));
// head -> A -> A -> A -> B -> C -> ... -> head
// head -> A(entry, last) -> A -> B -> C -> ... -> head
// head -> A -> B -> C -> ... -> head
if (cur || last) {
list_del(&entry->list);
q_release_element(entry);
last = cur;
}
}
commit: dbd0b19
目的:
以兩個元素為一組交換位置,完成後換下一組,直到下一個元素是未成對節點或是佇列的 head
。
特殊狀況:
head
指向 NULL
、空佇列或 singular
時: 直接返回。
實作流程:
list_for_each_entry_safe
的停止條件是當當前元素的 list
與 head
地址相同時。
safe
)的地址不是 head,否則結束。safe
),移到當前節點的前方 each->prev
safe -> each -> ...
safe
指向 each
後面繼續下一循環// head(each->prev) -> each -> safe -> ... -> head
// head -> safe -> each -> ... -> head
list_head *each, *safe;
list_for_each_safe (each, safe, head) {
if (safe != head) {
list_move(safe, each->prev);
safe = each->next;
}
}
commit: 5d1a49f
目的:
反轉佇列內所有元素的順序。
特殊狀況:
若 head
是 NULL
,或元素數量是 0
或者 1
: 直接返回。
實作流程:
我們知道只要最後一個元素的 list
地址 ( tail
),就能通過 list_for_each_safe
重複將節點移動到 tail
的下一個元素就好了。
// head -> A -> B -> ... -> Z(tail) -> head
// head -> Z(tail) <- ... <- B <- A <- head
const struct list_head *tail = head->prev;
for (each = head->prev, safe = each->prev; each != head;
each = safe, safe = each->prev) {
if (each == tail)
continue;
list_move_tail(each, head);
}
commit: e932a9a
void q_reverseK(struct list_head *head, int k)
{
// https://leetcode.com/problems/reverse-nodes-in-k-group/
int turn = q_size(head) / k;
list_head *front = head->next->next;
list_head *last = head->next;
for (size_t i = 0; i < turn; i++) {
list_head *ll = last->prev;
// Head -> A(last) -> B(front) -> C -> D -> E -> F -> Head
// Head <- B(front) <- A(last) <- Head
for (size_t j = 0; j < k; j++) {
last->next = last->prev;
last->prev = front;
front = front->next;
last = last->prev;
}
list_head *ff = last;
last = ll->next;
front = ff->prev;
last->next = ff;
ff->prev = last;
front->prev = ll;
ll->next = front;
last = ff;
front = ff->next;
}
}
commit: 1f0dbff
目的
透過刪除佇列中的元素達到升冪排列,並釋放被刪除節點所配置的記憶體空間。
特殊狀況
has a node with a strictly less value anywhere to the right side of it.
表示要有嚴格小於當前元素的值才需刪除當前節點。換句話說,若右邊的值與當前值相同,不用刪除。
2. 若 head
指向 NULL
或空佇列,直接返回 0
;若是單一元素佇列返回 1
。
實作流程
list_for_each_entry_safe
來逐步走訪整個佇列each
) 大於下一個節點( safe
) 就刪除 safe
。element_t *each, *safe;
list_for_each_entry_safe (each, safe, head, list) {
count++;
if (&safe->list != head && strcmp(each->value, safe->value) > 0) {
list_move(&safe->list, pending);
safe = each;
count--;
}
}
q_free(pending);
commit: 1f0dbff
目的:
要求前一個元素不可小於後面任一元素
特殊狀況:
若 head
指向 NULL
或空佇列,直接返回 0
;若是單一元素佇列返回 1
。
實作流程:
根據目的,我們應該從佇列的尾巴( prev
)開始,於迴圈中逐步向前搜尋。當目前節點( prev
)的值大於下一節點( prev->prev
),則移除 prev->prev
。
prev
為佇列的尾巴,以及 pending
用來保存即將被刪除的節點。prev->prev
)不是 head
prev->prev
)不是 head
,則透過 strcmp
比較其與目前節點( prev
)大小prev->prev
)小於目前節點( prev
),就將其移至 pending
並刪除;否則繼續向前移動。if (strcmp(list_entry(prev, element_t, list)->value,
list_entry(prev->prev, element_t, list)->value) > 0) {
pending = prev->prev;
list_del(pending);
element_t *str = list_entry(pending, element_t, list);
q_release_element(str);
} else {
prev = prev->prev;
}
commit: a19e22a
目的
該函式是 q_sort
與 q_merge
的輔助函式,專注於合併兩個佇列,並確保返回時,是第一個佇列擁有所有元素。
特殊狀況
在 qtest.c
中可以看到在合併佇列時,需要注意排序的穩定性:
ERROR: Not stable sort. The duplicate strings "%s\ are not in the same order.
實作流程
NULL
或空佇列 if (!ll1 || !ll2)
return q_size(ll1 ? ll1 : ll2);
// {ll1, ll2} = 2'b00, 2'b01, 2'b10
if (list_empty(ll1) || list_empty(ll2)) {
if (list_empty(ll1))
list_splice(ll2, ll1);
return q_size(ll1);
}
for
迴圈取得要合併的佇列元素,停止條件為當任一佇列為空時。
descend
判斷合併後佇列以升冪還是降冪排列strcmp
比較兩佇列內的元素結果:
0
,若兩佇列元素相等,則維持其輸入結果descend
,決定升冪或降冪排列 element_t *entry = list_first_entry(ll1, element_t, list);
element_t *safe = list_first_entry(ll2, element_t, list);
int cmp = strcmp(entry->value, safe->value);
element_t *next = !cmp ? entry
: (descend ? (cmp > 0 ? entry : safe)
: (cmp > 0 ? safe : entry));
list_move_tail(&next->list, &dummy);
commit: a19e22a
目的
排序給定佇列,順序依照 descend
的值決定。
特殊狀況
100, 000
個元素的測試。head
指向 NULL
、空佇列、或 singular
: 直接返回。實作流程
由於已經實作了 q_merge_two
用來合併兩個佇列。我們能基於此函式實作 merge sort
流程如下:
list_cut_position
將佇列的一半切給 left
right
則以 list_splice_init
獲取佇列剩餘的一半,並以 INIT_LIST_HEAD
初始化 head
singular
,此時會觸發特殊狀況(2) 返回q_merge_two
函式合併兩個佇列(left
、 right
) ,並以 descend
決定最終排列方式left
合併移至 head
裡,並返回commit: a19e22a
目的
將 head
所指向的佇列鏈 (queue chain) 中所有的佇列,按給定的規則 (升/降冪) 合併至該佇列鏈的第一個佇列中。佇列鏈中所有元素都被保證是依給定的規則排列。
特殊狀況
head
指向的是 NULL
或空佇列: 直接返回 0。 if (!head || list_empty(head))
return 0;
if (list_is_singular(head))
return q_size(list_first_entry(head, queue_contex_t, chain)->q);
實作流程
q_merge
可以使用 q_merge_two
函式將兩佇列給合併。
list_head *first = head->next;
queue_contex_t *first_q = list_entry(first, queue_contex_t, chain);
for (list_head *next = first->next; next != head; next = next->next) {
queue_contex_t *next_q = list_entry(next, queue_contex_t, chain);
count = q_merge_two(first_q->q, next_q->q, descend);
}
在本節中,預計達成以下目標:
shuffle
命令,使當前佇列進行洗牌。prng
選項,讓使用 ih RAND xx
時,使用者可選擇調用預設的 syscallPRNG
對 ih RAND
指令的影響,指標可使用香農熵 (Shannon entropy) 評估。以下先探討 lab0-c 如何新增命令,接著探討 Fisher–Yates shuffle 和 PRNG,並了解目前 lab0-c 預設的隨機字串是如何產生的。了解後研討如何設計相關介面來達成上述目標,最終透過香農熵輔以其他統計手段分析結果與差異性。
在 console.h
中,提供了 ADD_COMMAND
巨集用於新增命令,並對應至 add_cmd
函式,其宣告如下:
void add_cmd(char *name, cmd_func_t operation, char *summary, char *parameter);
#define ADD_COMMAND(cmd, msg, param) add_cmd(#cmd, do_##cmd, msg, param)
關鍵在於 operation
,透過連接運算子 ##
會自動展開為 do_<cmd>
。因此,一旦事先提供 do_*
函式,並在 console_init
中呼叫 add_cmd("<instruction>", <do_*>, "<documentation>")
,即可增加新命令!
cmd_func_t
的定義是:
typedef bool (*cmd_func_t)(int argc, char *argv[]);
命令的解析與執行由 interpret_cmd
負責,其中實際調用 do_*
的部分則由 interpret_cmda
處理。 interpret_cmda
會在 cmd_list
鏈結串列中搜尋對應的命令並執行。
Fisher-Yates 演算法最初於 1938 年提出,並在 1964 年針對計算機應用進行改良。隨後,它以 Algorithm P 的形式收錄於《 The Art of Computer Programming 》。該演算法的設計目標是確保所有排列結果的機率均勻且一致。
以長度為 n
的陣列 a
為例,演算法步驟如下:
i = n - 1
,並隨機產生索引 j
,範圍介於 0
到 i
之間,作為交換對象。a[i]
和 a[j]
,將選出的元素 a[i]
移至陣列尾端。i--
,然後重複步驟 1,直至 i < 0
為止。需要注意的是,Wiki 中的範例是以陣列為例進行說明。然而,在鏈結串列中,訪問第 n
個節點的時間複雜度為
熵可以這樣解釋:對於某個在
選擇該集合的方法是基於函數 $ \log∘P$(其中
因此,對於任意的
這是因為 獨立事件的機率是相乘的,如果對個別的機率取對數再取平均,則相乘關係轉換為加法,這剛好對應於從字元集
當資料表達過度簡化時,可能會導致解碼困難。例如,若將字母 a、b、c 均編碼為
因此,我們可以得出兩點結論:
這與熵的概念密切相關——熵描述的是最優壓縮的極限,而避免過度壓縮則確保了訊息能夠準確還原。
這個證明說明了兩點:
這 正是 Shannon entropy 的定義公式!
隨機數產生功能獨立封裝至 random.[ch]
,使其與主程式分離。這樣一來,q_shuffle
和隨機字串生成函式 fill_rand_string
皆可重複使用,減少程式碼冗餘。可選的實作方式目前包含預設方法與 Xorshift。此外,所有隨機數函式的接口應統一為:
typedef int (*rand_func_t)(uint8_t* buf, size_t len);
此接口與現有的 randombytes 保持一致(亦相容於 getrandom),提升兼容性,並允許未來擴展以支援不同的 PRNG 選項:
extern int randombytes(uint8_t *buf, size_t len);
const rand_func_t prng_func[] = {&randombytes, &xorshift, ...};
commit: 62b173f
Shuffle
commit: 41157a5
目的
對佇列中所有節點進行洗牌 (shuffle) 操作
實作方法
以 Fisher–Yates shuffle 演算法完成
同時,為確保 q_shuffle
能夠放在 queue.h
,更新了 chechsum.sh
中 queue.h
的 sha1sum
。
PRNG
commit: 62b173f
目前可透過 option prng 1
設定為 Xorshift,0
則為預設模式,使用 getrandom
。並未修改原程式中的 randombytes
,因此不受 prng
設定影響。該選項僅影響 fill_rand_string
和 q_shuffle
的行為。
這裡使用範例所提供的 python 程式碼進行實驗,分析 4 個元素進行 shuffle 是否符合 Uniform distribution,也就是分佈是平均的。首先提出須假說:
這裡是做了 1000000 次 shuffle 後,所得到的期望值與卡方值:
Expectation: 41666
Observation: {'1234': 41741, '1243': 41591, '1324': 41783, '1342': 41839, '1423': 41720, '1432': 41518, '2134': 41748, '2143': 41752, '2314': 41360, '2341': 41598, '2413': 41552, '2431': 41628, '3124': 41767, '3142': 41554, '3214': 41992, '3241': 41523, '3412': 41662, '3421': 41653, '4123': 41858, '4132': 41443, '4213': 41813, '4231': 41579, '4312': 41436, '4321': 41890}
chi square sum: 13.80046080737292
總共有
對於
從圖表來看 shuffle 的頻率是大致符合 Uniform distribution 的。
為了防範 Timing Attack,必須測試程式對不同輸入的執行時間穩定性,以避免資訊透過執行時間洩漏。傳統的分析方式通常需要對硬體建模,然而這並不容易。因此,本文提出了一種基於洩漏測試與統計分析的方法,以評估執行時間的穩定性。
這種方法透過統計分析來評估 Timing Attack 風險,避免繁瑣的硬體建模,並提供更直觀的評估方式。
在 trace-17-complexity
中,首先輸入 option simulation 1
,啟用該選項後,即可使用 GDB 進行動態分析。在 simulation 1
模式下,設置斷點於 interpret_cmda
,並執行 it
命令。
(gdb) break console.c:204
Breakpoint 1 at 0x6ae2: file console.c, line 205.
(gdb) run
cmd> option simulation 1
(gdb) continue
Continuing.
cmd> it
214 ok = next_cmd->operation(argc, argv);
程式會持續執行,直到執行 next_cmd->operation
,接著依序執行 do_it
和 queue_insert
。在 simulation 1
模式下,會呼叫 is_insert_tail_const
,
顯然,
is_insert_tail_const
的目的即在驗證q_insert_tail
的執行時間是否為。
在裡面會去呼叫 test_const
,第一個參數是 "insert_tail"
字串,第二個參數是 1
► 0x57fdbab0a13b <is_insert_tail_const+20> call test_const <test_const>
rdi: 0x57fdbab0d725 ◂— 'insert_tail'
rsi: 1
在 constrant.h
中,定義了:
#define DUT_FUNCS \
_(insert_head) \
_(insert_tail) \
_(remove_head) \
_(remove_tail)
#define DUT(x) DUT_##x
enum {
#define _(x) DUT(x),
DUT_FUNCS
#undef _
};
在 fixture.c
中定義了:
#define DUT_FUNC_IMPL(op) \
bool is_##op##_const(void) { return test_const(#op, DUT(op)); }
#define _(x) DUT_FUNC_IMPL(x)
DUT_FUNCS
#undef _
展開後等同於:
bool is_insert_head_const(void) { return test_const("insert_head", 0); }
bool is_insert_tail_const(void) { return test_const("insert_tail", 1); }
bool is_remove_head_const(void) { return test_const("remove_head", 2); }
bool is_remove_tail_const(void) { return test_const("remove_tail", 3); }
接下來是 doit
中呼叫的函式:
measure
:執行程式,測量開始與結束時間differentiate
:計算執行時間update_statistics
:更新各類別資料report
: 進行統計這裡來看看拿來統計的函數 t_push
和 t_compute
是怎麼計算的。
首先是 t_push
,這個平均數更新的方式實際上就是將每個變數減掉原本的平均數後,做平均,再把舊的平均數加回來,這樣的好處是原本的數在計算時能變 0:
double delta = x - ctx->mean[clazz];
ctx->mean[clazz] = ctx->mean[clazz] + delta / ctx->n[clazz];
ctx->m2[clazz] = ctx->m2[clazz] + delta * (x - ctx->mean[clazz]);
而 m2 指的就是離均差平方和
然後是 t_compute,可以發現,t 值是
double var[2] = {0.0, 0.0};
var[0] = ctx->m2[0] / (ctx->n[0] - 1);
var[1] = ctx->m2[1] / (ctx->n[1] - 1);
double num = (ctx->mean[0] - ctx->mean[1]);
double den = sqrt(var[0] / ctx->n[0] + var[1] / ctx->n[1]);
double t_value = num / den;
最終結果: 通過 trace-01-ops ~ trace-16-perf ,未通過 trace-17-complexity,得分: 95/100 (回歸測試結果)。
在 3/18 的課堂問答中,老師與 BennyWang1007
討論了本文及其程式碼,並提及在原先 lab0
專案的實作中,函式 percentiles
已被移除。
檢視 update_statistics
時可以發現,lab0
中的 update_statistics
並未執行 crop
和 second-order test
,但在 GitHub
上的專案中卻包含了這些步驟。此外,ttest_ctxs
在 GitHub
專案中包含多組資料,包括:
crop
預處理的數據(共 DUDECT_NUMBER_PERCENTILES
組)second-order test
的數據根據作業提示:
注意:現有實作存在若干致命缺陷,請討論並提出解決方案。
在oreparaz/dudect
的程式碼中具備percentile
處理,但在lab0-c
中則無。當你改進後,可提交pull request
,並務必提供對應的數學分析。
最初的實作將所有測量數據納入統計,但由於中斷(interrupt)、分頁錯誤(page fault)、I/O 延遲、CPU 排程等因素,可能會出現異常值,導致錯誤判定,使程式看起來不像常數時間(constant-time)實作。
為解決此問題,在測量初始化時新增了一個 percentiles[NUM_PERCENTILES]
陣列,用於儲存應捨棄的百分位數。隨著測量進行,我們應逐漸減少捨棄的數據,因此使用以下公式來判定是否納入統計:
其中,NUM_PERCENTILES
設為 100,得到以下曲線:
來源: 2025-03-18 問答簡記
**同樣地,我注意到 rota1001
和 weiso131
也進行了相關實驗與討論。
從結果來看,可以推斷 CPU cycle 較長的部分,其比例差異主要來自環境因素(如中斷(interrupt)、分頁錯誤(page fault)、I/O 延遲、CPU 排程等)。此外,由於兩者在 CPU cycle 較短的部分呈現明顯相似的分佈,因此我們可以透過 排除某個百分位數以上的數據來進行比較。
然而,若依照文中的方法,較大的差異將導致更高的 t 值,最終選取的 t 值 會對應到較高的 p 值,使得 右尾數據保留較多。因此,我選擇 取 50 百分位數作為閾值。在此情境下,即便是 do_nothing
也開始出現分叉,顯示 環境因素已經對執行時間產生影響。
由此可見,對異常值進行過濾是相當必要的。此外,藉由這次測試,我也成功獲得了一隻卡比。
首先,我們來理解 Linux 中的 list_sort
是怎麼做的:
陣列 pending
一開始是空的,每次 count+1
,即每新增一個元素時,count
會遞增。當 count
遞增至
隨著 count
進一步增長,當它達到
這樣的機制確保了合併與不合併始終維持 2:1 的比例,又可以避免 cache thrashing 的風險。以下舉例子: