内核里面内存越界问题是一类很常见的错误类型,当然有些也属于异常难调试的问题,这种错误行为会触发内核运行异常,假如越界访问修改了错误的数据区,情况较轻的可能导致应用获取的数据不对;情况严重的,比如修改了一个错误的地址,那么内核访问该地址时就会触发oops和panic。
下面来聊聊内存越界的类型,内存越界发生在哪些情况下呢?
内存访问异常可以分为如下几种类型:
数组越界
数组的长度是预先定义的,如果在代码中访问数组超过了数组的最大限制,那么就会修改到无法预期的位置。
内核栈溢出
在内核函数创建的临时变量或者数组,那么会从内核栈中分配,当一个进程的内核栈空间超过最大限制时,那么就会出现一些无法预知的问题。
slab/slub内存问题
作为内核开发者,最常用的内存应该就是slab/slub内存了,开发者一般使用kmalloc来申请内存,kmalloc内部的实现就是基于slab/slub的。比如:
$ sudo cat /proc/slabinfo | grep kmalloc
kmalloc-8192 4333 4340 8192 4 8 : tunables 0 0 0 : slabdata 1085 1085 0
kmalloc-4096 418 488 4096 8 8 : tunables 0 0 0 : slabdata 61 61 0
kmalloc-2048 1197 1248 2048 16 8 : tunables 0 0 0 : slabdata 78 78 0
kmalloc-1024 7282 9536 1024 32 8 : tunables 0 0 0 : slabdata 298 298 0
kmalloc-512 5191 5632 512 32 4 : tunables 0 0 0 : slabdata 176 176 0
kmalloc-256 25061 31072 256 32 2 : tunables 0 0 0 : slabdata 971 971 0
kmalloc-192 12019 16632 192 21 1 : tunables 0 0 0 : slabdata 792 792 0
kmalloc-128 18705 19072 128 32 1 : tunables 0 0 0 : slabdata 596 596 0
kmalloc-96 6918 13650 96 42 1 : tunables 0 0 0 : slabdata 325 325 0
kmalloc-64 136718 170496 64 64 1 : tunables 0 0 0 : slabdata 2664 2664 0
kmalloc-32 34639 37888 32 128 1 : tunables 0 0 0 : slabdata 296 296 0
kmalloc-16 9472 9472 16 256 1 : tunables 0 0 0 : slabdata 37 37 0
kmalloc-8 8192 8192 8 512 1 : tunables 0 0 0 : slabdata 16 16 0
该命令可以查看系统中存在多种size类型的slab对象,内核开发者使用kmalloc申请内存后,如果出现使用上的不当就会发生内存oob越界访问或者use-after-free等问题。一般分为几种情况:访问释放后的内存地址、多次释放同一块内存、越界访问kmalloc内存。
如何进行问题定位?
对于数组越界类型是比较难定位的,因为数组越界访问后,会导致其他的数据被异常的修改掉,并且当内核爆出问题时,一般都已经不是第一现场了,那么对于这类问题要怎么定位呢
上面是按照我的个人理解对不同的越界访问错误进行了大致的分类,可能有不全的地方,也欢迎指正,下面我就来聊一下如何debug内存问题。下面这一部分将以实例的方式做一下几种情况的梳理和记录,可能也不会cover所有的场景。
全局变量被异常修改
当我们在驱动或者内核模块中定义了一个全局变量,当我们在使用时发现该变量的值被异常的修改了,那么如何找到是谁修改了我们的变量值呢?一般考虑是由于其他代码路径发生了越界访问,异常修改了我们定义的变量内容。首先第一个考虑的就是数组越界,可以通过查看System.map找到我们定义的全局变量所在的address地址,并且在该address附近查看是否存在数组,如果存在数组,那么该数组可能就是重点怀疑的对象,可以搜索一下内核代码,找到对应的symbol进行查验。
内核访问地址异常触发oops
1.通过查看oops找到是哪个函数访问了异常地址;
2.查看内核代码找到传入的地址指针是否是一个具体的symbol,比如一个特定的数组或者结构体成员;
3.可以在该数组或者结构体成员的前后分别加上magic number,复现故障,使用crash工具查看对应的magic number是否被修改,判断是向前踩内存还是向后踩内存;
4.查看是否可能与memcpy有关,使用kprobe在memcpy前加上我们的hook,如果拷贝的目标地址在symbol所处的区间,那么就打印出来dump_stack;
5.重新复现问题后,如果恰好打印出kprobe中的dump_stack,那么基本可以定位是哪里引起的问题了。
slab/slub内存问题定位
对于slab/slub内存相关的问题,最简单的,我们可以使用内核的SLAB DEBUG功能和KASAN功能。不过这种方式需要重新编译内核,如果应用场景允许使能这些内核特性编译内核,那么这种方式是首选。假如很不幸,我们处在业务机器上,这种影响性能的特性是不开的,只能通过kdump和crash工具分析故障现场来进行debug,这个就属于疑难杂症了,属于比较难以debug的情况了,这里笔者也不能给出具体建议,自求多福吧,未来掌握的更多技巧后再来更新这一节吧。
栈溢出问题
gcc编译器本身带有堆栈溢出保护功能,通过配置如下内核选项:
CONFIG_HAVE_STACKPROTECTOR=y
CONFIG_CC_HAS_STACKPROTECTOR_NONE=y
CONFIG_STACKPROTECTOR=y
CONFIG_STACKPROTECTOR_STRONG=y
可以对内核栈溢出进行检测,除此之外,前面介绍的KASAN功能也具有这个功能,不过需要gcc比较高的版本才能使用该功能。
参考文章:
https://blog.csdn.net/hjkfcz/article/details/84500026
https://blog.csdn.net/cf125313/article/details/95611596
https://blog.csdn.net/chenyu105/article/details/9749549