文章目录
- 前言
- 概述
- 使用 /dev/kmem
- 使用 /proc/kallsyms
- 验证
- 进阶
前言
上篇文章我们介绍了 /dev/mem,今天再来介绍下它的好兄弟 /dev/kmem
crw-r----- 1 root kmem 1, 1 May 26 06:10 /dev/mem
crw-r----- 1 root kmem 1, 2 May 26 06:10 /dev/kmem
对比一下:
- /dev/mem:映射系统所有的物理内存。
- /dev/kmem:映射系统所有的内核态虚拟内存。
概述
简单来说,通过 /dev/mem 我们可以查询一个物理地址上的数据是多少,
通过 /dev/kmem 我们可以查询一个内核虚拟地址上的数据是多少。
要知道,CPU 或着说内核看到的地址都是虚拟地址,我们在内核中使用 printk 打印出一个全局变量的地址,后面想要继续追踪该地址上值的变化,这时候再用 /dev/mem 就不行了,因为打印出的地址是一个虚拟地址,而通过 /dev/mem 访问的是物理地址。这种情况下应该使用 /dev/kmem,通过它可以访问内核虚拟内存。
使用 /dev/kmem
由于/dev/kmem 暴露的权限过大,存在安全隐患,所以内核一般默认禁用该设备,仅仅保留 /dev/mem。我们想要使用 /dev/kmem,可以打开 CONFIG_DEVKMEM=y
这个编译选项。
使用 /proc/kallsyms
有了 /dev/kmem 设备,我们就可以访问内核虚拟内存了,但是我们访问哪个地址呢?如何知道一个全局变量的虚拟内存地址呢?答案是可以使用 /proc/kallsyms 查看内核符号信息。
# cat /proc/kallsyms
c0008000 T stext
c0008000 T _text
c000808c t __create_page_tables
c0008138 t __turn_mmu_on_loc
c0008144 t __fixup_smp
c00081ac t __fixup_smp_on_up
c00081d0 t __fixup_pv_table
c0008224 t __vet_atags
c0008280 T _stext
c0008280 T __turn_mmu_on
c0008280 T __idmap_text_start
c00082a0 T cpu_resume_mmu
c00082a0 t __turn_mmu_on_end
c00082c4 T cpu_ca15_reset
c00082c4 T cpu_ca8_reset
c00082c4 T cpu_ca9mp_reset
c00082c4 T cpu_v7_bpiall_reset
c00082c4 T cpu_v7_reset
c00082e0 T __idmap_text_end
c0009000 T asm_do_IRQ
c0009000 T __exception_text_start
c0009000 T __hyp_idmap_text_end
c0009000 T __hyp_idmap_text_start
c0009014 T do_undefinstr
c0009268 T handle_fiq_as_nmi
c00092ec T do_IPI
c00092f0 T do_DataAbort
c00093a4 T do_PrefetchAbort
c000943c T gic_handle_irq
c0009508 T __do_softirq
。。。
第一列为符号地址,第二列为类型,第三列为符号名。
第二列的类型:
大写字母表示该符号没有被 static 修饰,可以被整个内核代码使用;
小写字母表示该符号被 static 修饰了,只能在当前文件使用。
b 符号在未初始化数据区(BSS)
c 普通符号,是未初始化区域
d 符号在初始化数据区
g 符号针对小object,在初始化数据区
i 非直接引用其他符号的符号
n 调试符号
r 符号在只读数据区
s 符号针对小object,在未初始化数据区
t 符号在代码段
u 符号未定义
我们找个全局变量练练手
# cat /proc/kallsyms | grep " D "
#
却发现没有找到任何全局变量,这是怎么回事?
原来,内核只开启了 CONFIG_KALLSYMS=y
,
CONFIG_KALLSYMS=y # 符号表中包含所有的函数
我们还需要开启下面编译选项
CONFIG_KALLSYMS_ALL=y # 符号表中包括所有的变量(包括没有用 EXPORT_SYMBOL 导出的变量)
重新编译后,便可列出全局变量符号了
# cat /proc/kallsyms | grep " D "
c0884000 D __per_cpu_load
c0884000 D __per_cpu_start
c0884048 D cpu_data
c0884208 D harden_branch_predictor_fn
c0884210 D process_counts
c0884260 D ksoftirqd
c0884280 D kernel_cpustat
c08842d0 D kstat
c08842fc D select_idle_mask
c0884300 D load_balance_mask
c088432c D sd_llc
c0884330 D sd_llc_size
c0884334 D sd_llc_id
c0884338 D sd_llc_shared
c088433c D sd_numa
c0884340 D sd_asym
c0888480 D srcu_online
c08895c0 D hrtimer_bases
c0889818 D tick_cpu_device
c0889944 D pcpu_drain
c0889978 D dirty_throttle_leaks
c0889b2c D __kmap_atomic_idx
c088a008 D cpuidle_devices
c088a010 D cpuidle_dev
c088a4e8 D flush_works
比方说,我们想查看变量 flush_works 的值,其虚拟地址为 0xc088a4e8 = 3230180584,那么,使用下面命令就可以从 /dev/kmem 中读取该变量的值了
# dd if=/dev/kmem bs=1 count=4 skip=3230180584 | hexdump
4+0 records in
4+0 records out
0000000 60af 7ce4
0000004
验证
为了验证读取到值的正确性,我们在内核(usb.c)中添加一个全局变量进行测试,添加代码如下
int lyj_ccc = 0x1234;
/*
* Init
*/
static int __init usb_init(void)
{
int retval;
int lyj_ddd = 0x5678;
printk("&lyj_ccc = %p\n", &lyj_ccc);
printk("lyj_ccc = 0x%x\n", lyj_ccc);
printk("&lyj_ddd = %p\n", &lyj_ddd);
printk("lyj_ddd = 0x%x\n", lyj_ddd);
if (usb_disabled()) {
pr_info("%s: USB support disabled\n", usbcore_name);
return 0;
}
重新编译内核,内核启动阶段打印如下
SCSI subsystem initialized
&lyj_ccc = c08b65a8
lyj_ccc = 0x1234
&lyj_ddd = dd03fef4
lyj_ddd = 0x5678
usbcore: registered new interface driver usbfs
usbcore: registered new interface driver hub
usbcore: registered new device driver usb
系统启动完毕后,我们使用 /dev/kmem 进行读取,先使用 /proc/kallsyms 查看符号 lyj_ccc 对应的虚拟地址(其实,在内核启动阶段也打印了该变量的地址,只不过正常情况下我们不会手动添加代码去打印,使用 kallsyms 查看才是更通用的手段)
# cat /proc/kallsyms | grep lyj
c08b65a8 D lyj_ccc
看到在符号表中只有 lyj_ccc,而没有 lyj_ddd,因为 lyj_ddd 是局部变量,不是符号(我的理解:符号是全局变量和函数名称)。
拿到变量的虚拟地址 0xc08b65a8 后,我们使用如下命令查看变量的值
# dd if=/dev/mem bs=1 count=4 skip=3230361000 | hexdump
dd: /dev/mem: Bad address
# dd if=/dev/kmem bs=1 count=4 skip=3230361000 | hexdump
4+0 records in
4+0 records out
0000000 1234 0000
0000004
可以看到,使用 /dev/mem 是无法查看的,因为它查看的是物理地址。
使用 /dev/kmem 可以读取,且读到的值和代码一致,说明读取成功。🎈🎈🎈
进阶
我在使用另一款设备研究该问题时,发现使用 /dev/mem 竟然也可以查看内核虚拟内存,
不过最终发现是闹了乌龙,原因是这款设备在内核软件设计时,将内核虚拟地址映射到了地址相同的物理地址上
int lyj_aaa = 0x3344;
/*
* Init
*/
static int __init usb_init(void)
{
int retval;
int lyj_bbb = 0x5566;
if (usb_disabled()) {
pr_info("%s: USB support disabled\n", usbcore_name);
return 0;
}
usb_init_pool_max();
printk("&lyj_aaa = %p\n", &lyj_aaa);
printk("lyj_aaa = %d\n", lyj_aaa);
printk("lyj_bbb = %d\n", lyj_bbb);
retval = usb_debugfs_init();
if (retval)
goto out;
&lyj_aaa = 80a67674
lyj_aaa = 13124
lyj_bbb = 21862
root@ATK-IMX6U:~# cat /proc/kallsyms | grep lyj
80a67674 D lyj_aaa
root@ATK-IMX6U:~# dd if=/dev/mem bs=1 count=4 skip=2158392948 | hexdump
4+0 records in
4+0 records out
4 bytes copied, 0.000401333 s, 10.0 kB/s
0000000 3344 0000
0000004
root@ATK-IMX6U:~#
root@ATK-IMX6U:~#
root@ATK-IMX6U:~# dd if=/dev/kmem bs=1 count=4 skip=2158392948 | hexdump
4+0 records in
4+0 records out
4 bytes copied, 0.000410666 s, 9.7 kB/s
0000000 3344 0000
0000004
root@ATK-IMX6U:~# cat /proc/iomem
。。。
80008000-809d0ca3 : Kernel code
80a38000-80b02353 : Kernel data
可以看到 0x80a67674 正好落在 80a38000-80b02353 范围内,说明内核数据段的起始地址被映射到了相同的物理内存地址,
其实,内核代码段的起始地址也被映射到了相同的物理内存地址,证明如下
root@ATK-IMX6U:~# cat /proc/kallsyms | grep 80008000
80008000 T stext
80008000 T _text
root@ATK-IMX6U:~# cat /proc/kallsyms | grep 80a38000
80a38000 D init_thread_union
80a38000 D _data
80a38000 D _sdata
80a38000 D __data_loc
80a38000 D __init_end
分析过内核启动流程的小伙伴一眼就看出 stext
就是内核的入口函数,可以复习之前的两篇文章《Kernel 启动流程梳理》、《bootz 启动 kernel》。
而 _data 就是代码段的起始地址,这点可以查看内核的链接脚本 arch/arm/kernel/vmlinux.lds
。
作为对比,之前那款设备,内核的虚拟地址就和物理地址不同了,这才是普遍行为
# cat /proc/iomem
40008000-4083efff : Kernel code
4088c000-40a0a97f : Kernel data
。。。
# cat /proc/kallsyms | grep lyj
c08b65a8 D lyj_ccc