# 2021q1 Homework1 (lab0)
contributed by < [`tinyynoob`](https://github.com/tinyynoob) >
> [作業說明](https://hackmd.io/@sysprog/linux2021-lab0#)
## Quick Guide
[TOC]
## 開發環境
```
$ uname -a
Linux tinyynoob-home 5.11.0-43-generic #47~20.04.2-Ubuntu SMP Mon Dec 13 11:06:56 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 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.
```
## 實作流程
| 進度 | 內容敘述 |
|:-:|:--|
|1| q_new() |
|1| q_free() |
|1| q_insert_head() |
|1| q_insert_tail() |
|1| q_remove_head() |
|1| q_size() |
|1| q_reverse() |
|1| q_sort() |
|0| 開啟 [Address Sanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer),修正 `qtest` 執行過程中的錯誤 |
|0| 運用 Valgrind 排除 `qtest` 實作的記憶體錯誤,並透過 Massif 視覺化 "simulation" 過程中的記憶體使用量,需要設計對應的實驗 |
|0| 在 `qtest` 中實作 [coroutine](https://en.wikipedia.org/wiki/Coroutine),並提供新的命令 `web`,提供 web 伺服器功能,注意: web 伺服器運作過程中,`qtest` 仍可接受其他命令 |
|0| 說明 [antirez/linenoise](https://github.com/antirez/linenoise) 的運作原理,注意到 [termios](http://man7.org/linux/man-pages/man3/termios.3.html) 的運用|
|0| 研讀論文 [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) 及程式實作的原理。注意:現有實作存在若干致命缺陷,請討論並提出解決方案 |
|0| 指出現有程式的缺陷 (提示: 和 [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 有關),嘗試強化並提交 pull request |
### queue.h
- 加入指標變數 `tail` 幫助快速找到結尾以利 `q_insert_tail()` 實作。
- 為了能在 $O(1)$ 時間內返回 queue 的大小,因此於 `queue` 的結構中加入一個 `int` 變數 `size` 防止每次都要遍歷 `queue` 求解。
```c
/* Queue structure */
typedef struct {
list_ele_t *head; /* Linked list of elements */
list_ele_t *tail;
int size;
} queue_t;
```
### queue.c
#### q_new()
配置空間後先檢查是否有成功配置,接著為結構成員進行初始化
- 將 `head` 及 `tail` 預設為 `NULL` 防止不當操作
- 將 `size` 預設為0
```c=
queue_t *q_new()
{
queue_t *q = malloc(sizeof(queue_t));
if (!q)
return NULL;
q->head = NULL;
q->tail = NULL;
q->size = 0;
return q;
}
```
#### q_free()
從 `head` 指到的元素開始逐一將 `queue` 中的節點刪除,刪除時將節點內的字串及節點本身釋放。
##### before
```graphviz
digraph q_free0 {
node [shape=record];
rankdir=LR;
node_0 -> node_1 -> dots -> node_n;
q:f0 -> node_0;
q:f1 -> node_n;
q [label="<f0> head|<f1> tail|<f2> size"];
dots [shape=none label="。。。" fontsize=20];
}
```
##### after each step
```graphviz
digraph q_free1 {
node [shape=record];
rankdir=LR;
node_0 -> node_1 -> dots;
dots -> node_n;
q:f0 -> node_0 [style=dashed];
q:f0 -> node_1;
q:f1 -> node_n;
temp -> node_0 [rankdir=TB];
q [label="<f0> head|<f1> tail|<f2> size"];
temp [shape=diamond];
dots [shape=none label="。。。" fontsize=20];
}
```
```c=
void q_free(queue_t *q)
{
while (q->head) {
list_ele_t *temp = q->head;
q->head = temp->next;
free(temp->value);
free(temp);
}
free(q);
}
```
由於在測試中遇到錯誤訊息
```
Segmentation fault occurred. You dereferenced a NULL or invalid pointer
中止 (核心已傾印)
```
因此在 while 之前加入以下兩行程式碼,避免 doubly free 的問題
```c
if (!q) // prevent doubly free
return;
```
#### q_insert_head()
配置 `list_ele_t` 結構空間及字串空間,若配置失敗則立即跳離函式並回傳false;若順利配置則將新節點插入最前方,如果是唯一節點將需要設定 `q` 中的 `tail` 成員。
:::warning
目前不知如何解決 strlen() 若讀不到 ``'\0'`` 的問題
:::
```c=
bool q_insert_head(queue_t *q, char *s)
{
if (!q)
return false;
list_ele_t *newh = (list_ele_t *) malloc(sizeof(list_ele_t));
if (!newh)
return false;
int s_size = strlen(s) + 1; // strlen() may not be safe if there is no '\0'
char *s_in_ele = (char *) malloc(sizeof(char) * s_size);
if (!s_in_ele) {
free(newh);
return false;
}
for (int i = 0; i < s_size; i++) // copy the string including '/0'
s_in_ele[i] = s[i];
newh->value = s_in_ele;
newh->next = q->head;
q->head = newh;
if (!q->tail) // if newh is the only element
q->tail = newh;
q->size++;
return true;
}
```
#### q_insert_tail()
作法與 `q_insert_head()` 類似,但注意到須將原先的尾節點接到新的尾節點,如下方程式碼第 18 行。
```graphviz
digraph insert_tail {
node [shape=record];
rankdir=LR;
nodes -> dots;
dots -> old_tail;
old_tail -> new_tail[color=salmon];
tail -> old_tail[style=dashed];
tail -> new_tail;
tail[shape=diamond];
dots [shape=none label="。。。" fontsize=20];
}
```
```c=
bool q_insert_tail(queue_t *q, char *s)
{
if (!q)
return false;
list_ele_t *newt = (list_ele_t *) malloc(sizeof(list_ele_t));
if (!newt)
return false;
int s_size = strlen(s) + 1;
char *s_in_ele = (char *) malloc(sizeof(char) * s_size);
if (!s_in_ele) {
free(newt);
return false;
}
for (int i = 0; i < s_size; i++) // copy the string including '/0'
s_in_ele[i] = s[i];
newt->value = s_in_ele;
newt->next = NULL;
if (q->tail)
q->tail->next = newt;
q->tail = newt;
if (!q->head) // if newt is the only element
q->head = newt;
q->size++;
return true;
}
```
#### q_remove_head()
先檢查 queue 是否為空,之後執行
1. 若 `sp` 為 non-NULL ,將目標節點內的字串複製至 `sp`
2. 將目標節點從 queue 中移除
3. 更新 `q->size` 及 `q->head`
4. 若移除節點後 `queue` 為空,將 `tail` 成員指向 `NULL`
```c=
bool q_remove_head(queue_t *q, char *sp, size_t bufsize)
{
if (!q)
return false;
else if (!q->size)
return false;
char *it = q->head->value;
size_t sp_size = 0;
while (!*it && sp_size < bufsize) { // compute sp_size
it++;
sp_size++;
}
if (sp) { // If sp is non-NULL
sp = (char *) malloc(sizeof(char) * sp_size);
if (!sp)
return false;
for (size_t i = 0; i < sp_size; i++) // copy the removed string to sp
sp[i] = q->head->value[i];
}
if (sp_size == bufsize) // if the maximum is achieved
sp[bufsize - 1] = '\0';
free(q->head->value);
list_ele_t *toDelete = q->head;
q->head = q->head->next;
free(toDelete);
q->size--;
if (!q->size)
q->tail = NULL;
return true;
}
```
我認為這裡題目的原意有些不清,原先誤以為需要為 `sp` 分配空間,理解後重新修改程式,並用更易理解的版本寫 `sp` 的部份,其中 `min` 函式使用 macro 完成。
:::warning
不知道為何將下方程式第 5 行及第 6 行合併會發生錯誤
:::
```c=
bool q_remove_head(queue_t *q, char *sp, size_t bufsize)
{
if (!q || !q->size)
return false;
size_t sp_size = min(strlen(q->head->value), bufsize - 1);
sp_size = sp_size + 1;
if (sp) { // if sp is non-NULL
for (size_t i = 0; i < sp_size - 1; i++) // copy the string
sp[i] = q->head->value[i];
sp[sp_size - 1] = '\0';
}
free(q->head->value);
list_ele_t *toDelete = q->head;
q->head = q->head->next;
free(toDelete);
if (!q->head)
q->tail = NULL;
q->size--;
return true;
}
```
#### q_size()
```c
int q_size(queue_t *q)
{
if (!q)
return 0;
return q->size;
}
```
#### q_reverse()
使用三個 `list_ele_t` 指標一起向後遍歷整個 list 來完成反轉。 \
概念上等同於依序將節點 push 進一個新的 stack 。
```c=
void q_reverse(queue_t *q)
{
if (!q || q->size == 0 || q->size == 1)
return;
q->tail = q->head;
list_ele_t *prev = q->head;
list_ele_t *temp = q->head->next->next;
q->head = q->head->next;
q->head->next = prev;
while (temp) {
prev = q->head;
q->head = temp;
temp = temp->next;
q->head->next = prev;
}
q->tail->next = NULL;
}
```
#### q_sort()
第一個版本額外宣告了一組 array of pointers to elements 來幫助排序
```c=
void q_sort(queue_t *q)
{
if (!q || !q->size)
return;
list_ele_t **array = (list_ele_t **) malloc(sizeof(list_ele_t *) *
q->size); // array of pointers
list_ele_t *temp = q->head;
for (int i = 0; i < q->size; i++) {
array[i] = temp;
temp = temp->next;
}
q_sort_recur(array, 0, q->size - 1, string_compare);
/* relink the list with the result */
for (int i = 0; i < q->size - 1; i++)
array[i]->next = array[i + 1];
q->head = array[0];
q->tail = array[q->size - 1];
free(array);
}
```
配合遞迴函式
```c=
void q_sort_recur(list_ele_t **array,
int left,
int right,
int (*cmp)(char *, char *))
{
if (left >= right)
return;
/*choose array[right] as pivot*/
list_ele_t *temp;
int i = left;
for (int j = left; j < right; j++) {
if (cmp(array[j]->value, array[right]->value) < 0) {
/* exchange array[i] and array[j] */
temp = array[i];
array[i] = array[j];
array[j] = temp;
/* end exchange */
i++;
}
}
/* exchange array[i] and array[right] */
temp = array[i];
array[i] = array[right];
array[right] = temp;
/* end exchange */
q_sort_recur(array, left, i - 1, cmp);
q_sort_recur(array, i + 1, right, cmp);
}
```
在測試時發現題目並不允許使用 malloc() 。
後來我參考了 [quiz 1](https://hackmd.io/@sysprog/linux2021-quiz1) ,意識到在 quick sort 中,往下遞迴時元素的順序並不是很重要,只要求與 pivot 的相對大小。因此 linked list 就有天然的結構,完全不需要額外使用一組指標陣列。
於是重新寫出第二版本:
```c=
void q_sort(queue_t *q)
{
if (!q || !q->size)
return;
q->head = sortList(q->head, strcmp);
list_ele_t *it;
for (it = q->head; it->next; it = it->next)
;
q->tail = it;
}
```
同樣額外建構了一個方便遞迴的 quick sort 函式
```c=
list_ele_t *sortList(list_ele_t *head, int (*cmp)(const char *, const char *))
{
if (!head || !head->next)
return head;
const char *pivot = head->value;
list_ele_t *left = NULL, *right = NULL, *it = head->next;
while (it) {
list_ele_t *temp = it->next;
push(cmp(it->value, pivot) > 0 ? &right : &left, it);
it = temp;
}
left = sortList(left, cmp);
right = sortList(right, cmp);
if (left) {
for (it = left; it->next; it = it->next)
;
it->next = head;
}
head->next = right;
return left ? left : head;
}
```
及
```c
void inline push(list_ele_t **head, list_ele_t *newNode)
{
newNode->next = *head;
(*head) = newNode;
}
```
在這個過程中,我最有心得的是 ternary operator 跟 pointer to poiner 在 list 操作中的妙用。
然而,使用這個版本進行測試,會發現效率並不符合題目的要求。測試結果如下:
```shell=
scripts/driver.py -c
--- Trace Points
+++ TESTING trace trace-01-ops:
# Test of insert_head and remove_head
--- trace-01-ops 6/6
+++ TESTING trace trace-02-ops:
# Test of insert_head, insert_tail, and remove_head
--- trace-02-ops 6/6
+++ TESTING trace trace-03-ops:
# Test of insert_head, insert_tail, reverse, and remove_head
--- trace-03-ops 6/6
+++ TESTING trace trace-04-ops:
# Test of insert_head, insert_tail, size, and sort
--- trace-04-ops 6/6
+++ TESTING trace trace-05-ops:
# Test of insert_head, insert_tail, remove_head, reverse, size, and sort
--- trace-05-ops 5/5
+++ TESTING trace trace-06-string:
# Test of truncated strings
--- trace-06-string 6/6
+++ TESTING trace trace-07-robust:
# Test operations on NULL queue
--- trace-07-robust 6/6
+++ TESTING trace trace-08-robust:
# Test operations on empty queue
--- trace-08-robust 6/6
+++ TESTING trace trace-09-robust:
# Test remove_head with NULL argument
--- trace-09-robust 6/6
+++ TESTING trace trace-10-malloc:
# Test of malloc failure on new
--- trace-10-malloc 6/6
+++ TESTING trace trace-11-malloc:
# Test of malloc failure on insert_head
--- trace-11-malloc 6/6
+++ TESTING trace trace-12-malloc:
# Test of malloc failure on insert_tail
--- trace-12-malloc 6/6
+++ TESTING trace trace-13-perf:
# Test performance of insert_tail
--- trace-13-perf 6/6
+++ TESTING trace trace-14-perf:
# Test performance of size
--- trace-14-perf 6/6
+++ TESTING trace trace-15-perf:
# Test performance of insert_tail, size, reverse, and sort
ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient
Segmentation fault occurred. You dereferenced a NULL or invalid pointer
--- trace-15-perf 0/6
+++ TESTING trace trace-16-perf:
# Test performance of sort with random and descending orders
# 10000: all correct sorting algorithms are expected pass
# 50000: sorting algorithms with O(n^2) time complexity are expected failed
# 100000: sorting algorithms with O(nlogn) time complexity are expected pass
ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient
ERROR: Not sorted in ascending order
ERROR: Freed queue, but 19022 blocks are still allocated
ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient
ERROR: Not sorted in ascending order
ERROR: Freed queue, but 118568 blocks are still allocated
ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient
ERROR: Not sorted in ascending order
Error limit exceeded. Stopping command execution
--- trace-16-perf 0/6
+++ TESTING trace trace-17-complexity:
# Test if q_insert_tail and q_size is constant time complexity
Probably constant time
Probably constant time
--- trace-17-complexity 5/5
--- TOTAL 88/100
```
因此,改以 merge sort 來實作 `q_sort()` ,以下為第三個版本:
```c=
void q_sort(queue_t *q)
{
if (!q || !q->size)
return;
q->head = sortList(q->head, strcmp);
list_ele_t *it;
for (it = q->head; it->next; it = it->next)
;
q->tail = it;
}
```
在 linked list 版本的 merge sort 中,需要將原 list 分割成為兩個 sublist 進行遞迴, `truncate_and_findMid()` 即為進行分割之函式。
```c=
list_ele_t *sortList(list_ele_t *head, int (*cmp)(const char *, const char *))
{
if (!head || !head->next)
return head;
list_ele_t *mid = truncate_and_findMid(head);
head = sortList(head, cmp);
mid = sortList(mid, cmp);
return mergeSorted(head, mid, cmp);
}
```
```graphviz
digraph truncate{
//node [shape=record];
rankdir=LR;
dots1 -> toTruncate;
toTruncate -> slow [style=dashed];
toTruncate -> null [dir=none];
slow ->dots2;
toTruncate [shape=record];
slow [shape=record];
dots1 [shape=none label="。。。" fontsize=20];
dots2 [shape=none label="。。。" fontsize=20];
graph []
{
rank=same;
null [style=invis label="" width="0" height="0"];
null_ [style=invis label="" width="0" height="0"];
null -> null_ [arrowhead=teetee];
}
}
```
```c=
/* split the list and return a pointer to the mid element */
list_ele_t *truncate_and_findMid(list_ele_t *const head)
{
/* the list should be guarantee to have at least 2 elements */
list_ele_t *fast = head->next;
list_ele_t *slow = head;
list_ele_t *toTruncate = NULL;
while (fast) {
/* fast forwards 2 place and slow forwards 1 place each iteration */
if (fast->next)
fast = fast->next->next;
else
fast = fast->next;
toTruncate = slow;
slow = slow->next;
}
toTruncate->next = NULL;
return slow;
}
```
而 `sortList()` 中第 8 行有函式 `mergeSorted()` ,功能為將兩個已排序的 linked list 合而為一。
```c=
list_ele_t *mergeSorted(list_ele_t *first,
list_ele_t *second,
int (*cmp)(const char *, const char *))
{
list_ele_t *ans;
pop(cmp(first->value, second->value) < 0 ? &first : &second, &ans);
list_ele_t *it = ans;
while (1) {
if (!first) {
it->next = second;
break;
} else if (!second) {
it->next = first;
break;
} else {
pop(cmp(first->value, second->value) < 0 ? &first : &second,
&it->next);
it = it->next;
}
}
return ans;
}
```
為使 `mergeSorted()` 更為簡潔,我們引入 `pop()` inline ,該函式從 `stack` 的最前方移除一個元素交給 `receiver` 。
```c
void inline pop(list_ele_t **stack, list_ele_t **receiver)
{
*receiver = *stack;
*stack = (*stack)->next;
}
```
至第三版本終於通過自動評分機制,完成 `queue.c` 的實作。
```shell
--- TOTAL 100/100
```
### Address Sanitizer
首先我們看到以下錯誤訊息
```shell=
=================================================================
==36384==ERROR: AddressSanitizer: global-buffer-overflow on address 0x55ded5195400 at pc 0x55ded517e8ff bp 0x7ffc09e411a0 sp 0x7ffc09e41190
READ of size 4 at 0x55ded5195400 thread T0
#0 0x55ded517e8fe in do_help_cmd /home/tinyynoob/code/lab0/console.c:307
#1 0x55ded517ea12 in interpret_cmda /home/tinyynoob/code/lab0/console.c:221
#2 0x55ded517f1f7 in interpret_cmd /home/tinyynoob/code/lab0/console.c:244
#3 0x55ded518093a in run_console /home/tinyynoob/code/lab0/console.c:660
#4 0x55ded517d521 in main /home/tinyynoob/code/lab0/qtest.c:788
#5 0x7f76380ae0b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)
#6 0x55ded517ab7d in _start (/home/tinyynoob/code/lab0/qtest+0x8b7d)
0x55ded5195401 is located 0 bytes to the right of global variable 'echo' defined in 'console.c:59:13' (0x55ded5195400) of size 1
SUMMARY: AddressSanitizer: global-buffer-overflow /home/tinyynoob/code/lab0/console.c:307 in do_help_cmd
Shadow bytes around the buggy address:
0x0abc5aa2aa30: f9 f9 f9 f9 04 f9 f9 f9 f9 f9 f9 f9 00 f9 f9 f9
0x0abc5aa2aa40: f9 f9 f9 f9 00 f9 f9 f9 f9 f9 f9 f9 00 f9 f9 f9
0x0abc5aa2aa50: f9 f9 f9 f9 00 00 00 00 04 f9 f9 f9 f9 f9 f9 f9
0x0abc5aa2aa60: 00 00 00 00 00 00 00 00 00 00 f9 f9 f9 f9 f9 f9
0x0abc5aa2aa70: 01 f9 f9 f9 f9 f9 f9 f9 01 f9 f9 f9 f9 f9 f9 f9
=>0x0abc5aa2aa80:[01]f9 f9 f9 f9 f9 f9 f9 04 f9 f9 f9 f9 f9 f9 f9
0x0abc5aa2aa90: 04 f9 f9 f9 f9 f9 f9 f9 00 00 00 00 00 00 00 00
0x0abc5aa2aaa0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0abc5aa2aab0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0abc5aa2aac0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0abc5aa2aad0: 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
==36384==ABORTING
```