contributed by < amikai
>
sysprog2018
請補上實驗環境
課程助教
此 Makefile 可以大略由 3 個功能所組成:
可以在 scripts/pre-commit.hook
看見要先經過 clang-format
和 cppcheck
檢查通過才能會接受 commit message
另外 commit-msg.hook
裡是為了不讓你違反 How to Write a Git Commit Message 裡的規則
為了編譯出需要的程式而撰寫的相依性及動作,此處的 queue.o
和 qtest
都是真正會慘出的檔案
使用 make test
之後是自動化評分的系統, make clean
則是清空編譯後所產生的檔案
在這裡的兩個target
: test
和 clean
並不是真正的檔案, 使用PHONY target 較不容易使人誤會, 也可以避免名稱衝突的問題 (這份 Makefile 只適用於沒有一個檔案叫做 clean
的情況下能正常使用)
先將 queue 的 structure 補齊,要 commit 的時候 cppcehck
產生以下錯誤:
我只更動了 queue.h
觸發了這個, 仔細觀察 scripts/pre-commit.hook
的寫法: 對整個目錄進行 cpp_check
原本的程式 回傳 p
指標的位址的值, cpp_check
可能無法知道到 p
指向的地方的 life time
再回傳之後不會消失, 因為 new_block
所指向的記憶體空間是在 heap
中, 所以本來的程式並沒有什麼問題, 為了避開這個錯誤我就在行為不更改的情況下進行更改
這裡其實有涉及到一個神奇的東西叫做 Arrays of Length Zero, 這裡先不做詳述
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).
使用 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: 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.
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.
這些測試程式有趣的地方或許就是在你寫 malloc
和 free
時, 在以下某些錯誤時竟然可以抓出來:
我測試了三種情況:
q_free
寫造成 double free errorq_free
裡沒有將空間完全歸還:q_free
裡釋放 NULL 指標:故意將 q_free
寫造成 double free error:
故意在 q_free
裡沒有將空間完全歸還:
故意在 q_free
裡釋放 NULL 指標:
仔細觀察可以發現, 其實在 harness.h
使用 macro 將 malloc
替換為 test_malloc
, 將 free
替換為 test_free
可以發現 CMU 用了雙向鍊結串列來維護 heap, 並且以此來測試你的程式, 以下比較神奇的是 unsigned char payload[0]
, 這其實是 gcc 所提供的 Arrays of Length Zero, 在以下的情況, 這樣的技巧讓你能在 run time 決定 payload
的大小, 並且使用一個 payload_size
紀錄此 payload的長度
問: 為何不使用以下這種方式就好了
每次需要使用時在對 payload
這裡作 malloc
的動作
從 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
其實就是將此雙向鍊結串列刪除一個 element 而已
跟 test_malloc
類似, noallocate_mode
只有在 qtest.c
裡的 do_reverse
有動到, 在測試 q_reverse
的時候這個值被設為 true, 應該只是為了測試 q_reverse
所寫的, 因為在作這個動作的時候是不需要使用 free
的
在 find_head
這邊其實本意只是要取得 block_ele_t
裡的 header 欄位, L8-L22 之間只要 cautious_mode
為 true ,會作做個一個檢查, 就是維護的鍊結串列裡的位置有沒有參數傳近來的指標, 如果沒有的話就代表傳近來的指標所指向的地方, 根本就沒有配置 heap 記憶體
在每一個測試函式裡在開始真正使用自己寫的 queue
之前和之後呼叫 exception_setup
和 exception_cancel
(請看以下) 這就讓我好奇了
exception 的機制牽扯到了幾個神奇的函式 sigsetjmp
和 siglongjmp
, 在看過 man page 之後了解到, 這一組函式是用來達到 nonlocal goto
, 這種技巧也常常被拿來作例外處理的用途, 為了解釋個函式的用途, 我簡單寫了一支程式:
一開始 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
這時候在看 exception_setup
和 exception_cancel
的機制就簡單許多了
背景知識:
unsigned int alarm(unsigned int seconds)
: 會在alarm
呼叫後 second
秒會發出 SIGALRM
signal, ex: alarm(3) 就是在 3 秒後發出 SIGALRM
signal, 比較特殊的是 alarm(0)
, 它會取消調所有 pending 的 alarmSIGSEGV
signal 會在不合法的記憶體存 取情況 (segmentation fault) 下發出總結:
在觀察程式的用途後發現, 這裡的 exception
設計是為了:
詳細過程:
exception_setup
使用有兩種情況:
參數為 true
, 代表有時間限制 (若沒在時間限制內會發出 TLE 訊息), 以及記憶體錯誤的訊息, exception_setup
使用 alarm(1) 會發出在 1 秒後發出 SIGALRM
signal, 若在 1 秒內沒呼叫 exception_cancel
(此函式裡有 alarm(0)
裡取消所有 pending alarm) 則會跳到相對應 handler 裡
參數為 fase
, 僅有記憶體錯誤的訊息
我在底下畫出了整個流程
ref:
man alarm
在 github 上看到有人使用 setjmp
和 longjmp
實作了一個小小的 Exception 函式庫叫做 CException - github
此函式庫主打輕量, 函式庫只有兩個檔案 (CException.h 和 CException.c), 還可以支援 single tasking 和 multi tasking
官方給出了一個兩個簡單的範例,說明使用此框架的前後,程式碼長什麼樣子:
使用前:
使用後:
以下為節錄程式碼:
以下為省略過後的程式碼:
為了要讓 Try…Catch 能巢狀使用你必須要記住上一次的 frame, 底下為一個簡單的範例, 這個程式碼是可以正常運行的
if(1)
, Catch 的頭部需要else {}
?函式庫支援以下兩種寫法:
如果像以下這樣寫就會出現錯誤:
volatile
?