Try   HackMD

2018q3 Homework2 (lab0)

contributed by < amikai >

tags: sysprog2018

請補上實驗環境
課程助教

==作業題目==

  • C Programming Lab
  • 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

GIT_HOOKS := .git/hooks/applied
$(GIT_HOOKS):
	@scripts/install-git-hooks
	@echo

可以在 scripts/pre-commit.hook 看見要先經過 clang-formatcppcheck 檢查通過才能會接受 commit message

另外 commit-msg.hook 裡是為了不讓你違反 How to Write a Git Commit Message 裡的規則

編譯程式

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.oqtest 都是真正會慘出的檔案

自動化測試

+ .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: testclean 並不是真正的檔案, 使用PHONY target 較不容易使人誤會, 也可以避免名稱衝突的問題 (這份 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 --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, 這裡先不做詳述

q_new

q new: Create a new, empty queue.

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.

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).

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).

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.

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.

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.

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

這些測試程式有趣的地方或許就是在你寫 mallocfree 時, 在以下某些錯誤時竟然可以抓出來:

我測試了三種情況:

  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
// 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

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, 在以下的情況, 這樣的技巧讓你能在 run time 決定 payload 的大小, 並且使用一個 payload_size 紀錄此 payload的長度

問: 為何不使用以下這種方式就好了

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( )

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 的大小

並且在每次配置完之後將 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:

延伸閱讀: C99 裡的 flexible array member

test_free( )

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

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_setupexception_cancel (請看以下) 這就讓我好奇了

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 的機制牽扯到了幾個神奇的函式 sigsetjmpsiglongjmp, 在看過 man page 之後了解到, 這一組函式是用來達到 nonlocal goto, 這種技巧也常常被拿來作例外處理的用途, 為了解釋個函式的用途, 我簡單寫了一支程式:

#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_setupexception_cancel 的機制就簡單許多了

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, 僅有記憶體錯誤的訊息

我在底下畫出了整個流程

ref:

  • man alarm
  • CSAPP Figure 8.26

CException 解析

在 github 上看到有人使用 setjmplongjmp 實作了一個小小的 Exception 函式庫叫做 CException - github

此函式庫主打輕量, 函式庫只有兩個檔案 (CException.h 和 CException.c), 還可以支援 single tasking 和 multi tasking

官方給出了一個兩個簡單的範例,說明使用此框架的前後,程式碼長什麼樣子:
使用前:

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

使用後:

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 實作

以下為節錄程式碼:

#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. 為什麼每做經過一次 TryCatch block 就要記住上一個 frame?

以下為省略過後的程式碼:

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

為了要讓 TryCatch 能巢狀使用你必須要記住上一次的 frame, 底下為一個簡單的範例, 這個程式碼是可以正常運行的

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
 * /
  1. 為什麼 Try 的尾部需要 if(1), Catch 的頭部需要
    else {}?

函式庫支援以下兩種寫法:

//syntax 1
Try
    one line
Catch 
    one line
    
//syntax 2
Try {
    ...
} Catch(e) {
    ...
}

如果像以下這樣寫就會出現錯誤:

// only Try
Try {}

// only Catch
Catch {}
  1. 為什麼要使用關鍵字 volatile?
    ref: