# 2021q1 Homework1 (lab0)
contributed by < `tiffany6022` >
###### tags: `linux2021`
> [作業說明](https://hackmd.io/@sysprog/linux2021-lab0)
- [x] C Programming Lab
- [x] q_new
- [x] q_free
- [x] q_insert_head
- [x] q_insert_tail
- [x] q_remove_head
- [x] q_size
- [x] q_reverse
- [x] q_sort
- [x] Address Sanitizer
- [x] Valgrind
- [ ] Coroutine
- [ ] 解釋 select 系統
- [ ] 說明 antirez/linenoise 的運作原理
- [ ] 研讀論文
- [ ] 指出現有程式的缺陷
## C Programming Lab
以下為實做方式以及遇到過的問題
### 開始
* 進入 `queue.c` 和 `queue.h` 進行修改
* 輸入 `make` 、 `clean` 分別執行 編譯檔案 及 清除編譯檔案
* 輸入 `make test` 執行自動評分系統
* 輸入 `./qtest -f traces/[trace file].cmd` 執行單一測試檔
### q_new
* 建一個 empty queue
### q_free
* 若一個已動態配置記憶體的 struct,裡面也有成員已動態配置記憶體了,那在釋放時應先從裡面的成員開始釋放,避免從 struct 先釋放而導致要釋放成員時找不到記憶體位置
:::warning
改進你的漢語表達,上面那句到底是什麼意思?在面試或者電話會議的場合中,你若說「從裡面 free 出來」,到底多少人可會意?
:notes: jserv
:::
> 已修正
```cpp=
...
free(q->head->value);
free(q->head);
free(q);
...
```
### q_insert_head
* 當 newh->value 動態配置記憶體失敗時,要記得釋放掉之前 newh 配置的記憶體
* 計算分配記憶體長度給 value 時,除了 strlen(s) 還要 +1,因為 `strlen` 計算的長度不包含 terminating null byte ('\0')
* `strncpy` 複製字串時,會連同 '\0' 一起複製
```cpp=
...
newh = malloc(sizeof(list_ele_t));
if (!newh)
return false;
newh->value = malloc(strlen(s) + 1);
if (!newh->value) {
free(newh);
return false;
}
strncpy(newh->value, s, (strlen(s) + 1));
...
```
### q_insert_tail
* 同 `q_insert_head`
* 由於要達到 $O(1)$ 的常數時間複雜度,新增 `list_ele_t *tail` 這個新成員到 `struct queue_t`
```cpp=
typedef struct {
...
list_ele_t *tail;
} queue_t;
```
* 要記得考慮只有當 q->tail 不是 NULL 時,才可以設 q->tail->next (一開始沒考慮到,造成 `trace-17-complexity` 錯誤)
```cpp=
...
if (q->tail)
q->tail->next = newt;
...
```
### q_remove_head
* 需要考慮字串最長的長度 bufsize,當字串超過限制長度時,要複製原有的字串,長度設為 bufsize - 1 ,因為要留一位給 '\0' (一開始看不懂忽略它XD,造成 `trace-06-string` 錯誤)
```cpp=
...
if (sp) {
if (strlen(q->head->value) < bufsize) {
strncpy(sp, q->head->value, (strlen(q->head->value) + 1));
} else {
strncpy(sp, q->head->value, (bufsize - 1));
sp[bufsize - 1] = '\0';
}
}
...
```
### q_size
* 由於要在 $O(1)$ 的常數時間內執行完成,新增 `int size` 這個新成員到 `struct queue_t`
```cpp=
typedef struct {
...
int size;
} queue_t;
```
### q_reverse
* 利用3個 pointer 紀錄目前位置及前後位置,將他們的 next 轉換
```cpp=
...
list_ele_t *_prev = NULL;
list_ele_t *_curr = q->head;
list_ele_t *_next = q->head->next;
q->tail = _curr;
while (_next) {
_curr->next = _prev;
_prev = _curr;
_curr = _next;
_next = _curr->next;
}
_curr->next = _prev;
q->head = _curr;
```
### q_sort
* 參考 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 的 merge sort
* 原先用 `strcmp` 比較字串可以實做,但之後發現 `qtest.c` 裡面是用 `strcasecmp` 測試我們的程式,兩者的差別在於 `strcasecmp` 會自動忽略大小寫的差異,因而改用 `strcasecmp` 做比較
* 原本 merge() 是利用以下遞迴的方式寫,但會造成 `trace-15-perf` 發生 segmentation fault (core dumped)
```cpp=
list_ele_t *merge(list_ele_t *l1, list_ele_t *l2)
{
if (!l2)
return l1;
if (!l1)
return l2;
if (strcasecmp(l1->value, l2->value) < 0) {
l1->next = merge(l1->next, l2);
return l1;
} else {
l2->next = merge(l1, l2->next);
return l2;
}
}
```
* 改成沒有遞迴的版本
* 先比較兩個 list 的第一個 node 記錄起始位置
```cpp=
list_ele_t *merge(list_ele_t *l1, list_ele_t *l2)
{
list_ele_t *temp, *head;
if (strcasecmp(l1->value, l2->value) < 0) {
temp = l1;
l1 = l1->next;
} else {
temp = l2;
l2 = l2->next;
}
head = temp;
while (l1 && l2) {
if (strcasecmp(l1->value, l2->value) < 0) {
temp->next = l1;
temp = temp->next;
l1 = l1->next;
} else {
temp->next = l2;
temp = temp->next;
l2 = l2->next;
}
}
if (l1)
temp->next = l1;
if (l2)
temp->next = l2;
return head;
}
```
* 利用2個 pointer,找到分割點,一個一次走1步,另一個一次走2步,當走得快的那個到終點時,慢的那個的位置就是 list 的中間
```cpp=
list_ele_t *mergeSortList(list_ele_t *head)
{
...
list_ele_t *slow = head;
list_ele_t *fast = head->next;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
fast = slow->next;
slow->next = NULL;
...
}
```
### 結果
> 過程中遇到測資沒過,還傻傻的以為要改其他檔案的程式,其實到目前為止都只有改 `queue.c` 和 `queue.h` 這兩個檔案
```
--- TOTAL 100/100
```
:::danger
文字訊息不要用圖片展現!
:notes: jserv
:::
> 好
---
## Address Sanitizer
### 簡介
* [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer)
* 動態檢測 C/C++ 記憶體錯誤的工具 (程式執行時分析)
* 錯誤類型︰
* Heap / Stack / Global buffer overflow
* Stack: 儲存函式的區域變數
* Heap: 動態配置變數
* Golbal: 全域
* Use after free/return/scope
* Initialization order bugs
* Memory leaks
### 作業要求
* 開啟 Address Sanitizer,修正 `qtest` 執行過程中的錯誤
* 先執行 `qtest` 再於命令提示列輸入 `help` 命令,會使 開啟 Address Sanitizer 觸發錯誤,應予以排除
### 測試之前程式
```shell
$ make SANITIZER=1
$ make test
```
* ~~發現原本的100分是假的~~,`trace-17-complexity` 出錯了
* 不合法操作 `READ` 發生了 `global-buffer-overflow` 的錯誤,在 `console.c` 裡的 `simulation` 變數
```
==6540==ERROR: AddressSanitizer: global-buffer-overflow on address 0x55ea3ce4f620 at pc 0x55ea3ce37c3a bp 0x7ffdf55fbd40 sp 0x7ffdf55fbd30
READ of size 4 at 0x55ea3ce4f620 thread T0
#0 0x55ea3ce37c39 in do_option_cmd /home/tiffany/linux2021/lab0-c/console.c:369
#1 0x55ea3ce36a22 in interpret_cmda /home/tiffany/linux2021/lab0-c/console.c:221
#2 0x55ea3ce37207 in interpret_cmd /home/tiffany/linux2021/lab0-c/console.c:244
#3 0x55ea3ce38433 in cmd_select /home/tiffany/linux2021/lab0-c/console.c:594
#4 0x55ea3ce389ad in run_console /home/tiffany/linux2021/lab0-c/console.c:667
#5 0x55ea3ce35531 in main /home/tiffany/linux2021/lab0-c/qtest.c:788
#6 0x7fbf5b4bd0b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)
#7 0x55ea3ce32b8d in _start (/home/tiffany/linux2021/lab0-c/qtest+0x8b8d)
0x55ea3ce4f621 is located 0 bytes to the right of global variable 'simulation' defined in 'console.c:21:6' (0x55ea3ce4f620) of size 1
'simulation' is ascii string ''
SUMMARY: AddressSanitizer: global-buffer-overflow /home/tiffany/linux2021/lab0-c/console.c:369 in do_option_cmd
```
#### 關於 global-buffer-overflow
```cpp
// RUN: clang -O -g -fsanitize=address %t && ./a.out
int global_array[100] = {-1};
int main(int argc, char **argv) {
return global_array[argc + 100]; // BOOM
}
```
* argc 一定大於等於 1
* 全域變數 global_array 想存取到超過它被配置的記憶體空間
#### 修改過程
* 打開 `console.c` 找到 `simulation`,發現問題出在 `simulation` 在初始化時是 `boolean` 型態,但在 `add_param` 函式的第二個參數是需要 `integer pointer` 型態,所以它就把 `bool` 強制轉成 `(int *)`,`bool` 配置的記憶體長度是 1 個位元組,而 `int` 則是 4 個位元組,因此發生錯誤
```cpp=
/* --- console.c 原本程式碼 --- */
bool simulation = false;
...
add_param("simulation", (int *) &simulation, "Start/Stop simulation mode", NULL);
```
```cpp=
/* --- console.h 原本程式碼 --- */
extern bool simulation;
```
* 將 `simulation` 的初始化改成 `int` 型態
```cpp=
/* --- console.c 更改後程式碼 --- */
int simulation = 0;
...
add_param("simulation", &simulation, "Start/Stop simulation mode", NULL);
```
```cpp=
/* --- console.h 更改後程式碼 --- */
extern int simulation;
```
### 實做作業要求
```shell
$ make SANITIZER=1
$ ./qtest
cmd> help
```
* 不合法操作 `READ` 發生了 `global-buffer-overflow` 錯誤,在 `console.c` 裡的 `echo` 變數
```
==6627==ERROR: AddressSanitizer: global-buffer-overflow on address 0x5592c5c3c400 at pc 0x5592c5c2590f bp 0x7ffefd4bdf40 sp 0x7ffefd4bdf30
READ of size 4 at 0x5592c5c3c400 thread T0
#0 0x5592c5c2590e in do_help_cmd /home/tiffany/linux2021/lab0-c/console.c:307
#1 0x5592c5c25a22 in interpret_cmda /home/tiffany/linux2021/lab0-c/console.c:221
#2 0x5592c5c26207 in interpret_cmd /home/tiffany/linux2021/lab0-c/console.c:244
#3 0x5592c5c2794a in run_console /home/tiffany/linux2021/lab0-c/console.c:660
#4 0x5592c5c24531 in main /home/tiffany/linux2021/lab0-c/qtest.c:788
#5 0x7f23725460b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)
#6 0x5592c5c21b8d in _start (/home/tiffany/linux2021/lab0-c/qtest+0x8b8d)
0x5592c5c3c401 is located 0 bytes to the right of global variable 'echo' defined in 'console.c:59:13' (0x5592c5c3c400) of size 1
SUMMARY: AddressSanitizer: global-buffer-overflow /home/tiffany/linux2021/lab0-c/console.c:307 in do_help_cmd
```
#### 修改過程
* 與上述問題相似
```cpp=
/* --- console.c 原本程式碼 --- */
static bool echo = 0;
...
add_param("echo", (int *) &echo, "Do/don't echo commands", NULL);
```
* 將 `echo` 的初始化改成 `int` 型態
```cpp=
/* --- console.c 更改後程式碼 --- */
static int echo = 0;
...
add_param("echo", &echo, "Do/don't echo commands", NULL);
```
---
## Valgrind
### 簡介
* 使用者層級 (user space) 對程式在執行時期的行為提供動態分析的系統軟體框架,具備多種工具,可以用來追蹤及分析程式效能,最為人們所熟知的特性就是幫助使用者偵測記憶體錯誤,諸如使用未初始化的記憶體、不當的記憶體配置、或取消配置記憶體,並加以分析。
### 作業要求
* 運用 Valgrind 排除 `qtest` 實作的記憶體錯誤
* 並透過 Massif 視覺化 “simulation” 過程中的記憶體使用量,需要設計對應的實驗
### 排除 qtest 實作的記憶體錯誤
#### 執行
```shell
$ make valgrind
```
* 發現每一個測資都會出現以下錯誤訊息
* 訊息中指明
* 有 1 個 16 bytes 的 block 被偵測為 “still reachable”
* 有 19 個 129 bytes 的 block 被偵測為 “still reachable”
* 有 1 個 160 bytes 的 block 被偵測為 “still reachable”
* `still reachable`︰程式結束時有未釋放的記憶體,不過卻還有指標指著,通常會發生在 global 變數
```
==9056== 16 bytes in 1 blocks are still reachable in loss record 1 of 3
==9056== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==9056== by 0x4A4F50E: strdup (strdup.c:42)
==9056== by 0x110134: linenoiseHistoryAdd (linenoise.c:1236)
==9056== by 0x110CC7: linenoiseHistoryLoad (linenoise.c:1325)
==9056== by 0x10C287: main (qtest.c:777)
==9056==
==9056== 129 bytes in 19 blocks are still reachable in loss record 2 of 3
==9056== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==9056== by 0x4A4F50E: strdup (strdup.c:42)
==9056== by 0x1100A8: linenoiseHistoryAdd (linenoise.c:1236)
==9056== by 0x110CC7: linenoiseHistoryLoad (linenoise.c:1325)
==9056== by 0x10C287: main (qtest.c:777)
==9056==
==9056== 160 bytes in 1 blocks are still reachable in loss record 3 of 3
==9056== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==9056== by 0x1100F4: linenoiseHistoryAdd (linenoise.c:1224)
==9056== by 0x110CC7: linenoiseHistoryLoad (linenoise.c:1325)
==9056== by 0x10C287: main (qtest.c:777)
==9056==
```
#### 修改過程
* 打開 `linenoise.c` 的 1236 行 (下方程式碼的第7行) 發現 `strdup` 函式出現問題
* `strdup` 是可以把要複製的內容複製給沒有初始化的指標,因為它會自動 malloc 給它,但使用結束後應手動 free 記憶體空間
* 程式中 `linecopy` 複製 `line` 的內容,而到後面 `history[history_len]` 又指向 `linecopy` 這個 char pointer,因此這邊的問題應該是出在最後 `history` 的記憶體位置沒有釋放乾淨
```cpp=
/* --- linenoise.c --- */
int linenoiseHistoryAdd(const char *line)
{
char *linecopy;
...
linecopy = strdup(line);
if (!linecopy)
return 0;
if (history_len == history_max_len) {
free(history[0]);
memmove(history, history + 1, sizeof(char *) * (history_max_len - 1));
history_len--;
}
history[history_len] = linecopy;
history_len++;
return 1;
}
```
* 找出與變數 `history` 釋放相關的程式碼
==TODO==
### Massif 視覺化記憶體使用量
#### 執行測試
```shell
$ valgrind --tool=massif ./qtest -f ./traces/trace-17-complexity.cmd
```
* 產生的 massif.out.<pid> 檔,可以用以下指令輸出以文字呈現的圖表,以及詳細內容
```shell
$ ms_print massif.out.<pid>
```
* 也可用 [massif-visualizer](https://github.com/KDE/massif-visualizer) 進行視覺化分析
```shell
$ sudo apt-get install massif-visualizer
$ massif-visualizer massif.out.<pid>
```
![](https://i.imgur.com/c5OCUF8.png)
#### 設計實驗
==TODO==