contributed by <fcu-D0812998>
~$ gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
~$lscpu
架構: x86_64
CPU 作業模式: 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 6
On-line CPU(s) list: 0-5
供應商識別號: GenuineIntel
Model name: Intel(R) Core(TM) i5-9500 CPU @ 3.00GHz
CPU 家族: 6
型號: 158
每核心執行緒數: 1
每通訊端核心數: 6
Socket(s): 1
製程: 10
CPU(s) scaling MHz: 61%
CPU max MHz: 4400.0000
CPU min MHz: 800.0000
BogoMIPS: 6000.00
Create an empty queue whose next and prev pointer point to itself
Return: NULL for allocation failed
struct list_head *q_new()
{
struct list_head *new = malloc(sizeof(struct list_head));
if (!new)
return NULL;
INIT_LIST_HEAD(new);
return new;
}
使用 malloc
動態分配記憶體來建立一個 struct list_head
節點,並初始化其 next 和 prev 指向自身,形成一個空佇列。
去規格書參考有關malloc相關的資料,看到
- 7.20.3 Memory management functions
If the space cannot be allocated, a null pointer is returned.
這跟函式規則Return: NULL for allocation failed剛好一致,所以無需判斷。
Free all storage used by queue, no effect if header is NULL
先判斷queue中有無節點,有節點的話利用list_for_each_entry_safe依序刪除往後的節點,迴圈出來後再free head的空間。
void q_free(struct list_head *head)
{
if (!head) {
return;
}
element_t *entry = NULL, *safe;
list_for_each_entry_safe (entry, safe, head, list) {
list_del(&entry->list);
q_release_element(entry);
}
free(head);
}
Insert an element in the head.
Argument s points to the string to be stored.
The function must explicitly allocate space and copy the string into it.
Return: true for success, false for allocation failed or queue is NULL
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *new = malloc(sizeof(element_t));
if (!new)
return false;
int len = strlen(s) + 1;
new->value = malloc(len);
if (!(new->value)) {
free(new);
return false;
}
memcpy(new->value, s, len);
list_add(&new->list, head);
return true;
}
先利用 strlen(s)
取得s的字串長度,再利用 malloc 分配出等成的空間給字串s,再把s的資料複製到new中,再利用 list_add
插在queue的最前端。
然後根據 C99 規格書中有關 string length 的敘述。
The length of a string is the number of bytes preceding the null character
根據規格書中的說明,在C語言當中,字串的長度不包含結尾的 \0。
再去看看規格書中說明strlen的部分。
The strlen function returns the number of characters that precede the terminating null character
這段說明strlen
返回的是 null 字符 (\0)
之前的字元數量,所以如果我們需要正確儲存一個C語言字串,應該分配 strlen(s) + 1
,確保有額外空間存放 \0
。
接下來要將字串複製到new中,根據規格書中7.21.2.4
章節說明如果 src
的前 n 個字元中沒有 \0
,則結果不會有null
終止符,這樣 new_entry->value
可能變成未終止的字串,在後續使用時可能導致未定義行為,因此選擇使用memcpy
The strncpy function copies not more than n characters (characters that followanull character are not copied) from the array pointed to by s2 to the array pointed to by s1. If copying takes place between objects that overlap, the behavior is undefined.
最後再利用list_add
把新的 new 加入到 list 中。
Attempt to remove element from tail of queue.
Other attribute is as same as q_remove_head.
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *new = malloc(sizeof(element_t));
if (!new)
return false;
size_t len = strlen(s) + 1;
new->value = malloc(len);
if (!(new->value)) {
free(new);
return false;
}
memcpy(new->value, s, len);
list_add_tail(&new->list, head);
return true;
}
這邊想法跟q_insert_head
一樣,差在最後要使list_add_tail
確保值插入在尾端
Attempt to remove element from head of queue.
Return target element.
Return NULL if queue is NULL or empty.
If sp is non-NULL and an element is removed, copy the removed string to *sp
(up to a maximum of bufsize-1 characters, plus a null terminator.)
NOTE: "remove" is different from "delete"
The space used by the list element and the string should not be freed.
The only thing "remove" need to do is unlink it.
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *remove_node = list_first_entry(head, element_t, list);
list_del(&remove_node->list);
if (sp) {
memcpy(sp, remove_node->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
return remove_node;
}
利用list_first_entry
先取得queue的第一個element的地址且因為是remove所以要將移除點的直放進sp中且最多只複製bufsize - 1
的字元,因為超過就無法補\0
,且最後也需要手補\0
來表示結束。
Attempt to remove element from tail of queue.
Other attribute is as same as q_remove_head.
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *remove_node = list_last_entry(head, element_t, list);
list_del(&remove_node->list);
if (sp) {
memcpy(sp, remove_node->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
return remove_node;
}
跟q_remove_head
一樣,除了前面一開始要使用list_last_entry
。
Return number of elements in queue.
Return 0 if q is NULL or empty
int q_size(struct list_head *head)
{
if (!head || list_empty(head))
return 0;
struct list_head *node;
int size = 0;
list_for_each (node, head)
size++;
return size;
}
list_for_each
會保證完整遍歷鏈結串列,size
就是鏈結串列內的節點數。
Delete the middle node in list.
The middle node of a linked list of size n is the
⌊n / 2⌋th node from the start using 0-based indexing.
If there're six element, the third member should be return.
Return true if successful.
Return false if list is NULL or empty.
bool q_delete_mid(struct list_head *head)
{
// https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/
if (!head || list_empty(head))
return 0;
struct list_head *slow = head->next, *fast = slow->next;
while (fast != head && fast->next != head) {
slow = slow->next;
fast = fast->next->next;
}
if (fast != head)
slow = slow->next;
list_del(slow);
q_release_element(list_entry(slow, element_t, list));
return true;
}
使用快慢指標找出中間的位置再將此節點刪除。
Delete all nodes that have duplicate string,
leaving only distinct strings from the original list.
Return true if successful.
Return false if list is NULL.
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;
element_t *entry = NULL, *safe;
bool dup = false;
list_for_each_entry_safe (entry, safe, head, list) {
if (entry->list.next != head &&
strcmp(entry->value,
list_entry(entry->list.next, element_t, list)->value) == 0) {
dup = true;
list_del(&entry->list);
q_release_element(entry);
} else if (dup) {
list_del(&entry->list);
q_release_element(entry);
dup = false;
}
}
return true;
}
因為要刪除節點所以使用list_for_each_entry_safe
,利用strcmp
判斷entry和下一個節點的 value是否相同,直到判斷到不同,再將entry的list全部刪除掉,此外這邊在尚未將entry初始直射成NULL之前出現了這樣的錯誤
Attempt to swap every two adjacent nodes.
void q_swap(struct list_head *head)
{
// https://leetcode.com/problems/swap-nodes-in-pairs/
if (!head || list_empty(head))
return;
struct list_head *node = head->next;
while (node != head && node->next != head) {
struct list_head *node2 = node->next;
list_del(node);
list_add(node, node2);
node = node->next;
}
}
想法是先設node跟node2為頭兩個節點,先斷開第一個節點,再將node2插入到node前面,這樣就完成了 相鄰節點的交換。
Reverse elements in queue
No effect if q is NULL or empty
This function should not allocate or free any list elements
(e.g., by calling q_insert_head, q_insert_tail, or q_remove_head).
It should rearrange the existing ones.
void q_reverse(struct list_head *head)
{
if (!head || list_empty(head))
return;
struct list_head *pos, *safe;
list_for_each_safe (pos, safe, head)
list_move(pos, head);
}
使用list_move
從第一個節點開始將每個節點插入在head後面,這樣就完成反轉。
Given the head of a linked list, reverse the nodes of the list k at a time.
No effect if queue is NULL or empty. If there has only one element, do nothing.
void q_reverseK(struct list_head *head, int k)
{
// https://leetcode.com/problems/reverse-nodes-in-k-group/
if (!head || list_empty(head))
return;
struct list_head *pos, *safe, *cut = head;
int count = 0;
list_for_each_safe (pos, safe, head) {
count++;
if (count % k == 0) {
LIST_HEAD(tmp);
count = 0;
list_cut_position(&tmp, cut, pos);
q_reverse(&tmp);
list_splice(&tmp, cut);
cut = safe->prev;
}
}
}
利用count
做記數,當count % k == 0
的話就能確保k個一組做反轉,先用list_cut_position
把要反轉的節點剪切出來,存入 tmp,再利用剛剛q_reverse
做反轉,再用list_splice
把它存回去。
Remove every node which has a node with a strictly less value anywhere to the right side of it.
Remove every node which has a node with a strictly greater value anywhere to the right side of it.
No effect if queue is NULL or empty. If there has only one element, do nothing.Memory allocated to removed nodes must be freed.
int q_ascend(struct list_head *head)
{
// https://leetcode.com/problems/remove-nodes-from-linked-list/
if (!head || list_empty(head))
return 0;
element_t *right = list_entry(head->prev, element_t, list);
element_t *left = list_entry(head->prev->prev, element_t, list);
while (&left->list != head) {
if (strcmp(right->value, left->value) < 0) {
list_del(&left->list);
element_t *tmp = left;
left = list_entry(left->list.prev, element_t, list);
free(tmp->value);
free(tmp);
} else {
left = list_entry(left->list.prev, element_t, list);
right = list_entry(right->list.prev, element_t, list);
}
}
return q_size(head);
}
利用strcmp
比較最後兩個節點的大小,若left大於right的話,就用list_del()
從 queue 中移除 left,但不會釋放記憶體,然後再用tmp
暫存 left 指向的節點,這樣我們可以稍後 free(tmp)
,再將left往前指到前一個節點。
int q_descend(struct list_head *head)
{
// https://leetcode.com/problems/remove-nodes-from-linked-list/
if (!head || list_empty(head) || list_is_singular(head))
return 0;
element_t *right = list_entry(head->prev, element_t, list);
element_t *left = list_entry(head->prev->prev, element_t, list);
while (&left->list != head) {
if (strcmp(right->value, left->value) > 0) {
list_del(&left->list);
free(left->value);
free(left);
left = list_entry(right->list.prev, element_t, list);
} else {
left = list_entry(left->list.prev, element_t, list);
right = list_entry(right->list.prev, element_t, list);
}
}
return q_size(head);
}
descend 一樣的原理只是將strcmp
中改成大於。
Sort elements of queue in ascending/descending order.
No effect if queue is NULL or empty. If there has only one element, do nothing.
void _list_merge(struct list_head *left, struct list_head *right, bool descend)
{
struct list_head head;
INIT_LIST_HEAD(&head);
while (!list_empty(left) && !list_empty(right)) {
int cmp = strcmp(list_first_entry(left, element_t, list)->value,
list_first_entry(right, element_t, list)->value);
if ((descend && cmp > 0) || (!descend && cmp <= 0)) {
list_move_tail(left->next, &head);
} else {
list_move_tail(right->next, &head);
}
}
if (!list_empty(right))
list_splice_tail(right, &head);
list_splice(&head, left);
}
void q_sort(struct list_head *head, bool descend)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *mid = head;
int size = q_size(head) / 2;
for (int i = 0; i < size; i++)
mid = mid->next;
struct list_head left, right;
INIT_LIST_HEAD(&left);
INIT_LIST_HEAD(&right);
list_cut_position(&left, head, mid);
list_cut_position(&right, head, head->prev);
q_sort(&left, descend);
q_sort(&right, descend);
_list_merge(&left, &right, descend);
list_splice(&left, head);
}
參考了老師放在作業要求上的Linked List Sort知道了merge_sort
的複雜度為 O(nlogn)
為所有排序法中複雜度最低的,所以想法是實作merge sort,首先先用list_cut_position
且在用遞迴的方式將queue切成一個一個,再另外寫一個函式 _list_merge
主要是判斷傳入的兩個字串比較大小,再用把list_splice
把head 內的排序結果合併到 left,讓 left 變成合併後的完整 queue。
int q_merge(struct list_head *head, bool descend)
{
// https://leetcode.com/problems/merge-k-sorted-lists/
if (!head || list_empty(head))
return 0;
else if (list_is_singular(head))
return q_size(list_first_entry(head, queue_contex_t, chain)->q);
queue_contex_t *tmp = NULL,
*first = list_first_entry(head, queue_contex_t, chain);
list_for_each_entry (tmp, head, chain) {
if (tmp && tmp->q && tmp != first) {
list_splice_tail_init(tmp->q, first->q);
first->size += tmp->size;
tmp->size = 0;
}
}
q_sort(first->q, descend);
return first->size;
}
想法是將要merge 的 queue 全都塞進一個queue 裡,再呼叫剛剛寫好的q_sort
去做排序。
看了老師在作業放的Fisher–Yates shuffle 演算法
-- To shuffle an array a of n elements (indices 0..n-1):
for i from 0 to n−2 do
j ← random integer such that i ≤ j ≤ n-1
exchange a[i] and a[j]
bool q_shuffle(struct list_head *head)
{
if (!head || list_empty(head))
return false;
srand(time(NULL));
struct list_head *cur = head->next, *tail = head->prev;
for (int i = q_size(head); i > 0; i--) {
int rd_num = rand() % (i);
for (int j = 0; j < rd_num; j++) {
cur = cur->next;
}
struct list_head *tmp = cur->next;
list_move_tail(cur, tail);
tail = tail->prev;
cur = tmp;
}
return true;
}
想法很簡單,就先用兩個指標指到queue的頭尾,頭代表每次取到隨機數字就可以從頭開始往下指,尾的部分就是交換後完再將尾的部分往前指一個,並在qtest.c
加入shuffle
命令。
ADD_COMMAND(shuffle,
"Shuffle the queue with Fisher–Yates shuffle algorithm", "");
並在qtest.c
新增do_shuffle()
測試
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;
}
if (exception_setup(true))
q_shuffle(current->q);
exception_cancel();
q_show(0);
return !error_check();
}
後來還是測不過,發生錯誤
[!] You are not allowed to change the header file queue.h or list.h
詢問同學h0w726,才知道需要加入標頭檔。
bool q_shuffle(struct list_head *head);
後來先將 test_count 降到1000次先觀察每種結果各自的頻率,發現只有某四種結果有出現過,後來發現是 srand(time(NULL)) 的問題,是因為當你在極短時間內重複呼叫到srand(time(NULL))
,那麼 time(NULL)
傳回的值是一樣的(精度只有秒),你等於執行了多次 srand(相同的值)
,導致後續rand()
產生的亂數序列也一樣。
在測試 shuffle 1000000 次後,每種結果各自的頻率如下表:
觀察到的頻率 | 期待的頻率 | ||
---|---|---|---|
[1, 2, 3, 4] | 41526 | 41666 | 0.47040752652042434 |
[1, 2, 4, 3] | 41810 | 41666 | 0.497671962751404 |
[1, 3, 2, 4] | 41780 | 41666 | 0.3119089905438487 |
[1, 4, 2, 3] | 41522 | 41666 | 0.497671962751404 |
[1, 4, 3, 2] | 42020 | 41666 | 3.0076321221139537 |
[2, 1, 3, 4] | 41352 | 41666 | 2.3663418614697833 |
[2, 1, 4, 3] | 41724 | 41666 | 0.08073729179666875 |
[2, 3, 1, 4] | 41519 | 41666 | 0.5186242979887679 |
[2, 3, 4, 1] | 41435 | 41666 | 1.2806844909518553 |
[2, 4, 1, 3] | 41776 | 41666 | 0.2904046464743436 |
[2, 4, 3, 1] | 41520 | 41666 | 0.5115921854749677 |
[3, 1, 2, 4] | 41725 | 41666 | 0.08354533672538761 |
[3, 1, 4, 2] | 41351 | 41666 | 2.381438103009648 |
[3, 2, 1, 4] | 41727 | 41666 | 0.08930542888686219 |
[3, 2, 4, 1] | 41968 | 41666 | 2.1889310228963663 |
[3, 4, 1, 2] | 41685 | 41666 | 0.00866413862621802 |
[3, 4, 2, 1] | 41603 | 41666 | 0.09525752412038592 |
[4, 1, 2, 3] | 41779 | 41666 | 0.306460903374454 |
[4, 1, 3, 2] | 41227 | 41666 | 4.625378006048097 |
[4, 2, 1, 3] | 41592 | 41666 | 0.13142610281764508 |
[4, 2, 3, 1] | 41920 | 41666 | 1.5484087745403927 |
[4, 3, 1, 2] | 41957 | 41666 | 2.0323765180242885 |
[4, 3, 2, 1] | 41604 | 41666 | 0.09225747611961792 |
Total | 23.417126674026782 |
Side-channel attack 是一種不是針對密碼學演算法本身的破解方式,而是利用程式或硬體實作中非預期的特徵,來側面推測機密資訊。非預期的特徵就像是 Timing Attack,它會透過觀察程式執行所花的時間差異,來分析輸入資料是否接近正確。
比方說,某段程式會一個字元一個字元地比對密碼,只要遇到不符就立即結束並回傳錯誤。
攻擊者可以藉由大量嘗試,觀察執行時間的長短,就可能一步步推敲出正確密碼的內容。
為了對抗這種攻擊,現在已經有一些工具可以用來檢測程式是否有時間上的漏洞,像是
但這些工具有個共通限制,它們需要對底層硬體(如 CPU)做詳細建模。而這在實務上很困難,因為晶片製造商往往不會公開這些內部細節。
為了解決這個方法這篇論文就開發了一套叫做 dudect 的工具,它不透過靜態分析,而是直接執行程式,並大量蒐集執行時間,再用統計方法分析差異。
這種方式稱作 leakage detection test,它只需要在目標平台上執行就可以使用,完全不需要了解或模擬硬體的細節。因此 dudect 特別適合用來驗證程式在實際環境下是否為 constant-time,不受限於特定硬體或理論模型。
方法部分主要分成三種
當我們在進行統計檢定時,通常會用常態分布來估計資料的行為,但這只有在樣本數夠多的情況下才適用。當樣本數較小時,資料本身的變異性較大,這時使用常態分布可能會低估極端值出現的機率,導致檢定結果不夠保守。為了解決這個問題,就會使用 Student’s t-distribution(學生 t 分布)。t 分布與常態分布長得類似,都是中間高、兩側低的鐘形曲線,不同的是 t 分布在自由度較小時,兩側尾巴會更厚,表示更高的極端值機率,能反映小樣本時的不確定性。隨著樣本數增加,t 分布會漸漸趨近標準常態分布。這種分布通常會用在像 t-test 這類的假設檢定中,特別是當我們要比較兩組平均值是否顯著不同,而又不知道母體標準差時。在 dudect 這類工具中,Student’s t-distribution 被用來統計分析「固定輸入」與「隨機輸入」在執行時間上的差異,進而判斷一段程式是否具備 constant-time 性質,避免時間洩漏發生。
在 lab0-c 中,simulation 模式被設計來測試 queue 函式的時間複雜度是否為constant-time。就分成剛剛上面提及的三步驟來做分析
/dudect/fixture.c
可以看到exec_times[i] = after_ticks[i] - before_ticks[i];
這段程式會把每次操作的「結束時間 - 開始時間」算出來,記錄成每一次操作的實際執行時間,再來丟進下面一點的程式碼中
int64_t difference = exec_times[i];
t_push(t, difference, classes[i]);
把所有執行時間資料根據類別(固定or隨機)分組後,餵進 t-test 所需的統計計算中
/*
set different thresholds for cropping measurements.
the exponential tendency is meant to approximately match
the measurements distribution, but there's not more science
than that.
*/
static void prepare_percentiles(dudect_ctx_t *ctx) {
qsort(ctx->exec_times, ctx->config->number_measurements, sizeof(int64_t), (int (*)(const void *, const void *))cmp);
for (size_t i = 0; i < DUDECT_NUMBER_PERCENTILES; i++) {
ctx->percentiles[i] = percentile(
ctx->exec_times, 1 - (pow(0.5, 10 * (double)(i + 1) / DUDECT_NUMBER_PERCENTILES)),
ctx->config->number_measurements);
}
}
先利用 q_sort 將執行時間排序從小到大,會利用 cmp 去比較執行時間大小
static int cmp(const int64_t *a, const int64_t *b) {
if (*a == *b)
return 0;
return (*a > *b) ? 1 : -1;
}
percentile 則是利用來判斷在這個百分比下對應到的是排序好的執行時間陣列裡的哪一個地方
static int64_t percentile(int64_t *a_sorted, double which, size_t size) {
size_t array_position = (size_t)((double)size * (double)which);
assert(array_position < size);
return a_sorted[array_position];
所以這邊就要來在 lab0 中實作 percentile 的功能,先把 cmp 跟 percentile 這兩個 function 放入 lab0-c/dudect/ttest.c 以方便 fixture.c 呼叫。
而在 fixture.c 中在 doit 中放入 prepare_percentiles 函式讓他能避免掉極端值影響。
Step 3: Apply statistical test
最後在 t_compute() 中利用公式
分子為 兩組輸入的執行時間平均值
分母為 「標準誤差(standard error)」,是衡量兩組平均值的差異有多可靠的依據
而根據論文中提及
A t value larger than 4.5 provides strong statistical evidence that the distributions
are different.
當 t 值絕對值大於 4.5,表示兩個輸入類別的執行時間分布統計上有顯著差異