contributed by <Eddie-064
>
git fetch
取得最新更新:
git remote add upstream https://github.com/sysprog21/lab0-c
git fetch upstream
master
分支下git rebase upstream/master
git push -f
q_free
函式的說明為 Free all storage used by queue
,透過 list_for_each_entry_safe
刪除佇列的每個節點後,要再釋放 head
空間。
不懂為什麼只有 safe 需要初始化。
有一個很奇怪的地方,透過 valgrind 跑 free 指令的時候會出現錯誤:
但直接跑又不會出問題:
q_insert_head
, q_insert_tail
在處理字串複製時,需要注意字串是否為 NULL
、 有沒有包含 \0
字元,以及記憶體有沒有分配成功。
參考〈 你所不知道的C語言:指標篇 〉,教材中談到 strcpy
與 strdup
差異,strcpy
可能會發生 buffer overflow ,而 strncpy
可以避免這個狀況發生,而 strdup
直接回傳字串的副本,使用起來更加精簡。
目前還有下列錯誤訊息需要除錯:
進入 lldb
debugger 模式後,照著 trace-11
步驟進行測試,在執行 ih gerbil 20
出現以下錯誤:
Stop reason: signal SIGSEGV: address not mapped to object (fault address: 0x0)
然後發現其實 qtest
有跳出提示:
WARNING: Malloc returning NULL
也就是在分配 element_t
時回傳了 NULL
,此時應回傳 false
。
重新運行後 qtest
出現以下錯誤提示:
ERROR: Failed to save copy of string in queue
而佇列的最後一個節點變成 (null)
,再看到 qtest.c
中:
雖然題目沒有特別要求空字串的處理,但從這邊的測試可以看出佇列中不能包含有空字串的節點。
調整後如下。
q_remove_head
, q_remove_tail
題目要求:
考慮使用 strncpy
或 strndup
,參考 strncpy man page ,裡面提到 「If there is no null byte among the first n bytes of src, the string placed in dest will not be null-terminated.」,因此需要開發者自己檢查 null terminator
。
而查閱 strndup man page 後,發現與 strncpy
不同,即使字串大於 n 也會在結尾端補上 null terminator
,所以實際大小應該是 n + 1。
後來發現 sp
應該是原本就已經分配好 buffersize
的空間,所以這邊適合使用 strncpy
實作。
另外也可使用 strnlen 替代 strlen
,可節省計算超過 buffersize
的額外字串。
q_delete_mid
使用快慢指標實作,一開始實作時快慢指標初始值皆設為 head
,如果佇列只有 3 個節點,慢指標第一個節點就會停下,在第 ⎣n/2⎦ - 1 個節點,主要是因為快指標會先確認下下一個是不是終點造成的特例,改成 head->next
可以避免這個特例發生。
q_delete_dup
原本在思考要如何比較完整字串,後來看了 trace-06
的測資只有針對一個字節測試,因此實作上可以將字節轉成 ASCII 的數字形態,透過 hash table 的方式做比較,來移除重複的節點。
在跑 trace-06
的時候遇到 segmentation fault ,參考 README.md
後看到能用 debug -a
分析 core dump,實際操作後發現透過 gdb 工具可以還原 error 發生當下,不僅能夠使用單步查看執行過的每一行(call hierachy),還能透過 p
查看當下變數的數值。
q_swap
可以透過 list_move
完成
q_reverse
可以透過 list_for_each_safe
與 list_move
完成
q_reverseK
使用 list_cut_position
搭配 list_splice_tail
實作
q_sort
實作 merge sort 的時候遇到以下錯誤:
後來發現原因是因為跑 divide 後,尾巴的節點指向錯誤的節點造成迴圈。
目前雖然在少量的資料可以正確排序,但在 trace-14-perf
測試中出現以下錯誤:
錯誤發生在 qtest.c
檔案內,可能是我排序的時候哪裡出問題間接造成影響。中斷在以下位置:
原來 0xdeadbeef
有特別的涵義,參考 Hexspeak。
debug 後,這個也是排序邏輯問題導致,就是不當的節點連結,當原本排序 6 個節點排序完只剩下 4 個節點,後面 qtest 測試就出現以上錯誤。奇怪的是我後面有重新確認節點的連接,不懂為什麼會有節點指向 0xdeadbeef
,這個問題在 Valgrind 和 Sanitizer 都只能等到 qtest 的 628 行才發現,還沒找到發生問題的根本原因。(考慮使用其他 sanitizers 做實驗)
Commit 9e2b8e8
q_ascend
, q_descend
使用 list_for_each_entry_safe
疊代存取兩個節點進行比較,實作時遇到以下錯誤:
仔細檢查後發現 list_for_each_entry_safe
的迴圈條件是 &entry->list != head
,而 safe
是透過 list_entry
推算出下一個節點的起始位置,list_entry
實際上是 container_of
的重新命名,在解讀 container_of
的運作原理時參考到 Linux 核心原始程式碼巨集: container_of,裡面提到幾個我沒留意到的細節:
container_of
中包含typeof
屬於 GNU extension。在使用-pedantic
或某些編譯器選項時,如果程式碼用到 C89/C90 以外的特徵會跳出警告訊息,__extension__ 可以避免編譯器跳出警告,除此之外沒有其他影響。container_of(ptr, type, member)
當中的const __typeof__(((type *) 0)->member) *(__pmember) = (ptr);
其實就是在檢查ptr
是不是 member 型別,但並不會檢查__pmember - offsetof(type, member)
輸出後是不是 type 型別。
所以如果下一個節點指向 head ,list_entry 回傳的只是減去 offsetof 後推算的地址,隨意操作可能造成不可預期行為。
在除錯的過程,因為想觀察運行中的行為,所以設中斷點進行單步執行,但 qtest
程式碼中包含有超時發出 SIGALRM
的機制,process handle SIGALRM --stop false --notify false --pass false
指令可以告訴 lldb
忽略這個 signal,gdb 的話 handle SIGALRM ignore
即可。
在跑 trace-06
測試失敗,發現是因為排序順序錯誤,q_ascend
是移除所有 value 大於後面節點的節點,從 head 開始檢查需要 backtracking 返回確認有沒有節點的 value 更大,而從 tail 往回就只要每個節點都大於等於下一個節點就好。相當於記錄最小值,只要大於就刪除節點。
後來的修正如下:
q_merge
實作的過程不知為什麼 merge 一直卡在 list_for_each
裡面出不來?
最後發現是因為
list_entry
取錯記憶體位置,因為使用list_for_each
並不會 dereference 所以沒造成 segment fault,變成無窮迴圈了。
測試命令為:
進入除錯模式查看 merge_queue
,發現 list_head * merge_queue = {prev:0x0000555555569110, next:0x00005555555654a0}
,重新查看 q_new
初始化的 adress 及比對 q_merge
中看到的 head address ,發現到了 q_merge
,merge_queue 的 next 居然指向其他某個不明的位置,而 prev
和在 q_new
看到的一樣,仍然是指向自己。
後來往 qtest.c
查看才發現這個 head 是 queue_chain_t
的成員,而不是 queue_contex_t
的成員,所以上面的 chain
指向的位置是一個未知的位子,dereference 會造成不可預期的結果。(使用 container_of
要小心,不要存取到錯誤的位置,即使沒造成 segment fault,也可能會像這種案例進入無窮迴圈)
先以 q_insert_head
為 use case 了解實作的脈絡。
首先看到 qtest.c
->queue_insert
->is_insert_head_const
->test_const
->do_it
,在 do_it
這個函式中實作了 dutect
的 leakage detection test
測試程式,以下為個步驟實作細節:
prepare_input
dutect
實作一樣隨機將一部分的 input_data
清空,其餘的 input_data
為隨機生成。random_string
是原實作中沒用到相同或對應的數據,一樣隨機生成。
input_data
每個測試單位的 chunk size
為 2 byterandom_string
每個測試字串長度為 8 bytemeasure
q_insert_head
前,從程式碼的流程來看,先是隨機生成 s
字串、新增佇列,接著:因為 input_data
是前面隨機生成的,所以這邊會取得一個 16 bits 的隨機數字,帶入以下 macro:
會執行 n 次 q_insert_head
,跑完之後才是測量一次的 q_insert_head
,最後檢查測量前後的 q_size
。這裡不太懂的地方是為什麼在測量之前要先執行 n 次的 q_insert_head
?
還有就是這個函式測試數據為什麼要丟掉頭跟尾巴?
for (size_t i = DROP_SIZE; i < N_MEASURES - DROP_SIZE; i++)
最後程式碼整個看下來,DROP_SIZE
所影響的數值應該只有 input_data
是從中間開始取,猜測可能和亂數的亂度有關?
3. differentiate
和論文的實作一樣,計算 execution time
4. update_statistics
這裡和論文的實作中有兩個不同點。a. 原實作中丟棄掉前 10 個樣本,lab0
沒有。b. 原實作中還有計算 percentile 及 cropped 部分。
5. report
這裡會計算 ,最後比較 threshold
小於等於 10 才有可能是 constant time
。
論文中提到:
We discard measurements that are larger than the p percentile, for various values of p. The rationale here is to test a restricted distribution range, especially the lower cycle count tail. The upper tail may be more influenced by data-independent noise.
以及註腳:
The exact values for p follow an inverse exponential trend
論文實作的 percentile 函式會先將 execu_time 由小排到大,而根據上面註腳 p 為 inverse exponential,所以大部分 t 值的 threshold 會在執行時間較大的位置,疑惑的是這樣不是就等於包含了較多 data-independent noise?
原本覺得 noise 越多,越可能造成 t 超過臨界值,但其實考慮 公式,當 noise 增加會導致 與 增加,不過分子還是分母增加的多有待分析。
問了 AI 後得到以下解釋:
這張圖顯示了整體執行時間的分布,並標出了幾個常用的 percentile threshold(90%、95%、98%、99%):
這些虛線(threshold)都位於 右側的 upper tail。實際上 dudect 會 丟掉右側超過這些 threshold 的資料,只保留左側的 sample 來進行統計分析。這樣做的目的是避免受 upper tail 中 noisy outlier 的影響,從而提高 t-test 的穩定性與可信度。
疑惑的點還有 t-test 設定的臨界值 4.5, 10, 500 從何而來?對應到的 significance level 又是多少?
接著參考論文實作的 percentile 加入 lab0,還是沒辦法通過 constant time,實驗之後發現 measure
函式中:
如果隨機產生的節點小於 500,可以通過,不太懂為什麼節點長度會影響結果。
後來想想,之所以會有這個結果可能是在測量之前大量的節點添加運算導致的 noise,考慮將 放在 pre-processing 先處理完成,以此避免過多 noise。
看了get_random_string
函式實作後,發現是在已經生成的 150 個隨機字串中,按照每次呼叫順序取得。而每次疊代採樣之前都會先取得測量要用的字串,然後生成節點,呼叫get_random_string
函式*(uint16_t *) (input_data + i * CHUNK_SIZE) % 10000)
次,這樣下一次取得測量的字串時會依賴於前一次的random_string_iter
。
根據中央極限定理來自相同的母體的樣本平均值分佈會與常態分佈相似,隨著樣本數量越大越區近於標準常態分佈。
下圖來自 AI 的範例,當 t 值超過臨界值,則可以說兩樣本有顯著差異,可能來自不同母體,也就可能包含 timing leakage。
下圖也來自 AI,視覺化了 t 分佈和樣本平均數分佈範例,幫助理解。
以下為執行 q_insert_tail
simulation 的執行時間分佈圖
案資料生成順序排列
把數量縮小到一組 N_MEASURES
,再將相鄰相同 class 的加上虛線,發現 fix class 只要出現第二次執行時間都會大幅下降,好神奇。
q_remove_head
simulation 的執行時間分佈圖
按資料生成順序排列
shuffle
command參考作業要求及隱藏的傅立葉轉換實作 Fisher–Yates shuffle。
熵(entropy)的定義是某個隨機事件平均每個結果發生時所能傳達出的資訊量。
例如:一個隨機字串,平均每個 char 所包含的資訊量就是熵。
所以當熵越小,代表資料能夠壓縮的比率越高。
lab0 中實作了 entropy 計算方式,運用定點數方式實作,實際計算的 entropy
和 ent
有些出入,待進一步分析原因。
程式碼中是運用定點數技巧實作,加上沒有補償誤差,entropy 運算完後取百分比才是最後的結果,也就是除以最大值 8(每 byte 最大的 entropy)。
卡方檢定可以用於檢測觀察值是否符合理論上的分佈。
令 為 shuffle 的每個排列發生機率相同,為 Uniform distribution。
對立假說就是「shuffle 所有可能排列中存在至少一個發生機率不同」。
參考 卡方分布表:
自由度為 ,, 落在小於 0.01 的 p value,假設顯著水準 為 0.05,因為 因此拒絕 ,這次實作應該有很大問題。
修改亂數取得方式:
, p value 介於 0.9 與 0.1 之間,大於 ,因此無法拒絕虛無假設 。
目前參考的卡方分布表沒有列出 p value 0.9 至 0.1 的詳細表格,待查找其他來源。