# 2019q1 Homework1 (lab0) contributed by < `0n3t04ll` > 說明 - [作業說明](https://hackmd.io/s/BJA8EgFB4) - [C Programming Lab](http://www.cs.cmu.edu/~213/labs/cprogramminglab.pdf) ## 作業要求 ### 實作 FIFO、LIFO 的 Queue - q_new(): Create a new, empty queue - q_free(): Free all storage used by a queue - q_insert_head(): Attempt to insert a new element at the head of the queue (LIFO discipline). - q_insert_tail(): Attempt to insert a new element at the tail of the queue (FIFO discipline). - q_remove_head(): Attempt to remove the element at the head of the queue. - q_size(): Compute the number of elements in the queue. --- 以上節錄自 [C Programming Lab](http://www.cs.cmu.edu/~213/labs/cprogramminglab.pdf) ## 環境 ```shell $ uname -a Linux starpt-K55VD 4.15.0-45-generic #48~16.04.1-Ubuntu SMP Tue Jan 29 18:03:48 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux ``` ## 實作 ### queue.h 在 queue.h 中看到 queue structure 的註解有說要加入其他 fields 來更有效率的實作 q_size(), q_insert_tail() ,在此我添加了 tail pointer 指向 queue 的尾端,用 size 在新增刪除中更新 queue 的大小,考慮到 size 應該不會出現負數,我用 unsigned int 來保存 data 。 ```clike typedef struct{ list_ele_t *head; /*linked list of elements*/ list_ele_t *tail; unsigned int size; } ``` ### q_new() 透過 malloc 要一個 queue structure 的空間並初始化,需要判斷 malloc 成功與否。 ```clike /* Create empty queue. Return NULL if could not allocate space. */ queue_t *q_new() { queue_t *q = (queue_t *) malloc(sizeof(queue_t)); /* What if malloc returned NULL? */ if (!q) return NULL; // initial queue q->head = NULL; q->tail = NULL; q->size = 0; return q; } ``` ### q_free() 用一個 element pointer curr 來遍歷整個 queue。 用另一個 element pointer tmp 來接收準備要被 free 掉的 element ,在 curr 更新後再 free 掉 tmp ```clike /* Free all storage used by queue */ void q_free(queue_t *q) { /* How about freeing the list elements and the strings? */ /* Free queue structure */ if (q) { list_ele_t *curr = q->head; while (curr != NULL) { if (curr->value) free(curr->value); list_ele_t *tmp = curr; curr = curr->next; free(tmp); } free(q); } } ``` ### q_insert_head() 要確認 newh, value 都有 malloc 成功,失敗就要 free 掉之前 malloc 的空間。 以 strlen(), strcpy() 來存資料,新增的同時也要更新 q->size ,由於增加了 tail pointer ,所以我透過 q->size 判斷是否要更新 `q->tail`。 ```clike bool q_insert_head(queue_t *q, char *s) { /* What should you do if the q is NULL? */ if (!q) return false; list_ele_t *newh; newh = (list_ele_t *) malloc(sizeof(list_ele_t)); if (!newh) // allocate fail return false; /* Don't forget to allocate space for the string and copy it */ /* What if either call to malloc returns NULL? */ newh->next = NULL; newh->value = (char *) malloc(strlen(s) + 1); if (!(newh->value)) { free(newh); return false; } strcpy(newh->value, s); newh->next = q->head; q->head = newh; q->size += 1; if (q->size == 1) // the first one q->tail = newh; return true; } ``` ### q_insert_tail() 先判斷 newh, value 是否 malloc 成功,其中一個失敗就要 free 掉先前 malloc 的。 insert tail 的時候要特別注意 tail, head 的更新。 ```clike bool q_insert_tail(queue_t *q, char *s) { /* You need to write the complete code for this function */ /* Remember: It should operate in O(1) time */ if (!q) return false; list_ele_t *newh = (list_ele_t *) malloc(sizeof(list_ele_t)); if (!newh) return false; newh->next = NULL; newh->value = (char *) malloc(strlen(s) + 1); if (!(newh->value)) { free(newh); return false; } strcpy(newh->value, s); if (q->tail) q->tail->next = newh; q->tail = newh; if (!(q->head)) q->head = q->tail; q->size += 1; return true; } ``` ### q_remove_head() 先判斷 queue 的 pointer 和 size,確保不為 NULL 或 empty。 用 tmp pointer 接收原本的 head 再對 queue 做更新,再判斷 sp 地址的有無來決定要不要複製 head->value ,最後 free 掉 tmp。 ```clike bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { /* You need to fix up this code. */ if (!q || q->size == 0) // q is NULL or q is empty return false; list_ele_t *tmp = q->head; q->head = q->head->next; q->size -= 1; if (q->size <= 1) q->tail = q->head; // if(sp && q->head != tmp) if (sp) { strncpy(sp, tmp->value, bufsize - 1); sp[bufsize - 1] = '\0'; } free(tmp->value); free(tmp); return true; } ``` ### q_size() 由於在增減 queue 的過程就在維護 queue 的 size ,故在此只需回傳 q->size 即可 ```clike int q_size(queue_t *q) { /* You need to write the code for this function */ /* Remember: It should operate in O(1) time */ if (!q) return 0; return q->size; } ``` ### q_reverse() reverse 的部份我是以三個 pointer: prev, curr, last 來遍歷整個queue ,每經過一個 element 就修正 next pointer ,使其指回 prev ,最後更新這三個 pointer 。 head, tail 的更新是當所有 element reverse 後將 head assign 給 tail , last assign 給 head。 ```clike void q_reverse(queue_t *q) { /* You need to write the code for this function */ if (!q) return; if (q->size == 0 || q->size == 1) return; list_ele_t *prev = NULL; list_ele_t *curr = q->head; list_ele_t *last = NULL; while (curr != q->tail) { last = curr->next; // reverse link curr->next = prev; // update prev = curr; curr = last; } last->next = prev; q->tail = q->head; q->head = last; } ``` ## Valgrind Test 之前有用過 valgrind 一個一個測試過 qtest ,大致上跟 Naetw 說的一樣,會在13、14的時候壞掉,不過那時有看到 Naetw 跟老師說會 PR ,所以我就乖乖的等更新。 實際跑過一次是沒有問題的,只不過在測試13、14、15的時後跑的特別慢,讓我一度以為壞掉。 :::warning 「路見不平,拿 patch 來填」,請見 [PR #5](https://github.com/sysprog21/lab0-c/pull/5) :notes: jserv ::: ```shell $ make valgrind cp qtest /tmp/qtest.TLAtCE chmod u+x /tmp/qtest.TLAtCE sed -i "s/alarm/isnan/g" /tmp/qtest.TLAtCE scripts/driver.py -p /tmp/qtest.TLAtCE --valgrind ... +++ TESTING trace trace-01-ops: --- trace-01-ops 6/6 +++ TESTING trace trace-02-ops: --- trace-02-ops 6/6 +++ TESTING trace trace-03-ops: --- trace-03-ops 6/6 +++ TESTING trace trace-04-ops: --- trace-04-ops 6/6 +++ TESTING trace trace-05-ops: --- trace-05-ops 6/6 +++ TESTING trace trace-06-string: --- trace-06-string 7/7 +++ TESTING trace trace-07-robust: --- trace-07-robust 7/7 +++ TESTING trace trace-08-robust: --- trace-08-robust 7/7 +++ TESTING trace trace-09-robust: --- trace-09-robust 7/7 +++ TESTING trace trace-10-malloc: --- trace-10-malloc 7/7 +++ TESTING trace trace-11-malloc: --- trace-11-malloc 7/7 +++ TESTING trace trace-12-malloc: --- trace-12-malloc 7/7 +++ TESTING trace trace-13-perf: --- trace-13-perf 7/7 +++ TESTING trace trace-14-perf: --- trace-14-perf 7/7 +++ TESTING trace trace-15-perf: --- trace-15-perf 7/7 --- TOTAL 100/100 ``` ## AddressSanitizer 檢測 修改了 Makefile 中的 CFLAGS ,添加 sanitizer 參數,若有發生 UAF, heap overflow, memory leak 就會報錯。 參數如下: ```make= CFLAGS = -O0 -g -Wall -Werror -fsanitize=address ``` 因為沒有問題,所以什麼都沒顯示。 ## 研究 q_test ### q_test.c qtest.c main function 中的 while loop 主要是用來接收外部傳進來的參數,像是 verbose level, testcase file 等等,在此先忽略避免程式區塊過大。 後續主要程式如下: ```c= queue_init(); // set q ptr, file_cnt, signalhandler init_cmd(); // use linked list to link global cmd structure, paramter // structure console_init(); // same with above but linked queue function set_verblevel(level); if (level > 1) { set_echo(true); } if (logfile_name) set_logfile(logfile_name); add_quit_helper(queue_quit); bool ok = true; ok = ok && run_console(infile_name); ok = ok && finish_cmd(); return ok ? 0 : 1; } ``` queue_init() 負責初始化 queue pointer 為 NULL 和註冊 sigsegv, sigalrm 的 handler 。 init_cmd() 用 add_cmd() 函數把跟 queue 無關的 cmd 用 cmd_ptr 保存並串成 linked list 。 之所以用 linked list ,個人猜測是為了之後修改做的彈性設定。 下面是 cmd 的 structure : ```c= struct CELE { char *name; cmd_function operation; // funtion ptr point to cmd function char *documentation; cmd_ptr next; }; ``` 而 console_init() 用了一樣的方式串連跟 queue 相關的 cmd 成 linked list 。 ### interpret_cmda 真正處理 cmd 的輸入和操作是在 cmd_select() 內部 ,可以分為從檔案讀取 cmd 或是從 stdin 讀取。 讀取後後進入 interpret_cmd ,在裡面做 parse 接著傳給 interpret_cmda() 操作該 cmd ,方法是遍歷 cmd_list 並用 strcmp() 比對 cmd name 並且用該 cmd structure 中的 function ptr 執行 cmd 。 ```c= static bool interpret_cmda(int argc, char *argv[]) { if (argc == 0) return true; /* Try to find matching command */ cmd_ptr next_cmd = cmd_list; bool ok = true; while (next_cmd && strcmp(argv[0], next_cmd->name) != 0) next_cmd = next_cmd->next; if (next_cmd) { ok = next_cmd->operation(argc, argv); if (!ok) record_error(); } else { report(1, "Unknown command '%s'", argv[0]); record_error(); ok = false; } return ok; } ``` ### Exception Handler cmd 操作都是透過 function ptr ,而在我比對所有的 cmd 後發現執行自己寫的 queue 操作前都會有一段應該跟 exception handler 相關的 statement ,以 do_new() 的 13~15 為例: ```c= bool do_new(int argc, char *argv[]) { if (argc != 1) { report(1, "%s takes no arguments", argv[0]); return false; } bool ok = true; if (q != NULL) { report(3, "Freeing old queue"); ok = do_free(argc, argv); } error_check(); if (exception_setup(true)) q = q_new(); exception_cancel(); qcnt = 0; show_queue(3); return ok && !error_check(); } ``` 從名稱上來看應該是 exception 的前置作業,進入後發現用了 sigsetjmp ,該 function 從 manual 來看是用來保存 stack context/ environment ,好在處理 interrupt 或 error 後可以繼續執行。 ```c= bool exception_setup(bool limit_time) { if (sigsetjmp(env, 1)) { /* Got here from longjmp */ jmp_ready = false; if (time_limited) { alarm(0); time_limited = false; } if (error_message) { report_event(MSG_ERROR, error_message); } error_message = ""; return false; } else { /* Got here from initial call */ jmp_ready = true; if (limit_time) { alarm(time_limit); time_limited = true; } return true; } } ``` 而專司處理 signal 的兩個 handler 內部都是執行 harness.c 中的 trigger expression 其中的 siglongjmp() 根據 manual 描述為回復在 sigsetjmp 中保存的 env 好返回先前執行狀態以便繼續執行,達到 exception handler 的效果。 ```c= void trigger_exception(char *msg) { error_occurred = true; error_message = msg; if (jmp_ready) siglongjmp(env, 1); else exit(1); } ``` 因為對 signal 處理過程不熟,所以決定親自用 gdb 跟蹤一次。 若用單純的下斷點對 signal 是行不通的,必須要用 handle 處理,根據這份[manual](https://sourceware.org/gdb/onlinedocs/gdb/Signals.html),必須用 ```shell= gdb$ handle SIGSEGV pass ``` 讓該 sigsegv pass 到 handler 才能繼續,不然 gdb 只會卡死在發生 sigsegv 的指令上。 發現過程如下: 1. 進入 sigsegvhandler 2. 進入 trigger_expression 3. 由 jmp_ready 判斷 sigjmp_buf 是否準備好 * 若尚未準備好,立即 exit(1) * 準備好則進入下個步驟 > 這邊我是參考 0xff77 同學的描述 4. 由 siglongjmp() 跳回 exception_setup() 中的 sigsetjmp() , code 如下: ```c= if (sigsetjmp(env, 1)) { /* Got here from longjmp */ jmp_ready = false; if (time_limited) { alarm(0); time_limited = false; } if (error_message) { report_event(MSG_ERROR, error_message); } error_message = ""; return false; ``` 5. 由於是從 siglongjmp 過來,故 return value 不為 0 ,進入 if scope ,重設好相關變數並印出錯誤訊息,最後返回至 exception_cancel() 6. exception_cancel() 取消 alarm 限制和重設 至此完成了一個 exception handler 的過程。 ### test_malloc, test_free 在跟進 harness.c 後,發現裡面有個比較特別的 test_malloc, test_free ,在 harness.h 找了一下後發現程式測試的 malloc, free 會被 test_malloc, test_free 取代: ```c=57 /* Tested program use our versions of malloc and free */ #define malloc test_malloc #define free test_free ``` 下面為 test_malloc 程式碼,我把程式碼分為數個片段搭配文字解釋。 line 122 中用 noallocate_mode 來決定要不要 malloc 。 ```c=120 void *test_malloc(size_t size) { if (noallocate_mode) { report_event(MSG_FATAL, "Calls to malloc disallowed"); return NULL; ``` 比較有趣的是 fail_allocation() ,用隨機的方式判斷 malloc 有沒有成功,我以前都是直接測完全失敗或完全成功,用隨機的方式進行測試應該是模擬 malloc 突然壞掉的情形下有無做好檢查。 ```c=+ } if (fail_allocation()) { report_event(MSG_WARN, "Malloc returning NULL"); return NULL; } ``` malloc 出來的 chunk 還要再包一層 block_ele_t , block_ele_t 在實作上用到了之前在 C_and_CPP 板上看到的[一篇](https://www.ptt.cc/bbs/C_and_CPP/M.1545460670.A.A20.html)討論提過的技巧,透過 zero length array 可以動態改變 struct 空間,這邊因為 user 要求的空間不固定但又想包在同個 struct 所以用這種方法。 ```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; ``` malloc 出來的 block 會拆分為三個部分:原本放 data 的空間、額外的 block metadata 空間、 footer 空間。 ```c=+ block_ele_t *new_block = malloc(size + sizeof(block_ele_t) + sizeof(size_t)); if (new_block == NULL) { report_event(MSG_FATAL, "Couldn't allocate any more memory"); error_occurred = true; } ``` 從下面程式碼可以看出 qtest 透過 double linked list 維護 malloc 出來的 block。 除了自己用 double linked list 維護 block 外, test_malloc 還在 block 的 header, footer 加上 MAGICHEADER, MAGICFOOTER 兩個常數,猜測是為了要檢查有無 heap overflow 的發生。 ```c=+ new_block->magic_header = MAGICHEADER; new_block->payload_size = size; *find_footer(new_block) = MAGICFOOTER; void *p = (void *) &new_block->payload; memset(p, FILLCHAR, size); new_block->next = allocated; new_block->prev = NULL; if (allocated) allocated->prev = new_block; allocated = new_block; allocated_count++; return p; } ``` test_free() 對 test_malloc() 出來的 block 做操作,開頭先判斷有無設定 noallocate_mode 和 p 的合法性。 ```c= void test_free(void *p) { if (noallocate_mode) { report_event(MSG_FATAL, "Calls to free disallowed"); return; } if (p == NULL) { report(MSG_ERROR, "Attempt to free NULL"); error_occurred = true; return; } ``` 透過找出 block footer 位置並且比對 MAGICFOOTER 有無被修改來確認是否發生 heap overflow ,驗證了上面的猜想。這種設計理念跟 canary 如出一轍。 ```c=+ block_ele_t *b = find_header(p); size_t footer = *find_footer(b); if (footer != MAGICFOOTER) { report_event(MSG_ERROR, "Corruption detected in block with address %p when " "attempting to free it", p); error_occurred = true; } ``` 在 header, footer 的地方放上 MAGICFREE ,在 data 的地方填上 FILLCHAR ,表示該 block 已經被 free 掉。把要 free 的 block 從 double linked list 中 unlink 掉後再將之 free 。 ```c=+ b->magic_header = MAGICFREE; *find_footer(b) = MAGICFREE; memset(p, FILLCHAR, b->payload_size); /* Unlink from list */ block_ele_t *bn = b->next; block_ele_t *bp = b->prev; if (bp) bp->next = bn; else allocated = bn; if (bn) bn->prev = bp; free(b); allocated_count--; } ``` test_free(), test_malloc() 之所以用額外的 double linked list 做串連是為了在測試結束後,透過遍歷 block list 評估 leak memory 的情形。這點在 do_free() 中的 allocation_check() 得到解答。 ## /scripts/driver.py 從 Makefile 的 test 可以發現用來自動評測的方式是透過 python script 實現。 主要的評測 class 為 Tracer ,而評測主要的 method 為 Tracer.run() 。 scoreDict 用字典保存 testcase 和相對應的得分, tid 顧名思義為 testcase 的 id 。 ```python3= def run(self, tid = 0): scoreDict = { k : 0 for k in self.traceDict.keys() } print("---\tTrace\t\tPoints") if tid == 0: tidList = self.traceDict.keys() else: if not tid in self.traceDict: print("ERROR: Invalid trace ID %d" % tid) return tidList = [tid] score = 0 maxscore = 0 if self.useValgrind: self.command = 'valgrind' else: self.command = self.qtest self.qtest = '' ``` 從 self.traceDict[t] 取出 testcase name 輸出測是訊息,接著把從 tidList 取出的 t 傳入 self.runTrace() ,裡面會透過 subprocess 執行: ```shell= ./qtest -v 1 -f [testcase name] ``` subprocess 的回傳值再傳回給 Tracer.run ,並將字串格式化輸出檔名、得分、總分。然後把該項得分加總至 score 當作最後的總分。 ```python3= for t in tidList: tname = self.traceDict[t] if self.verbLevel > 0: print("+++ TESTING trace %s:" % tname) ok = self.runTrace(t) maxval = self.maxScores[t] tval = maxval if ok else 0 print("---\t%s\t%d/%d" % (tname, tval, maxval)) score += tval maxscore += maxval scoreDict[t] = tval ```