# 2019q1 Homework1 (lab0) contributed by <`f26401004`> * [C Programming Lab](http://www.cs.cmu.edu/~213/labs/cprogramminglab.pdf) * [F01: lab0](https://hackmd.io/s/BJA8EgFB4) :::success 尚未完成: * trace harness.h / harness.c * qtest 機制分析 ::: # 目標 1. 實作簡單的 queue data structure,並提供 insert head / insert tail / remove head 等操作 2. 題目中特別限制部份程式碼須在特定的時間複雜度下 3. 練習 github 以及 git hook # 程式碼 ### queue_t *q_new() ```c queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); /* return null if malloc failed */ if (q == NULL) { return NULL; } /* reset the queue */ q->head = NULL; q->tail = NULL; q->size = 0; return q; } ``` :::info * 該 function 建立一個 queue * 需要小心的是當 malloc 不成功時需要 return null 以及 malloc 成功後須**重設 queue 的 member** ::: ### void q_free(queue_t *q) ```c /* Free all storage used by queue */ void q_free(queue_t *q) { /* just return if queue do not exist */ if (q == NULL) { return; } /* use two pointer to traversal all the queue entry */ list_ele_t *iter = q->head; list_ele_t *prev = iter; q->head = NULL; q->tail = NULL; while(iter) { prev = iter; iter = iter->next; prev->next = NULL; free(prev->value); free(prev); } free(q); } ``` :::info * 在 free 一個 queue 時需要注意:因為每個 entry 的 value 當初也是被 malloc 的,因此也需要特別去 free 掉,否則會產生 dangling pointer (最一開始在寫的時候忘記 free 掉,因此在 make test 時會產生有 block 仍然存在的 error) * 更多閱讀:[dangling pointer](https://en.wikipedia.org/wiki/Dangling_pointer) ::: ### bool q_insert_head(queue_t *q, char *s) ```c bool q_insert_head(queue_t *q, char *s) { /* return false if queue do not exist */ if (q == NULL) { return false; } list_ele_t *newh = malloc(sizeof(list_ele_t)); /* return false if malloc a new entry failed */ if (newh == NULL) { return false; } char *buf = malloc((strlen(s) + 1) * sizeof(char)); /* return false if malloc a new buffer to copy the string failed */ if (buf == NULL) { /* [Important]: also need to free the newh entry */ free(newh); return false; } q->size++; strcpy(buf, s); /* reset the newh entry */ newh->value = buf; newh->next = NULL; /* insert the new entry to the queue head */ if (q->head == NULL && q->tail == NULL) { q->head = q->tail = newh; return true; } newh->next = q->head; q->head = newh; return true; } ``` :::info * 在 insert head 時需要注意因為本次的 malloc 了兩次記憶體,因此不論在哪個階段 malloc 都需要檢查該 malloc 是否成功,不成功則**需要將上一個階段的 malloc 成功的記憶體 free 掉並且 return false** * 若 insert head 時發現 head 和 tail 皆為 null 時則初始將 head 和 tail 皆設為新的 entry ::: ### bool q_insert_tail(queue_t *q, char *s) ```c bool q_insert_tail(queue_t *q, char *s) { /* return false if queue do not exist */ if (q == NULL) { return false; } list_ele_t* newh = malloc(sizeof(list_ele_t)); /* return false if malloc a new entry failed */ if (newh == NULL) { return false; } char *buf = malloc((strlen(s) + 1) * sizeof(char)); /* return false if malloc a new buffer to copy the string failed */ if (buf == NULL) { /* [Important]: also need to free the newh entry */ free(newh); return false; } q->size++; strcpy(buf, s); /* reset the newh entry */ newh->value = buf; newh->next = NULL; /* insert the new entry to the queue head */ if (q->head == NULL && q->tail == NULL) { q->head = q->tail = newh; return true; } q->tail->next = newh; q->tail = newh; return true; } ``` :::info * 因為 insert tail 也要求時間複雜度只能為 O(1),故**在 queue 的 data structure 新增 tail pointer link**,每次 insert tail 時同時更新最新的 tail pointer link。有了 tail pointer link 便可以和 insert head 相同使用 O(1) 時間複雜度插入 ::: ### bool q_remove_head(queue_t *q, char *sp, size_t bufsize) ```c bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { /* if queue do not exist or q->head do not exist or q->size equal to zero, then just return false */ if (q == NULL || q->head == NULL || q->size == 0) { return false; } /* minus the queue size first */ q->size--; list_ele_t *iter = q->head; if (sp != NULL) { /* reset sp first */ memset(sp, '\0', (int)sizeof(sp)); strncpy(sp, iter->value, bufsize - 1); /* [Important]: truncated the rest length of the sp */ sp[bufsize - 1] = '\0'; } q->head = q->head->next; /* free the memory space */ iter->next = NULL; free(iter->value); free(iter); return true; } ``` :::info * 題目要求當 sp 不為 NULL 時便將正要移除的 entry value 複製到 sp 中 * 然而一開始使用 strncpy 時根據原始碼的實作會發現若 from 的長度大於 to 時會自動 expend to 的長度,在某些情況會造成效率低落。(當要複製的 string 長度遠小於指定的長度時) * **此外strncpy 不能保證 to 存在 \\0,因此必須手動填上 \\0 在 to 的尾巴確保存在 null character** * 另一種的作法可以使用 strlcpy,它可以確保 to 的結尾存在 null character,查閱 C89/C99/C11 pdf 後發現並不存在 strlcpy,我的電腦系統為 Unbuntu 18.04LTS 需要額外安裝相關 package 才可以使用。 * `$ sudo apt-get install libbsd-dev` * 引用 `#include <bsd/string.h>` * 或者使用 memcpy 直接複製指定的記憶體區段,但同樣的他也不能保證 null character,因此也需要額外填上 \0 在 to 的尾巴。 * [更多閱讀]:[strncpy source code (linux v5.0-rc7)](https://elixir.bootlin.com/linux/v5.0-rc7/source/lib/string.c#L114) * [更多閱讀]:[memcpy source code (linux v5.0-rc7)](https://elixir.bootlin.com/linux/v5.0-rc7/source/lib/string.c#L804) ::: :::warning 避免說「此函式似乎沒有被納入 standard ANSI C」這種話,工程人員的論述要清晰,要指出 C89/C99/C11 是否存在 strlcpy,以及 POSIX 相關規範 (包含 4BSD) 是否有 conformance 描述 :notes: jserv ::: ### int q_size(queue_t *q) ```c int q_size(queue_t *q) { /* if queue do not exist or q->head do not exist or q->size equal to zero, then just return zero */ if (q == NULL || q->size == 0 || q->head == NULL) { return 0; } return q->size; } ``` :::info * 直接回傳在每次操作時紀錄的長度即可 ::: ### void q_reverse(queue_t *q) ```c void q_reverse(queue_t *q) { /* if queue do not exist or q->head do not exist or q->size equal to zero, then just return */ if (q == NULL || q->size == 0 || q->head == NULL) { return; } list_ele_t *prev = NULL; // use prev to store previous entry list_ele_t *next = q->head; // use next to store next entry list_ele_t *current = q->head; // use current entry to reverse the next link /* traversal through the list and reverse the entry next pointer link */ while(current) { next = next->next; current->next = prev; prev = current; current = next; } /* [Important]: also need to reset the head and tail pointer link */ q->tail = q->head; q->head = prev; } ``` :::info * 利用 3 個 pointer 來 traversal 整個 queue,過程如下: 1. 初始化 next / current 為 head,prev 為 null 2. 只要 current 不為 null 則繼續進行下面的步驟 3. next 移往下一個 entry 4. 將 current->next 設置為 prev 5. 設置 prev 為 current、current = next 6. 回到第 2 步驟 * 需要特別注意的是別忘記回來重設 head 和 tail pointer ::: # 討論 ### 1. 自動評分系統運作原理 首先可以先查看 makefile 中的 test,其對應行為為執行了一段 python 程式碼 ```makefile test: qtest scripts/driver.py scripts/driver.py ``` 若有安裝 python 環境也可以在我們 repo 檔案路徑下執行下面程式碼驅動 ```shell $ python3 scripts/driver.py ``` 而裡面定義了一個 Tracer class ,其中在這個 class 讀入了每個 trace 的檔案以及會 output trace 過程 而最主要執行每次驗證的結果是在 Tracer 中的 runTrace: ```python def runTrace(self, tid): if not tid in self.traceDict: print("ERROR: No trace with id %d" % tid) return False fname = "%s/%s.cmd" % (self.traceDirectory, self.traceDict[tid]) vname = "%d" % self.verbLevel clist = [self.qtest, "-v", vname, "-f", fname] try: retcode = subprocess.call(clist) except Exception as e: print("Call of '%s' failed: %s" % (" ".join(clist), e)) return False return retcode == 0 ``` 由上面程式碼的第 9 行可以看到他會產生 subprocess 去執行 traces 資料夾底下的 cmd,在裡面定義了許多執行 qtest 的指令,再將執行的結果抓出來計算分數。 而值得注意的是,因為跑 subprocess,因此實際上如果真的 malloc 失敗或 free 失敗,如果程式寫的嚴謹可以跑完 process,程式寫的不嚴謹可能會造成程式錯誤而令 process 中止,但是不論哪一種因為在 c 裡面沒有類似 c++ 的 try catch 語法,因此實際上我們的 malloc 以及 free 的 function 都經過改寫,塞入了錯誤訊息的紀錄程式碼,如此一來我們便可以測試自己的程式邏輯是否正確。(not sure) malloc 與 free 的改寫被定義在 harness.h / harness.c 中,利用 define 將 test_malloc 定義為 malloc、 test_free 定義為 free #### harness.c / harness.h 在 harness.c / harness.h 中實際上自行定義了一個 double linked list 來完成我們在 qtest 的操作 ```c= typedef struct BELE { struct BELE *next; struct BELE *prev; size_t payload_size; size_t magic_header; /* Marker to see if block seems legitimate */ unsigned char payload[0]; /* Also place magic number at tail of every block */ } block_ele_t; ``` 其中可以發現每個 entry 的 data structure 存在一個 magic_header field ,他的作用是在於每次 insert 新的 entry 時會同時紀錄當前 queue 的 magic_header,以下程式碼節錄自 harness.c 的 `test_malloc(size_t size)` ```c= if (new_block == NULL) { report_event(MSG_FATAL, "Couldn't allocate any more memory"); error_occurred = true; } new_block->magic_header = MAGICHEADER; new_block->payload_size = size; *find_footer(new_block) = MAGICFOOTER; void *p = (void *) &new_block->payload; ``` 如此一來在之後進行 free operation 時便可以檢查是否 free 到 unallocated block 而 payload_size 則紀錄了要 malloc 記憶體的大小;payload 則是模擬實際獲得的記憶體空間,他會在 test_malloc 時順便全部被初始化為 0x55 > 不過我發現 MAGICHEADER 都會被初始化為 0xdeadbeef,不知道是不是故意的XD > 後來我去查了一下發現有一種專有名詞 **Hexspeak** 就是只這種將 16 進位表達成英文單詞,不同處理器、作業系統會有不同的意義的樣子 :::danger TODO: 解讀這裡 magic number 存在的必要,以及找出類似追蹤記憶體使用而出現的 magie number 設計 :notes: jserv ::: 接著為了確保 queue 的執行,在 harness 中也特別定義 ```c= static block_ele_t *allocated = NULL; static size_t allocated_count = 0; ``` allocated 會紀錄最近一次 malloc 的 block_ele_t pointer,而 allocated_count 則會紀錄目前 allocated block_ele_t 數 --- 而在 harness.c 中透過下面這兩個函式檢查每次 free 一個 pointer 是否合法 ``` static block_ele_t *find_header(void *p) static size_t *find_footer(block_ele_t *b) ``` * 因為 test_malloc 會回傳的是 payload 的指標,因此當使用者需要 free 一個空間的時候也會把 payload 的指標作為參數丟給 test_free * find_head 中首先直接用數學運算得到該 payload 的 block_ele_t pointer,接著自 allocated 開始尋找是否存在相同的 block_ele_t pointer,如果不存在即紀錄錯誤訊息並報錯 * 而 find_footer 則可以用來檢查每次 test_malloc 會特別註記在每個 block_ele_t 尾巴的 MAGICFOOTER ### 2. qtest 的行為以及裡面的技巧 簡單繪製了一下各個 .c / .h 檔被引用的狀況 ### [額外筆記] 為什麼 strlcpy 被視為不安全的操作? 像是 strlcpy 以及 strlcat,兩者皆沒有被納入標準的 ANSI C 中,根據 ANSI C maintainer Ulrich Drepper 認為這兩個 function 是不安全的,一旦使用者使用了此兩個 function 很可能會造成一些 truncation error 被忽視,導致未來可能會產生更多的 bug 需要被處理。 Ulrich Drepper 建議使用 mempcpy,利用回傳得到的最後一個 character pointer 直接寫入該 pointer value 為 \\0 `*((char *) mempcpy (dst, src, n)) = '\0';` (雖然 mempcpy 也沒有被納入標準 ANSI C 中) 另外還有一個議題是因為沒有被納入標準的 ANSI C,因此實際上不同的 OS 的 implementation 也不相同。因此可能會導致在不同作業系統編譯出來的執行檔執行結果錯誤。 # 參考資料 1. [bootlin Elixir Cross Referencer](https://elixir.bootlin.com/linux/v5.0-rc7/source) 2. [C Programming/C Reference/nonstandard/strlcpy](https://en.wikibooks.org/wiki/C_Programming/C_Reference/nonstandard/strlcpy)