- 如果第 0 位被清 0 , 则异常是由一个不存在的页所引起的;否则是由无效的访问权限引起的 。
- 如果第 1 位被清 0 , 则异常由读访问或者执行访问所引起;否则异常由写访问引起 。
- 如果第 2 位被清 0 , 则异常发生在内核态;否则异常发生在用户态 。
- 异常由无效的访问权限引起 , 也就是说被访问的地址存在对应的物理页 , 但是没有权限访问;
- 异常由写操作引起;
- 异常发生在内核态 , 总结来说就是该异常由于在内核态对没有写权限的地址进行写操作时产生;
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 #1
xxx_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 启动过程的一堆警告
在出错的 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() 函数将数据写入到内存中 。
推荐阅读
- 内存|DDR3内存要淘汰?华邦电子:再战10多年、持续扩产
- 一次完整的JVM堆外内存泄漏故障排查记录
- 安卓|谷歌确认!Android 13最大改进 解决杀后台:CPU/内存使用率暴降
- 全局变量、事件绑定、缓存爆炸?Node.js内存泄漏问题分析
- 内存界的金条长啥样?一条顶人家三条
- Android Camera 内存问题剖析
- 铠侠 极至光速系列内存卡评测:经典红白复刻,唯有品质依旧
- 多进程编程 - 共享内存
- 虚拟内存 和 page fault 的解释
- 对于内存结构的简单理解