LSM Oops 内存错误根因分析与解决( 二 )


  1. 如果第 0 位被清 0 , 则异常是由一个不存在的页所引起的;否则是由无效的访问权限引起的 。
  2. 如果第 1 位被清 0 , 则异常由读访问或者执行访问所引起;否则异常由写访问引起 。
  3. 如果第 2 位被清 0 , 则异常发生在内核态;否则异常发生在用户态 。
所以 , 上述样例中的 error code 0003 表示:
  1. 异常由无效的访问权限引起 , 也就是说被访问的地址存在对应的物理页 , 但是没有权限访问;
  2. 异常由写操作引起;
  3. 异常发生在内核态 , 总结来说就是该异常由于在内核态对没有写权限的地址进行写操作时产生;
而 Oops 中的 [#1] 表示发生 Crash 次数 。
5. 通过 gdb 定位出错的内核代码文件和行号接下来再通过 gdb 调试做进一步动态的分析 , 以便确定出错的代码文件和行号 。
首先需要通过 add-symbol-file 命令将符号文件添加到调试器 。符号文件即通过 insmod 命令插入 LSM 模块出错时用的内核模块的 .o 文件 , 即日志中出现的 xxx_security 。
insmod 命令:
[415.746851] CPU: 0 PID: 8366 Comm: insmod Tainted: GOE4.19.82-wwh #1xxx_security 模块:
[415.746883]init_module+0x34/0xc0 [xxx_security]add-symbol-file 命令有两个参数:
  • 第一个参数是 xxx_security.o
  • 第二个参数是该模块代码正文区域的地址
该地址通过如下方式获得:
$ sudo cat /sys/module/xxx_security/sections/.text0xffffffffc0ada000接着通过 gdb 来调试 xxx_security.ko:
(gdb) add-symbol-file xxx_security.o 0xffffffffc0ada000add symbol table from file "xxx_security.o" at.text_addr = 0xffffffffc0ada000(y or n) yReading symbols from xxx_security.o...done.上文已经根据 RIP 行可以得到报错函数名以及偏移:
RIP: 0010:init_lsm_hooks+0x1cd/0x1f0 [xxx_security]接着就是反汇编 init_lsm_hooks 函数如下:
(gdb) disassemble init_lsm_hooksDump of assembler code for function init_lsm_hooks:Address range 0x150 to 0x33c:0x0000000000000150 <+0>: callq0x155 <init_lsm_hooks+5>0x0000000000000312 <+450>: movq$0x0,(%rcx)0x0000000000000319 <+457>: mov%rdx,0x8(%rcx)0x000000000000031d <+461>: mov%rcx,(%rdx)0x0000000000000320 <+464>: add$0x28,%rcx0x0000000000000324 <+468>: cmp%rdi,%rcx从上可以看出 init_lsm_hooks 函数的起始地址是 0x150 , 出错所在的偏移是 0x1cd 。
0x150+0x1cd=0x31d , 那么如何通过这个地址对应到 .c 中具体某一行呢?
(gdb) l *0x000000000000031d0x31d is in init_lsm_hooks (./include/linux/compiler.h:220).215 {216switch (size) {217case 1: *(volatile __u8 *)p = *(__u8 *)res; break;218case 2: *(volatile __u16 *)p = *(__u16 *)res; break;219case 4: *(volatile __u32 *)p = *(__u32 *)res; break;220case 8: *(volatile __u64 *)p = *(__u64 *)res; break;221default:222barrier();223__builtin_memcpy((void *)p, (const void *)res, size);224barrier();从调试信息可以看出 , 出错位置为:
./include/linux/compiler.h:220 case 8: *(volatile __u64 *)p = *(__u64 *)res; break`打开该文件以及所在行可以确认:
$ vim include/linux/compiler.h +220static __always_inline void __write_once_size(volatile void *p, void *res, int size){switch (size) {case 1: *(volatile __u8 *)p = *(__u8 *)res; break;case 2: *(volatile __u16 *)p = *(__u16 *)res; break;case 4: *(volatile __u32 *)p = *(__u32 *)res; break;case 8: *(volatile __u64 *)p = *(__u64 *)res; break;default:barrier();__builtin_memcpy((void *)p, (const void *)res, size);barrier();}}当然 , 也可以通过 set listsize 20 之后看到具体的 __write_once_size 函数 。该函数为 __always_inline 的 , 编译时被嵌入到了 init_lsm_hooks 中 。
以上过程也可以通过其他方式快速定位 , 可进一步阅读:
  • 诊 & 断:如何快速定位 Linux Panic 出错的代码行
  • Linux Lab: 消除 qemu/raspi3 启动过程的一堆警告
6. 出错代码详细分析上述 __write_once_size 接口是内核中所有双链表操作最终进入的函数 。
在出错的 init_lsm_hooks 函数中通过调用 hlist_add_tail_rcu 调用 WRITE_ONCE, 从而进入了 __write_once_size, 接下来看看这个接口的实现:
#define WRITE_ONCE(x, val) ({union { typeof(x) __val; char __c[1]; } __u ={ .__val = (__force typeof(x)) (val) };__write_once_size(&(x), __u.__c, sizeof(x));__u.__val;})WRITE_ONCE() 用于向变量对应的内存写入值 。x 对应变量 , val 对应写入的值 。函数首先定义并初始化一个联合体 , 使 __u.__val 的值为参数 val ,  然后调用 __write_once_size() 函数将数据写入到内存中 。


推荐阅读