--- 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; ```