contributed by < cjwind
>
為了後續的實驗能夠順利,建議 Linux 核心升級到 v4.15 以上
做個 element data 是 C string 的 queue,支援 FIFO 跟 LIFO。
queue_t *
是 NULL
queue_t *
指向 valid structure,其中 list_ele_t *head
為 NULL
queue operations:
queue_t *
被 free 了remove_head
overflow實作過程中在 q_remove_head()
犯了很蠢的 sp[bufsize] = '\0';
,make test
得到錯誤訊息 ERROR: copying of string in remove_head overflowed destination buffer.
trace qtest.c
的 do_remove_head()
了解它如何 check overflow:
malloc
一個 size 為 string length + padding (1024) + 1
的 buffer removes
,存放 q_remove_head()
的 string。'\0'
,其餘為 'X'
。q_remove_head()
後檢查 removes[string_length + 1]
是否還是 'X'
,否則表示 overflow。以這個檢查方式來說,如果 q_remove_head()
實作寫成 sp[bufzie + 1] = '\0'
,雖然是有問題的程式碼,卻可以通過檢查。針對這個問題的 PR。
請提交 pull request 並參與其後 GitHub 討論
實作過程用到忘記檢查的 pointer,qtest
會顯示錯誤訊息 Segmentation fault occurred. You dereferenced a NULL or invalid
並且繼續執行,通常 segmentation fault 會直接 crash 的。
qtest.c
的 queue_init()
用 signal()
設定 SIGSEGV
以及 SIGALRM
的 signal handler。signal handler 裡 call trigger_exception()
。
signal()
在不同平台甚至不同 Linux 版本有不同行為。基於 portability,manual 建議 signal()
只用在將 signal handler 設為 default(SIG_DFL
)或忽略(SIG_IGN
),需要改變 signal handler 的情境中要用 sigaction()
。
sigsetjmp()
& siglongjmp()
The functions described on this page are used for performing "nonlocal gotos": transferring execution from one function to a predetermined location in another function. The setjmp() function dynamically establishes the target to which control will later be transferred, and longjmp() performs the transfer of execution.
setjmp(jmp_buf env)
會在 env
裡儲存 calling 環境的資訊,包含 stack pointer、instruction pointer、signal mask 以及其他 register 的內容。longjmp(jmp_buf env, int val)
使用 env
儲存的資訊來回到 call setjmp()
的地方繼續執行。
call setjmp()
時它會 return 0,而 call longjmp()
後程式會以「看起來像從 setjmp()
return」的方式繼續執行,此時 return 值會是 call longjmp()
指定的 val
,如果不小心指定成 0 還會很貼心的幫你 return 1。
setjmp()
在不同平台上對於是否存 signal mask 有不同行為。在 qtest
這種 error handling 的使用情境上,用 sigsetjmp()
及 siglongjmp()
來保存及恢復 signal mask。
qtest
的 exception handling 機制是在 call queue operation 前先 call exception_setup()
,之後再 call exception_cancel()
清理 exception handling 相關變數。exception_setup()
裡使用 sigsetjmp()
設定 nonlocal goto 回到的點,而自訂的 signal handler call 的 trigger_exception()
裡會 call siglongjmp()
讓執行流程回到 trigger_exception()
,最後再回到原本 call queue operation 前,以 q_free()
為例:
exception_setup()
在正常 call sigsetjmp()
時 return true,由 siglongjmp()
進入時 return false。像上面進行檢查就能在發生 SIGSEGV
以及 SIGALRM
signal 後仍然繼續正常執行。
Android 底層有個名為 debuggerd
的特殊程式,可用以捕捉其他程式運作過程中的 SIGSEGV,可參見:
搭配對照,思考這類例外處理機制對於發展一個完整的 framework 的作用
test_malloc()
& test_free()
在 free queue 尚未實作完成時,測試會印出 ERROR: Freed queue, but X blocks are still allocated
訊息。
harness.h
裡用 marco define 被測試程式的 malloc
為 test_malloc
、free
為 test_free
,所以在 queue.c
裡寫 malloc()
會 call 到 test_malloc()
。它使用 INTERNAL
決定是測試程式還是被測試程式:
qtest.c
及 harness.c
有 define INTERNAL
。
用 test_malloc()
把 malloc()
包起來,在測試時可以控制 malloc()
的行為,就能模擬不同情境進行測試。有一點像是將被測試程式跟真正記憶體操作隔開,在中間加了一層中介。
test_malloc()
會 allocate size + sizeof(block_ele_t) + sizeof(size_t)
大小的記憶體,size 是原本打算 allocate 的大小,由 block_ele_t *
指向這塊記憶體。
block_ele_t
是個 doubly linked list 的 element,新 allocate 的 block 會接在這個 list 的前面。
可以看到其中記錄 payload size,是原本要 allocate 的記憶體大小。還有 magic header 以及 test_malloc()
實作中會存的 magic footer,因為要存 magic footer 而在 allocate 時多 sizeof(size_t)
。
記憶中 structure 內 field 順序在 memory 中是照順序排下來的,用 gdb 看看:
magic header 跟 magic footer 是用來檢查 allocated memory 有沒有被搞壞。
test_free(void *p)
會檢查 *p
是否為 NULL 以及要 free 的 memory 是否依然合法(沒有被弄壞),沒問題的話會將對應的 blok_ele_t
從 list 移除,最後 free memory。
一開始提到 qtest
能夠知道是否有 memory block 沒 free,是透過 test_malloc()
及 test_free()
記錄 allocate memory block 的數量來檢查的。
debuggerd
Native crash 是 C/C++ 層面的 crash。
Debugging Native Android Platform Code 提到一個 dynamically linked 的執行檔開始執行時會 register 一些 signal handler,crash 時 signal、把 crash dump 寫到 logcat(大概是某種 logging 機制)以及稱為 tombstone(墓碑XD)的檔案。tombstone 包含 crashed process 的各種額外資訊,像是所有 thread 的 stack trace、full mempry map 以及開啟的 file descriptor。
開發人員可以透過 crash dump 跟 tombstone 分析 crash 原因。
debuggerd
跟 debuggerd64
是 Android 8.0 前處理 crash 的 daemon。Android 8.0 及之後版本,會依需要產生 crash_dump32
跟 crsah_dump64
。
Diagnosing Native Crashes 提到各種 crash 情況。除了常見的 SIGABT
跟 SIGSEGV
,還有安全性檢查(像是 C library 檢查 buffer、有沒有 call 不能 call 的 system call、是否發生 stack buffer overflow),以及 fd 有沒有被誤用(use-after-close、double-close 等)都有各自的 signal。看起來是使用 signal 跟 crash 機制對程式進行更多檢查,來補充語言本身沒有直接提供的功能?
compiler -fstack-protector
選項可以檢查 function stack 是否 overflow。它透過在 function prologue 加些用來檢查的 data,只要檢查那些 data 有沒有改變就能知道是否有 buffer overflow。
Emit extra code to check for buffer overflows, such as stack smashing attacks. This is done by adding a guard variable to functions with vulnerable objects.
這樣的機制可以讓開發者在程式 crash 或者 hang 住的時候擁有更多資訊,更容易找出問題所在。
debuggerd
可以用來取得正在執行的 process 的 stack trace 及 tombstone。這表示它可以拿到 hang 住(例如 deadlock 造成的)的 process 的 stack trace?
有點不記得以前在 Windows 上 application hang 住是不是每次都拿得到 full dump(至少包含所有 thread call stack 的 dump)。application 本身有做類似 crash dump 記錄的機制,但 full dump 要用別的方式取得(都是 QA 生好的…不清楚怎麼生的…)
有時候可以從 full dump 分析出 deadlock(好像有即使有 full dump 還是什麼都看不出來的情況…也不知道是不會看還是真的沒資訊…)。沒有 full dump 的時候…呃…只能從操作步驟、log、hang 住時整個系統的反應猜測可能有關的 code,再從 code 去看哪裡有問題,非常吃靈感的一件事。
後來 application porting 到 Linux 上,crash 只有基本的 core dump,遇到 hang 住或沒有 core dump 的時候…就是場災難的通靈大會。(雖然 debuggerd
是在 Android 上,但為什麼沒有早點知道有這種東西…)
debugger_handler.cpp 的 debuggerd_signal_handler()
用 clone()
生了 child process 執行 debuggerd_dispatch_pseudothread()
,裡面再用 __fork()
生了 child process 去 execle()
crash_dump32
或 crash_dump64
。debuggerd_dispatch_pseudothread()
之後會再生一隻 child process 給 crash_dump
用。
中間有生 process 跟 pipe 的操作,沒有很懂生 process 的目的跟各個 pipe 連到哪裡去了。
debuggerd_init()
會 register signal handler,但除了 debuggerd/crasher/crasher.cpp
找不到 call 它的地方。
crash_dump
會 ptrace
parent process、暫停 parent process 所有 thread。從 crash_dump.cpp 來看,中斷 target process 後會從 pipe 讀 crash info,接著從 tombstoned
拿到 output fd 再寫 crash info 到 output fd。
debuggerd
command 主要做事的 function 是 debuggerd_trigger_dump()
,看起來先用 socket 連到 tombstoned
,接著送 signal 給 command line 指定的 pid 的 process,最後把從 pipe read 到的東西寫出去。
可撰寫 GDB script,用以自動化特定操作
Linux 核心設計 2019