執行人: fewletter
專題解說影片
CONFIG_KASAN
, CONFIG_KASAN_GENERIC
, CONFIG_KASAN_SW_TAGS
, CONFIG_KASAN_HW_TAGS
這四個選項,第一個為是否要開啟 kasan,後面三個為開啟的選項。virtme-run --kdir . --mods=auto --qemu-opts 2048
可以增加虛擬環境的記憶體配置,所以記憶體空間不足不會有問題。為了探索 Linux VFS (virtual file system) 介面及檔案系統實作機制,我們從無到有撰寫一個運作於 Linux 核心模式中的精簡檔案系統,原始程式碼約一千餘行,支援基本的檔案和目錄處理,同時也考慮到權限和並行處理的議題。
參見 Issue #23
作法: 利用 user-mode-linux 或 virtme (第 7 週教材) 編譯 Linux v6.3 核心,並嘗試編譯 simplefs 及修正相關錯誤,隨後提交 pull request。
根據 Issue #23 的錯誤資訊可以知道錯誤的程式碼出現在 include/linux/fs.h ,而這段程式碼是在 Linux v6.3 版本才出現,在 6.3 版本以前皆為以下宣告
在 6.3 版本以後改成以下的宣告
參考 測試 Linux 核心的虛擬化環境 ,基本上就是按照教材上的步驟實作
使用 virtme 選取預設核心組態並編譯:
編譯完成後出現
接著啟動虛擬測試環境
此虛擬環境就是另一個版本的 Linux ,只不過命令方法從 $
變成 #
,然後來確認一下此環境是否為一開始設定的 6.3
測試能否從 github 直接複製整個專案到此環境
看起來不能直接從 github 複製專案到環境中,接著試試將已經複製到本地端的專案複製到此環境中
編譯此專案並重現錯誤
從上面的結果來看在 Linux v6.3 確實存在著會讓此專案產生 bug 的程式碼,接著就是著手修改程式碼。
在 simplefs/super.c 中有著這段程式碼如下
其中 USER_NS_REQUIRED()
在 simplefs/simplefs.h 被如此定義
所以此 bug 的原因就十分明顯了,此程式碼的定義 Linux 核心版本的分界點在 5.12,在 6.3 版本才會出現無法編譯過的問題。而 Linux 核心在 v6.2 到 v6.3 版本中間到底改了什麼,或許可以從以下程式碼窺探一二
從以上程式碼可知他只把 struct user_namespace *mnt_userns
改成 struct mnt_idmap *idmap
,而結構體 mnt_idmap
則可以在 linux/fs/mnt_idmapping.c 中找到定義,簡單來說它只是將原本的結構體 user_namespace
加上 reference counting 變成新的結構體。
在 simplefs.h 多定義一行程式碼
代表如果此專案如果要從 Linux v6.3 以上的核心編譯則需透過此定義執行以下程式碼。
改完程式碼後在虛擬環境中測試後發現 super.c
中這個bug 只是整個專案裡面的一部分,有很多會編譯的錯誤都出現在 inode.c
中。
在 inode.c 中所修改的程式碼都是跟 Linux 版本有關的程式碼,同時必須要注意到在 Linux v6.3 之後結構體 user_namespace
已經被包裝成結構體 mnt_idmap
,而下面則是在虛擬環境中的測試結果。
從上述結果可以看到已經沒有編譯錯誤。
TODO: 提交 pull request
根據 Issue #25 所說,上面程式碼發生錯誤,為了找出錯誤去看了 inode_init_owner
的函式的作用,在 Linux v6.3 中,inode_init_owner
的描述如下
從最後一行程式碼可以看出 inode->i_mode
該由 mode
這個參數初始化,而根據這個函式 simplefs_new_inode
的作用為透過 dir
和 mode
兩個參數去初始化新的 inode
,所以 inode_init_owner
最後一個參數應為 mode
。
ls -a
問題參見 2022 年報告,確認在 Linux v5.15+ 是否仍有相關問題,並嘗試排除。
ls -a
從上面兩個環境來看, ls -a
皆有出現 .
和 ..
,根據 2022年報告 , ls -a
的 bug 會出現在 Linux v5.11 的開發裝置和 Linux v5.18 的虛擬裝置上。
使用 QEMU 作為 Linux v5.18 虛擬環境也還是沒有發生 bug。
參考資料建構 User-Mode Linux 的實驗環境,開發紀錄-3
首先先取得 Linux v5.18 的程式碼
設定核心組態,特別是 ARCH=um
,之後的核心模組都需要用到這項指定 UML
在準備 UML.sh
前,先準備 root file system (簡稱 rootfs)
撰寫 init.sh
來作為 UML.sh
的啟動程序,內容如下
撰寫 UML.sh
來啟動 UML
啟動 UML
接著在 rootfs 中建構 User-Mode Linux 的實驗環境,以下的 kernel modules 是我們所需要編譯的核心,同樣記得 ARCH=um
,編譯完後到所指定的目錄,這裡是 lib/modules/5.18.0
,檢查是否有成功編譯。
順便連 ARCH=um
也一起考慮進去
接著準備就緒,試試能否在不同版本下編譯 simplefs,並且在 UML 中載入核心模組
首先不管是何種版本,都需要先在 Linux 核心程式碼內建構 GDB script,並且需要修正核心程式碼中的 .config
檔案,在此檔案其中需要修改 CONFIG_GDB_SCRIPTS=y
,也就是下面命令所執行的事,還有註解掉 CONFIG_DEBUG_INFO_NONE
和增加 CONFIG_DEBUG_INFO=y
。
但是在 Linux v5.18 中,.config
檔一直無法正確運作,即使已經改動上述幾項,使的在開啟 gdb 時,一直會出現無法找到 debug symbols。
所以在後面決定使用 Linux v6.3 來進行 GDB 追蹤程式碼。首先也是修改 .config
檔,在 Linux v6.3 中,需要修改的地方只有 CONFIG_GDB_SCRIPTS=y
,在建構 GDB script 後使用下面命令啟動 gdb。
接著在 gdb 中啟動 UML,卻發現仍然在 gdb 的終端中,非常不尋常。
接著根據 建構 User-Mode Linux 的實驗環境-搭配GDB進行核心追蹤和分析 的步驟準備 gdbinit
檔案,並且將啟動 gdb 的命令改成 gdb -q -x gdbinit
。
終於出現 UML 了。
在 2022年報告-Make and Mount a simplefs Filesystem 中提到 ls -a
的 bug 會出現在 host 端,重現此 bug 的步驟大概為下:
ls -a
是否有出現 .
和 ..
從以上結果來看 ls -a
在掛載檔案系統後,不管在 host 端還是在 test 目錄內都有出現 .
和 ..
,此 bug 或許已經不存在。
擴充 2022 年報告,改進其中關於 VFS 及 simplefs 實作的描述 (可運用 HackMD 的書本模式 分類展現)
透過 fill_super
初始化 Superblock
在掛載函式中會有一個 function pointer 的參數 fill_super
,會根據不同的檔案系統使用不同的初始化函式,在找到 Superblock 之後就會透過這個函式對 Superblock 進行初始化。
不同的檔案系統的 fill_super()
會有些許的不同,不過最主要都有對 Superblock 做初始化的功能,比如 ramfs 檔案系統:
首先在 Linux 中 mount 是根據不同掛載裝置而有不同的掛載函式,如同 VFS 內所提到三種函式,而在 simplefs 中是選擇使用 mount_bdev()
作為掛載函式,並且利用 simplefs_fill_super
作為初始化 superblock 的函式
至於卸載檔案系統則是將 superblock 的資訊給消除掉,主要會利用到 linux/fs/super.c 的 kill_block_super
simplefs 同時提供將 page cache 讀寫和從硬碟上將 block 從檔案系統中寫入至硬碟,而 block 中的資料可以同時包括 superblock, inode 和 bitmaps,而在 simplefs 中如同本章之前所提大約為 4 KB。
在了解方法之前,先了解結構體 extent
,結構體 extent
是在較新的檔案系統中才有的結構體,目的是為了解決處理大型檔案的問題,比如說在檔案系統如 bfs 就是直接對 block 進行操作,如以下程式碼
上述情況會有什麼問題,那就是在處理大型檔案比如說超過 10 MiB 的檔案,以一個單位只有 4 KiB 的 block 來處理,那勢必會耗費大量時間,但是透過結構體 extent
,一次會分配 8 個 block 來處理檔案不管是要寫入或是讀取,以減少每次都要對每個 block 處理的時間。
如何將 file system 中的資料映射到要儲存在硬碟上的 block,要知道在 simplefs 中運用了 superblock,inode 等結構體來記錄一個檔案系統的資訊,所以一開始使用下面兩個定義來取得 superblock 和 inode 的資訊
而為了知道檔案要映射到硬碟的哪個區域,simplefs 使用結構體 simplefs_extent
來管理硬碟的起始地址,長度,以及起始 block 的地址,sb_bread
則是透過 superblock 和 inode 來得知檔案的 block 的大小範圍,最後在找到 index
後,利用 iblock
來找出這個檔案在硬碟的 block 編號範圍。
最後則是靠至著 get_free_blocks
在硬碟中找尋空的位置並且透過 map_bh
將 buffer_head
映射到映射到硬碟上。
參見 WIP on page cache hooks for disaggregated fs
首先在 linux/fs.h 中可以找到結構體 address_space
,linux/mm_types.h 中可以找到結構體 page
,其中結構體 page
便是在描述資料在記憶體中的型態,而 page
也是在記憶體中的最小單位,下方則是 page
與 address_space
之間的關聯。
address_space *mapping
提供 page
的所在地址,index
則是提供偏移量(offset)。
至於結構體 address_space
則是讓 page
讀取或寫入檔案的重要結構體,而操作方式則是透過另外一個結構體 address_space_operations *a_ops
,其提供 dirty page 寫回硬碟或是從位址讀取 page
的方法。
首先先把檔案系統以虛擬環境的 Linux 版本編譯,這邊虛擬環境的版本為 6.3.0,並且需要注意的是由於要在 UML 中編譯,所以要將核心組態 ARCH=um
,然後以此版本建立檔案系統映像檔 make test.img
。
接著將整個 simplefs 編譯過的檔案和檔案系統全部複製到 rootfs (root file system) 當中,然後啟動 GDB 按下 r 後切到 UML ,在 UML 中載入模組,掛載檔案系統到 test 目錄中。
在另外一個視窗打上pkill -SIGUSR1 -o vmlinux
將 UML 切回 GDB。
首先一定要先在 GDB 中輸入 lx-symbols
,否則 GDB 無法進行 debug
設立斷點於 simplefs_get_block
, simplefs_write_begin
,simplefs_write_end
, simplefs_readahead
, simplefs_writepage
, simplefs_ext_search
當中,觀察其在建立檔案和修改檔案時的行為。
實驗修改檔案
切回 UML 後建立檔案於檔案系統,可以看到建立檔案於檔案系統中需要經過 simplefs_write_begin
開始寫入,並且結束於 simplefs_write_end
。
但是如果要修改檔案的話,在最後面就會多了一行 simplefs_writepage
,代表著檔案從 page cache 中將修改過後的檔案寫回實體硬碟上。
從這個簡單的實驗可以看到,在建立檔案時檔案資訊就會存在 page cache 中,而當我們要修改檔案時,檔案會寫入 page cache 而不是原本的硬碟上,最後在結束時,也就是在上方最後的 UML:/ #
時候,自行寫入硬碟中。
首先從 kmemleak 文件中知道, kmemleak 就像是一個檔案系統,它的檔案系統的型態為 debugfs ,將他開啟的方式首先要先從 .config 檔開始修改,而我原本想在本地端修改 .config 檔,後來為了保險起見,決定直接在虛擬化環境修改,而建構虛擬化環境則是參考 測試 Linux 核心的虛擬化環境。
.config 檔案是編譯核心的重要檔案,此檔案決定核心編譯時會有什麼功能,而核心本身也有提供腳本 scripts/kconfig/merge_config.sh .config .config-fragment
來修改 .config 檔,在此核心中會需要修改 CONFIG_DEBUG_INFO
和 CONFIG_DEBUG_KMEMLEAK
。
不同版本的 Linux .config 檔會有些許的不同
接著編譯所需要之核心環境
成功編譯完環境後會出現以下訊息,後面的 # 代表的是編譯次數。
利用 QEMU 作為虛擬環境有個缺點,也就是在外面的核心無法直接對此環境的版本編譯,在裝載著此虛擬環境的目錄中執行以下命令 ls .virtime_mods/lib/modules/
你會得到 0.0.0
,代表一定要進到此虛擬環境才能編譯核心。
從上面看到 simplefs 無法正常編譯,非常不尋常,因為在先前的章節 建立虛擬化測試環境 此專案是可以被編譯的。試著用 搭配 crash 進行核心偵錯 來看什麼地方出錯。
首先先將呼叫虛擬化環境的命令改成以下命令,--mods=auto
為的是能夠讓 lib/modules/
下的建構模組的版本為虛擬化環境的版本,--qemu-opts -qmp tcp:localhost:4444,server,nowait
是為了能夠連上 telnet 並將記憶體內容倒給映像檔然後讓 crash 來除錯。
在另一個終端輸入以下命令連到 telnet
會有以下畫面
接著輸入以下兩行命令,"file:vmcore.img"
這行為輸出的檔案名稱,最好將其命名為映像檔(img),因為 crash 主要是對映像檔偵錯。
再開另一個終端輸入以下指令,crash 會讀取你給的 vmcore.img
雖然沒有發生 kernel panic,但是發生了 oom (out of memory),所以從 crash 中查詢到底發生了什麼事造成 simplefs 無法編譯。
從上面錯誤訊息看到在 PID 98 時有看到有 oom 的發生,所以直接去查 PID 98 發生了什麼事。
其中一條 warning 說到 must be called with preemption disabled
,所以到 .config 檔案中修改 CONFIG_PREEMPT_NONE_BUILD=y
和 CONFIG_PREEMPT_NONE=y
,然後重新編譯核心環境跟之前的動作一樣。
重新進入虛擬化環境並且嘗試編譯 simplefs,還是失敗,測試結果如下。
既然結果顯示 error 跟 preemption 無關,再看一次 crash 的報表 kmemleak 也出現在上面,也就是說 kmemleak 也有可能導致在編譯 simplefs 時候,虛擬環境出現 oom 的狀況。
以下是 kmemleak 在 .config 檔中相關的設定,我首先先調整 CONFIG_DEBUG_KMEMLEAK_MEM_POOL_SIZE
的大小為 200,然後編譯核心環境,進入虛擬化環境編譯 simplefs ,結果還是失敗。
最後我調整 CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF=y
此命令為讓使用者在進入環境時, kmemleak 先不要啟動,然後編譯核心環境,得到以下畫面,原來我已經編譯 9 次環境了。
接著再試圖在環境編譯 simplefs 就成功了
但是此時卻無法將關閉的 kmemleak 開啟
所以從之前的錯誤可得出結論,虛擬環境配置的記憶體無法同時開啟 kmemleak 和編譯 simplefs,必須要配置更大的記憶體空間給虛擬環境。