# 2021q1 Homework1 (lab0)
contributed by < `secminhr` >
## 牛刀小試
根據 [牛刀小試](https://hackmd.io/@sysprog/linux2021-lab0#%E7%89%9B%E5%88%80%E5%B0%8F%E8%A9%A6) 小節實做 `q_size`
根據 `queue.[hc]` 之中的註解以及小節中的指示,為了使 `q_size(queue_t *)` 在 $O(1)$ 時間內達成,我們在 `queue.h` 的 `struct queue_t` 裡加入成員 `int size` 來維護 queue 的長度。
```cpp
typedef struct {
list_ele_t *head; /* Linked list of elements */
int size;
} queue_t;
```
接著修改 `queue.c` 中的 `q_size(queue_t *)`,使其回傳 `q->size`。
```cpp
int q_size(queue_t *q)
{
return q->size;
}
```
[commit 209521c6e7](https://github.com/secminhr/lab0-c/tree/209521c6e7f284ea21416bfccf511e5605e87810)
## C Programming Lab
### 實做 q_new
根據 `queue.c` 裡面的註解:
> TODO: What if malloc returned NULL?
我們知道需要處理錯誤時 `malloc` 傳回 `NULL` 的情況,另外也要記得將 `size` 初始化成0。
```cpp
queue_t *q_new()
{
queue_t *q = malloc(sizeof(queue_t));
if (q != NULL) {
q->head = NULL;
q->size = 0;
}
return q;
}
```
[commit 8474de3c94](https://github.com/secminhr/lab0-c/tree/8474de3c949221f341ddde26a98270d8f3239ab9)
### 實做 q_free
根據 `queue.c` 裡的 `TODO` 註解:
> TODO: How about freeing the list elements and the strings?
`q_free` 註解:
> Free all storage used by queue
和 `queue.h` 在 `struct ELE` 對 `char *value` 的註解:
> Pointer to array holding string.
> This array needs to be explicitly allocated and freed
我們知道 `q_free` 需要釋放 `q` 、所有 `list_ele_t` 和其 `char *value`。
```cpp
void q_free(queue_t *q)
{
if (q == NULL) {
return;
}
/* Free all list elements and strings */
while (q->head) {
list_ele_t *tmp = q->head;
q->head = q->head->next;
free(tmp->value);
free(tmp);
}
/* Free queue structure */
free(q);
}
```
[commit 69a395b98e](https://github.com/secminhr/lab0-c/tree/69a395b98ea890a497261b3d5b87684f71bd1055)
### 實做 q_insert_head
首先根據 `queue.c` 的註解:
> TODO: What should you do if the q is NULL?
我們先在 `q_insert_head` 加上一個 NULL check
```c=
if (q == NULL) {
return false;
}
```
再來就是把新的 `list_ele_t *` 跟 `char *` `malloc` 出來。加上 NULL check 並在 `malloc` 失敗時回傳 `false`。
```cpp
list_ele_t *newh = malloc(sizeof(list_ele_t));
if (newh == NULL) {
return false;
}
char *str = malloc(strlen(s) + 1);
if (str == NULL) {
free(newh);
return false;
}
```
注意 `str` 申請的空間長度是 `strlen(s) + 1` ,因為 `strlen` 不計算結尾 `\0` 。
最後設定 `newh` 和 `str` 並維護 `q` 。
```cpp
bool q_insert_head(queue_t *q, char *s)
{
if (q == NULL) {
return false;
}
list_ele_t *newh = malloc(sizeof(list_ele_t));
if (newh == NULL) {
return false;
}
char *str = malloc(strlen(s) + 1);
if (str == NULL) {
free(newh);
return false;
}
strncpy(str, s, strlen(s) + 1);
newh->next = q->head;
newh->value = str;
q->head = newh;
q->size++;
return true;
}
```
[commit 9714cc6d37](https://github.com/secminhr/lab0-c/tree/9714cc6d37d23676fadab882681ae78abc40e234)
### 實做 q_insert_tail
根據 `queue.c` 的註解:
> Remember: It should operate in O(1) time
要在 $O(1)$ 時間內插入元素到串列尾端,我們可以在 `queue_t` 裡多維護一個成員 `list_ele_t *tail` 。
```cpp
typedef struct {
list_ele_t *head; /* Linked list of elements */
list_ele_t *tail;
int size;
} queue_t;
```
定義 `tail` 的表示如圖
**single element**
```graphviz
digraph tail_single_ele {
Head[shape=box]
Tail[shape=box]
Ele[shape=box]
Head -> Ele
Tail -> Ele
}
```
**multiple elements**
```graphviz
digraph tail_mul_ele {
node [shape=box]
Head -> Ele1
Tail -> Ele2
Ele1 -> Ele2
{rank=same;Ele1 Ele2}
}
```
#### 修改 q_new
加上對於 `tail` 的初始化
```cpp
queue_t *q_new()
{
queue_t *q = malloc(sizeof(queue_t));
if (q != NULL) {
q->head = NULL;
q->tail = NULL;
q->size = 0;
}
return q;
}
```
#### 修改 q_insert_head
當 q 是 empty queue 時,須更新 `tail`
```cpp
if (q->size == 0) {
q->tail = newh;
}
```
#### q_insert_tail
新節點的處理跟 `q_insert_head` 都一樣。
但是在把新節點加入 queue 時會因為 queue size 不同而有不同的操作。
```cpp
if (q->size) {
q->tail->next = newt;
} else {
q->head = newt;
}
```
最後,維護 `q`。
```cpp
bool q_insert_tail(queue_t *q, char *s)
{
if (q == NULL) {
return false;
}
list_ele_t *newt = malloc(sizeof(list_ele_t));
if (newt == NULL) {
return false;
}
char *str = malloc(strlen(s) + 1);
if (str == NULL) {
free(newt);
return false;
}
strncpy(str, s, strlen(s) + 1);
newt->next = NULL;
newt->value = str;
if (q->size) {
q->tail->next = newt;
} else {
q->head = newt;
}
q->tail = newt;
q->size++;
return true;
}
```
[commit a1b0028600](https://github.com/secminhr/lab0-c/tree/a1b0028600b4ae9e444f2f8088d313e4a2bcb879)
### 實做 q_remove_head
根據 `queue.c` 的註解:
> Attempt to remove element from head of queue.
> Return true if successful.
> Return false if queue is NULL or empty.
先在開頭加上 NULL 或者 empty 判定
```cpp
if (q == NULL || q->size == 0) {
return false;
}
```
再來就是對 `q` 的維護,並且把舊的 `head` 和其 `value` 釋放。
注意當 `size` 是 1 的時候, `tail` 也需要更新。
```cpp
bool q_remove_head(queue_t *q, char *sp, size_t bufsize)
{
if (q == NULL || q->size == 0) {
return false;
}
list_ele_t *old_head = q->head;
q->head = q->head->next;
if (q->size == 1) {
q->tail = NULL;
}
q->size--;
if (sp != NULL) {
strncpy(sp, old_head->value, bufsize - 1);
*(sp + bufsize - 1) = '\0';
}
free(old_head->value);
free(old_head);
return true;
}
```
[commit c487dafce0](https://github.com/secminhr/lab0-c/tree/c487dafce032b70f87dd72d2f66f6665fe455c3e)
### 實做 q_reverse
根據 `queue.c` 的註解
> Reverse elements in queue
> No effect if q is NULL or empty
先加入 NULL 或 empty 的判定
```cpp
if (q == NULL || q->size == 0) {
return;
}
```
接下來走過整個 queue,把 `next` 改成上一個就好。最後更新 `q` 的 `head` 和 `tail` 。
```cpp
void q_reverse(queue_t *q)
{
if (q == NULL || q->size == 0) {
return;
}
list_ele_t *prev = NULL;
list_ele_t *cur = q->head;
while (cur) {
list_ele_t *next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
q->tail = q->head;
q->head = prev;
}
```
[corresponding commit]()
### 修正 q_size
這時候發現測試的 `trace-07` 有問題如下:
> +++ TESTING trace trace-07-robust:
> \# Test operations on NULL queue
> Segmentation fault occurred. You dereferenced a NULL or invalid pointer
> --- trace-07-robust 0/6
透過逐一執行 `trace-07` 內含的命令後,可以發現 segmentation fault 的原因是 `q_size` 缺少了 NULL check。
根據 `queue.c` 的註解:
> Return number of elements in queue.
> Return 0 if q is NULL or empty
加入 NULL check
```cpp
int q_size(queue_t *q)
{
return q == NULL ? 0 : q->size;
}
```
再測試可發現 `trace-07` 的問題已修正。
[commit 06f035eed0](https://github.com/secminhr/lab0-c/tree/06f035eed044b2567f99d812ec67d3886327ea1b)
### 提取重複
發現 `q_insert_tail` 和 `q_insert_head` 在配置空間時的動作一致,將它提取出來。
```cpp
static list_ele_t *alloc_ele(queue_t *q, size_t value_size)
{
if (q == NULL) {
return NULL;
}
list_ele_t *new = malloc(sizeof(list_ele_t));
if (new == NULL) {
return NULL;
}
char *value = malloc(value_size);
if (value == NULL) {
free(new);
return NULL;
}
new->value = value;
return new;
}
```
[commit 33bdf06be8](https://github.com/secminhr/lab0-c/tree/33bdf06be855bd569baf10613b2db15fd69aa9f3)
### 實做 q_sort
這裡使用 merge sort 來實做
原因在於:
- trace-16 要求 q_sort 應達到時間複雜度 $O(nlogn)$
> sorting algorithms with O(nlogn) time complexity are expected pass
- 要達到 $O(nlogn)$ ,想到的就是 quicksort 或 merge sort
- merge sort 不論是最佳、平均或者最差情況都是 $O(nlogn)$ ,但是 quicksort 最差的表現可能達到 $O(n^2)$
- <s>我比較熟悉 merge sort</s>
:::warning
需要提及你對 sorting 演算法的選擇考量
:notes: jserv
:::
`q_sort` 的內容很直接
```cpp
void q_sort(queue_t *q)
{
if (q == NULL || q->size == 0 || q->size == 1) {
return;
}
queue_t q1, q2;
split(q, &q1, &q2);
q_sort(&q1);
q_sort(&q2);
merge(q1, q2, q);
}
```
`split` 會將 `q` 分成兩半,並把前半放在 `q1` 、後半放在 `q2`。
```cpp
static void split(queue_t *q, queue_t *q1, queue_t *q2)
{
int split_pos = q->size / 2 - 1;
q1->size = split_pos + 1;
q1->head = q->head;
q2->size = q->size - q1->size;
q2->tail = q->tail;
list_ele_t *split_ele = q->head;
for (int i = 0; i < split_pos; i++) {
split_ele = split_ele->next;
}
q1->tail = split_ele;
q2->head = split_ele->next;
q1->tail->next = NULL;
}
```
其中 `int split_pos = q->size / 2 - 1`, `-1` 是為了避免切出 empty queue 。
`merge` 則是合併 `q1` 和 `q2` 到 `dest` 。
```cpp
static void merge(queue_t q1, queue_t q2, queue_t *dest)
{
list_ele_t **merge_cur = &(dest->head);
while (q1.head && q2.head) {
if (strcmp(q1.head->value, q2.head->value) < 0) {
*merge_cur = q1.head;
q1.head = q1.head->next;
} else {
*merge_cur = q2.head;
q2.head = q2.head->next;
}
merge_cur = &((*merge_cur)->next);
}
queue_t last = q1.head ? q1 : q2;
*merge_cur = last.head;
dest->tail = last.tail;
}
```
至此,評分程式的要求已全數完成。
> --- TOTAL 100/100
[commit 66d400790b](https://github.com/secminhr/lab0-c/tree/66d400790bf35b1b1bcbbef834446214a7756b24)
## Address Sanitizer
### 尋找錯誤
先打開 Adress Sanitizer 。
```shell
$ make clean
rm -f qtest.o report.o console.o harness.o queue.o random.o dudect/constant.o dudect/fixture.o dudect/ttest.o linenoise.o .qtest.o.d .report.o.d .console.o.d .harness.o.d .queue.o.d .random.o.d .dudect/constant.o.d .dudect/fixture.o.d .dudect/ttest.o.d .linenoise.o.d *~ qtest /tmp/qtest.*
rm -rf .dudect
rm -rf *.dSYM
(cd traces; rm -f *~)
$ make SANITIZER=1
CC qtest.o
CC report.o
CC console.o
CC harness.o
CC queue.o
CC random.o
CC dudect/constant.o
CC dudect/fixture.o
CC dudect/ttest.o
CC linenoise.o
LD qtest
```
看錯誤訊息。
```shell
$ ./qtest
cmd> help
Commands:
# ... | Display comment
free | Delete queue
help | Show documentation
ih str [n] | Insert string str at head of queue n times. Generate random string(s) if str equals RAND. (default: n == 1)
it str [n] | Insert string str at tail of queue n times. Generate random string(s) if str equals RAND. (default: n == 1)
log file | Copy output to file
new | Create new queue
option [name val] | Display or set options
quit | Exit program
reverse | Reverse queue
rh [str] | Remove from head of queue. Optionally compare to expected value str
rhq | Remove from head of queue without reporting value.
show | Show queue contents
size [n] | Compute queue size n times (default: n == 1)
sort | Sort queue in ascending order
source file | Read commands from source file
time cmd arg ... | Time command execution
Options:
=================================================================
==3161==ERROR: AddressSanitizer: global-buffer-overflow on address 0x5614b90293c0 at pc 0x5614b90127bd bp 0x7ffeeea60bd0 sp 0x7ffeeea60bc0
READ of size 4 at 0x5614b90293c0 thread T0
#0 0x5614b90127bc in do_help_cmd /home/ioncamp/Desktop/linux_ncku/lab0-c/console.c:307
#1 0x5614b90128d0 in interpret_cmda /home/ioncamp/Desktop/linux_ncku/lab0-c/console.c:221
#2 0x5614b90130b5 in interpret_cmd /home/ioncamp/Desktop/linux_ncku/lab0-c/console.c:244
#3 0x5614b90147f8 in run_console /home/ioncamp/Desktop/linux_ncku/lab0-c/console.c:660
#4 0x5614b90113e5 in main /home/ioncamp/Desktop/linux_ncku/lab0-c/qtest.c:780
#5 0x7fc2e9eba0b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)
#6 0x5614b900eb8d in _start (/home/ioncamp/Desktop/linux_ncku/lab0-c/qtest+0x8b8d)
0x5614b90293c1 is located 0 bytes to the right of global variable 'echo' defined in 'console.c:59:13' (0x5614b90293c0) of size 1
SUMMARY: AddressSanitizer: global-buffer-overflow /home/ioncamp/Desktop/linux_ncku/lab0-c/console.c:307 in do_help_cmd
Shadow bytes around the buggy address:
0x0ac3171fd220: f9 f9 f9 f9 04 f9 f9 f9 f9 f9 f9 f9 04 f9 f9 f9
0x0ac3171fd230: f9 f9 f9 f9 00 f9 f9 f9 f9 f9 f9 f9 00 f9 f9 f9
0x0ac3171fd240: f9 f9 f9 f9 00 f9 f9 f9 f9 f9 f9 f9 00 00 00 00
0x0ac3171fd250: 04 f9 f9 f9 f9 f9 f9 f9 00 00 00 00 00 00 00 00
0x0ac3171fd260: 00 00 f9 f9 f9 f9 f9 f9 01 f9 f9 f9 f9 f9 f9 f9
=>0x0ac3171fd270: 01 f9 f9 f9 f9 f9 f9 f9[01]f9 f9 f9 f9 f9 f9 f9
0x0ac3171fd280: 04 f9 f9 f9 f9 f9 f9 f9 04 f9 f9 f9 f9 f9 f9 f9
0x0ac3171fd290: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0ac3171fd2a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0ac3171fd2b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0ac3171fd2c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==3161==ABORTING
```
錯誤出現在 `console.c:307`
> #0 0x5614b90127bc in do_help_cmd /home/ioncamp/Desktop/linux_ncku/lab0-c/console.c:307
直接到錯誤訊息指定的地方看
```c=307
report(1, "\t%s\t%d\t%s", plist->name, *plist->valp,
plist->documentation);
```
錯誤中有一段寫:
> READ of size 4 at 0x5614b90293c0 thread T0
所以我們知道是在 307 行試圖讀取 4 個 bytes 的時候出了錯,而這行只有三個讀取
- `plist->name`
- `*plist->valp`
- `plist->documentation`
我們可以從型態下手,看看其中有沒有長度正好是 4 個 bytes 的。可以在上面幾行找到 `plist` 的定義。
```c=304
param_ptr plist = param_list;
```
在 `console.h` 可以找到 `param_ptr` 。
```c=30
/* Integer-valued parameters */
typedef struct PELE param_ele, *param_ptr;
struct PELE {
char *name;
int *valp;
char *documentation;
/* Function that gets called whenever parameter changes */
setter_function setter;
param_ptr next;
};
```
看來是讀取 `valp` 的時候出了錯, int 的長度是 4 個 bytes 。
做個實驗把 `console.c:307` 改成直接印出 `*plist->valp` ,果然錯誤再度出現。
這裡讀取的錯誤很可能代表這個 `param_list` 裡面有錯誤的值。我們發現在 `console.c` 裡只有 `add_param()` 在對 `param_list` 做更改。
(`init_cmd()` 裡面只給 `param_list = NULL` ,只是初始化這個變數而已)
查看 `add_param()` 的呼叫者
```c=104
add_param("simulation", (int *) &simulation, "Start/Stop simulation mode",
NULL);
add_param("verbose", &verblevel, "Verbosity level", NULL);
add_param("error", &err_limit, "Number of errors until exit", NULL);
add_param("echo", (int *) &echo, "Do/don't echo commands", NULL);
```
`(int *) &echo` 和 `(int *)simulation` 這兩個強轉型很顯眼及奇怪。
查看兩者宣告分別是
```c=
static bool echo = 0;
```
```c=
bool simulation = false;
```
到這裡,問題已經顯現出來。
`bool` 的長度只有一個 byte ,但是他們的指標被強轉成 `int *` ,會一次讀 4 個 bytes ,於是不正常的讀取行為就發生了。
### 修正
先修正 `echo` ,因為這正是錯誤訊息描述的
> 0x5614b90293c1 is located 0 bytes to the right of global variable 'echo' defined in 'console.c:59:13' (0x5614b90293c0) of size 1
先查看一下 `echo` 可以發現,它所有的操作在型態變成 `int` 的情況下都可以運作。
所以可以直接把 `echo` 的型態換成 `int` 。
```c=
static int echo = 0;
//...
add_param("echo", &echo, "Do/don't echo commands", NULL);
```
試著再跑一次 Address Sanitizer ,發現下個錯誤確實在 `simulation` 發生。
> 0x5555ef8345e1 is located 0 bytes to the right of global variable 'simulation' defined in 'console.c:21:6' (0x5555ef8345e0)
但是這次沒辦法直接換型態了,因為在 `console.h`中:
```c=11
extern bool simulation;
```
如果直接改型態將會向外影響。
我們再看看 `add_param()` 的定義:
```c=
void add_param(char *name,
int *valp,
char *doccumentation,
setter_function setter);
```
`setter_function` 的定義:
```c=
typedef void (*setter_function)(int oldval);
```
及 `struct PELE` 的對 `setter` 的註解:
> Function that gets called whenever parameter changes
我們既然有 `valp` 被改變時會呼叫的 function ,這代表我們可以維護一個 int 型態的變數,並利用 `setter` 來與 `simlation` 連動,在該變數被更新時一起更新 `simulation` 。
```c=
bool simulation = false;
static int simulation_backing = 0;
//...
static void simulation_setter(int oldVal)
{
simulation = simulation_backing ? true : false;
}
//...
add_param("simulation", &simulation_backing, "Start/Stop simulation mode",
simulation_setter);
```
修正後,`help` 命令將不會出現錯誤訊息。
[commit 0215e12577](https://github.com/secminhr/lab0-c/tree/0215e1257785f012e58d7f5e30b153f1f4a2d088)
:::danger
留意到 instruction (指令) 和 command (命令) 這兩個不同語義的詞彙。細節!
:notes: jserv
:::
## Valgrind
看訊息
```shell=
$ make valgrind
# Explicitly disable sanitizer(s)
make clean SANITIZER=0 qtest
make[1]: Entering directory '/home/ioncamp/Desktop/linux_ncku/lab0-c'
rm -f qtest.o report.o console.o harness.o queue.o random.o dudect/constant.o dudect/fixture.o dudect/ttest.o linenoise.o .qtest.o.d .report.o.d .console.o.d .harness.o.d .queue.o.d .random.o.d .dudect/constant.o.d .dudect/fixture.o.d .dudect/ttest.o.d .linenoise.o.d *~ qtest /tmp/qtest.*
rm -rf .dudect
rm -rf *.dSYM
(cd traces; rm -f *~)
CC qtest.o
CC report.o
CC console.o
CC harness.o
CC queue.o
CC random.o
CC dudect/constant.o
CC dudect/fixture.o
CC dudect/ttest.o
CC linenoise.o
LD qtest
make[1]: Leaving directory '/home/ioncamp/Desktop/linux_ncku/lab0-c'
cp qtest /tmp/qtest.pt5fTC
chmod u+x /tmp/qtest.pt5fTC
sed -i "s/alarm/isnan/g" /tmp/qtest.pt5fTC
scripts/driver.py -p /tmp/qtest.pt5fTC --valgrind
--- Trace Points
+++ TESTING trace trace-01-ops:
# Test of insert_head and remove_head
==3737== 5 bytes in 1 blocks are still reachable in loss record 1 of 3
==3737== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==3737== by 0x4A4D50E: strdup (strdup.c:42)
==3737== by 0x11006E: linenoiseHistoryAdd (linenoise.c:1236)
==3737== by 0x110C01: linenoiseHistoryLoad (linenoise.c:1325)
==3737== by 0x10C234: main (qtest.c:769)
==3737==
==3737== 160 bytes in 1 blocks are still reachable in loss record 2 of 3
==3737== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==3737== by 0x11002E: linenoiseHistoryAdd (linenoise.c:1224)
==3737== by 0x110C01: linenoiseHistoryLoad (linenoise.c:1325)
==3737== by 0x10C234: main (qtest.c:769)
==3737==
==3737== 178 bytes in 19 blocks are still reachable in loss record 3 of 3
==3737== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==3737== by 0x4A4D50E: strdup (strdup.c:42)
==3737== by 0x10FFE2: linenoiseHistoryAdd (linenoise.c:1236)
==3737== by 0x110C01: linenoiseHistoryLoad (linenoise.c:1325)
==3737== by 0x10C234: main (qtest.c:769)
==3737==
--- trace-01-ops 6/6
...
```
這個訊息發生多次,雖然寫的是 still reachable ,但還是檢查一下。
看 `linenoise.c` ,可以發現訊息指出的地方都是把 `malloc` 拿到的結果放在全域變數 `history` ,沒有流失。
```c=1224
history = malloc(sizeof(char *) * history_max_len);
```
```c=1236
linecopy = strdup(line);
...
history[history_len] = linecopy;
```
### massif
TODO: