# 2021q1 Homework1 (lab0)
contributed by < `AmyLin0210` >
###### tags: `2021q1 Linux`
> [2020q1 lab0 作業解說](https://hackmd.io/@sysprog/linux2021-lab0)
## 基本實做
### queue_t
```cpp
typedef struct {
list_ele_t *head;
list_ele_t *tail;
int size;
} queue_t;
```
為了要達成時間複雜度 $O(1)$,加上 `*tail` 與 `size`。
:::warning
需要更多解釋,包含後續的使用和資料更新方式。
:notes: jserv
:::
>`tail` 指向 linked list 的最末端,用途為在尾端插入數值時,可以不用從最前方走訪所有節點,使得時間複雜度降為 $O(1)$ 。它會在 list 的最末端有變動時,例如 `q_insert_tail`, `q_sort`, `q_reverse` 等函式執行時變更他的位置,以確保它正確指向最末端。
>
>`size` 用於紀錄 linked list 的長度,所以在呼叫 `q_size` 時,不用遍歷整串 linked list,時間複雜度降為 $O(1)$。它會在 linked list 的長度有變化時被改變,例如 `q_insert_tail`, `q_insert_head`, `q_remove_head`。
### q_new
```cpp
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;
}
```
確認 malloc 的回傳值是否合法,然後將所有參數初始化。
### q_free
```cpp
void q_free(queue_t *q)
{
list_ele_t *tmp_e = NULL;
if (q) {
while (q->head) {
if (q->head->value)
free(q->head->value);
tmp_e = q->head;
q->head = tmp_e->next;
free(tmp_e);
}
free(q);
}
}
```
將所有動態分配 ( malloc ) 的空間給釋放。
### q_insert_head, q_insert_tail
```cpp=
bool q_insert_head(queue_t *q, char *s)
{
if (!q)
return false;
list_ele_t *newh = malloc(sizeof(list_ele_t));
if (!newh)
return false;
newh->value = malloc(sizeof(char) * (strlen(s) + 1));
if (!newh->value) {
free(newh);
return false;
}
snprintf(newh->value, BUFFER_SIZE, "%s", s);
newh->next = q->head;
q->head = newh;
(q->size)++;
if (!q->tail)
q->tail = q->head;
return true;
}
bool q_insert_tail(queue_t *q, char *s)
{
if (!q)
return false;
list_ele_t *newt = malloc(sizeof(list_ele_t));
if (!newt)
return false;
newt->value = malloc(sizeof(char) * (strlen(s) + 1));
if (!newt->value) {
free(newt);
return false;
}
if (!q->head)
q->head = newt;
else
q->tail->next = newt;
snprintf(newt->value, BUFFER_SIZE, "%s", s);
newt->next = NULL;
q->tail = newt;
(q->size)++;
return true;
}
```
這兩個函式實做的部份差不多,在第 13 行與 41 行的部份曾經犯過一個錯誤,記得檢查是否有成功 malloc 給 value,但在發現 malloc 失敗時直接 return false,忘記先清掉前面 malloc 給 list_element_t 的空間。
:::warning
避免說「採過一個坑」這樣不精準的話,請記住,這份共筆可能是日後代表你程度的作品,務必斟酌用詞。
:notes: jserv
> 已修正
:::
### q_remove_head
```cpp
bool q_remove_head(queue_t *q, char *sp, size_t bufsize)
{
if (!q || !(q->head))
return false;
list_ele_t *tmp = q->head;
snprintf(sp, bufsize, "%s", tmp->value);
q->head = q->head->next;
(q->size)--;
free(tmp->value);
free(tmp);
if (!q->head)
q->tail = NULL;
return true;
}
```
### q_size
```cpp
int q_size(queue_t *q)
{
if (!q)
return 0;
return q->size;
}
```
為了實做時間複雜度 $O(1)$,故在 queue_t 中新增`size`,儲存目前 list 的長度。
### q_reverse
```cpp
void q_reverse(queue_t *q)
{
if (!q || !q->head)
return;
list_ele_t *prev = q->head, *next = q->head->next;
q->tail = q->head;
q->head->next = NULL;
while (next) {
q->head = next;
next = next->next;
q->head->next = prev;
prev = q->head;
}
}
```
有想過否要使用遞迴的方式,程式碼會比較直覺簡單,但是空間複雜度會變成 $O(n)$,相比較之下,迴圈的空間複雜度為 $O(1)$ ,所以最終是使用迴圈來實做。
### q_sort
最初是使用 quick sort,但它的 worst case 可能發生在大多數需要被排序的數的優先序相同,時間複雜度為 $O(n^2)$,而在測試資料中就有對應的項目,故之後會改用時間複雜度為 $O(n\log n)$ 的 merge sort 來實做。
```cpp
list_ele_t *merge(list_ele_t *front, list_ele_t *back)
{
if (!front)
return back;
if (!back)
return front;
list_ele_t *tmp = front;
list_ele_t **result = &tmp;
list_ele_t *head =
(strcasecmp(back->value, front->value) >= 0) ? front : back;
while (front && back) {
while (front && strcasecmp(back->value, front->value) >= 0) {
result = &((*result)->next);
front = front->next;
}
*result = back;
if (!front) {
break;
}
while (back && strcasecmp(front->value, back->value) >= 0) {
result = &((*result)->next);
back = back->next;
}
*result = front;
}
return head;
}
void mergesort(list_ele_t **list, int list_len)
{
list_ele_t *tmp = *list;
list_ele_t *front = *list;
list_ele_t *back = NULL;
if (list_len <= 1)
return;
for (int i = 0; i < (list_len / 2) - 1; ++i) {
tmp = tmp->next;
}
back = tmp->next;
tmp->next = NULL;
mergesort(&front, list_len / 2);
mergesort(&back, list_len - list_len / 2);
*list = merge(front, back);
}
```
## 分析 Makefile
參考資料:
[Makefile 語法和示範](https://hackmd.io/@sysprog/SySTMXPvl)
[gcc(1) — Linux manual page](https://linux.die.net/man/1/gcc)
開始看 Makefile 之後又發現自己的一無所知,接下來就是一連串找資料、學習、寫筆記的開始,在此感謝同學 [randy870819](https://hackmd.io/@randy870819/system-prog-lab0#Makefile-%E5%88%86%E6%9E%90) 與 [KYG-yaya573142](https://hackmd.io/@KYWeng/S1DPSVSQ8#%E5%88%86%E6%9E%90-Makefile) 的共筆!
```
CFLAGS = -O1 -g -Wall -Werror -Idudect -I.
```
- 在這裡用到了 gcc 的一些參數
- `-O1` : 在編譯器裡有 `-O0` `-O1` `-O2` `-O3` 四個優化程度可以選擇,`-O0` 為不優化而 `-O3` 的優化級別最高
- `-g` : 產生 debugging information
- `-Werror` : 讓每個 warnings 變成 error
- `-Idir` : 會在編譯時先在 dir 尋找有沒有符合的 header file,找不到才會依照常規順序繼續尋找
### sanitizer
在編譯時加入一些 sanitizer 的參數
```
# Enable sanitizer(s) or not
ifeq ("$(SANITIZER)","1")
# https://github.com/google/sanitizers/wiki/AddressSanitizerFlags
CFLAGS += -fsanitize=address -fno-omit-frame-pointer -fno-common
LDFLAGS += -fsanitize=address
endif
```
### verbosity
在這裡決定要不要印出詳細的編譯過程
- 當 VERBOSE 為 1 時會印出詳細編譯過程
- `@` 的作用是不顯示執行的指令
- `@true`可以使該行命令被視作成成功
```
# Control the build verbosity
ifeq ("$(VERBOSE)","1")
Q :=
VECHO = @true
else
Q := @
VECHO = @printf
endif
```
```
qtest: $(OBJS)
$(VECHO) " LD\t$@\n"
$(Q)$(CC) $(LDFLAGS) -o $@ $^ -lm
```
### objects and dependencies
這邊使用了 [Auto-Dependency Generation](http://make.mad-scientist.net/papers/advanced-auto-dependency-generation/) 來處理 dependency 的問題
- 第 5 行: 將所有 `OBJS` 中的 `.o` 檔代換成 `.檔案名稱.o.d`
- 第 10 行: 在建立 object file 的同時也建立 dependency file
- 第 12 行: include 所有的 dependency file
- `-MMD` the driver uses its argument but with a suffix of .d, and mention only user header files, not system header files
- `-MF` overrides the default dependency output file
當目標 ( target ) 比起自己的相依項目 ( dependency ) 還要建立的更早時,表示其相依項目已經被修改,在此利用 GNU make 中 auto-rebuild 的特徵重新建立目標。
```=
OBJS := qtest.o report.o console.o harness.o queue.o \
random.o dudect/constant.o dudect/fixture.o dudect/ttest.o \
linenoise.o
deps := $(OBJS:%.o=.%.o.d)
%.o: %.c
@mkdir -p .$(DUT_DIR)
$(VECHO) " CC\t$@\n"
$(Q)$(CC) -o $@ $(CFLAGS) -c -MMD -MF .$@.d $<
-include $(deps)
```
### valgrind
```
tid := 0
# Control test case option of valgrind
ifeq ("$(tid)","0")
TCASE :=
else
TCASE := -t $(tid)
endif
```
```
valgrind_existence:
@which valgrind 2>&1 > /dev/null || (echo "FATAL: valgrind not found"; exit 1)
valgrind: valgrind_existence
# Explicitly disable sanitizer(s)
$(MAKE) clean SANITIZER=0 qtest
$(eval patched_file := $(shell mktemp /tmp/qtest.XXXXXX))
cp qtest $(patched_file)
chmod u+x $(patched_file)
sed -i "s/alarm/isnan/g" $(patched_file)
scripts/driver.py -p $(patched_file) --valgrind $(TCASE)
@echo
@echo "Test with specific case by running command:"
@echo "scripts/driver.py -p $(patched_file) --valgrind -t <tid>"
```
// 施工研究中
## 開啟 Address Sanitizer,修正 qtest 執行過程中的錯誤
先執行 qtest 再於命令提示列輸入 help 命令,會使 開啟 Address Sanitizer 觸發錯誤
```
$ make clean
$ make SANITIZER=1// 開啟 Address Sanitizer
$ ./qtest
```
以下是錯誤訊息的節錄
```=
==22083==ERROR: AddressSanitizer: global-buffer-overflow on address 0x555ca3a7b400 at pc 0x555ca3a647bd bp 0x7fff08f34f00 sp 0x7fff08f34ef0
READ of size 4 at 0x555ca3a7b400 thread T0
#0 0x555ca3a647bc in do_help_cmd /home/amy/Desktop/NCKU/School-Work/Linux/lab0-c/console.c:307
#1 0x555ca3a648d0 in interpret_cmda /home/amy/Desktop/NCKU/School-Work/Linux/lab0-c/console.c:221
#2 0x555ca3a650b5 in interpret_cmd /home/amy/Desktop/NCKU/School-Work/Linux/lab0-c/console.c:244
#3 0x555ca3a667f8 in run_console /home/amy/Desktop/NCKU/School-Work/Linux/lab0-c/console.c:660
#4 0x555ca3a633e5 in main /home/amy/Desktop/NCKU/School-Work/Linux/lab0-c/qtest.c:780
#5 0x7f6b23bfa0b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)
#6 0x555ca3a60b8d in _start (/home/amy/Desktop/NCKU/School-Work/Linux/lab0-c/qtest+0x8b8d)
0x555ca3a7b401 is located 0 bytes to the right of global variable 'echo' defined in 'console.c:59:13' (0x555ca3a7b400) of size 1
SUMMARY: AddressSanitizer: global-buffer-overflow /home/amy/Desktop/NCKU/School-Work/Linux/lab0-c/console.c:307 in do_help_cmd
```
從第 11 行來看,我們要從 `console.c` 去尋求進一步的線索。
在 `console.c` 的第 59 行,有一個變數宣告
```cpp
static bool echo = 0;
```
往下搜尋還有其他哪些地方有用到這個 `echo` 變數,發現以下程式碼的第 21 行對 `echo` 進行了強制轉型,但是原 `echo` 的型態為 bool ,它卻將指向 bool 位置的指標改成指向 int 位置的指標。
```cpp=
void init_cmd()
{
cmd_list = NULL;
param_list = NULL;
err_cnt = 0;
quit_flag = false;
add_cmd("help", do_help_cmd, " | Show documentation");
add_cmd("option", do_option_cmd,
" [name val] | Display or set options");
add_cmd("quit", do_quit_cmd, " | Exit program");
add_cmd("source", do_source_cmd,
" file | Read commands from source file");
add_cmd("log", do_log_cmd, " file | Copy output to file");
add_cmd("time", do_time_cmd, " cmd arg ... | Time command execution");
add_cmd("#", do_comment_cmd, " ... | Display comment");
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);
init_in();
init_time(&last_time);
first_time = last_time;
}
```
當 add_param 以為該指標指向 integer 但實際上是指向 bool 時,就有可能會發生 buffer overflow 的問題。
在我的電腦中執行以下程式碼, int 和 bool 的大小不同增強了我對那段程式碼的懷疑。
```cpp
#include <stdbool.h>
#include <stdio.h>
int main() {
printf("bool size: %ld\n", sizeof(bool));
printf("int size: %ld\n", sizeof(int));
return 0;
}
```
```
bool size: 1
int size: 4
```
在 `console.c` 中有一段程式碼
```cpp=
static bool do_help_cmd(int argc, char *argv[])
{
cmd_ptr clist = cmd_list;
report(1, "Commands:", argv[0]);
while (clist) {
report(1, "\t%s\t%s", clist->name, clist->documentation);
clist = clist->next;
}
param_ptr plist = param_list;
report(1, "Options:");
while (plist) {
report(1, "\t%s\t%d\t%s", plist->name, *plist->valp,
plist->documentation);
plist = plist->next;
}
return true;
}
```
在第 12 行中的 `*plist->valp` 存取到的就是上述所提的 `echo` 變數,但它卻是用 %d 來讀取一個 1 byte 的數值,因此 buffer overflow。
目前我想到的改善方式是改變該程式碼中 `echo` 與有相同問題的 `simulation` ,將他們由 bool 改為 int ,但改變資料型態後可能會造成其餘相關程式碼出現問題,所以還在思考有沒有更優雅的改法。
## 運用 Valgrind 排除 qtest 實作的記憶體錯誤
以下擷取自 `make valgrind` 後得到的訊息
```
==46210== 10 bytes in 1 blocks are still reachable in loss record 1 of 3
==46210== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==46210== by 0x4A5250E: strdup (strdup.c:42)
==46210== by 0x1100B6: linenoiseHistoryAdd (linenoise.c:1236)
==46210== by 0x110C49: linenoiseHistoryLoad (linenoise.c:1325)
==46210== by 0x10C22C: main (qtest.c:769)
==46210==
==46210== 160 bytes in 1 blocks are still reachable in loss record 2 of 3
==46210== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==46210== by 0x110076: linenoiseHistoryAdd (linenoise.c:1224)
==46210== by 0x110C49: linenoiseHistoryLoad (linenoise.c:1325)
==46210== by 0x10C22C: main (qtest.c:769)
==46210==
==46210== 170 bytes in 19 blocks are still reachable in loss record 3 of 3
==46210== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==46210== by 0x4A5250E: strdup (strdup.c:42)
==46210== by 0x11002A: linenoiseHistoryAdd (linenoise.c:1236)
==46210== by 0x110C49: linenoiseHistoryLoad (linenoise.c:1325)
==46210== by 0x10C22C: main (qtest.c:769)
```
首先我們看到 valgrind 的錯誤訊息,`still reachable` 表示程式結束時有未釋放的記憶體,不過卻還有指標指著。
接下來我們找到 `qtest.c` 的第 769 行。
```cpp
linenoiseHistoryLoad(HISTORY_FILE); /* Load the history at startup */
```
接下來看看 `linenoise.c` 的 1325 行,可以發現又呼叫了另一個函式。
```cpp
linenoiseHistoryAdd(buf);
```
在這份程式碼中,有使用一個名為 history 的參數,型態為 `char**` ,當中做的事情是儲存字串,而 `linenoiseHistoryAdd` 做的就是將這些字串加入 history。
因此我猜測由於 `history` 所指向的記憶體空間在程式結束前沒有被成功的清掉,所以才會出現 `still reachable` 這項錯誤。
最後我在程式結束前將 history 指向的空間清掉,就不會再跳出錯誤訊息了。