• /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
    
    • 1
    • 2

    对比一下:

    • /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
    。。。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    第一列为符号地址,第二列为类型,第三列为符号名

    第二列的类型:
    大写字母表示该符号没有被 static 修饰,可以被整个内核代码使用;
    小写字母表示该符号被 static 修饰了,只能在当前文件使用。

    b 符号在未初始化数据区(BSS)
    c 普通符号,是未初始化区域
    d 符号在初始化数据区
    g 符号针对小object,在初始化数据区
    i 非直接引用其他符号的符号
    n 调试符号
    r 符号在只读数据区
    s 符号针对小object,在未初始化数据区
    t 符号在代码段
    u 符号未定义
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们找个全局变量练练手

    # cat /proc/kallsyms | grep " D "
    #
    
    • 1
    • 2

    却发现没有找到任何全局变量,这是怎么回事?
    原来,内核只开启了 CONFIG_KALLSYMS=y

    CONFIG_KALLSYMS=y # 符号表中包含所有的函数
    
    • 1

    我们还需要开启下面编译选项

    CONFIG_KALLSYMS_ALL=y # 符号表中包括所有的变量(包括没有用 EXPORT_SYMBOL 导出的变量)
    
    • 1

    重新编译后,便可列出全局变量符号了

    # 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    比方说,我们想查看变量 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
    
    • 1
    • 2
    • 3
    • 4
    • 5

    验证

    为了验证读取到值的正确性,我们在内核(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;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    重新编译内核,内核启动阶段打印如下

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    系统启动完毕后,我们使用 /dev/kmem 进行读取,先使用 /proc/kallsyms 查看符号 lyj_ccc 对应的虚拟地址(其实,在内核启动阶段也打印了该变量的地址,只不过正常情况下我们不会手动添加代码去打印,使用 kallsyms 查看才是更通用的手段

    # cat /proc/kallsyms | grep lyj
    c08b65a8 D lyj_ccc
    
    • 1
    • 2

    看到在符号表中只有 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    可以看到,使用 /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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    &lyj_aaa = 80a67674
    lyj_aaa = 13124
    lyj_bbb = 21862
    
    • 1
    • 2
    • 3
    root@ATK-IMX6U:~# cat /proc/kallsyms | grep lyj
    80a67674 D lyj_aaa
    
    • 1
    • 2
    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    root@ATK-IMX6U:~# cat /proc/iomem
    。。。
      80008000-809d0ca3 : Kernel code
      80a38000-80b02353 : Kernel data
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    分析过内核启动流程的小伙伴一眼就看出 stext 就是内核的入口函数,可以复习之前的两篇文章《Kernel 启动流程梳理》《bootz 启动 kernel》
    而 _data 就是代码段的起始地址,这点可以查看内核的链接脚本 arch/arm/kernel/vmlinux.lds

    作为对比,之前那款设备,内核的虚拟地址就和物理地址不同了,这才是普遍行为

    # cat /proc/iomem 
      40008000-4083efff : Kernel code
      4088c000-40a0a97f : Kernel data
    。。。
    
    • 1
    • 2
    • 3
    • 4
    # cat /proc/kallsyms | grep lyj
    c08b65a8 D lyj_ccc
    
    • 1
    • 2
  • 相关阅读:
    如何为3D模型设置自发光材质?
    Chromium 开发指南2024 Mac篇-开始编译Chromium(五)
    好代码 ,坏代码:你的代码和其他工程师的代码
    VS工程的“多dll与exe文件合并”
    零基础也能制作电子期刊,这个网站你一定不能错过
    离线强化学习论文学习 Critic Regularized Regression
    Java基础-方法-可变参数
    用DIV+CSS技术设计的网页与实现制作【体育文化】dreamweaver学生网页设计
    js实现pdf、word、excel、图片、html文件预览及下载
    假如面试官问你Babel的原理该怎么回答
  • 原文地址:https://blog.csdn.net/lyndon_li/article/details/130867921