关于调试,除开内存之外,离不开的一点就是各类寄存器。
x86_64
下的16种通用寄存器
寄存器名 | 寄存器简介 | 主要功能 | 63-0 | 31-0 | 15-0 | 8-0 |
---|---|---|---|---|---|---|
rax | 累加器,是算术运算的主要寄存器 | 存储返回值 | rax | eax | ax | al |
rbx | 基址寄存器,被调用者保存 | 存放存储区的起始地址 | rbx | ebx | bx | bl |
rcx | 计数寄存器 | 循环操作和字串处理的计数控制;函数调用时的第4个参数 | rcx | ecx | cx | cl |
rdx | I/O指针 | I/O操作时提供外部设备接口的端口地址;函数调用时的第3个参数 | rdx | edx | dx | dl |
rsi | (source index)源变址寄存器,与rds段寄存器联用,可以访问数据段中的任一个存储单元 | 函数调用时的第2个参数 | rsi | esi | si | sil |
rdi | (destination index)目的变址寄存器,与res段寄存器联用,可以访问附加段中的任一个存储单元 | 函数调用时的第1个参数 | rdi | edi | di | dil |
rbp | (base pointer)基址指针寄存器,用于提供堆栈内某个单元的偏移地址,与rss段寄存器联用,可以访问堆栈中的任一个存储单元,被调用者保存 | rbp | ebp | bp | bpl | |
rsp | (stack pointer)栈顶指针寄存器,提供堆栈栈顶单元的偏移地址,与rss段寄存器联用,以控制数据进栈和出栈 | rsp | esp | sp | spl | |
r8 | 函数调用时的第5个参数 | r8 | r8d | r8w | r8b | |
r9 | 函数调用时的第6个参数 | r9 | r9d | r9w | r9b | |
r10 | 调用者保存 | r10 | r10d | r10w | r10b | |
r11 | 调用者保存 | r11 | r11d | r11w | r11b | |
r12 | 被调用者保存 | r12 | r12d | r12w | r12b | |
r13 | 被调用者保存 | r13 | r13d | r13w | r13b | |
r14 | 被调用者保存 | r14 | r14d | r14w | r14b | |
r15 | 被调用者保存 | r15 | r15d | r15w | r15b |
段寄存器:
寄存器 | 功能 |
---|---|
CS(code segment) | 代码段地址寄存器,存放代码段的起始地址 |
DS(data segment) | 数据段地址寄存器,存放数据段的起始地址 |
SS(stack segment) | 堆栈段地址寄存器,存放堆栈段的起始地址 |
ES(extra segment) | 附加段地址寄存器,存放附加段的起始地址 |
控制寄存器
寄存器 | 功能 |
---|---|
Cr0 | 存放控制处理器操作模式和状态的系统控制标志 |
Cr1 | 保留 |
Cr2 | 存放导致页错误的线性地址 |
Cr3 | 存放页目录表基地址 |
Cr4 | 处理器扩展功能标志位 |
Cr8 | 当前lrql权限等级 |
现在内核基本都开了这个编译选项,导致调式的时候根本无法修改内存
wr: cannot write to /proc/kcore
然后通过Systemtap
修改是可以:
stap -g -e 'probe kernel.function("devmem_is_allowed").return { $return =1 }'
我在
vmware
中修改了devmem_is_allowed
后再去打开crash
就会直接导致vmware
卡住,不清楚是什么原因导致的,起初以为是因为新编译的内核缺少一个vmware
需要的模块导致的,即报错dracut[38489]: Failed to install module libnvdimmvmxnet3
,这其实是两个模块libnvdimm
和vmxnet3
,可以通过修改/etc/dracut.conf.d/nvdimm-security.conf
,把add_drivers+="libnvdimm"
改成add_drivers+="libnvdimm "
即可,后来我发现就算修复了这个问题还是会卡住。
没办法只能去看一下内核崩溃的原因,就得保存内核崩溃时的状态,这就用到了kdump
,这是一个基于kexec
的内核崩溃捕获机制,可以将kernel
崩溃前的内存镜像保存下来。原理很简单,大概就是在内核崩溃时,kdump
启动kexec
启动到第二个内核,也称为捕获内核,这个内核会与相应的ramdisk
组件一个微型环境搜集生产内核(第一内核)
的内存信息并转存。
kdump
的实现也是需要条件的,捕获内核的启动绕过了BIOS
由生产内核
在崩溃时候通过kexec
启动,这就需要捕获内核
一开始就已经加载完成,这就是转存机制实现的关键两点:
kexec_load
系统调用会在生产内核
启动时候加载捕获内核
到指定地址- 用户态下通过
kexec-tools
将捕获内核
地址传递给生产内核
,得以在崩溃时运行。
排查系统时候发现,kdump
并没有被打开,且去尝试systemctl start kdump
时候还会直接报错,报错信息简单来说就是内存不足
,内核启动参数中有一个传入参数是crashkernel
,这个参数用于配置捕获内核
的大小和位置,一般会看到内核参数设置都是crashkernel=auto
,这会根据内存自动reserve
内存出来使用,然而如果内存在8G
以下的话,其实并不会保留内存出来,因此对于加内存来说,我手动修改了/boot/grub/grub.cfg
的内核参数为crashkernel=256M
,成功开启kdump
,从而获取到了崩溃的内核镜像。
不过我自己物理机排查都发现保留内存为0,那有点怀疑是可能是哪个版本的内核直接就自动关闭了这部分的保留,除非手动设置。
cat /sys/kernel/kexec_crash_size
这样查看保留了多少内存。
调试崩溃内核时发现错误出现在了mm/usercopy.c:72
这个位置,当直接翻开源码观察后,发现这儿本来就会去主动发起一个BUG()
,然后产生中断,那就说明是一个内核的feature
,结果发现在内核4.8
以后内核主线中引入了一个特性是CONFIG_HARDENED_USERCOPY
,简单来说就是针对copy_form_user()/copy_to_user()
这两个函数做安全加固,从而避免内核内存泄露
和内核内存覆盖
的问题出现,至于加固细节大概就是指针检查
,指向检查
,拷贝大小不允许越界(slab分配对象)
,涉及内核栈的拷贝的内容需要在当前进程内核栈中
,为了方便就直接修改了.config
然后重编译了内核。
上面说过写入的操作最大的障碍就是devmem_is_allowed
,这个函数影响是是crash
针对/dev/mem
的访问,/dev/mem
可以看做是一个内存入口
,这个入口提供了一个针对全物理地址
访问的能力。简单来说只要能确定地址空间分布,就可以指定任意有效地址映射到用户空间中,因此kernel
自然要对其加以限制,而最核心的检查就是devmem_is_allowed
,简单来说如果关了这个选项用户可以获得完全读写能力。