# 2024q1 Homework1 (lab0)
contributed by < `Vincent550102` >
### Reviewed by `weihsinyeh`
1. Fix some implement mistake [commit 66d7745](https://github.com/sysprog21/lab0-c/commit/66d774537544ab71133c3686650ffbf5eeafe6e7) 可以將具體修正寫在在 commit 的描述中。
2. [commit cdb194b](https://github.com/Vincent550102/lab0-c/commit/cdb194b7b68861e3f43ad067bfd607f3d1f12e97) 沒有寫修改的原因。
3. [commit 9126285](https://github.com/Vincent550102/lab0-c/commit/912628531dea01d3938a9001a56c871a0ae5066f) 這裡參閱規格書中 free 的說明。
4. 有一些中文字打錯字。
### Reviewed by `w96086123`
1. 在 q_free 中的實作註記的第一段有一個無意義的空格。
2. [commit e2df64f](https://github.com/sysprog21/lab0-c/commit/e2df64fb3feae5e4ba31f96efcc573eb8c715507) 只有寫可以解決什麼問題,但沒有提出本來的問題是由哪裡出現。另外,在 `queue.h` 中有提供 `q_release_element` 可以解決相同的問題。
3. 在 commit 中,有使用 Fix 可以在說明中寫修改原因。
## 開發環境
```shell
$ gcc --version
gcc (Debian 10.2.1-6) 10.2.1 20210110
Copyright (C) 2020 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
Byte Order: Little Endian
Address sizes: 46 bits physical, 57 bits virtual
CPU(s): 32
On-line CPU(s) list: 0-31
Thread(s) per core: 1
Core(s) per socket: 8
Socket(s): 4
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 106
Model name: Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
Stepping: 6
CPU MHz: 2899.998
BogoMIPS: 5799.99
Virtualization: VT-x
Hypervisor vendor: KVM
Virtualization type: full
L1d cache: 1 MiB
L1i cache: 1 MiB
L2 cache: 128 MiB
L3 cache: 64 MiB
```
### 事前準備
#### list_entry / container_of
透過研讀老師的 [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/linux-macro-containerof),<s>個人總結</s> 出以下結論:
- 該巨集利用一種巧妙的技巧,通過一個被繼承結構中的成員變量,反向追蹤以獲取繼承它的結構體,展現了一種反向繼承鏈的實踐方法。
- 它的存在旨在透過模仿一些高級語言所具備的特性(如繼承)來實現特定的功能,展現了一種模擬高級語言特性的技術。
- 此技術使得繼承關係更加松散,減少了耦合度,無需額外定義反向繼承機制,從而提升了程式碼的靈活性和可維護性。
:::warning
列出關於「反向繼承機制」的權威材料 (如語言規格書和技術報告),並充分討論 Linux 核心風格的鏈結串列與其到底有什麼關聯。
:::
另外,我想到一件很有趣的事情,若 `container_of` 能回傳一個物件的上一層繼承的物件,那如果這個物件上一層繼承的物件尚未初始化,那這時候再對其 `container_of` 會發生什麼事?
以下程式碼,我們對一個物件初始化 `list_head`,並且取得上一層繼承的物件 `element_t` 的成員 `value`。
```c
struct list_head *li = malloc(sizeof(struct list_head));
INIT_LIST_HEAD(li);
element_t *e = list_entry(li, element_t, list);
e->value = strdup("test");
printf("%s\n", e->value);
```
發現輸出是 `test`,且無錯誤產生,進一步驗證可用 gdb 觀察 stack frame
## 指定的佇列操作
### q_new
> commit [f6ee1dc](https://github.com/Vincent550102/lab0-c/commit/f6ee1dce0f82423347f1e59724c5a629d6df7b90)
回傳一個空的 `list_head`。確認 `malloc` 成功後,我們可以透過 `INIT_LIST_HEAD` 這個巨集來初始化這個 `list_head`。
### q_free
> commit [e2df64f](https://github.com/Vincent550102/lab0-c/commit/e2df64fb3feae5e4ba31f96efcc573eb8c715507)
接下來使用 `list_for_each_entry_safe` 進行走訪,對每個走訪到的節點都先透過 `list_del` 將這個節點從鏈結串列中移開,之後再釋放該元素,走訪完畢後再釋放 `head`。
實作註記:
透過 實作 `e_free` 等函式,讓 `element_t` 的物件能夠完整釋放,當初忘記釋放 `e->value` 而直接釋放 `e` 導致釋放不完全。
### q_insert_* / q_remove_*
> commit [e171a43](https://github.com/Vincent550102/lab0-c/commit/e171a43d2dc99e10abad9ce62c50d65165cded0e)
實作上類似的函式,須注意使用 `strncpy` 使該 buf 能控制寫入長度,否則會導致[緩衝區溢位](https://www.cloudflare.com/zh-tw/learning/security/threats/buffer-overflow/) 等資安問題。
並且要在 `sp[bufsize - 1] = '\0';`,避免造成記憶體位置洩漏。
### q_delete_mid
透過快慢指標的技巧以及 `list_for_each` 巨集完成在環狀雙向的鏈結串列上找中點並刪除。
commit [9126285](https://github.com/Vincent550102/lab0-c/commit/912628531dea01d3938a9001a56c871a0ae5066f)
發現原本的實作多加到一處 `fast = fast->next` 造成每次迴圈會使得快指標多跑一次(共三次),將其刪除即可修正。 commit [f3dddbe](https://github.com/Vincent550102/lab0-c/commit/f3dddbeceec711ba82a9397b75e376650c1eab0c)
```diff
struct list_head *slow = head, *fast;
list_for_each (fast, head) {
- fast = fast->next;
if (fast != head && fast->next != head)
fast = fast->next;
else
break;
slow = slow->next;
}
```
### q_swap
> commit [d188f4b](https://github.com/Vincent550102/lab0-c/commit/d188f4b0a679d4ab3d022548875188f115bc8112)
透過 `list_for_each` 走訪每個元素,並且嘗試取得下個元素,若有,則將兩者交換。
### q_reverse
透過 `list_for_each_safe` 走訪每個元素,並且每次都將該元素放到 head,達到反轉的效果。
commit [930e774](https://github.com/Vincent550102/lab0-c/commit/930e774a9dfe8a54af071f0baa0b08f34b73a8ec)
### q_reverseK
參考了 WangHanChi 同學的 [實作](https://hackmd.io/tqrBvKRgTTm-LZzlde0V5A?both#q_reverseK),過程中透過 `list_for_each_safe` 走訪各個元素,並且每第 k 個元素都做以下事情:
- 使用 `list_cut_position` 將 i ~ i+k-1 的元素剪下,並放到 `tmp_head`
- 透過 `q_reverse` 將 `tmp_head` 反轉
- 使用 `list_splice_init` 將 `tmp_head` 的內容串回原鏈結串列,並重制 `tmp_head`
commit [2c415ed](https://github.com/Vincent550102/lab0-c/commit/2c415ed5449961c0f3f80450af58371d7f4548f7)
### q_delete_dup
觀察性質可以知道,由於此鏈結串列是排序好的,因此同樣的元素會<s>兩兩相臨</s> 兩兩相鄰。
透過 `list_for_each_entry_safe` 走訪各個元素,維護一個 flag,每個元素和下個元素數值相同,則將目前這個元素刪除,並將 flag 啟用,以便下次迴圈也將該元素移除,若不同則將 flag 重<s>制</s>置。
commit [eb67d38](https://github.com/Vincent550102/lab0-c/commit/eb67d389e958b006b76b5d55ac54d72a9a4ad63e)
### q_sort
> commit [ab859cf](https://github.com/Vincent550102/lab0-c/commit/ab859cf8fbd452f83d5e09c73ff4518c89edac5b)
首先,先將此環狀鏈結串列的 `head->prev->next` 設為 NULL,使得退化為單向的鏈結串列。
我們透過快慢指標的技巧找到該鏈結串列的中點,再用分治技巧中的分(Divide)將鏈結串列分為多個小點,接下來,治(conquer)的過程中我們用間接指標的技巧輔助,將兩個已排序的鏈結串列合併為一個已排序的鏈結串列,最後我們便可以得到一個排序好的鏈結串列,最後再將回邊接回來即可。
在檢查過程中,由於測試資料是 1~12,但這個作業是以字串形式處理,所以 `strcmp` 的時候會以字典序排列,導致以下的狀況誤判實作沒做好,造成多餘的排錯時間浪費。
```
l = []
l = [3]
l = [3 1]
l = [3 1 5]
l = [3 1 5 10]
l = [3 1 5 10 11]
l = [3 1 5 10 11 12]
l = [3 1 5 10 11 12 9]
l = [1 10 11 12 3 5 9]
Freeing queue
```
參考了 [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-prog/%2Fs%2FSkE33UTH) 中的實作。
### q_merge
使用很直覺的做法,透過 `list_for_each_safe` 走訪每個元素,並將該元素使用 `list_splice_init` 串到第一個元素並重製,結束走訪後,使用 `q_sort` 將其排序。
TODO: 可透過 mergesort 的技巧直接對 `head` 做事,由於符合對應的性質。
commit [2279bd2](https://github.com/Vincent550102/lab0-c/commit/2279bd27475e1d76031cb5a28f385be8de4978b5)
### q_descend / q_ascend
這題是個經典的單調序列問題,我們的最終目標為找到給定序列中的單調遞增/遞減序列,因此需要維護一個目前最大值 `max_val`,用 `list_for_each_safe` 走訪每個元素,當目前的元素比 `max_val` 大就更新它,否則就將這個元素移除。
遞增或遞減兩者作法雷同,遞減只要先將原序列反轉,處理完後再將結果反轉即為答案。
commit [3d14944](https://github.com/Vincent550102/lab0-c/commit/3d14944b10404846353dc850d3b33f19cbc98fd6)
---
## 研讀論文〈Dude, is my code constant time?〉
這篇論文介紹了一種名為 [dudect](https://github.com/oreparaz/dudect) 的工具,該工具用於評估特定平台上的代碼是否以恆定時間執行,這對於避免時間攻擊([Timing Attack](https://en.wikipedia.org/wiki/Timing_attack))相當重要。
與需要對硬<s>件</s>體行為進行建模的先前方法不同,dudect 通過不假設這些前提,使用執行時間的統計分析,使其適合於黑盒測試。
:::info
時間攻擊([Timing Attack](https://en.wikipedia.org/wiki/Timing_attack))
是一種旁路攻擊(Side-channel attack),透過電腦系統的**實作**缺陷而洩漏的資訊來做的攻擊,而不是利用演算法本身的弱點
:::
此論文首先先進行了時間攻擊的檢測,他們使用了 **fix-vs-random tests**,也就是準備兩筆測資,其中一筆是固定的常數,另一個則為隨機的選定。他們進行了 **null hypothesis** the two timing distributions are equal。
### 改進 qtest 中的常數複雜度判斷
可以發現,當我們在測試資料中使用 `option simulation 1` 時,將會進行 q_insert_head / q_insert_tail / q_remove_head / q_remove_tail 的常數時間複雜度檢查。
對應的程式碼如下:
constant.h
```c
#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
```c
static void update_statistics(const int64_t *exec_times, uint8_t *classes)
{
for (size_t i = 0; i < N_MEASURES; i++) {
int64_t difference = exec_times[i];
/* CPU cycle counter overflowed or dropped measurement */
if (difference <= 0)
continue;
/* do a t-test on the execution time */
t_push(t, difference, classes[i]);
}
}
```
```c
static bool test_const(char *text, int mode)
{
bool result = false;
t = malloc(sizeof(t_context_t));
for (int cnt = 0; cnt < TEST_TRIES; ++cnt) {
printf("Testing %s...(%d/%d)\n\n", text, cnt, TEST_TRIES);
init_once();
for (int i = 0; i < ENOUGH_MEASURE / (N_MEASURES - DROP_SIZE * 2) + 1;
++i)
result = doit(mode);
printf("\033[A\033[2K\033[A\033[2K");
if (result)
break;
}
free(t);
return result;
}
#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 _
```
同時,發現到論文中有提及會將測量後的第一批結果丟棄,也閱讀了 [dudect](https://github.com/oreparaz/dudect/blob/master/src/dudect.h) 的原始碼後,他會將迴圈從 10 開始:
fixture.c
```c
static void update_statistics(dudect_ctx_t *ctx) {
for (size_t i = 10 /* discard the first few measurements */; i < (ctx->config->number_measurements-1); i++) {
int64_t difference = ctx->exec_times[i];
if (difference < 0) {
continue; // the cpu cycle counter overflowed, just throw away the measurement
}
```
## 研讀 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 並嘗試改進
首先,在檔案中時常出現形如 `__attribute__((nonnull(2,3)))` 的語法,該語法有出現在 gcc 的手冊中,被稱為 [Function-Attributes](https://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Function-Attributes.html),大意就是標示這個函式的第 2, 3 個參數不能是 null。
在此檔案中實作了三個函式,分別為 `list_sort` `merge` `merge_final`,將特點註記:
### `list_sort`
- 整體是透過 bottom up 的方式來達成,充分發揮 cache 機制。
- 透過 `head->prev->next = NULL;` 將此環狀鏈結串列變單向的。
- 透過 `count` 變數來控制合併。
- 控制合併的數量來確保可以放的進 L1 cache。
### `merge`
- 重複的程式碼,參考 [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-LeetCode-21-Merge-Two-Sorted-Lists) 中的 案例探討: LeetCode 21. Merge Two Sorted Lists,可進行以下程式碼縮減。
```diff
- struct list_head *head, **tail = &head;
+ struct list_head *head, **tail = &head, **node;
- for (;;) {
+ for (node = NULL; a && b; *node = (*node)->next) {
/* if equal, take 'a' -- important for sort stability */
- if (cmp(priv, a, b) <= 0) {
- *tail = a;
- tail = &a->next;
- a = a->next;
- if (!a) {
- *tail = b;
- break;
- }
- } else {
- *tail = b;
- tail = &b->next; - b = b->next;
- if (!b) {
- *tail = a;
- break;
- }
- }
+ node = cmp(priv, a, b) <= 0 ? &a : &b;
+ *tail = *node;
+ tail = &(*node)->next;
}
+ *tail = (struct list_head *)((uintptr_t)a | (uinptr_t)b);
return head;
}
```
### `merge_final`
- 進行最後一次合併,並將各個斷鍊結點接回來。