contributed by < jasperlin1996
>
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 46 bits physical, 48 bits virtual
CPU(s): 64
On-line CPU(s) list: 0-63
Thread(s) per core: 2
Core(s) per socket: 16
Socket(s): 2
NUMA node(s): 2
Vendor ID: GenuineIntel
CPU family: 6
Model: 85
Model name: Intel(R) Xeon(R) Gold 5218 CPU @ 2.30GHz
Stepping: 7
CPU MHz: 1000.011
CPU max MHz: 3900.0000
CPU min MHz: 1000.0000
BogoMIPS: 4600.00
Virtualization: VT-x
L1d cache: 1 MiB
L1i cache: 1 MiB
L2 cache: 32 MiB
L3 cache: 44 MiB
NUMA node0 CPU(s): 0-15,32-47
NUMA node1 CPU(s): 16-31,48-63
q_new
struct list_head *q_new()
{
struct list_head *head =
(struct list_head *) malloc(sizeof(struct list_head));
if (!head)
return NULL;
INIT_LIST_HEAD(head);
return head;
}
q_free
原本在實作過程中沒有想清楚 free 該釋放哪些指標的記憶體導致會跳出 0xdeadbeef
或是 Null pointer dereference 之錯誤,經修正後程式碼如下:
void q_free(struct list_head *l)
{
if (l == NULL)
return;
struct list_head *node = l->next;
while (!list_empty(l)) {
struct list_head *tmp = node;
node = node->next;
list_del(tmp);
q_release_element(list_entry(tmp, element_t, list));
}
free(l);
}
q_insert_head
初版程式如下,但 Cppcheck 靜態分析工具提示會有 Memory Leak 的問題
bool q_insert_head(struct list_head *head, char *s)
{
/*
* Consider the following two situation:
* (1) head == NULL or head->prev == NULL or head->next == NULL
* (2) head != NULL
* For the (1) situation, just simply return false.
* For the (2) situation, since the initialized list_head pointer's
* prev was pointed to the list_head pointer itself, we can finish the
* insertion via `list_add` API.
*/
// If the queue is NULL or uninitialized
if (!head || !(head->prev) || !(head->next))
return false;
// Create a node
element_t *node = (element_t *) malloc(1 * sizeof(element_t));
if (node == NULL)
return false;
// Calculate the length of s then copy it into the node
size_t len_s = strlen(s) + 1;
node->value = (char *) malloc(len_s * sizeof(char));
if (node->value == NULL) {
free(node);
return false;
}
strncpy(node->value, s, len_s);
// Linux Kernel style API, create a struct list_head variable
LIST_HEAD(list);
node->list = list;
// Linux Kernel style API, add the node before the head
list_add(&(node->list), head);
return true;
}
錯誤訊息如下:
$ cppcheck queue.c
Checking queue.c ...
Checking queue.c: INTERNAL...
queue.c:97:5: error: Memory leak: node [memleak]
return true;
^
Checking queue.c: LIST_POISONING...
Checking queue.c: __GNUC__...
此問題導致在 git commit
前的 pre-commit hook 檢查失敗,根據提示發現問題出在 node 未被釋放,但理論上本實作應在 q_free
函式釋放關於該 linked list 的記憶體。
原本一開始對這個問題的解法並沒有頭緒,後來在觀摩多位去年同學的實作後看到有人將 malloc
函式部分獨立出一個 function,我認為這樣做的原意應該是因為 q_insert_head
與 q_insert_tail
在新增節點 (node) 部分有相似的程式,因此獨立出來,但我同時也很好奇為什麼這樣有辦法通過 Cppcheck 的檢查。因此我也嘗試將 malloc
部分獨立出一個 function:
element_t *create_node(void)
{
return (element_t *) malloc(sizeof(element_t));
}
之後即通過 Cppcheck 的測試。
後來在 Facebook 社團討論發現有人將 list_add(&(node->list), head);
改成 list_add(&node->list, head);
也能通過測試,此部分我認為的確算是有可能造成 Memory Leak 的問題,但卻能通過不同的方法 Bypass,其實的確算是一種 Bug,預計在近期回報此問題給 Cppcheck。
而當我實作到 q_remove_head
與 q_remove_tail
時因為遇到了另一個問題才發現可以手動 suppress cppcheck error,我認為遇到此種狀況時的確應該手動以 Comment Suppression 的方法解決此問題。
我同時也注意到,strlen
在遇到 truncated string 的時候可能會花費大量時間並且回傳一個錯誤的值,因此改用 strnlen
並限制輸入之字串最長只能到 1024 個字元長。
最後實作程式如下:
bool q_insert_head(struct list_head *head, char *s)
{
/*
* Consider the following two situation:
* (1) head == NULL or head->prev == NULL or head->next == NULL
* (2) head != NULL
* For the (1) situation, just simply return false.
* For the (2) situation, since the initialized list_head pointer's
* prev was pointed to the list_head pointer itself, we can finish the
* insertion via `list_add` API.
*/
// If the queue is NULL or uninitialized
if (!head || !(head->prev) || !(head->next))
return false;
// Create a node
// element_t *node = (element_t *) malloc(1 * sizeof(element_t));
element_t *node = create_node();
if (node == NULL)
return false;
// Calculate the length of s then copy it into the node
// Maximum length of the string is 1024 (excluding '\0')
size_t len_s = strnlen(s, 1024) + 1;
node->value = (char *) malloc(len_s * sizeof(char));
if (node->value == NULL) {
free(node);
return false;
}
strncpy(node->value, s, len_s);
// Linux Kernel style API, create a struct list_head variable
LIST_HEAD(list);
node->list = list;
// Linux Kernel style API, add the node before the head
list_add(&(node->list), head);
return true;
}
q_insert_tail
程式碼如下,遇到的問題大致與 q_insert_head
相同,主要差異為將 list_add
改成 list_add_tail
。
bool q_insert_tail(struct list_head *head, char *s)
{
// If the queue is NULL or uninitialized
if (!head || !(head->prev) || !(head->next))
return false;
// Create a node
// element_t *node = (element_t *) malloc(1 * sizeof(element_t));
element_t *node = create_node();
if (node == NULL)
return false;
// Calculate the length of s then copy it into the node
// Maximum length of the string is 1024 (excluding '\0')
size_t len_s = strnlen(s, 1024) + 1;
node->value = (char *) malloc(len_s * sizeof(char));
if (node->value == NULL) {
free(node);
return false;
}
strncpy(node->value, s, len_s);
// Linux Kernel style API, create a struct list_head variable
LIST_HEAD(list);
node->list = list;
// Linux Kernel style API, append the node at the tail of the queue
list_add_tail(&node->list, head);
return true;
}
q_remove_head
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || head->next == head)
return NULL;
element_t *ret_p = list_entry(head->next, element_t, list);
list_del(head->next);
if (sp) {
strncpy(sp, ret_p->value, bufsize);
// Credit to @laneser's note
sp[bufsize - 1] = '\0';
}
return ret_p;
}
實作過程中,根據註解要求在 sp
不為 NULL
時複製資料過去,但沒想清楚,將判斷式寫成 if (!sp)
,且在改成 if (sp != NULL)
時問題就消失了。原以為是 Cppcheck 的問題,後來才想到應該是我自己犯蠢。
後來又發現在 trace-08-robust
測試中會出錯,在 queue 為空時,q_remove_head
& q_remove_tail
並不會回傳 NULL,因此增加判斷 head->next
是否與 head
一樣,也就是是否沒有任何 node 在 linked-list 內。
在 trace-07-robust
中,關於 truncated string 的問題我想了蠻久一直無法解決,後來在觀摩 @laneser 的筆記 後發現我的盲點,非常感謝他。
順利完成其他部分程式後,又遇到靜態檢查問題,錯誤訊息如下:
$ cppcheck queue.c
Checking queue.c ...
Checking queue.c: INTERNAL...
Checking queue.c: LIST_POISONING...
Checking queue.c: __GNUC__...
queue.c:43:27: error: Null pointer dereference: (struct element_t*)0 [nullPointer]
q_release_element(list_entry(tmp, element_t, list));
^
queue.c:155:24: error: Null pointer dereference: (struct element_t*)0 [nullPointer]
element_t *ret_p = list_entry(head->next, element_t, list);
^
queue.c:176:24: error: Null pointer dereference: (struct element_t*)0 [nullPointer]
element_t *ret_p = list_entry(head->prev, element_t, list);
^
queue.c:233:23: error: Null pointer dereference: (struct element_t*)0 [nullPointer]
q_release_element(list_entry(tmp, element_t, list));
^
訊息顯示為有 Null pointer dereference 的情況發生,在詳細 trace code 之後發現,問題如下:
list_entry
macrolist_entry
為 container_of
之包裝container_of
中有段程式碼如下:__typeof__(((type *)0)->member) ...
而 Cppcheck 在檢查時發現 0 轉型成 type
pointer 後為 Null pointer。之後在 Facebook 討論區也有發現有同學遇到同樣的問題,在此引用我自己在討論區的回覆:
這邊是因為 list.h 中 container_of 的實作中,有一行是
__typeof__(((type *) 0)->member)
而(type *) 0
的確是 null pointer。不過這邊的用法是為了取得 member 的 type,且這個用法是符合 ANSI C 的標準的,詳細的說明可以看這篇:https://stackoverflow.com/questions/13723422/why-this-0-in-type0-member-in-c
以及規格書的第 6.3.2 章關於 pointer 的說明:http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf
jasperlin1996
這邊的問題因為主要發生在 list.h
而我們是無法修改 list.h
的程式碼的,因此也無法在對應的地方加上 comment suppression,後來在與 @jserv 老師討論後建議我可以直接修改 pre-commit.hook
並提交 pull Request #67 到作業專案。這次的 PR 算是我第一次不只是回報 bug 而是參與專案的進展,雖然實際上只改了兩行程式碼,但整個過程比我想像的繁瑣許多,例如:專案開發者可能會要求你對於問題發生的版本提出驗證資料;或是實際上 PR 所包含的程式雖然能解決問題但可能會造成其他困擾。
list.h
而跳出的 Null pointer dereference
誤報。為了測試此問題發生之 Cppcheck 版本,我從 Cppcheck 的 GitHub 抓了一份 source code 下來,直接 compile 並測試。因為怕弄壞現有環境,且本次實驗不需考慮效能,因此我以 Docker 製作測試用環境。
以下是測試用之 Dockerfile:
FROM ubuntu:20.04
MAINTAINER jasperlin1996
ENV TZ=Asia/Taipei
RUN ln -snf "/usr/share/zoneinfo/$TZ" /etc/localtime
RUN echo "$TZ" > /etc/timezone
RUN apt update && apt install -y \
vim \
g++ \
make \
cmake \
git
RUN git clone https://github.com/danmar/cppcheck.git
RUN git clone https://github.com/sysprog21/lab0-c.git
測試用的程式片段:
#include "lab0-c/queue.h"
int main() {
struct list_head *head = {&head, &head};
element_t node = {"Testing\n", head};
list_entry(head, element_t, list);
return 0;
}
因為將每個要求的版本(from v1.9 to v2.7)逐一編譯並記錄問題是個有點累的事情,因此我寫了一點 shell script 輔助我完成這件事(或許看起來有點醜,如果有更好的寫法請多指教XD)。其中 cmake -j32
是因為剛好有足夠的 CPU core 加速 compile,不然真的會等到天荒地老,如果要用本段程式的話記得改成符合你的硬體的參數:
#!/bin/bash
TEST_FILE=/test.c
LOG_PATH=/verify.log
rm $LOG_PATH
cd cppcheck
VERSIONS_STRING=$(git tag -l | tr '\n' ',')
readarray -td, ALL_VERSION <<< "$VERSIONS_STRING,"
unset 'ALL_VERSION[-1]'
unset 'ALL_VERSION[-1]'
declare -p ALL_VERSION
VERSIONS=()
for i in ${!ALL_VERSION[@]}; do
if [ "${ALL_VERSION[$i]}" == "1.90" ]; then
START_VERSION=$i
fi
if [ "${ALL_VERSION[$i]}" == "2.7" ]; then
END_VERSION=$i
fi
done
for i in ${!ALL_VERSION[@]}; do
if [ $i -eq $START_VERSION ] || [ $i -gt $START_VERSION ]; then
VERSIONS+=(${ALL_VERSION[$i]})
fi
done
function build_cppcheck() {
git checkout $1
rm -rf build
mkdir build
pushd build
TMP=$(cmake ..)
if [[ "$TMP" == *"written"* ]]; then
echo "- CMAKE SETUP SUCCESS"
echo "- CMAKE BUILDING..."
cmake --build . -j32 > /dev/null 2>&1
if [ ! -f "./bin/cfg" ]; then
cp -r ../cfg/ bin/cfg
fi
RESULT=$(./bin/cppcheck $TEST_FILE 2>&1)
if [[ "$RESULT" == *"error"* ]]; then
echo "** Cppcheck v$1 failed to pass the test**" >> $LOG_PATH
echo $RESULT >> $LOG_PATH
else
echo "** Cppcheck v$1 passes the test **" >> $LOG_PATH
fi
else
echo "** Cppcheck v$1 failed to compile **" >> $LOG_PATH
fi
popd
}
for i in ${VERSIONS[@]}; do
build_cppcheck $i;
done
實驗後發現,在 Cppcheck v2.1 ~ v2.7 皆會產生此問題(v2.0 無法順利 compile),回報之後再修正 unmatchedSuppression
,即完成本次貢獻。
這裡我也提供 Cppcheck 的 Compilation Guide,其實基本上跟 Cppcheck 的 GitHub 上之說明差不多。不過如果你對於從原始碼編譯專案並將執行檔加入你在用的系統的執行目錄這件事不太熟悉的話,可以參考一下,或許會對你有點幫助。
Facebook Infer 是由 Facebook(現在可能要叫做 meta)推出的靜態程式碼檢查工具,支援 C/C++/Objective-C 與 Java,在我遇到第一個 Cppcheck issue 時, @jserv 老師有提到可以嘗試使用本工具代替 Cppcheck。
經過了一點小實驗後我稍微有了一點心得,但礙於時間關係,教學待補,我可能得先寫畢業論文QQ。
q_delete_mid
這裡參考 你所不知道的 C 語言: linked list 和非連續記憶體 中所提到的快慢指標技巧,並稍做修改以符合 doublly-linked-list 的需求(應用 Linux Kernel style API)並適當刪除節點,處理不需要的記憶體空間。
bool q_delete_mid(struct list_head *head)
{
// https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/
if (!head || list_empty(head))
return false;
struct list_head **indirect = &(head->next);
for (struct list_head *fast = head->next;
fast != head && fast->next != head; fast = fast->next->next) {
indirect = &(*indirect)->next;
}
struct list_head *tmp = *indirect;
list_del(tmp);
q_release_element(list_entry(tmp, element_t, list));
return true;
}
q_reverse
我的基本想法是,將 linked-list 中的每個 node 所指向的 prev
& next
對調的話,那就等於是做了 reverse 這個操作了。實作的程式碼如下:
void q_reverse(struct list_head *head)
{
if (!head || head->next == head)
return;
struct list_head *node = head;
// Swap all node's next and prev pointer
do {
struct list_head *tmp = node->next;
node->next = node->prev;
node->prev = tmp;
node = node->prev;
} while (node != head);
}
q_swap
void q_swap(struct list_head *head)
{
// https://leetcode.com/problems/swap-nodes-in-pairs/
if (!head)
return;
struct list_head *node;
for (node = head->next; node != head && node->next != head;
node = node->next) {
struct list_head *tmp = node->next;
list_move(node, tmp);
}
}
本函式之概念不難,重點是要想清楚 pointer 之間要如何連接。思考過後我用純 pointer 實作了一次,之後發現 swap 可以用以下程式即可將 node
移動到 position
後:
list_del(node);
list_add(node, position);
之後又發現,有個 list_move
已經是這兩個 API 的包裝了,因此可以直接使用它。
q_sort
此 function 以 merge sort 的 recursive 版本實作 linked-list 排序功能。
首先以快慢指標找到 linked-list 的中間點,並以該點為分界(不包含),建立一個新的 linked-list。得到左右兩邊後以 recursive call 方式將兩邊各自排序好,再透過 merge_two_list
合併兩個 list。
void q_sort(struct list_head *head)
{
if (!head || head->next == head || list_is_singular(head))
return;
struct list_head *slow = head->next, *fast = head->next->next;
while (fast != head && fast->next != head) {
slow = slow->next;
fast = fast->next->next;
}
// Now the slow pointer is at the middle
LIST_HEAD(right);
// Split the list from the next node of slow pointer
right.next = slow->next;
right.next->prev = &right;
right.prev = head->prev;
right.prev->next = &right;
slow->next = head;
head->prev = slow;
q_sort(&right);
q_sort(head);
merge_two_list(head, &right);
}
在 merge_two_list
function 中,我用 list_for_each_safe
將右邊的 list 中的 node 去和左邊 list 中的當前 node value 進行比較,如果發現右邊的 value 大於或等於左邊的 value 則將左邊當前節點往 next 移動並繼續比較。
如果發現右邊的 value 小於左邊的 value 或是左邊已經是最後一個 node 了,則看最後 value 比較結果將右邊的 node 移動到左邊當前 node 的前面或是後面。
在寫 q_sort
的時候我花了比較久的時間,再加上其他事務繁重,最後趕在截止日期前幾個小時才寫完。歸根究柢發現原因其實是熬夜導致邏輯思考能力下降。今天在寫的時候實際上我只花了一小時不到就找到卡了幾天的問題,比起學到了 merge sort 的實作方法,我想更多的是我更了解自己的身體狀況了。
/*
* Merge two sorted list to a sorted one
* Must ensure the left list isn't empty
*/
void merge_two_list(struct list_head *left_head, struct list_head *right_head)
{
struct list_head *safe, *right, *left = left_head->next;
// if the left list is empty but the right isn't
if (left == left_head && right_head->next != right_head) {
// left_head = right_head;
list_splice(right_head, left_head);
return;
}
// Put right list's node to left
list_for_each_safe (right, safe, right_head) {
element_t *l_node = list_entry(left, element_t, list);
element_t *r_node = list_entry(right, element_t, list);
int cmp_result = strcmp(l_node->value, r_node->value);
// if left value <= right value, move the left pointer to it's next
while (left->next != left_head && cmp_result <= 0) {
left = left->next;
l_node = list_entry(left, element_t, list);
cmp_result = strcmp(l_node->value, r_node->value);
}
list_del(right);
if (cmp_result > 0)
list_add_tail(right, left);
else
list_add(right, left);
}
}
q_delete_dup
這邊我的作法是用 q_new
新增一個專門存放 duplicate strings 的 linked-list,在發現字串重複之後就將該 node 從原本的 linked-list 移動到 dup_strs
。此方法不僅以一個較直觀的方式處理問題,且因此 function 要求要將 node 刪除,因此最後可以直接使用 q_free
將整個記憶體一次清空。只要 q_free
的實做是完善的,即可確保這個 function 在處理記憶體的問題上不會有問題。
另,原本我想要使用 list_for_each_safe
來替代主要的迴圈,但因為我先用 val
變數去儲存第一個元素的 value,因此我希望這個 for loop 從第二個元素開始執行。可惜 list_for_each_safe
雖然直觀,但若我將開始點從 head
改成 head->next
則也同時會改變結束條件,因此還是決定自己仿照 list_for_each_safe
寫一個允許刪除 node 的迴圈。
bool q_delete_dup(struct list_head *head)
{
// https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
/* This implementation will gather all duplicate strings then
* delete them together.
*/
if (!head || head->next == head)
return NULL;
struct list_head *dup_strs = q_new();
struct list_head *node, *safe;
char *val = list_entry(head->next, element_t, list)->value;
for (node = head->next->next, safe = node->next; node != head;
node = safe, safe = node->next) {
char *tmp = list_entry(head->next, element_t, list)->value;
if (strcmp(val, tmp) == 0)
list_move(node, dup_strs);
else
val = tmp;
}
q_free(dup_strs);
return true;
}