# 2018q3 Homework2 (lab0) contributed by < [`amikai`](https://github.com/amikai/lab0-c) > ###### tags: `sysprog2018` >請補上實驗環境 >[name=課程助教][color=red] ### [==作業題目==](https://hackmd.io/s/BJp_jq-tm) * [C Programming Lab](http://www.cs.cmu.edu/~213/labs/cprogramminglab.pdf) * Studying man page * linked list * queue * implements each function * 探討一下不同的實作品味 * 解釋自動評分系統運作的原理 * 研究自動測試機制 ### 實驗環境 ``` gcc version 7.3.0 (Ubuntu 7.3.0-16ubuntu3) Ubuntu 18.04 kernel: 4.15.0-34-generic Cppcheck 1.82 ``` --- # Makefile 此 Makefile 可以大略由 3 個功能所組成: 1. 安裝 git hook 2. 編譯程式 3. 自動化測試 ## 安裝 git hook ```make GIT_HOOKS := .git/hooks/applied $(GIT_HOOKS): @scripts/install-git-hooks @echo ``` 可以在 `scripts/pre-commit.hook` 看見要先經過 `clang-format` 和 `cppcheck` 檢查通過才能會接受 `commit message` 另外 `commit-msg.hook` 裡是為了不讓你違反 [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) 裡的規則 ## 編譯程式 ```make queue.o: queue.c queue.h harness.h $(CC) $(CFLAGS) -c queue.c qtest: qtest.c report.c console.c harness.c queue.o $(CC) $(CFLAGS) -o qtest qtest.c report.c console.c harness.c queue.o ``` 為了編譯出需要的程式而撰寫的相依性及動作,此處的 `queue.o`和 `qtest` 都是真正會慘出的檔案 ## 自動化測試 ```diff + .PHONY: test clean test: qtest scripts/driver.py scripts/driver.py clean: rm -f *.o *~ qtest rm -rf *.dSYM (cd traces; rm -f *~) ``` 使用 `make test` 之後是自動化評分的系統, `make clean` 則是清空編譯後所產生的檔案 在這裡的兩個`target`: `test` 和 `clean` 並不是真正的檔案, 使用[PHONY target](https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html) 較不容易使人誤會, 也可以避免名稱衝突的問題 (這份 Makefile 只適用於沒有一個檔案叫做 `clean` 的情況下能正常使用) # Queue implementation 先將 queue 的 structure 補齊,要 commit 的時候 `cppcehck` 產生以下錯誤: ``` [harness.c:147]: (error) Address of auto-variable 'new_block->payload' returned Fail to pass static analysis. ``` 我只更動了 `queue.h` 觸發了這個, 仔細觀察 `scripts/pre-commit.hook` 的寫法: 對整個目錄進行 `cpp_check` ```diff diff --git a/harness.c b/harness.c index a19b2cd..428170c 100644 --- a/harness.c +++ b/harness.c @@ -136,15 +136,14 @@ void *test_malloc(size_t size) 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); + memset((void *) &new_block->payload, FILLCHAR, size); new_block->next = allocated; new_block->prev = NULL; if (allocated) allocated->prev = new_block; allocated = new_block; allocated_count++; - return p; + return (void *)&new_block->payload; } ``` 原本的程式 回傳 `p` 指標的位址的值, `cpp_check` 可能無法知道到 `p` 指向的地方的 `life time` 再回傳之後不會消失, 因為 `new_block` 所指向的記憶體空間是在 `heap` 中, 所以本來的程式並沒有什麼問題, 為了避開這個錯誤我就在行為不更改的情況下進行更改 這裡其實有涉及到一個神奇的東西叫做 [Arrays of Length Zero](https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html), 這裡先不做詳述 ## q_new q new: Create a new, empty queue. ```clike= queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); if (q) { q->head = q->tail = NULL; q->size = 0; } return q; } ``` ## q_free q free: Free all storage used by a queue. ```clike= void q_free(queue_t *q) { if (!q) return; list_ele_t *visit = q->head; while (visit) { q->head = visit; visit = visit->next; free(q->head->value); free(q->head); } free(q); } ``` ## q_insert_head q insert head: Attempt to insert a new element at the head of the queue (LIFO discipline). ```clike= bool q_insert_head(queue_t *q, char *s) { if (!q) return false; list_ele_t *newh = (list_ele_t *) malloc(sizeof(list_ele_t)); if (!newh) return false; newh->value = (char *) malloc(strlen(s) + 1); if (!newh->value) { free(newh); return false; } strcpy(newh->value, s); if (q->size == 0) { q->tail = newh; } newh->next = q->head; q->head = newh; q->size++; return true; } ``` 使用 `man strlen` 查到以下: The `strlen()` function returns the number of characters that precede the terminating NUL 字元 這代表 `strlen` 回傳的字串長度並不包含 NUL character 的長度, 所以在配置空間的時候必須要加1 使用 `man strcpy` 查到以下: `char *strcpy(char * dst, const char * src);` The `strcpy()` functions copy the string `src` to `dst` (including the terminating `\0` character.) 所以不必在複製過後, 自行塞入 NUL 字元 ## q_insert_tail q insert tail: Attempt to insert a new element at the tail of the queue (FIFO discipline). ```clike= bool q_insert_tail(queue_t *q, char *s) { if (!q) return false; list_ele_t *newh = (list_ele_t *) malloc(sizeof(list_ele_t)); if (!newh) return false; newh->value = (char *) malloc(strlen(s) + 1); if (!newh->value) { free(newh); return false; } strcpy(newh->value, s); newh->next = NULL; if (q->size == 0) { q->head = newh; } else { q->tail->next = newh; } q->tail = newh; q->size++; return true; } ``` ## q_remove_head q remove head: Attempt to remove the element at the head of the queue. ```clike= bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { // if queue is NULL or empty if (!q || q->size == 0) return false; int size; // if sp is non-NULL if (sp) { size = strlen(q->head->value); size = size > bufsize - 1 ? bufsize - 1 : size; strncpy(sp, q->head->value, size); sp[size] = '\0'; } list_ele_t *temp = q->head; q->head = temp->next; q->size--; free(temp->value); free(temp); return true; } ``` ## q_size q size: Compute the number of elements in the queue. ```clike= int q_size(queue_t *q) { return q ? q->size : 0; } ``` ## q_reverse Reorder the list so that the queue elements are reversed in order. This function should not allocate or free any list elements (either directly or via calls to other functions that allocate or free list elements.) Instead, it should rearrange the existing elements. ```clike= void q_reverse(queue_t *q) { if (!q || q->size == 0) return; list_ele_t *prev_ele = NULL; list_ele_t *temp_ele = q->head; q->tail = q->head; while (temp_ele) { temp_ele = temp_ele->next; q->head->next = prev_ele; prev_ele = q->head; q->head = temp_ele; } q->head = prev_ele; } ``` ## Testing ``` --- Trace Points +++ TESTING trace trace-01-ops: # Test of insert_head and remove_head --- trace-01-ops 6/6 +++ TESTING trace trace-02-ops: # Test of insert_head, insert_tail, and remove_head --- trace-02-ops 6/6 +++ TESTING trace trace-03-ops: # Test of insert_head, insert_tail, reverse, and remove_head --- trace-03-ops 6/6 +++ TESTING trace trace-04-ops: # Test of insert_head, insert_tail, and size --- trace-04-ops 6/6 +++ TESTING trace trace-05-ops: # Test of insert_head, insert_tail, remove_head reverse, and size --- trace-05-ops 6/6 +++ TESTING trace trace-06-string: # Test of truncated strings --- trace-06-string 7/7 +++ TESTING trace trace-07-robust: # Test operations on NULL queue --- trace-07-robust 7/7 +++ TESTING trace trace-08-robust: # Test operations on empty queue --- trace-08-robust 7/7 +++ TESTING trace trace-09-robust: # Test remove_head with NULL argument --- trace-09-robust 7/7 +++ TESTING trace trace-10-malloc: # Test of malloc failure on new --- trace-10-malloc 7/7 +++ TESTING trace trace-11-malloc: # Test of malloc failure on insert_head --- trace-11-malloc 7/7 +++ TESTING trace trace-12-malloc: # Test of malloc failure on insert_tail --- trace-12-malloc 7/7 +++ TESTING trace trace-13-perf: # Test performance of insert_tail --- trace-13-perf 7/7 +++ TESTING trace trace-14-perf: # Test performance of size --- trace-14-perf 7/7 +++ TESTING trace trace-15-perf: # Test performance of insert_tail, size, and reverse --- trace-15-perf 7/7 --- TOTAL 100/100 ``` # 測試程式 ## malloc and free in harness.c 這些測試程式有趣的地方或許就是在你寫 `malloc` 和 `free` 時, 在以下某些錯誤時竟然可以抓出來: 我測試了三種情況: 1. 故意將 `q_free` 寫造成 double free error 2. 故意在 `q_free` 裡沒有將空間完全歸還: 3. 故意在 `q_free` 裡釋放 NULL 指標: 故意將 `q_free` 寫造成 double free error: ``` cmd > new cmd > free ERROR: Attempted to free unallocated block. Address = 0x7ff58b500050 ERROR: Attempted to free unallocated or corrupted block. Address = 0x7ff58b500050 ERROR: Corruption detected in block with address 0x7ff58b500050 when attempting to free it ``` 故意在 `q_free` 裡沒有將空間完全歸還: ``` cmd > new cmd > ih 1 cmd > free ERROR: Freed queue, but 1 blocks are still allocated ``` 故意在 `q_free` 裡釋放 NULL 指標: ``` cmd > new cmd > free Attempt to free NULL ``` ```clike= // harness.h void *test_malloc(size_t size); void test_free(void *p); #define malloc test_malloc #define free test_free ``` 仔細觀察可以發現, 其實在 `harness.h` 使用 macro 將 `malloc` 替換為 `test_malloc`, 將 `free` 替換為 `test_free` ### struct block_ele_t ```clike= 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; ``` 可以發現 CMU 用了雙向鍊結串列來維護 heap, 並且以此來測試你的程式, 以下比較神奇的是 `unsigned char payload[0]`, 這其實是 gcc 所提供的 [Arrays of Length Zero](https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html), 在以下的情況, 這樣的技巧讓你能在 run time 決定 `payload` 的大小, 並且使用一個 `payload_size` 紀錄此 payload的長度 ![](https://i.imgur.com/Kaf6JvL.png) :::warning 問: 為何不使用以下這種方式就好了 ```clike typedef struct BELE { struct BELE *next; struct BELE *prev; size_t magic_header; /* Marker to see if block seems legitimate */ unsigned char *payload; } block_ele_t; ``` 每次需要使用時在對 `payload` 這裡作 `malloc` 的動作 ::: ### test_malloc( ) ```clike= void *test_malloc(size_t size) { if (noallocate_mode) { report_event(MSG_FATAL, "Calls to malloc disallowed"); return NULL; } if (fail_allocation()) { report_event(MSG_WARN, "Malloc returning NULL"); return NULL; } 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; } new_block->magic_header = MAGICHEADER; new_block->payload_size = size; *find_footer(new_block) = MAGICFOOTER; memset((void *) &new_block->payload, FILLCHAR, size); new_block->next = allocated; new_block->prev = NULL; if (allocated) allocated->prev = new_block; allocated = new_block; allocated_count++; return (void *) &new_block->payload; } ``` 從 L11-L12 可以看到它真正配置記憶體的大小也就是 `呼叫者所需要的記憶體大小` + `block_ele_t 的大小` + `size_t` 的大小 ![](https://i.imgur.com/rOMxTGH.png) 並且在每次配置完之後將 `migic_header` 欄位塞進 `MAGICHEADER` 常數, 在 `payload` 所有空間塞入 `FILLCHAR` 常數, 在最後一塊 `size_t` 的大小裡塞入 `MAGICFOOTER`, 邏輯上是以上那張圖, (注意: magic footer 是邏輯上的, 並沒有這個欄位) 在每一個你用 `malloc` 配置的記憶體位置上的頭部 (`header`) 和尾部 (`footer`) 塞了 magic number 應該就只是在測試: 你配置記憶體過後, 寫入的地方有沒有超過這個範圍 (header 到 footer 的範圍), 只要 header 和 footer 被更改了,就代表你寫入的記憶體往上超過或往下超過你配置的記憶體了 每當你使用到到 `malloc` 時都會在這個雙向鍊結串列的頭加上一個 element, 而指向這個雙向鍊結串列的指標則是 `allocated` 變數, 另外以一個 `allocated_count` 計算這個雙向鍊結串列有多長, 這也是為什麼測試程式知道你沒有將 malloc 後的記憶體歸還的數量 `noallocate_mode` 只有在 `qtest.c` 裡的 `do_reverse` 有動到, 在測試 `q_reverse` 的時候這個值被設為 true, 應該只是為了測試 `q_reverse` 所寫的, 因為在作這個動作的時候是不需要使用 `malloc` 的 CMU 助教為了測試程式的可靠性, 所以在你呼叫 `malloc` 的時候有機率的回傳 NULL 給你, 這就是 `fail_allocation()` 的用處了, 可以在 `qtest` 使用 `option malloc prob` 停整這個值, `prob` 指的是失敗的機率要多少百分比 ref: - [Arrays of Length Zero](https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html) - [C Struct Hack - Structure with variable length array](http://frankchang0125.blogspot.com/2013/01/c-struct-hack-structure-with-variable.html) :::warning 延伸閱讀: C99 裡的 `flexible array member` ::: ### test_free( ) ```clike= 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_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; } 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` 其實就是將此雙向鍊結串列刪除一個 element 而已 跟 `test_malloc` 類似, `noallocate_mode` 只有在 `qtest.c` 裡的 `do_reverse` 有動到, 在測試 `q_reverse` 的時候這個值被設為 true, 應該只是為了測試 `q_reverse` 所寫的, 因為在作這個動作的時候是不需要使用 `free` 的 ```clike= static block_ele_t *find_header(void *p) { if (p == NULL) { report_event(MSG_ERROR, "Attempting to free null block"); error_occurred = true; } block_ele_t *b = (block_ele_t *) ((size_t) p - sizeof(block_ele_t)); if (cautious_mode) { /* Make sure this is really an allocated block */ block_ele_t *ab = allocated; bool found = false; while (ab && !found) { found = ab == b; ab = ab->next; } if (!found) { report_event(MSG_ERROR, "Attempted to free unallocated block. Address = %p", p); error_occurred = true; } } if (b->magic_header != MAGICHEADER) { report_event( MSG_ERROR, "Attempted to free unallocated or corrupted block. Address = %p", p); error_occurred = true; } return b; } ``` 在 `find_head` 這邊其實本意只是要取得 `block_ele_t` 裡的 header 欄位, L8-L22 之間只要 `cautious_mode` 為 true ,會作做個一個檢查, 就是維護的鍊結串列裡的位置有沒有參數傳近來的指標, 如果沒有的話就代表傳近來的指標所指向的地方, 根本就沒有配置 heap 記憶體 ## exception flow 在每一個測試函式裡在開始真正使用自己寫的 `queue` 之前和之後呼叫 `exception_setup` 和 `exception_cancel` (請看以下) 這就讓我好奇了 ```clike bool do_new(int argc, char *argv[]) { ... if (exception_setup(true)) q = q_new(); exception_cancel(); ... } bool do_free(int argc, char *argv[]) { ... if (exception_setup(true)) q_free(q); exception_cancel(); ... } bool do_insert_head(int argc, char *argv[]) { ... if (exception_setup(true)) { for (r = 0; ok && r < reps; r++) { bool rval = q_insert_head(q, inserts); ... } } exception_cancel(); ... } bool do_insert_tail(int argc, char *argv[]) { ... if (exception_setup(true)) { for (r = 0; ok && r < reps; r++) { bool rval = q_insert_tail(q, inserts); ... } exception_cancel(); ... } bool do_remove_head(int argc, char *argv[]) { ... if (exception_setup(true)) rval = q_remove_head(q, removes, string_length + 1); exception_cancel(); ... } bool do_remove_head_quiet(int argc, char *argv[]) { ... if (exception_setup(true)) rval = q_remove_head(q, NULL, 0); exception_cancel(); ... } bool do_reverse(int argc, char *argv[]) { ... if (exception_setup(true)) q_reverse(q); exception_cancel(); ... } bool do_size(int argc, char *argv[]) { if (exception_setup(true)) { for (r = 0; ok && r < reps; r++) { cnt = q_size(q); ok = ok && !error_check(); } } exception_cancel(); } ``` --- exception 的機制牽扯到了幾個神奇的函式 `sigsetjmp` 和 `siglongjmp`, 在看過 man page 之後了解到, 這一組函式是用來達到 `nonlocal goto`, 這種技巧也常常被拿來作例外處理的用途, 為了解釋個函式的用途, 我簡單寫了一支程式: ```clike= #include <signal.h> #include <setjmp.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> sigjmp_buf buf; int error1 = 0; int error2 = 1; void handler(int sig) { printf("handler\n"); if(error1) siglongjmp(buf, 1); if(error2) siglongjmp(buf, 2); } int main() { switch (sigsetjmp(buf,1)) { case 0: signal(SIGINT, handler); printf("starting\n"); break; case 1: printf("error1\n"); break; case 2: printf("error2\n"); break; } while(1) { sleep(1); printf("processing\n"); } exit(0); } ``` 一開始 L23 `sigsetjmp` 會返回 0, 所以會在 L25 安裝一個 `signal handler`( 這一個 handler 會在按下 `ctrl + c` 會觸發 (`SIGINT`) ), 執行到 while 無窮迴圈時按下 `ctrl + c` 則會跳到 handler 裡, 因為 `error2` 為 1, 則 L19 的 `siglongjmp(buf, 2)` 就會跳到 L23 去判斷, `sigsetjmp` 則會回傳 2 (這時候 `sigsetjmp` 回傳的值是 `siglongjmp` 第二個參數), 則會跳到 L32 印出 error2 ref: - `man sigsetjmp` - CSAPP Figure 8.43 8.44 --- 這時候在看 `exception_setup` 和 `exception_cancel` 的機制就簡單許多了 ```clike= 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; } } void exception_cancel() { if (time_limited) { alarm(0); time_limited = false; } jmp_ready = false; error_message = ""; } void trigger_exception(char *msg) { error_occurred = true; error_message = msg; if (jmp_ready) siglongjmp(env, 1); else exit(1); } void sigsegvhandler(int sig) { trigger_exception( "Segmentation fault occurred. You dereferenced a NULL or invalid " "pointer"); } void sigalrmhandler(int sig) { trigger_exception( "Time limit exceeded. Either you are in an infinite loop, or your " "code is too inefficient"); } static void queue_init() { fail_count = 0; q = NULL; signal(SIGSEGV, sigsegvhandler); signal(SIGALRM, sigalrmhandler); } ``` 背景知識: 1. `unsigned int alarm(unsigned int seconds)`: 會在`alarm` 呼叫後 `second` 秒會發出 `SIGALRM` signal, ex: alarm(3) 就是在 3 秒後發出 `SIGALRM` signal, 比較特殊的是 `alarm(0)`, 它會取消調所有 pending 的 alarm 2. `SIGSEGV` signal 會在不合法的記憶體存 取情況 (segmentation fault) 下發出 總結: 在觀察程式的用途後發現, 這裡的 `exception` 設計是為了: 1. 時間限制 - 發出警告訊息讓你知道你超時了 (預設是 1 秒) 2. 發出記憶體錯誤訊息 詳細過程: `exception_setup` 使用有兩種情況: 1. 參數為 `true`, 代表有時間限制 (若沒在時間限制內會發出 TLE 訊息), 以及記憶體錯誤的訊息, `exception_setup` 使用 alarm(1) 會發出在 1 秒後發出 `SIGALRM` signal, 若在 1 秒內沒呼叫 `exception_cancel` (此函式裡有 `alarm(0)` 裡取消所有 pending alarm) 則會跳到相對應 handler 裡 2. 參數為 `fase`, 僅有記憶體錯誤的訊息 我在底下畫出了整個流程 ![](https://i.imgur.com/IrBXEfh.png) ref: - `man alarm` - CSAPP Figure 8.26 # CException 解析 在 github 上看到有人使用 `setjmp` 和 `longjmp` 實作了一個小小的 Exception 函式庫叫做 [CException](http://www.throwtheswitch.org/cexception/) - [github](https://github.com/ThrowTheSwitch/CException) 此函式庫主打輕量, 函式庫只有兩個檔案 (CException.h 和 CException.c), 還可以支援 single tasking 和 multi tasking 官方給出了一個兩個簡單的範例,說明使用此框架的前後,程式碼長什麼樣子: 使用前: ```clike BOOL ProductionLine_DoWholeBunchOfStuff( int a ) { BOOL retVal = FALSE; if (Worker_DoStepA(a) == SUCCESS) { if (Worker_DoStepB(a+1) == SUCCESS) { if (Worker_DoStepC(a+2) == SUCCESS) { retVal = TRUE; } } } return retVal; } ``` 使用後: ```clike void ProductionLine_DoWholeBunchOfStuff( int a ) { CEXCEPTION_T e; Try { Worker_DoStepA(a); Worker_DoStepB(a+1); Worker_DoStepC(a+2); } Catch(e) { SystemLogger_Error(e); } } void Worker_DoStepA( int a ) { if (a < 100) { Throw( BOOM_GOES_THE_DYNAMITE ); } // do something useful } // --- Tests --- void test_ProductionLine_DoWholeBunchOfStuff( void ) { Worker_DoStepA_Expect( 50 ); Worker_DoStepB_ExpectAndThrow( 51, BOOM_GOES_THE_DYNAMITE ); SystemLogger_Error_Expect( BOOM_GOES_THE_DYNAMITE ); ProductionLine_DoWholeBunchOfStuff( 50 ); } void test_Worker_DoStepA( void ) { CEXCEPTION_T e; Worker_DoStepA( 101 ); Worker_DoStepA( 100 ); Try { Worker_DoStepA( 99 ); TEST_FAIL_MESSAGE( "Should have thrown!" ); } Catch(e) { TEST_ASSERT_EQUAL( BOOM_GOES_THE_DYNAMITE, e ); } } ``` ## Try Catch 實作 以下為節錄程式碼: ```Clike= #define CEXCEPTION_T unsigned int #define CEXCEPTION_NUM_ID (1) //there is only the one stack by default #define CEXCEPTION_GET_ID (0) //use the first index always because there is only one anyway #define CEXCEPTION_NONE (0x5A5A5A5A) typedef struct { jmp_buf* pFrame; CEXCEPTION_T volatile Exception; } CEXCEPTION_FRAME_T; volatile CEXCEPTION_FRAME_T CExceptionFrames[CEXCEPTION_NUM_ID] = {{ 0 }}; #define Try \ { \ jmp_buf *PrevFrame, NewFrame; \ unsigned int MY_ID = CEXCEPTION_GET_ID; \ PrevFrame = CExceptionFrames[MY_ID].pFrame; \ CExceptionFrames[MY_ID].pFrame = (jmp_buf*)(&NewFrame); \ CExceptionFrames[MY_ID].Exception = CEXCEPTION_NONE; \ CEXCEPTION_HOOK_START_TRY; \ if (setjmp(NewFrame) == 0) { \ if (1) //Catch (see C file for explanation) #define Catch(e) \ else { } \ CExceptionFrames[MY_ID].Exception = CEXCEPTION_NONE; \ CEXCEPTION_HOOK_HAPPY_TRY; \ } \ else \ { \ e = CExceptionFrames[MY_ID].Exception; \ (void)e; \ CEXCEPTION_HOOK_START_CATCH; \ } \ CExceptionFrames[MY_ID].pFrame = PrevFrame; \ CEXCEPTION_HOOK_AFTER_TRY; \ } \ if (CExceptionFrames[CEXCEPTION_GET_ID].Exception != CEXCEPTION_NONE) void Throw(CEXCEPTION_T ExceptionID) { unsigned int MY_ID = CEXCEPTION_GET_ID; CExceptionFrames[MY_ID].Exception = ExceptionID; if (CExceptionFrames[MY_ID].pFrame) { longjmp(*CExceptionFrames[MY_ID].pFrame, 1); } CEXCEPTION_NO_CATCH_HANDLER(ExceptionID); } ``` 1. 為什麼每做經過一次 Try...Catch block 就要記住上一個 frame? 以下為省略過後的程式碼: ```clike // Try block jmp_buf *PrevFrame, NewFrame; unsigned int MY_ID = CEXCEPTION_GET_ID; PrevFrame = CExceptionFrames[MY_ID].pFrame; ... // Catch block CExceptionFrames[MY_ID].pFrame = PrevFrame; ``` 為了要讓 Try...Catch 能巢狀使用你必須要記住上一次的 frame, 底下為一個簡單的範例, 這個程式碼是可以正常運行的 ```clike void stepA(int val) { if(val > 100) { Throw(10); } } int main() { CEXCEPTION_T e; Try { Try { stepA(101); printf("continue"); } Catch(e) { printf("inner error\n"); } stepA(102); } Catch(e) { printf("outer error\n"); } return 0; } /* print * inner error * outer error * / ``` 2. 為什麼 Try 的尾部需要 `if(1)`, Catch 的頭部需要 `else {}`? 函式庫支援以下兩種寫法: ```clike //syntax 1 Try one line Catch one line //syntax 2 Try { ... } Catch(e) { ... } ``` 如果像以下這樣寫就會出現錯誤: ```clike // only Try Try {} // only Catch Catch {} ``` 3. 為什麼要使用關鍵字 `volatile`? ref: - [CS360 Lecture notes -- Setjmp](http://web.eecs.utk.edu/~huangj/cs360/360/notes/Setjmp/lecture.html)