---
title: 2021q1 Homework (lab0)
---
# 2021q1 Homework1 (lab0)
contributed by < `ParkerMactavish` >
## 環境設定
- [x] Fork [lab0-c](https://github.com/sysprog21/lab0-c) 至自己的 github 儲藏庫中並且 clone 至開發環境中
- [x] 安裝 Valgrind
- [x] 安裝 Cppcheck
### Fork [lab0-c](https://github.com/sysprog21/lab0-c) 至自己的 github 儲藏庫中並且 clone 至開發環境中
透過 github 的 GUI 將 `sysprog21/lab0-c` fork 至 `ParkerMactavish/lab0-c` 並利用 github 提供的 ssh 位址 clone 至自己的電腦。
完成後進行 `make check` 以測試是否成功 clone 成功,並且得到以下結果,
```shell
$ make check
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
./qtest -v 3 -f traces/trace-eg.cmd
cmd>
cmd> # Demonstration of queue testing framework
...(後略)
```
顯示 lab0-c 已經成功下載到本地端。
### 安裝 Valgrind
透過
```shell
$sudo apt install valgrind
```
來安裝 valgrind 以便進行功課中的動態程式分析。
待命令列顯示安裝完成後,用
```shell
$ valgrind
```
測試執行檔是否能正常執行,如果得到
```shell
valgrind: no program specified
valgrind: Use --help for more information.
```
的資訊,因為目前沒有指定所需分析的程式,所以 valgrind 回報 `no program specified` 錯誤便代表正常執行。
### 安裝 build-essential 與 git-core
由於這台 Ubuntu 主機原先有進行其他工作,因此執行完
```shell
$ sudo apt install build-essential git-core
```
後, apt 的回傳資訊顯示 build-essential 與 git 皆為最新版本,無須額外進行安裝。
### 安裝 clang-format 、 aspell 與 colordiff
透過
```shell
$ sudo apt install clang-format aspell colordiff
```
的指令,成功安裝為了稍後 git-hook 所需要的套件。
### 安裝 Cppcheck
因為電腦的 Ubuntu 版本為 18.04 ,因此在使用
```shell
$ sudo apt install cppcheck
$ cppcheck --version
```
後如作業說明上所寫的得到版本為 `Cppcheck 1.82` ,所以先透過
```shell
$ sudo apt remove cppcheck
```
後嘗試使用 snap 進行 cppcheck 的安裝。
然而使用
```shell
$ sudo snap install cppcheck
```
之後, snap 回報 `error: snap "cppcheck" not found` ,在查找 https://snapcraft.io/ 後發現, snap 中只有 `cppcheckgui` 這個可安裝的執行檔,不過在 `lab0-c/.git/hooks/pre-commit` 中,定義要求需要 cppcheck ,而不能使用 cppcheckgui ,因此放棄使用 snap ,轉向自己編譯 cppcheck 這個專案。
透過以下幾行命令後, cppcheck 執行檔成功編譯,並且被保存在 `/tmp/cppcheck/build/bin` 中,
```shell
$ cd /tmp
$ git clone https://github.com/danmar/cppcheck.git
$ cd cppcheck
$ mkdir build
$ cd build
$ cmake .. -j6
```
其中,會使用 `-j6` 是因為自己的電腦是6核心處理器。
爾後,為了讓在執行 cppcheck 時不需要使用絕對位置,因此我找到 `/bin` 這個位置並透過
```shell
$ sudo cp /tmp/cppcheck/build/bin/cppcheck /bin
```
將執行檔複製過去,讓 bash 能夠透過環境變數找到 cppcheck 執行檔。
接著,為了測試 git hook 是否能正常被執行,我在 `lab0-c/queue.c` 中隨意一個位置加了空行後執行
```shell
$ git add .
$ git commit -m "Test githook"
```
在 `lab0-c/.git/hooks/pre-commit` 的clang-format 檢查部分有正常執行,檢查到多空一行,然而在進行 cppcheck 檢查時時得到以下輸出
```shell
nofile:0:0: information: Failed to load std.cfg. Your Cppcheck installation is broken, please re-install. The Cppcheck binary was compiled with FILESDIR set to "
/usr/local/share/Cppcheck" and will therefore search for std.cfg in /usr/local/share/Cppcheck/cfg. [failedToLoadCfg]
Fail to pass static analysis.
```
原因是我沒有將 `/tmp/cppcheck` 裡的 `cfg` 資料夾複製到 `/usr/local/share/Cppcheck` 中,因此執行了
```shell
$ sudo mkdir /usr/local/share/Cppcheck
$ cd /tmp/cppcheck
$ sudo cp -r cfg /usr/local/share/Cppcheck
```
後,再回到 `lab0-c` 專案中重新進行 git hook 測試,最後的錯誤訊息便會消失,只留下 clang-format 因為多空一行的錯誤。
最後,再透過執行
```shell
$ cppcheck
```
來確認 cppcheck 是否能夠正確被執行。
根據執行上述命令的回應資訊
```
Cppcheck - A tool for static C/C++ code analysis
Syntax:
cppcheck [OPTIONS] [files or paths]
If a directory is given instead of a filename, *.cpp, *.cxx, *.cc, *.c++, *.c,
*.tpp, and *.txx files are checked recursively from the given directory.
Options:
--addon=<addon>
Execute addon. i.e. --addon=cert. If options must be
(下略)
```
判斷 cppcheck 已經能夠在 bash 命令列中成功呼叫。
## 作業內容
- [ ] 完善 queue.[ch]
- [x] 完善資料結構
- [x] `q_new`
- [x] `q_free`
- [x] `q_insert_head`
- [x] `q_remove_head`
- [x] `q_insert_tail`
- [x] `q_reverse`
- [ ] 實作 q_sort 函式
### 完善 queue.[ch]
#### 完善資料結構
在 queue.h 裡面有定義兩個資料結構,分別為 `list_ele_t` 代表節點的資料結構, `queue_t` 代表佇列的資料結構。
節點內有兩個資料成員, `char *value` 儲存這個節點元素所儲存的字串, `struct ELE *next` 代表鍊結到的下一個節點元素。實現如下,
```c
typedef struct ELE {
char *value;
struct ELE *next;
} list_ele_t;
```
佇列內則有3個資料成員, `list_ele_t *head` 代表這個佇列的鍊節串鍊開頭; `list_ele_t *tail` 代表這個佇列的最後一個節點,是為了讓鍊結串列尾端的插入與刪除能在 $O(1)$ 時間複雜度完成所增加的資料成員; `int size` 代表這個佇列的長度,是為了讓得到佇列長度能夠在 $O(1)$ 時間複雜度完成所增加的資料成員。實現如下,
```c
typedef struct {
list_ele_t *head;
list_ele_t *tail;
int size;
} queue_t;
```
#### q_new
此函式被呼叫後,如果成功配置一塊 `sizeof(queue_t)` 大小的記憶體,則將該記憶體內的 `head` 與 `tail` 資料成員設為空指標、 `size` 設為 0,並且回傳該記憶體的位址;否則回傳空指標,實現如下。
```c
queue_t *q = malloc(sizeof(queue_t));
if (!q)
return NULL;
q->head = NULL;
q->tail = NULL;
q->size = 0;
return q;
```
#### q_free
此函式被呼叫時,會傳入一個指向 `queue_t` 型態的指標 `q` ,在函式內需要將此鍊結串列的所有元素都從記憶體內釋放。
在釋放時需要注意 `q` 為空指標的特殊情況,為此會進行以下檢查。
```c
if (!q)
return;
```
如果 `q` 指向有效記憶體區塊,則迭代過鍊結串列所有的元素,將元素內 `value` 字元指標所指向的字串釋放,再將元素本身釋放,實現如下。
```c
list_ele_t *currentEleToFree = q->head, *nextEleToFree;
while (currentEleToFree) {
nextEleToFree = currentEleToFree->next;
free(currentEleToFree->value);
free(currentEleToFree);
currentEleToFree = nextEleToFree;
}
free(q);
```
#### q_insert_head
此函式傳入 `queue_t *q` 與 `char *s` ,在函式中完成將 `s` 字串插入 `q` 佇列開頭的動作。
實現在從頭插入新值的功能時,需要注意以下幾個特殊情況。
* 傳入的佇列指標如果指向 NULL ,需要直接回傳 `false`,實現如下。
```c
if (!q)
return false;
```
* 如果傳入的佇列指標有指向可用的記憶體空間,但新的元素沒有被成功分配記憶體,則需要直接回傳 `false`,實現如下。
```c
list_ele_t *newh = malloc(sizeof(list_ele_t));
if (!newh)
return false;
```
* 如果新的元素成功被分配到記憶體,但元素裡的字串沒有被成功分配記憶體,則需要先將新元素的記憶體釋放,再回傳 `false`,實現如下。
```c
char *srcPtr = s;
while (*srcPtr)
srcPtr++;
newh->value = malloc((srcPtr - s + 1) * sizeof(char));
if (!newh->value) {
free(newh);
return false;
}
```
最後就是進行字串複製,同時把新元素指向的下個元素指向傳入佇列的開頭元素,並將傳入佇列的開頭指向新元素,回傳 `true` 後完成整個函式的功能,實現如下。
```c
srcPtr = s;
char *dstPtr = newh->value;
while (*srcPtr) {
*dstPtr = *srcPtr;
dstPtr++;
srcPtr++;
}
*dstPtr = 0;
newh->next = q->head;
q->head = newh;
return true;
```
**針對 tail 所做的修改**
由於為了滿足 $O(1)$ 時間複雜度的鍊結串列尾端插入與移除,所以在 `queue_t` 內增加了 `list_ele_t *tail` 這個資料成員。在 `q` 所指向的記憶體空間內指向一個空的鍊結串列時, `q->head` 與 `q->tail` 會指向空指標,而在從頭插入元素時,指向開頭與結尾的元素指標應該都被修改,因此有下面的修正。
```c
(上略)...
newh->next = q->head;
q->head = newh;
if (!q->tail)
q->tail = newh;
return true;
```
**針對 size 所做的修改**
為了滿足在 $O(1)$ 時間複雜度能得到鍊結串列大小,因此特別加了 `size` 這個資料成員在 `queue_t` 中,而每經過一次成功的元素插入, `size` 都會遞增 1 。
#### q_remove_head
此函式傳入 `queue_t *q` 、 `char *sp` 與 `size_t bufsize` ,在函式中完成將 `q` 的開頭元素移除,並且如果 `sp` 指向已配置空間,則將佇列開頭元素內的字串複製 `bufsize - 1` 個字元到 `sp` 裡,最後在 `sp` 的結尾加上一個 '\0' 字元。
實現從頭移除元素的功能時,需要注意以下幾個特殊情況。
* `q` 的值為空指標或是 `q` 的 `head` 資料成員的值為空指標,則需要回傳 `false`,實現如下。
```c
if (!q || !q->head)
return false;
```
* `sp` 的值為已配置空間時,需要將 `q->head->value` 所指向的內容複製到 `sp` 所指向的空間,複製長度為 `bufsize - 1` 個或是 `q->head->value` 的內容長度較小者,實現如下。
```c
if (sp) {
char *srcPtr = q->head->value, *dstBound = sp + bufsize - 1;
while (*srcPtr && sp < dstBound) {
*sp = *srcPtr;
sp++;
srcPtr++;
}
*sp = 0;
}
```
最後就是將元素從佇列中移出,釋放元素內字串的空間與元素自身的空間,並回傳 `true` 後完成整個函式的功能,實現如下。
```c
list_ele_t *eleToRemove = q->head;
q->head = q->head->next;
free(eleToRemove->value);
free(eleToRemove);
return true;
```
在完成 `q_insert_head` 與 `q_remove_head` 後,可以透過測試資料 `trace-01-ops` 看到元素成功被從頭插入與移出。
**針對 tail 所做的修改**
由於為了滿足 $O(1)$ 時間複雜度的鍊結串列尾端插入與移除,所以在 `queue_t` 內增加了 `list_ele_t *tail` 這個資料成員。當 `q` 所指向的記憶體空間內指向一個只有一個元素的鍊結串列時, `q->head` 與 `q->tail` 應當指向同一個記憶體位置,同時如果該鍊結串列沒有任何元素時, `q->head` 與 `q->tail` 都應當為空指標。移除開頭元素的動作會在這兩種情況下修改到 `q->tail` ,因此有了如下實現。
```c
(上略)...
list_ele_t *eleToRemove = q->head;
q->head = q->head->next;
if (!q->head)
q->tail = NULL;
else if (!q->head->next)
q->tail = q->head;
free(eleToRemove->value);
free(eleToRemove);
return true;
```
**針對 size 所做的修改**
為了滿足在 $O(1)$ 時間複雜度能得到鍊結串列大小,因此特別加了 `size` 這個資料成員在 `queue_t` 中,而每經過一次成功的元素插入, `size` 都會遞減 1 。同時,針對 `q->tail` 的修改可以變成「當 `q->size` 小於 2 時, `q->tail` 應該維持與 `q->head` 一致」,因此有以下修改。
```c
(上略)...
list_ele_t *eleToRemove = q->head;
q->head = q->head->next;
if (q->size < 2)
q->tail = q->head;
free(eleToRemove->value);
free(eleToRemove);
q->size--;
return true;
```
#### q_insert_tail
此函式傳入 `queue_t *q` 與 `char *s` ,在函式中完成以 $O(1)$ 時間複雜度將 `s` 字串插入 `q` 佇列結尾的動作。
在實現結尾插入新值的功能時,需要注意的特殊情形與 `q_insert_head` 無二致,針對 `q` 指標是否為空指標、 `newt` 指標是否為空指標、 `newt->value` 指標是否為空指標等檢查實作如下。
```c
if (!q)
return false;
list_ele_t *newt = malloc(sizeof(list_ele_t));
if (!newt)
return false;
char *srcPtr = s;
while (*srcPtr)
srcPtr++;
newt->value = malloc((srcPtr - s + 1) * sizeof(char));
if (!newt->value) {
free(newt);
return false;
}
```
接著便是複製字串 `s` 的內容至 `newt->value` 所指向的記憶體空間。
```c
(上略)...
srcPtr = s;
char *dstPtr = newt->value;
while (*srcPtr) {
*dstPtr = *srcPtr;
dstPtr++;
srcPtr++;
}
*dstPtr = 0;
```
最後要考慮當 `q->head` 原先所指向的鍊結串列沒有任何元素的情況,在尾端插入則要將 `q->head` 與 `q->tail` 都指向目前插入的元素;反之,要將 `q->tail->next` 指向目前的插入的元素,再將 `q->tail` 指向目前插入的數值(等價於 `q->tail = q->tail->next` ),並將 `newt->next` 指向空指標,讓佇列可以保持以空指標為結尾,同時把 `q->size` 遞增 1,實現如下。
```c
(上略)...
if (!q->head) {
q->head = newt;
q->tail = newt;
}
else {
q->tail->next = newt;
q->tail = newt;
}
newt->next = NULL;
q->size++;
return true;
```
#### q_size
此函式傳入 `queue_t *q` ,在函式中完成以 $O(1)$ 時間複雜度得到 `q` 的長度的行為。
在得到佇列長度時需要注意 `q` 為空指標的情形,必須以 0 作為回傳值,其餘就回傳在 `queue_t` 中所定義的資料成員 `size` 即可,實現如下。
```c
if (!q)
return 0;
return q->size;
```
#### q_reverse
此函式傳入 `queue_t *q` ,在函式中完成將 `q` 的順序頭尾反轉的動作。
在反轉佇列順序時需要注意 `q` 為空指標的情形,不能對空指標進行任何操作,同時在元素數量小於2的狀況下,因為 `q->head` 與 `q->tail` 皆指向空指標或同一個元素,因此沒有進入後面迴圈的必要;若指向已被配置的記憶體區塊,則進行順序反轉的動作,因此會有如下判斷 `q` 所指向的地址值的判斷式。
```c
if (!q || q->size < 2)
return;
```
接著宣告三個指向鍊結串列元素的指標來進行稍後的操作,同時利用其中一個指標來進行 `q` 頭尾交換的動作,實現如下。
```c
list_ele_t *prevPtr = NULL, *opPtr = q->head, *tmpPtr = q->head;
q->head = q->tail;
q->tail = tmpPtr;
```
接著以「在 `tmpPtr` 中暫存原先排列方向的下個元素;以原先排列方向的前個元素 `prevPtr` 作為目前元素的下個元素;接著將 `prevPtr` 與 `opPtr` 都原先排序方向的後端移動一格」這種方式迭代,直到目前指向的元素為新的 `q->head` 。
```graphviz
digraph {
rankdir="LR";
tail -> ele0;
ele0 -> ele1;
ele1 -> ele2;
ele2 -> ele3;
ele3 -> NULL;
prevPtr -> NULL;
opPtr -> ele0;
head -> ele3;
}
```
順序反轉禁行的初始狀態
```graphviz
digraph {
rankdir="LR";
tail -> ele0;
ele0 -> ele1;
ele1 -> ele2;
ele2 -> ele3;
ele3 -> NULL;
prevPtr -> NULL;
opPtr -> ele0;
tmpPtr -> ele1;
head -> ele3;
}
```
透過 `tmpPtr` 暫存 `opPtr` 下一個元素的位址
```graphviz
digraph {
rankdir="LR";
tail -> ele0;
ele0 -> NULL;
ele1 -> ele2;
ele2 -> ele3;
ele3 -> NULL;
prevPtr -> NULL;
opPtr -> ele0;
tmpPtr -> ele1;
head -> ele3;
}
```
將 `opPtr` 指向的元素的次個元素指定為 `prevPtr` (此時為空指標)
```graphviz
digraph {
rankdir="LR";
tail -> ele0;
ele0 -> NULL;
ele1 -> ele2;
ele2 -> ele3;
ele3 -> NULL;
prevPtr -> ele0;
opPtr -> ele0;
tmpPtr -> ele1;
head -> ele3;
}
```
將 `prevPtr` 指向目前 `opPtr` 指向的元素
```graphviz
digraph {
rankdir="LR";
tail -> ele0;
ele0 -> NULL;
ele1 -> ele2;
ele2 -> ele3;
ele3 -> NULL;
prevPtr -> ele0;
opPtr -> ele1;
tmpPtr -> ele1;
head -> ele3;
}
```
將 `opPtr` 指向 `tmpPtr` 指向的元素
```graphviz
digraph {
rankdir="LR";
tail -> ele0;
ele0 -> NULL;
ele1 -> ele2;
ele2 -> ele3;
ele3 -> NULL;
prevPtr -> ele0;
opPtr -> ele1;
tmpPtr -> ele2;
head -> ele3;
}
```
透過 `tmpPtr` 暫存下一個元素的位址
```graphviz
digraph {
rankdir="LR";
tail -> ele0;
ele0 -> NULL;
ele1 -> ele0;
ele2 -> ele3;
ele3 -> NULL;
prevPtr -> ele0;
opPtr -> ele1;
tmpPtr -> ele2;
head -> ele3;
}
```
將 `opPtr` 指向的元素的次個元素指定為 `prevPtr` (此時為 ele0)
```graphviz
digraph {
rankdir="LR";
tail -> ele0;
ele0 -> NULL;
ele1 -> ele0;
ele2 -> ele3;
ele3 -> NULL;
prevPtr -> ele1;
opPtr -> ele1;
tmpPtr -> ele2;
head -> ele3;
}
```
將 `prevPtr` 指向目前 `opPtr` 指向的元素
```graphviz
digraph {
rankdir="LR";
tail -> ele0;
ele0 -> NULL;
ele1 -> ele0;
ele2 -> ele3;
ele3 -> NULL;
prevPtr -> ele1;
opPtr -> ele2;
tmpPtr -> ele2;
head -> ele3;
}
```
將 `opPtr` 指向 `tmpPtr` 指向的元素
如此重複直到 `opPtr` 指向佇列開頭。
```c
while (prevPtr != q->head) {
tmpPtr = opPtr->next;
opPtr->next = prevPtr;
prevPtr = opPtr;
opPtr = tmpPtr;
}
```
### 實作 q_sort 函式
參考 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/),使用 merge sort 來實現 `q_sort` 的排序功能。
Merge sort 的兩個大步驟 — Splitting 與 Merging ,
```graphviz
digraph {
start[label="q_sort"];
check_base[label="傳入的佇列 q \n的 size 等於 1?" shape="diamond"];
return[label="return" shape="parallelogram"];
step1[shape="parallelogram" label="Index = 0\nptr= 傳入的佇列 q 的 head"];
step2[label="Index < q->size/2 ?" shape="diamond"];
step3[label="ptr = ptr->next" shape="parallelogram"];
step4[label="新建兩個 queue_t 型態的物件,\n並且用指向 queue_t 的指標 left 與 right 指向他們\nleft->head = q->head; left->tail = ptr;\nright->head = ptr->next; right->tail = q->tail" shape="parallelogram"];
step5[label="呼叫 q_sort(left);\n與 q_sort(right);\n 進行遞迴呼叫進行佇列分割" shape="parallelogram"];
start -> check_base;
check_base -> step1[label=否];
check_base -> return[label=是]
step1 -> step2;
step2 -> step4[label=否];
step2 -> step3[label=是];
step3 -> step2;
step4 -> step5;
}
```
## 延伸討論
### 使用 Address Sanitizer 時導致的連結時期錯誤
在嘗試使用 Address Sanitizer 進行 `console.c` 的除錯時,同時進行 git push 觸發 pre-push.hook 會導致編譯不成功,而在 pre-push.hook 中發現有這樣兩行,
```shell=35
# build the project
make
```
會在 push 前進行編譯嘗試,然而如果已經先進行過隨同 Address Sanitizer (也就是有增加 `-fsanitize=address -fno-omit-frame-pointer -fno-common` 等 flag )的編譯,則再進行 `make` 會出現編譯失敗的狀況。
之後嘗試在終端機直接進行 `make` 則會跳出
```shell
LD qtest
qtest.o: In function `usage':
/home/parker/Desktop/LinuxKernelInternals/W1/lab0-c/qtest.c:684: undefined reference to `__asan_handle_no_return'
qtest.o: In function `sigsegvhandler':
/home/parker/Desktop/LinuxKernelInternals/W1/lab0-c/qtest.c:638: undefined reference to `__asan_handle_no_return'
qtest.o: In function `show_queue':
/home/parker/Desktop/LinuxKernelInternals/W1/lab0-c/qtest.c:578: undefined reference to `__asan_report_load4'
...(下略)
```
的錯誤訊息,可以研判使用 `fsanitize=address` 等 flag 是利用添加額外的函式已達成記憶體檢查的功能,但如果編譯好的 object files 內有包含這些函式,卻沒有在連結時期使用 `fsanitize=address` 這個 flag 的話,會導致無法找到以 `__asan` 開頭的函式,便會報錯。
如果要成功通過 pre-push hook ,針對 pre-push.hook 合理的修改應該是在讀出執行 `make` 後的回傳值 `rc=$?` 後再進行以下額外的判斷,
```shell=39
rc=$?
# build the project with address sanitizer
if [[ $rc != 0 ]] ; then
make clean
make SANITIZER=1
rc=$?
fi
if [[ $rc != 0 ]] ; then
echo -e "${RED}Failed to build the project, please fix this and push again${NC}"
echo ""
exit $rc
fi
...(下略)
```
這樣便可以同時檢查沒有使用 Address Sanitizer 與使用 Address Sanitizer 的情形。
### 使用 Address Sanitizer 修改 console.c 以避免非法記憶體存取
使用 `make SANITIZER=1` 進行 `qtest` 執行檔的編譯後,執行後輸入 `help` 指令會出現
```shell
(上略)...
0x55b81cc663e1 is located 0 bytes to the right of global variable 'echo' defined in 'console.c:59:13' (0x55b81cc663e0) of size 1
SUMMARY: AddressSanitizer: global-buffer-overflow /home/parker/Desktop/LinuxKernelInternals/W1/lab0-c/console.c:307 in do_help_cmd
Shadow bytes around the buggy address:
...(下略)
```
查詢 `console.c` 的第 60 行後,會看到
```c=60
static bool echo = 0;
```
的宣告,同時繼續根據 Address Sanitizer 提供的錯誤訊息找到 `console.c` 的第 307 行,後面有一段
```c=308
while (plist) {
report(1, "\t%s\t%d\t%s", plist->name, *plist->valp,
plist->documentation);
plist = plist->next;
}
```
將所有的選項以及目前的設定值印出來,其中可以看到 `plist` 初始化的地方,
```c=306
param_ptr plist = param_list;
```
再看向 `param_list` 這個全域變數,可以發現 `echo` 這個變數在 `console.c#90` 的 `init_cmd` 函式中,會被當作參數放進 `add_param` 中,並且被取址強行轉型成 `int *` 型態,並 assign 給 `param_ele` 資料型態中的 `valp` 資料成員。
然而在 `console#309` 中, `&echo` 這個地址會被取值,並傳入 `report` 這個函式的不定長度引數( va_list )中,對應的是 %d 這個 specifier ,導致只有分配一個 byte 以儲存 bool 型態的 echo 會被一次讀取四個 byte ,剩下的 3 個 byte 全是非法讀取,因此 Address Sanitizer 會發出如此的警告。
由於上面的描述較為複雜,因此我把整個出錯的步驟畫成下圖,
```graphviz
digraph {
declare[label="console.c#60\nstatic bool echo = 0;" shape="parallelogon"];
init[label="console.c#90\ninit_cmd()" shape="parallelogon"];
add_param[label="console.c#110\nadd_param(\"echo\", (int *) &echo, ...\n\nvoid add_param(char *name, int *valp, char *..." shape="parallelogon"];
in_add_param[label="console.c#150\nele->valp = valp;" shape="parallelogon"];
end_declaration[label="Now address of `echo` is\n in `valp` of first element `param_list`,\nwhich is a linked list storing all options." shape="parallelogon"];
declare -> init -> add_param -> in_add_param->end_declaration;
}
```
```graphviz
digraph {
help[label="console.c#97 in init_cmd()\nwhen type \"help\" would trigger `do_help_cmd` function" shape="parallelogon"];
do_help_cmd[label="console.c#306\nplist = param_list;" shape="parallelogon"];
iteration[label="console.c#309\nreport(1, \"\t%s\t%d\t%s\", plist->name, *plist->valp....)" shape="parallelogon"];
va_list[label="\"%d\" is corresponding to *(int *)&echo\nwhile `echo` is bool type which is 1-byte" shape="parallelogon"];
sanitized[label="Address Sanitizer alerts developer about\nillegal memory access." shape="dimond"];
help->do_help_cmd->iteration->va_list->sanitized;
}
```
因此,需要修改的地方便是將 `echo` 變數改為 `int` 型態,如此一來,在取值時便可以對 4 個有效 byte 取值,不會有非法存取的狀況。
在修改完 `echo` 的型態後,再重新使用 `make SANITIZER=1` 編譯,並且執行
```shell
$ ./qtest
cmd> help
```
則會變成以下的錯誤訊息,
```shell
(上略)...
==4979==ERROR: AddressSanitizer: global-buffer-overflow on address 0x5576c30f26c0 at pc 0x5576c30e0d0e bp 0x7ffd9c08fa50 sp 0x7ffd9c08fa40
READ of size 4 at 0x5576c30f26c0 thread T0
#0 0x5576c30e0d0d in do_help_cmd /home/parker/Desktop/LinuxKernelInternals/W1/lab0-c/console.c:309
#1 0x5576c30e0e21 in interpret_cmda /home/parker/Desktop/LinuxKernelInternals/W1/lab0-c/console.c:223
#2 0x5576c30e1606 in interpret_cmd /home/parker/Desktop/LinuxKernelInternals/W1/lab0-c/console.c:246
#3 0x5576c30e2d4c in run_console /home/parker/Desktop/LinuxKernelInternals/W1/lab0-c/console.c:662
#4 0x5576c30dfe25 in main /home/parker/Desktop/LinuxKernelInternals/W1/lab0-c/qtest.c:780
#5 0x7f21bd59a0b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)
#6 0x5576c30dd5cd in _start (/home/parker/Desktop/LinuxKernelInternals/W1/lab0-c/qtest+0x65cd)
0x5576c30f26c1 is located 0 bytes to the right of global variable 'simulation' defined in 'console.c:21:6' (0x5576c30f26c0) of size 1
'simulation' is ascii string ''
SUMMARY: AddressSanitizer: global-buffer-overflow /home/parker/Desktop/LinuxKernelInternals/W1/lab0-c/console.c:309 in do_help_cmd
...(下略)
```
再根據錯誤訊息所提到 console.c 第 21 行所定義的 `simulation` 變數,可以看到作為 `bool` 型態的 `simulation` 在 console.c 第 106 行一樣在
```c
add_param("simulation", (int *) &simulation, ....(後略)
```
的相同函式呼叫下被取址並且傳入 `add_param` 作為指向 `int` 型態的指標使用,因此根據剛才針對 `echo` 變數的追蹤結果, `simulation` 在目前的使用方法下同樣會出現宣告一個 byte 卻存取 4 個 byte 的狀況,因此將 `simulation` 改為 `int` 型態,同時在 console.h 會利用外部連結到 `simulation` 的變數也要將其型態改成 `int` 才行。
總結而言,為了解決 Address Sanitizer 所報告的錯誤,在 console.c 與 console.h 共有三樣修改。
console.c
```c=22
// bool simulation = false;
int simulation = 0;
```
```c=60
// static bool echo = 0;
static int echo = 0;
```
console.h
```c=11
// extern bool simulation;
extern int simulation;
```