5 內存調試
很多系統的穩定性問題與內存相關, 特別是內存的越界訪問, 本節介紹幾種kernel原生的內存調試技術
5.1 Page_Owner
5.1.1 原理介紹
page_owner的目的是存儲頁面分配時的調用棧信息, 這樣我們就能知道每個一個頁面是由誰分配的.
要實現這個目的, 得回答3個問題: 怎么存、存哪里、何時存.
怎么存
怎么存就是說如何獲取調用棧信息. 這個好辦, kernel官方提供了save_stack_trace這個API, 我們只需要調用這個函數, 就能得到調用棧信息.
存哪里
得到調用棧信息后存儲在哪里呢? 既然這個信息是與每個頁面相關的, 那存儲在struct page結構體里面應該算合情合理. 但是page結構體是內存初始化階段創建的, 每個物理頁面都會分配一個page結構體; 我們僅需要針對已分配的頁面保存調用棧信息, 如果直接在page結構體里面為調用棧信息分配存儲空間, 會增加page的size, 進而會浪費內存(回頭查看page結構體介紹可知kernel為了縮小此結構體的size做了很多工作).
因此kernel將調用棧信息存儲在了struct page_ext中, page_ext結構體也是每個page對應一個, 除了避免內存浪費, mm/page_ext.c的注釋部分還介紹了其它引入page_ext的原因.struct page和struct page_ext沒有直接的指向關系, 它們都是通過頁面編號來索引到每個物理頁面對應的數據結構.
何時存
存儲的調用棧信息最好的時機當然是頁面分配的時候. page_owner.h定義了set_page_owner函數, 此函數會負責獲取調用棧信息並將信息存儲在page_ext中; 而page_alloc.c在prep_new_page函數中調用了set_page_owner.
更多資料
目前網上好像沒有太多對page_owner的說明.
官方的Documentation/vm/page_owner.txt里面有對如何使能&使用page_owner機制的簡介.
另外如果想了解代碼歷史, 也可以通過git log mm/page_owner.c看看都修改了哪些代碼.
5.1.2 示例
示例章節我們打算使能kernel的page_owner功能, 然后編寫一個內核模塊分配一個page, 然后查看調用棧信息.
enable page_owner
兩個條件:
編譯時使能CONFIG_PAGE_OWNER
uboot bootargs里面傳遞參數“page_owner=on”
編寫內核模塊示例代碼
https://gitlab.com/study-kernel/memory_management/tree/master/page_owner
運行結果
以下步驟在板子上完成
insmod page_owner_test.ko
cat /sys/kernel/debug/page_owner > page_owner_full.txt
以下步驟在PC上完成:
cd tools/vm
make page_owner_sort
grep -v ^PFN page_owner_full.txt > page_owner.txt
./page_owner_sort page_owner.txt sorted_page_owner.txt
然后查看sorted_page_owner.txt, 在里面搜索”do_init_module”, 就能看到如下信息:
Page allocated via order 0, mask 0x24200c0
PFN 635287 Block 310 type 0 Flags
[<bf84803d>] 0xbf84803d
[<c0009713>] do_one_initcall+0x9b/0x198
[<c00fb215>] do_init_module+0x4d/0x310
[<c00a116b>] load_module+0x16eb/0x1b80
[<c00a17c7>] SyS_finit_module+0x77/0x9c
[<c000ed21>] ret_fast_syscall+0x1/0x52
[<ffffffff>] 0xffffffff
如果你閱讀編寫的示例代碼, 可知我們特意編寫了一個分配函數”alloc_page_owner”, 希望能在調用棧里面看到該函數名, 可惜只能看到bf84803d這個地址. 原因是在獲取調用棧信息時, save_stack_trace函數會判斷地址是否為kernel_text_address, 如果是才會查找與地址對應的函數名, 所以無法顯示內核模塊中的函數名.
5.1.3 小節
page_owner功能可以被編譯進內核, 然后在需要的時候通過page_owner=on開關打開, 這一點還比較方便.
page_owner只能針對page級別跟蹤調用棧信息, 如果是slab分配, 能無能為力了.
另外通過我自己的實驗, 發現這個page_owner功能好像左右並不是很大, 暫時也不知道它對跟蹤調試內存錯誤有什么樣的幫助.
5.2 Kasan
5.2.1 簡介
Kasan 是Kernel Address Sanitizer 的縮寫,它是一個動態檢測內存錯誤的工具,主要功能是檢查內存越界訪問和使用已釋放的內存等問題。Kasan 集成在Linux 內核中,隨Linux 內核代碼一起發布,並由內核社區維護和發展
Kasan 可以追溯到LLVM 的sanitizers 項目(https://github.com/google/sanitizers), Andrey Ryabinin 借鑒了AddressSanitizer 的思想,並在Linux 內核中實現了Kernel Address Sanitizer。所以Kasan 也可以看成是用於內核空間的Address Sanitizer
5.2.2 使用
5.2.2.1 先決條件
GCC version
KASAN uses compile-time instrumentation for checking every memory access, 因此, 編譯內核的GCC版本需要>= 4.9.2
GCC 5.0 or later is required for detection of out-of-bounds accesses to stack or global variables.
Kernel version
Kasan 是內核的一部分,使用時需要重新配置、編譯並安裝內核。Kasan 在Linux 內核4.0 版本時被引入內核,當時只支持X86, 在4.4引入ARM64的支持.
Kernel config
- To enable KASAN configure kernel with :
- CONFIG_KASAN = y
- CONFIG_KASAN_OUTLINE and CONFIG_KASAN_INLINE 二選一
- Outline : produces smaller binary
- INLINE : is 1.1 - 2 times faster, it requires a GCC version 5.0 or later
- Currently KASAN works only with the SLUB memory allocator
- For better bug detection and nicer reporting, enable CONFIG_STACKTRACE
- To disable instrumentation for specific files or directories, add a line similar to the following to the respective kernel Makefile:
- For a single file (e.g. main.o):
KASAN_SANITIZE_main.o := n
- For all files in one directory:
KASAN_SANITIZE := n
5.2.2.2 測試
編譯內核
根據前述條件, 配置、編譯並啟動內核.
編譯測試程序
Linux 內核的源碼中已經包含了針對Kasan 的測試代碼,其位置在linux/lib/test_kasan.c
將其編譯為ko, 在裝載此模塊時, 會運行相應的測試函數.
錯誤報告解讀
test_kasan.c中有針對各種情形的測試函數, 例如kmalloc_oob_right模擬了內存越界的情況:申請了123 字節的空間,卻寫訪問第124 個字節的內容,則會造成越界訪問的問題。
當運行以上測試代碼的時候,在內核日志中會詳細打印以下內容:
http://lxr.free-electrons.com/source/Documentation/kasan.txt?v=4.4:1.1 Error reports
解析工具:To simplify reading the reports you can use our symbolizer script :
https://github.com/google/kasan/wiki#reports
該工具的作用是把”Error reports”中形如”funcname+offset”的信息轉換為”filename : line number”, 使用該工具時, 有幾點注意事項
該腳本使用正則表達式去匹配每一行, 以獲取funcname和offset, 因此輸出的”Error reports”要符合腳本規范, 具體可閱讀腳本代碼和查看上述wiki中的示例log
腳本里面用到了addr2line和nm, 這兩個需要用和編譯器相對應的版本. 腳本默認使用的是$PATH路徑下提供的addr2line和nm, 這可能不對應.
5.2.3 原理
5.2.3.1 影子區域
Kasan 的原理是利用“額外”的內存來標記那些可以被使用的內存的狀態。這些做標記的區域被稱為影子區域(shadow region)。了解Linux 內存管理的讀者知道,內存中的每個物理頁在內存中都會有一個struct page 這樣的結構體來表示,即每4KB 的頁需要40B 的結構體,大約1% 的內存用來表示內存本身。Kasan 與其類似但“浪費”更為嚴重,影子區域的比例是1:8,即總內存的九分之一會被“浪費”。
做標記的方法比較簡單,將可用內存按照8 子節的大小分組,如果每組中所有8 個字節都可以訪問,則影子內存中相應的地方用全零(0x00)表示;如果可用內存的前N(1 到7 范圍之間)個字節可用,則影子內存中相應的位置用N 表示;其它情況影子內存用負數表示該內存不可用,取值范圍為0xFA ~ 0xFF, 具體意義可查詢mm/kasan/kasan.h。
5.2.3.2 地址轉換
所謂地址轉換是指從實際訪問的內存地址到影子區域內存地址的轉換. ARM64體系架構中, 在內存虛擬地址空間, 分配了一段虛擬地址(VA_START ~ (VA_START + KASAN_SHADOW_SIZE))給影子區域, 當然, 這段虛擬地址最終都會分配相應的物理頁並建立頁表映射.
內核代碼提供了一個函數來做地址轉換:
//include/linux/kasan.h
static inline void *kasan_mem_to_shadow(const void *addr)
{
return (void *)((unsigned long)addr >> KASAN_SHADOW_SCALE_SHIFT)
+ KASAN_SHADOW_OFFSET;
}
轉換的邏輯很簡單, 這里KASAN_SHADOW_SCALE_SHIFT = 3, 首先把addr >> 3, 然后把得到的值作為索引從影子區域中找到對應的memory.
5.2.3.3 影子區域的初始化
影子區域的初始化函數名是kasan_init. 4.4的內核中, arch/arm64/mm/kasan_init.c 和arch/x86/mm/kasan_init_64.c. 閱讀初始化代碼, 能大致了解影子區域初始化的過程. 概況起來也就是分配物理頁幀(當然不可能通過伙伴系統, 此時內核的整個內存系統還沒起來, arm64中是通過memblock_region), 建立頁表映射.
git log -p arch/arm64/mm/kasan_init.c, 在提交歷史中能夠看到作者對初始化過程的一些描述.
At early boot stage the whole shadow region populated with just one physical page (kasan_zero_page). Later, this page reused as readonly zero shadow for some memory that KASan currently don't track (vmalloc). After mapping the physical memory, pages for shadow memory are allocated and mapped.
影子區域初始化完畢后, 接下來就是在內核分配與釋放的過程中, 對影子區域進行標記, 標記出哪些是被占用的內存, 哪些是空閑的內存.
在作者關於kasan的一次代碼提交信息中(https://lwn.net/Articles/611410/),我們可以看到作者對mm/page_alloc.c和mm/slab_common.c , mm/slub.c都做了修改, 用git log -p查看這些代碼的修改歷史, 可以知道在內存分配和釋放的API中, 都加入了對影子區域的操作. 這也說明不管是直接通過page_alloc分配整塊頁面, 或者通過slab分配小塊內存, kasan都能處理.
5.2.3.4 如何檢測非法訪問
Compile-time instrumentation used for checking memory accesses. Compiler inserts function calls (__asan_load*(addr), __asan_store*(addr)) before each memory access of size 1, 2, 4, 8 or 16. These functions check whether memory access is valid or not by checking corresponding shadow memory.
GCC 5.0 has possibility to perform inline instrumentation. Instead of making function calls GCC directly inserts the code to check the shadow memory. This option significantly enlarges kernel but it gives x1.1-x2 performance boost over outline instrumented kernel.
另外在git log -p arch/arm64/mm/kasan_init.c的提交信息中, 也發現了這樣一段話:
Functions like memset/memmove/memcpy do a lot of memory accesses.
If bad pointer passed to one of these function it is importantto catch this. Compiler's instrumentation cannot do this sincethese functions are written in assembly.
KASan replaces memory functions with manually instrumented variants.
Original functions declared as weak symbols so strong definitionsin mm/kasan/kasan.c could replace them. Original functions have aliaseswith '__' prefix in name, so we could call non-instrumented variant
if needed.
Some files built without kasan instrumentation (e.g. mm/slub.c).
Original mem* function replaced (via #define) with prefixed variantsto disable memory access checks for such files.
5.2.3.5 歷史信息
請參見作者原話: https://lwn.net/Articles/611410/ : Changes since v1: …
5.2.3.6 優劣對比
以下內容摘自作者原話: https://lwn.net/Articles/611410/
A lot of people asked about how kasan is different from other debuggin features, so here is a short comparison:
KMEMCHECK: - KASan can do almost everything that kmemcheck can. KASan uses compile-time instrumentation, which makes it significantly faster than kmemcheck. The only advantage of kmemcheck over KASan is detection of unitialized memory reads.
DEBUG_PAGEALLOC: - KASan is slower than DEBUG_PAGEALLOC, but KASan works on sub-page granularity level, so it able to find more bugs
SLUB_DEBUG (poisoning, redzones): - SLUB_DEBUG has lower overhead than KASan.
- SLUB_DEBUG in most cases are not able to detect bad reads, KASan able to detect both reads and writes.
- In some cases (e.g. redzone overwritten) SLUB_DEBUG detect bugs only on allocation/freeing of object. KASan catch bugs right before it will happen, so we always know exact place of first bad read/write.
5.2.3.7 Reference link
https://www.ibm.com/developerworks/cn/linux/1608_tengr_kasan/index.html
https://lwn.net/Articles/611410/
http://lxr.free-electrons.com/source/Documentation/kasan.txt?v=4.4
https://github.com/google/kasan/wiki
5.3 Asan
5.3.1 基本原理
Asan的原理與Kasan類似, 分為兩部分:
a run-time library, 用於替換默認的malloc/free函數, 以便在分配和釋放內存時對shadow區域進行標記
編譯器, 用於在內存訪問代碼前加入檢測代碼.
但是從Kasan所描述的原理來看, 它貌似只能檢測訪問未分配的內存這種異常.
針對內存越界的檢測, Asan采用了另一種方式, 就是在被檢測內存的前后加入一些reserve的內存, 並將這些內存poison. 這樣當出現越界訪問時就能檢測到了.
關於Asan其原理的詳細說明, 參考: https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm
5.3.2 使用條件
LLVM >= 3.1 或GCC >= 4.8 或Android >= 4.2
5.3.3 Asan可檢測的錯誤類型
AddressSanitizer (aka ASan) is a memory error detector for C/C++. It finds:
- Use after free (dangling pointer dereference)
- Heap buffer overflow
- Stack buffer overflow
- Global buffer overflow
- Use after return
- Use after scope
- Initialization order bugs
- Memory leaks
5.3.4 Asan與其它類似工具對比
https://github.com/google/sanitizers/wiki/AddressSanitizerComparisonOfMemoryTools
5.3.5 在Linux上使用Asan
Clang : https://github.com/google/sanitizers/wiki/AddressSanitizer#using-addresssanitizer
GCC : 暫時未找到好的link
5.3.6 在Android上使用Asan
基於NDK使用Asan : https://github.com/google/sanitizers/wiki/AddressSanitizerOnAndroid
基於Android系統使用Asan : https://source.android.com/devices/tech/debug/asan.html
5.3.7 調整Asan輸出的調用棧
https://github.com/google/sanitizers/wiki/AddressSanitizerCallStack