# 2022q1 Homework1 (labc0)
contributed by < `void110916` >
## 實驗環境
~~~shell
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
$ uname -a
Linux void-pc 5.13.0-28-generic #31~20.04.1-Ubuntu SMP Wed Jan 19 14:08:10 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 39 bits physical, 48 bits virtual
CPU(s): 12
On-line CPU(s) list: 0-11
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 158
Model name: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
Stepping: 10
CPU MHz: 2200.000
CPU max MHz: 4100.0000
CPU min MHz: 800.0000
BogoMIPS: 4399.99
Virtualization: VT-x
L1d cache: 192 KiB
L1i cache: 192 KiB
L2 cache: 1.5 MiB
L3 cache: 9 MiB
NUMA node0 CPU(s): 0-11
~~~
## 作業要求
- [x] 在 GitHub 上 fork [lab0-c](https://github.com/sysprog21/lab0-c)
* 參閱 [Git 教學和 GitHub 設定指引](https://hackmd.io/@sysprog/git-with-github) ^附教學影片^
- [x] 依據上述指示著手修改 `queue.[ch]` 和連帶的檔案,測試後用 Git 管理各項修改,要能滿足 `$ make test` 自動評分系統的所有項目。
- [x] 在提交程式變更前,務必詳閱 [如何寫好 Git Commit Message](https://blog.louie.lu/2017/03/21/%E5%A6%82%E4%BD%95%E5%AF%AB%E4%B8%80%E5%80%8B-git-commit-message/)
:::spoiler `queue.c`實做
- [x] q_new: 建立空 (empty) 佇列
- [x] q_free: 釋放所有被佇列使用的空間
- [x] q_insert_head: 從佇列開頭插入新節點
- [x] q_insert_tail: 從佇列結尾插入新節點
- [x] q_remove_head: 從佇列開頭**移除**新節點
- [x] q_remove_tail: 從佇列結尾**移除**新節點
- [x] q_size: 回傳佇列個數
- [x] q_delete_mid: **刪除**佇列中間的節點
- [x] q_delete_dup: **刪除**佇列中重複字串的節點
- [x] q_swap: 交換鄰近的節點
- [x] q_reverse: 反向排列佇列
- [x] q_sort: 以升幕排列佇列
:::
- [x] 詳閱「[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list)」並應用裡頭提及的手法,例如 pointer to pointer (指向某個指標的指標,或說 indirect pointer),讓程式碼更精簡且有效
- [ ] 研讀 Linux 核心原始程式碼的 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 並嘗試引入到 `lab0-c` 專案,比較你自己實作的 merge sort 和 Linux 核心程式碼之間效能落差
- [ ] 在 `qtest` 提供新的命令 `shuffle`,允許藉由 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 演算法,對佇列中所有節點進行洗牌 (shuffle) 操作
- [ ] 在 `qtest` 提供新的命令 `web`,提供 web 伺服器功能,注意: web 伺服器運作過程中,`qtest` 仍可接受其他命令
- [ ] 可依據前述說明,嘗試整合 [tiny-web-server](https://github.com/7890/tiny-web-server)
- [ ] 除了修改程式,也要編輯「[作業區](https://hackmd.io/@sysprog/linux2022-homework1)」,增添開發紀錄和 GitHub 連結,除了提及你如何逐步達到自動評分程式的要求外,共筆也要涵蓋以下:
- [ ] 開啟 [Address Sanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer),修正 `qtest` 執行過程中的錯誤
- [ ] 先執行 `qtest` 再於命令提示列輸入 `help` 命令,會使 開啟 [Address Sanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer) 觸發錯誤,應予以排除
- [ ] 運用 Valgrind 排除 `qtest` 實作的記憶體錯誤,並透過 Massif 視覺化 "simulation" 過程中的記憶體使用量,需要設計對應的實驗
- [ ] 解釋 [select](http://man7.org/linux/man-pages/man2/select.2.html) 系統呼叫在本程式的使用方式,並分析 [console.c](https://github.com/sysprog21/lab0-c/blob/master/console.c) 的實作,說明其中運用 CS:APP [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 的原理和考量點。可對照參考 [CS:APP 第 10 章重點提示](https://hackmd.io/@sysprog/H1TtmVTTz)
- [ ] 研讀論文〈[Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf)〉,解釋本程式的 "simulation" 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 [Student's t-distribution](https://en.wikipedia.org/wiki/Student%27s_t-distribution) 及程式實作的原理。注意:現有實作存在若干致命缺陷,請討論並提出解決方案;
- [ ] 指出現有程式的缺陷 (提示: 和 [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 有關) 或者可改進之處 (例如更全面地使用 Linux 核心風格的鏈結串列 API),嘗試實作並提交 pull request
## Knowledge in This Homeworks
[Linux 核心原始程式碼巨集: container_of](https://hackmd.io/@sysprog/linux-macro-containerof): 如何取得該子結構的父結構
## 程式開發紀錄
### q_new
::: spoiler 問題程式
問題連結:[記憶體釋放錯誤](#記憶體釋放錯誤)
```c
struct list_head *q_new()
{
struct list_head *q_head = malloc(sizeof(struct list_head));
if (!q_head)
return NULL;
INIT_LIST_HEAD(q_head);
return q_head;
}
```
:::
修正後程式:
```c
struct list_head *q_new()
{
struct list_head *q_head = malloc(sizeof(struct list_head));
INIT_LIST_HEAD(q_head);
return q_head;
}
```
### q_free
:::spoiler 問題程式
問題連結:[記憶體釋放錯誤](#記憶體釋放錯誤)
```c
void q_free(struct list_head *l)
{
struct list_head *node = l->next;
while (!list_empty(l)) {
list_del(node);
element_t *e=container_of(node,element_t,list);
free(e->value);
free(node);
node = l->next;
}
free(l);
}
```
:::
修正後程式:
```c
void q_free(struct list_head *l)
{
if (!l)
return;
while (!list_empty(l)) {
element_t *e = list_entry(l->next, element_t, list);
list_del(&e->list);
free(e->value);
free(e);
}
free(l);
}
```
### q_insert_head
:::spoiler 問題程式
問題連結:[記憶體釋放錯誤](#記憶體釋放錯誤)
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (head == NULL)
return false;
element_t *e = malloc(sizeof(element_t));
list_add(&(e->list), head);
int len = strlen(s);
e->value = malloc(sizeof(char) * ++len);
strncpy(e->value, s, len);
return true;
}
```
由 `man strlen`的敘述:
> The strlen() function calculates the length of the string pointed to by
s, excluding the terminating null byte ('\0').
strlen 回傳的值不包含'\\0',因此要先將長度值加一再動作。
:::
修正後程式:
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *e = malloc(sizeof(element_t));
if (!e)
return false;
e->value = strdup(s);
if (!e->value) {
free(e);
return false;
}
list_add(&e->list, head);
return true;
}
```
### q_insert_tail
:::spoiler 問題程式
問題連結:[記憶體釋放錯誤](#記憶體釋放錯誤)
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if (head == NULL)
return false;
element_t *e = malloc(sizeof(element_t));
list_add_tail(&(e->list), head);
int len = strlen(s);
e->value = malloc(sizeof(char) * ++len);
strncpy(e->value, s, len);
return true;
}
```
:::
修正後程式:
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *e = malloc(sizeof(element_t));
if (!e)
return false;
e->value = strdup(s);
if (!e->value) {
free(e);
return false;
}
list_add_tail(&e->list, head);
return true;
}
```
同 [q_insert_head](#q_insert_head)
### q_remove_head
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
element_t *e = NULL;
if (!head)
return NULL;
if (!list_empty(head)) {
e = container_of(head->next, element_t, list);
list_del(&e->list);
if (sp) {
strncpy(sp, e->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
}
return e;
}
```
### q_remove_tail
[trace-17 無法總是通過](#trace_17-無法總是通過)
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
element_t *e = NULL;
if (!head)
return NULL;
if (!list_empty(head)) {
e = container_of(head->prev, element_t, list);
list_del(&e->list);
if (sp) {
strncpy(sp, e->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
}
return e;
}
```
### q_size: 回傳佇列個數
```c
int q_size(struct list_head *head)
{
if (!head)
return 0;
int num = 0;
struct list_head *node = head;
while (node->next != head) {
node = node->next;
num++;
}
return num;
}
```
### q_delete_mid: **刪除**佇列中間的節點
~~~c
bool q_delete_mid(struct list_head *head)
{
struct list_head *front, *tail;
int i = 0;
if (!head)
return false;
if (list_empty(head))
return false;
for (front = head->next, tail = head->prev;
front != tail && front->next != tail;
front = front->next, tail = tail->prev, i++) {
}
list_del(tail);
q_release_element(list_entry(tail, element_t, list));
return true;
}
~~~
### q_delete_dup: **刪除**佇列中重複字串的節點
~~~c
bool q_delete_dup(struct list_head *head)
{
if (!head)
return false;
element_t *e, *safe, *tmp = NULL;
for (e = list_entry((head)->next, element_t, list),
safe = list_entry(e->list.next, element_t, list);
&safe->list != (head);
e = safe, safe = list_entry(safe->list.next, element_t, list)) {
if (!(strcmp(e->value, safe->value) & 0x7fffffff) ||
!(strcmp(e->value, safe->value))) {
list_del(&e->list);
q_release_element(e);
tmp = safe;
} else if (tmp) {
list_del(&tmp->list);
q_release_element(tmp);
tmp = NULL;
}
}
if (tmp) {
list_del(&tmp->list);
q_release_element(tmp);
tmp = NULL;
}
return true;
}
~~~
程式內使用到的遞迴幾乎等效於 list_for_each_entry_safe ,但是因為 list_for_each_entry_safe 的條件判斷是判斷 entry 而非 safe ,以至於最後會對 head 取 entry 並造成 segement fault。
### q_swap: 交換鄰近的節點
~~~c
void q_swap(struct list_head *head)
{
struct list_head *next, *nnext;
for (next = head->next, nnext = next->next;
(next != head) && (nnext != head);
next = next->next, nnext = next->next) {
list_move(next, nnext);
}
}
~~~
### q_reverse: 反向排列佇列
~~~c
void q_reverse(struct list_head *head)
{
if (!head)
return;
struct list_head *iter = head;
do {
struct list_head *tmp = iter->next;
iter->next = iter->prev;
iter->prev = tmp;
iter = iter->next;
} while (iter != head);
}
~~~
### q_sort: 以升幕排列佇列
~~~c
void q_sort(struct list_head *head)
{
if (!head)
return;
int count = 0;
// insertion sort
struct list_head *i, *j, *isafe, *jsafe;
for (i = (head->next)->next, isafe = i->next; i != head;
i = isafe, isafe = i->next) {
for (j = head->next, jsafe = j->next; j != i;
j = jsafe, jsafe = j->next) {
char *s1, *s2;
s1 = list_entry(i, element_t, list)->value;
s2 = list_entry(j, element_t, list)->value;
int cmp = strcmp(s1, s2);
count++;
if (cmp < 0) {
list_move(i, j->prev);
break;
}
}
}
}
~~~
## 問題紀錄
### cppcheck
#### 遇到問題
```shell
$ git commit -m "edit function q_new, q_insert_head/tail, q_remove_head/tail"
Following files need to be cleaned up:
queue.c
queue.c:50:5: error: Memory leak: e [memleak]
return true;
^
queue.c:69:5: error: Memory leak: e [memleak]
return true;
^
queue.c:50:5: error: Memory leak: e.value [memleak]
return true;
^
queue.c:69:5: error: Memory leak: e.value [memleak]
return true;
^
Fail to pass static analysis.
```
該位置為 [q_insert_tail](#q_insert_tail) 及 [q_insert_head](#q_insert_head) 的 return 位置,不知道為何導致 git 無法 commit 成功。
這裡指的 memory leak 是記憶體洩漏那個 memory leak 嗎?
:::warning
是的,你應該追蹤「完整」的程式碼,而非只注視單一函式。
:notes: jserv
:::
#### 解決方法
此處有兩個問題:
1. git commit 首字英文要大寫,且不能超過 50 個字元
2. 因為 cppcheck 有檢查,所以 list 需要用 INIT_LIST_HEAD 做初使化: value 要用 strdup 配置動態記憶體,不能使用 calloc 或 malloc 。
### 記憶體釋放錯誤
#### 遇到問題:
```shell
ERROR: Attempted to free unallocated block. Address = 0x55b5876a7e50
ERROR: Attempted to free unallocated or corrupted block. Address = 0x55b5876a7e50
ERROR: Corruption detected in block with address 0x55b5876a7e50 when attempting to free it
Segmentation fault occurred. You dereferenced a NULL or invalid pointer
[1] 73359 abort (core dumped) ./qtest
```
確定能從該位置取得字串,但不確定為何無法釋放。
#### 解決方法
測試後得知,在 `harness.h` 中, malloc 、 free 、 strdup 被重新撰寫以追蹤記憶體配置,因此此作業在動態記憶體配置上只能用此三個函式,無法使用像 calloc 、 strndup 等函式。
### trace_17 無法總是通過
#### 遇到問題:
q_remove_head 及 q_remove_tail函式幾乎一樣,差別只在移除 head 前面的還是後面的 list ,但是在跑 trace-17 的測試時 q_remove_tail 有機率無法通過。
::: spoiler q_remove_head disassemble
```assembler
0000000000000158 <q_remove_head>:
158: f3 0f 1e fa endbr64
15c: 41 54 push r12
15e: 55 push rbp
15f: 53 push rbx
160: 48 85 ff test rdi,rdi
163: 74 45 je 1aa <q_remove_head+0x52>
165: 48 89 f5 mov rbp,rsi
168: 49 89 d4 mov r12,rdx
16b: 48 8b 47 08 mov rax,QWORD PTR [rdi+0x8]
16f: 48 39 c7 cmp rdi,rax
172: 74 3b je 1af <q_remove_head+0x57>
174: 48 8d 58 f8 lea rbx,[rax-0x8]
178: 48 8b 48 08 mov rcx,QWORD PTR [rax+0x8]
17c: 48 8b 10 mov rdx,QWORD PTR [rax]
17f: 48 89 11 mov QWORD PTR [rcx],rdx
182: 48 89 4a 08 mov QWORD PTR [rdx+0x8],rcx
186: 48 85 f6 test rsi,rsi
189: 74 17 je 1a2 <q_remove_head+0x4a>
18b: 49 8d 54 24 ff lea rdx,[r12-0x1]
190: 48 8b 70 f8 mov rsi,QWORD PTR [rax-0x8]
194: 48 89 ef mov rdi,rbp
197: e8 00 00 00 00 call 19c <q_remove_head+0x44>
19c: 42 c6 44 25 ff 00 mov BYTE PTR [rbp+r12*1-0x1],0x0
1a2: 48 89 d8 mov rax,rbx
1a5: 5b pop rbx
1a6: 5d pop rbp
1a7: 41 5c pop r12
1a9: c3 ret
1aa: 48 89 fb mov rbx,rdi
1ad: eb f3 jmp 1a2 <q_remove_head+0x4a>
1af: bb 00 00 00 00 mov ebx,0x0
1b4: eb ec jmp 1a2 <q_remove_head+0x4a>
```
:::
:::spoiler q_remove_tail disassemble
```assembler
00000000000001b6 <q_remove_tail>:
1b6: f3 0f 1e fa endbr64
1ba: 41 54 push r12
1bc: 55 push rbp
1bd: 53 push rbx
1be: 48 85 ff test rdi,rdi
1c1: 74 45 je 208 <q_remove_tail+0x52>
1c3: 48 89 f5 mov rbp,rsi
1c6: 49 89 d4 mov r12,rdx
1c9: 48 3b 7f 08 cmp rdi,QWORD PTR [rdi+0x8]
1cd: 74 3e je 20d <q_remove_tail+0x57>
1cf: 48 8b 07 mov rax,QWORD PTR [rdi]
1d2: 48 8d 58 f8 lea rbx,[rax-0x8]
1d6: 48 8b 48 08 mov rcx,QWORD PTR [rax+0x8]
1da: 48 8b 10 mov rdx,QWORD PTR [rax]
1dd: 48 89 11 mov QWORD PTR [rcx],rdx
1e0: 48 89 4a 08 mov QWORD PTR [rdx+0x8],rcx
1e4: 48 85 f6 test rsi,rsi
1e7: 74 17 je 200 <q_remove_tail+0x4a>
1e9: 49 8d 54 24 ff lea rdx,[r12-0x1]
1ee: 48 8b 70 f8 mov rsi,QWORD PTR [rax-0x8]
1f2: 48 89 ef mov rdi,rbp
1f5: e8 00 00 00 00 call 1fa <q_remove_tail+0x44>
1fa: 42 c6 44 25 ff 00 mov BYTE PTR [rbp+r12*1-0x1],0x0
200: 48 89 d8 mov rax,rbx
203: 5b pop rbx
204: 5d pop rbp
205: 41 5c pop r12
207: c3 ret
208: 48 89 fb mov rbx,rdi
20b: eb f3 jmp 200 <q_remove_tail+0x4a>
20d: bb 00 00 00 00 mov ebx,0x0
212: eb ec jmp 200 <q_remove_tail+0x4a>
```
:::
並且,不管字串複製是使用 memcpy 還是 strncpy ,在轉譯後的組語是完全一樣的。
#### 解決方法(未解決)
目前觀測到在測量 q_remove_tail 時會發生執行核心交換的情況( ex. 原本在 cpu0 執行,但是跑到一半變成 cpu12 執行 ),當發生時,便會發生測量時間非常數的錯誤。