# 2020q3 Homework1 (lab0)
contributed by < `jonec76` >
###### tags: `sysprog-2020`
> [作業說明](https://hackmd.io/@sysprog/2020-lab0)
## 開發環境
```shell
$ uname -a
Linux jonec76-Aspire-M7720 5.4.0-47-generic #51-Ubuntu SMP GNU/Linux
```
## 作業要求
實作基本功能,測驗分數達到 100 分,但是加入 `make SANITIZE=1` 之後只有 95 分 ( trace-17 仍錯),以下是實作過程。
::::info
Jserv 老師上課說:q_reverse 還可以考慮如果 size=2 時,還需要做下面的 while-loop 嗎?
::::
### `q_insert_head(queue_t *q, char *s)`
- **錯誤過程**
- **複製字串出現亂碼**
```shell
cmd> ih jkl
q = [jkl��� qwer���]
```
原因出於 `strncpy(newh->value, s, strlen(s))`,在 [strncpy man page](https://linux.die.net/man/3/strncpy) 提到,以上程式碼會複製 src 的前 n bytes ,但這 n 個 bytes 不包含 null terminator,所以到了 dst 時該字串沒有 null terminator,就在字串的最後面產生亂碼。
解決方法就是改成 `strncpy(newh->value, s, strlen(s)+1)` ,讓 `strncpy` 複製前 n 個 bytes,並且他會在 n+1 的位置放入 '\0',也就是 null terminator。
- **code**
```cpp
bool q_insert_head(queue_t *q, char *s)
{
if (!q)
return false;
list_ele_t *newh = malloc(sizeof(list_ele_t));
size_t len = strlen(s);
/* TODO: What should you do if the q is NULL? */
if (newh == NULL) {
free(newh);
return false;
}
char *value = malloc((len + 1) * sizeof(char));
if (value == NULL) {
free(newh);
free(value);
return false;
}
newh->value = value;
strncpy(newh->value, s, len + 1);
/* Don't forget to allocate space for the string and copy it */
/* What if either call to malloc returns NULL? */
q->size++;
if (q->size == 1) {
q->tail = newh;
}
newh->next = q->head;
q->head = newh;
return true;
}
```
### `q_free(queue_t *q)`
- **錯誤過程**
```+++ TESTING trace trace-04-ops:
# Test of insert_head, insert_tail, size, and sort
ERROR: Freed queue, but 4 blocks are still allocated
--- trace-04-ops 0/6
```
尚未實作 `q_free()` 之前,執行 `make test` 後會發現 `trace-04-ops` 給了以上的錯誤回應,經過觀察 `trace-04-ops` 知道它使得 list size 達到 6,但是只做了 4 次的 "rh" 刪除,意思就是說在 04-ops 測試結束時,`q->head` 仍然有 2 個 `list_ele_t` 並沒有被刪除。
於是開始查找,這個 `qtest` 是如何得知還剩下多少 block?如何在程式結束(主動按下 ctrl+c)時執行的?
在 `main()` 裏頭有個 function `add_quit_helper(queue_quit)` ,會在一開始將 `queue_quit` 加入 `quit_helpers` queue,`queue_quit` 就是當程式離開之前要執行的 function。當程式執行 `quit` 或者 ==ctrl+c(還不懂為什麼 ctrl+c 會進入 do_quit_cmd)== 時,就會去執行`do_quit_cmd`,裡頭會執行 `quit_helpers` 就會執行到 `queue_quit`。
在 `queue_quit` 裡面會執行 `q_free()` ,並且執行 `allocation_check` ,這個 function 會回傳曾經 allocated 多少個 block 。在 `malloc` 時會對變數 `allocated_count`+1 ,`free` 會 `allocated_count`-1,於是只要當 `allocated_count` 不為 0 時,那就表示還有 block 是沒有被 freed 的。
- **code**
```cpp=
void q_free(queue_t *q)
{
if (!q)
return;
if (q->head == NULL) {
free(q);
return;
}
do {
list_ele_t *tmp = q->head;
char *tmp_value = tmp->value;
q->head = q->head->next;
free(tmp_value);
free(tmp);
} while (q->head != NULL);
free(q);
}
```
### `q_sort()`
::::info
Jserv 老師上課說:我們應該思考,當 list 的 size 應該大於多少時,才做 q_sort?因為做 linked-list 的 sort 成本過大
::::
- **錯誤過程**
- **Disallowed malloc**
最一開始的想法是把 q->head 這條 list 的所有 value 複製到一個新的陣列,並且使用 c library 提供的 `qsort()` 來完成,但是在 `make test` 時遇到 “Disallowed malloc” 錯誤。
在 `qtest.c` 裏頭的 `do_sort` 看到了以下的程式碼
```cpp
set_noallocate_mode(true);
if (exception_setup(true))
q_sort(q);
exception_cancel();
set_noallocate_mode(false);
```
於是再去看 `set_noallocate_mode(true);` 裏頭的程式碼,可以看到他是將 `noallocate_mode` 設為 `true`,再去找 `noallocate_mode` 用在哪裡,可以發現跟 `void *test_malloc` 有關。
得到的結論是,當 `qtest` 在執行時,因為在 `harness` 可以找到 `#define malloc test_malloc`,也就是自行額外定義 `malloc()` ,當在程式裡面執行 `malloc()` 時,其實都是執行 `void *test_malloc()` ,而該 function 在分配一塊所需空間並且回傳 pointer 之前,會先做一連串的檢查,因為在 `do_sort()` 裡面有將 `noallocate_mode` 設定為 `true`,所以會進到 *test_malloc() 的
```cpp
if (noallocate_mode) {
report_event(MSG_FATAL, "Calls to malloc disallowed");
return NULL;
}
```
也就可以在該作業規定 "在 sort 時不能額外分配新的空間,只能用 `q->head` list 內部做 swap"。
- **Time limit exceeded**
- 在跑 qsort 時無法通過 `trace-15-perf` 、 `trace-16-perf` 的測驗,原因是因為實作的 `ih` 不夠有效率的儲存資料
- **`trace-16-perf`**
原本是透過 `insertion sort` 來實作 q->head 的排序,但是在這個測試 ih 大量的 data,於是想說用 q1 上課的 [測驗題](https://hackmd.io/@sysprog/linux2020-quiz1) 提到的 optimizing merge-sort 去 sort,即通過此測驗。
```c=1
list_ele_t *left, *right, *tail, *half;
left = right = tail = half = start;
while(tail){
if(tail->next!=NULL )
tail = tail->next;
if(tail->next!=NULL )
tail = tail->next;
else break;
half = half->next;
}
right = half->next;
half->next = NULL;
left = sort(left);
right = sort(right);
```
原本的 merge-sort 會產生出 T(n) = T(n-1)+n 的時間複雜度,也就是 `O(n^2)`,於是在這邊將該程式改成 T(n) = 2T(n/2)+n 的方法,讓 merge-sort 的 left 以及 right 都是目前 size 的一半,並且去跑遞迴。
- **code**
```cpp=1
void q_sort(queue_t *q)
{
if (!q)
return;
q->head = mergeSortList(q->head);
}
list_ele_t *mergeSortList(list_ele_t *start)
{
if (!start || !start->next)
return start;
list_ele_t *left, *right, *tail, *half;
left = tail = half = start;
while (tail) {
if (tail->next != NULL)
tail = tail->next;
if (tail->next != NULL)
tail = tail->next;
else
break;
half = half->next;
}
right = half->next;
half->next = NULL;
left = mergeSortList(left);
right = mergeSortList(right);
for (list_ele_t *merge = NULL; left || right;) {
if (!right || (left && strcasecmp(left->value, right->value) <= 0)) {
if (!merge) {
start = merge = left;
} else {
merge->next = left;
merge = merge->next;
}
left = left->next;
} else {
if (!merge) {
start = merge = right;
} else {
merge->next = right;
merge = merge->next;
}
right = right->next;
}
}
return start;
}
```
### `q_remove_head(queue_t *q, char *sp, size_t bufsize)`
- **錯誤過程**
- **`ERROR: Freed queue, but 3 blocks are still allocated`**
當 free 了 `list_ele_t *tmp` 之後,卻沒有 free `tmp->value` 此 char pointer 指到的空間。
- **`ERROR: copying of string in remove_head overflowed destination buffer.`**
```cpp
if (i != string_length + STRINGPAD) {
report(1,
"ERROR: copying of string in remove_head overflowed "
"destination buffer.");
ok = false;
}
發現在執行 `trace-06-strings` 會有以上錯誤,去看了之後發現是在這邊出錯,`i=31` 但是 `string_length + STRINGPAD=30+1024`,這和 `trace-06-strings` 裡頭的 `option length 30` 有關係。
options length=30,意思是最大呈現出字串的長度為 30,在該行設定完 length=30 的下一行,是 `rh meerkat_panda_squirrel_vulture` ,這行進來的 `argv[1]` 參數是 30 個字,而試著用 `gdb` 去找錯誤,發現在 `removes[31]` 的位置並不是 'X',而是 "w",也就是 pop 出來的字 "meerkat_panda_squirrel_vulture_wolf" 的第 31 個位置 w,於是修改成以下程式碼
```cpp
strncpy(sp, tmp->value, bufsize);
sp[bufsize-1] = '\0';
```
使得複製最大只到 bufsize(含 null terminator) 的大小位置,因為這段複製的 src 有可能不包含 '\0',所以根據 `strncpy` 並不會在 dst 的最後一個位置加入 '\0',這部分需要自行處理。
- **Program received signal SIGSEGV, Segmentation fault.**
在 `trace-09-robust` 偵測出以下程式碼產生問題
```c=1
sp[bufsize - 1] = '\0';
```
於是去看了 `trace-09-robust` 裡頭的 op,發現 `rhq` 的設計,是希望單純移除 head 而不要複製,這邊會在 `do_remove_head_quiet` 的 function 傳入 bufsize=0 的參數,所以加入 if-condition 去偵測即可。
- **code**
```cpp
bool q_remove_head(queue_t *q, char *sp, size_t bufsize)
{
/* TODO: You need to fix up this code. */
/* TODO: Remove the above comment when you are about to implement. */
if (q == NULL || q->size == 0)
return false;
q->size--;
list_ele_t *tmp = q->head;
if (bufsize != 0) {
strncpy(sp, tmp->value, bufsize);
sp[bufsize - 1] = '\0';
}
q->head = q->head->next;
free(tmp->value);
free(tmp);
return true;
}
```
### `queue_t *q_new()`
- **錯誤過程**
- **Program received signal SIGSEGV, Segmentation fault.**
在 `trace-10-malloc` 偵測出以下程式碼產生問題
```cpp
queue_t *q = malloc(sizeof(queue_t));
q->head = NULL;
```
於是去觀察 "option malloc 50" 裡頭的參數,這項 op 會使得 fail_probability=50(初始值是 0 ),也就是他會有 0.5 的機率會產生 malloc 失敗(在 `test_malloc` 會回傳 `NULL` 給 pointer),加入 NULL check 即可。
- **code**
```cpp
queue_t *q_new()
{
queue_t *q = malloc(sizeof(queue_t));
/* TODO: What if malloc returned NULL? */
if (!q)
return q;
q->head = NULL;
q->size = 0;
return q;
}
```
### 其他錯誤
- 使用 `make SANITIZER=1` 後再跑 `make test`,會在 trace-17 出現以下的錯誤,評量分數只有 95,仍在找尋解決辦法。
>
==126215==ERROR: AddressSanitizer: global-buffer-overflow on address 0x5647678ad960 at pc 0x56476789d9be bp 0x7fff5b8849d0 sp 0x7fff5b8849c0
- **`git commit` fail to pass static analysis**
偵測到以下程式碼會有 `memory leak` 的問題
```cpp
newh->value = malloc((len + 1) * sizeof(char));
if (newh->value == NULL)
return false;
```
參考了 [simple-rules-to-avoid-memory-leaks-in-c](https://mousomer.wordpress.com/2010/11/03/simple-rules-to-avoid-memory-leaks-in-c/) 裡面提到的,在 malloc 一塊記憶體給一變數後,如果沒有 free() 該空間,則會發生 memory leak 的問題。另外,也有提到在 malloc 的時候,應將記憶體空間 malloc 給一個 char 變數,再透過 assign 的方式讓 pointer(這邊是 `newh->value`) 指到該空間作操作。
於是將程式碼改成
```cpp
char *value = malloc((len + 1) * sizeof(char));
if (value == NULL) {
free(newh);
free(value);
return false;
}
newh->value = value;
```
### `研讀論文 Dude, is my code constant time?,解釋本程式的 “simulation” 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 Student’s t-distribution 及程式實作的原理;`
- **說明**
在 `trace-17` 檔案裡頭可以看到會使用 simulation mode 去測試函式 `insert_tail` 以及 `q_size`。
以 `is_insert_tail_const` 為例,裡頭可以看到 `doit` 函式做了一連串的運算並把時間記錄下來,程式碼如下
```c=1
prepare_inputs(input_data, classes);
measure(before_ticks, after_ticks, input_data, mode);
differentiate(exec_times, before_ticks, after_ticks);
update_statistics(exec_times, classes);
bool ret = report();
```
`prepare_inputs` 裡頭的 `randombytes(input_data, number_measurements * chunk_size);` 能夠使 `input_data` 的每個 element 獲得 0~255 的亂數([ 參考 stack ](https://stackoverflow.com/questions/11147044/dev-urandom-range)),這部分是透過讀取 `fd = open("/dev/urandom", O_RDONLY);` 將亂數後放入 `input_data`,另外,也隨機產生 classes (0~1) 以及 `random_string`。
在 `measure` 時,將會執行 `insert_tail` 若干次,每次插入的資料是 `random_string` * `insert_tail[i]` 筆,並且將執行前後的 cpucycles
記錄下來,並在 `differentiate` 獲得執行時間。
`update_staticstics` 是將執行時間隨機的加入 class0 或者 class1 裡面,並且更新該 class 的平均數、m2(變異數),最後當計算出 t-value 大於 10 時,則會回報 "ERROR: Probably not constant time"。其原理是使用 Welch t-testing 檢測兩個 class 的分佈相近程度,若該函式執行時間是常數時間的話,則 input 不管大小如何都不會影響其執行時間。
- 如何產生隨機的 `input_data`
```c=1
for (size_t i = 0; i < number_measurements; i++) {
classes[i] = randombit();
if (classes[i] == 0)
*(uint16_t *) (input_data + i * chunk_size) = 0x00;
}
```
`classes[i]` 會隨機的得到 0 或 1 的值,接著若是 0 的話,會將 0x00 放入對應的位置。原先的 `input_data` 是 `uint8_t`,也就是每格大小為 1 byte,但這邊轉型成 `input_data + i * chunk_size` 指向的是一個 2 bytes 的大小位置,依據 little endian 規則,0x00 會從最低位開始放(目前指標指導的位置),接著後面就是補 0。
接著看 `measure` 裡頭的程式碼
```c=1
dut_insert_head(
get_random_string(),
*(uint16_t *) (input_data + i * chunk_size) % 10000);
)
```
可以看到這邊取值時,是取出指標目前指到的 2 bytes 的值出來,所以數字範圍才會是從 0 到 65535。
接著,思考同學的 pr [commit:master (#42)](https://github.com/sysprog21/lab0-c/commit/2d975875eecad8ddc0c5fc5ec6d4582f9a706439),原先的做法是將 chunk 的前面 2 個 bytes 清為 0,新的 pr 的做法是將整個 chunk 都清空,這樣的優點在目前的隨機取值似乎無法體現出來,如下
```c=1
*(uint16_t *) (input_data + i * chunk_size) % 10000);
```
但若是將 `*(uint16_t *)` 改成 `*(uint32_t *)`,也就是希望能夠使亂數的範圍更大(0~$2^{32}$)時,因為每次取出 4 bytes,此時若是舊的方法則會有未清空的部分(因為舊的方法只清空每個 chunk 的起始 2 bytes),是此 pr 的一項改進。
- ~~更改 `update_statistics` 計算方式~~
- ~~這邊其實是有疑問的~~,(==發現自己想法錯誤==)。舊的 code 如下
```c=1
// t_push(t_ctx *ctx, double x, uint8_t class)
double delta = x - ctx->mean[class];
ctx->mean[class] = ctx->mean[class] + delta / ctx->n[class];
ctx->m2[class] = ctx->m2[class] + delta * (x - ctx->mean[class]);
```
每次會傳入一個執行時間 x,並且將此 x 去更新對應的 class 的 mean,然而,計算標準差的公式中的平均值,應該是整個 class 的平均值,而不是像此 "動態" 調整過後的平均值。
但是看了同學的筆記 ([oscardshiang](https://hackmd.io/@oscarshiang/sysprog_lab0-c)) 以及 [welfords-method](https://jonisalonen.com/2013/deriving-welfords-method-for-computing-variance/#:~:text=The%20definition%20can%20be%20converted,squared%20differences%20from%20the%20mean.) 才知道這樣計算方式的優點,是能夠減少 overflow 的風險,以及能以 single pass 將 data 遍歷一遍即可得出平均值以及標準差。
算平均值的部分較為直觀,故以下僅摘錄計算標準差的部分,在 `t_push` 裡頭能看到
```c=1
double delta = x - ctx->mean[class];
ctx->m2[class] = ctx->m2[class] + delta * (x - ctx->mean[class]);
```
上述的 code 若做了移項,即可知道 `delta * (x - ctx->mean[class])` 化成數學式如下
$\sum_{k=1}^{N}(x_i - \overline{x}_{N})^2 - \sum_{k=1}^{N-1}(x_i - \overline{x}_{N-1})^2$
也就是程式碼的 $m2_N - m2_{N-1}$,接著經過一連串的化減(可參考 [welfords-method](https://jonisalonen.com/2013/deriving-welfords-method-for-computing-variance/#:~:text=The%20definition%20can%20be%20converted,squared%20differences%20from%20the%20mean.))才得到下面式子
$(x_n - \overline{m}_{n-1})(x_n - \overline{m}_{n})$
其中 $\overline{m}_{n}$ 表示算到第 n 個值的 mean 平均數,也就是 code 中的 `delta * (x - ctx->mean[class])`
## TODO
- [ ] 運用 Valgrind 排除 qtest 實作的記憶體錯誤,並透過 Massif 視覺化 “simulation” 過程中的記憶體使用量,需要設計對應的實驗
- [ ] 挑戰題