内存管理(5): 内存调试


5       内存调试

很多系统的稳定性问题与内存相关, 特别是内存的越界访问, 本节介绍几种kernel原生的内存调试技术

5.1             Page_Owner

5.1.1         原理介绍

page_owner的目的是存储页面分配时的调用栈信息, 这样我们就能知道每个一个页面是由谁分配的.

 

要实现这个目的, 得回答3个问题: 怎么存、存哪里、何时存.

 

怎么存

怎么存就是说如何获取调用栈信息. 这个好办, kernel官方提供了save_stack_trace这个API, 我们只需要调用这个函数, 就能得到调用栈信息.

 

存哪里

得到调用栈信息后存储在哪里呢? 既然这个信息是与每个页面相关的, 那存储在struct page结构体里面应该算合情合理. 但是page结构体是内存初始化阶段创建的, 每个物理页面都会分配一个page结构体; 我们仅需要针对已分配的页面保存调用栈信息, 如果直接在page结构体里面为调用栈信息分配存储空间, 会增加pagesize, 进而会浪费内存(回头查看page结构体介绍可知kernel为了缩小此结构体的size做了很多工作.

 

因此kernel将调用栈信息存储在了struct page_ext, page_ext结构体也是每个page对应一个, 除了避免内存浪费, mm/page_ext.c的注释部分还介绍了其它引入page_ext的原因.struct pagestruct page_ext没有直接的指向关系, 它们都是通过页面编号来索引到每个物理页面对应的数据结构.

 

何时存

存储的调用栈信息最好的时机当然是页面分配的时候. page_owner.h定义了set_page_owner函数, 此函数会负责获取调用栈信息并将信息存储在page_ext; page_alloc.cprep_new_page函数中调用了set_page_owner.

 

更多资料

目前网上好像没有太多对page_owner的说明.

官方的Documentation/vm/page_owner.txt里面有对如何使能&使用page_owner机制的简介.

另外如果想了解代码历史, 也可以通过git log mm/page_owner.c看看都修改了哪些代码.

5.1.2         示例

示例章节我们打算使能kernelpage_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.41.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”, 使用该工具时, 几点注意事项

         该脚本使用正则表达式去匹配每一行, 获取funcnameoffset, 因此输出的”Error reports”符合脚本规范, 具体可阅读脚本代码和查看上述wiki中的示例log

         脚本里面用到了addr2linenm, 两个需要用和编译器相对的版本. 脚本默认使用的是$PATH路径下提供的addr2linenm, 可能不对应.

5.2.3         原理

5.2.3.1    影子区域

Kasan 的原理是利用“额外”的内存来标记那些可以被使用的内存的状态。这些做标记的区域被称为影子区域(shadow region)。了解Linux 内存管理的读者知道,内存中的每个物理页在内存中都会有一个struct page 这样的结构体来表示,即每4KB 的页需要40B 的结构体,大约1% 的内存用来表示内存本身。Kasan 与其类似但“浪费”更为严重,影子区域的比例是1:8,即总内存的九分之一会被“浪费”。

 

做标记的方法比较简单,将可用内存按照8 子节的大小分组,如果每组中所有8 个字节都可以访问,则影子内存中相应的地方用全零(0x00)表示;如果可用内存的前N1 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.cmm/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:

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


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM