02-内核符号导出

什么是内核符号?

内核符号表就是内核中 “名字 → 信息(地址、类型、可见性)” 的映射表。名字通常是内核的函数名或全局变量名,符号表让内核本身与可加载模块(.ko)相互找到并链接这些名字。而表项的名字就是内核符号

内核符号表存在于哪里?

  1. 构建时:vmlinux(未压缩的内核镜像)包含完整符号信息。内核构建后会生成 System.map,格式为 地址 类型 名称,并通常放在 /boot/System.map-$(uname -r)
  2. 运行时:内核会把符号信息以可查询的形式暴露到 /proc/kallsyms(如果内核启用了 CONFIG_KALLSYMS)。

符号行的格式与常见类型

通过head /proc/kallsyms获取符号表的前几行:

80008000 T stext
80008000 T _text
8000808c t __create_page_tables
80008138 t __turn_mmu_on_loc
80008144 t __fixup_smp
800081ac t __fixup_smp_on_up
800081d0 t __fixup_pv_table
80008224 t __vet_atags
80100000 T _stext
80100000 T __turn_mmu_on

其输出格式为:地址 类型字母 名称
常见类型字母:

  • T / t:代码段(text,函数),T 表示全局可见(外部),t 表示本地/静态。
  • D / d:已初始化的数据段(全局/本地)。
  • B / b:BSS(未初始化全局变量)。
  • R:只读数据(rodata)。
  • U:未定义(undefined;通常出现在模块,表示对某符号有引用但还未解析)。

对于构建好的内核或者内核模块文件,也可以通过readelf -s vmlinux,readelf -s hello.ko来获取他们的符号表,它的输出内容更为丰富。以前面解释的第一个内核模块为例:

readelf --wide -s simplest_drv.ko

Symbol table '.symtab' contains 42 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 SECTION LOCAL  DEFAULT    1 .note.gnu.build-id
     2: 00000000     0 SECTION LOCAL  DEFAULT    2 .text
     3: 00000000     0 SECTION LOCAL  DEFAULT    3 .init.text
     4: 00000000     0 SECTION LOCAL  DEFAULT    5 .exit.text
    ...............
    18: 00000000     0 FILE    LOCAL  DEFAULT  ABS simplest_drv.c
    19: 00000000     0 NOTYPE  LOCAL  DEFAULT    3 $a
    20: 00000000    32 FUNC    LOCAL  DEFAULT    3 simplest_drv_init
    25: 00000000    28 FUNC    LOCAL  DEFAULT    5 simplest_drv_exit
    ...............
    36: 00000000   192 OBJECT  LOCAL  DEFAULT   15 ____versions
    37: 00000000   512 OBJECT  GLOBAL DEFAULT   17 __this_module
    38: 00000000    28 FUNC    GLOBAL DEFAULT    5 cleanup_module
    39: 00000000    32 FUNC    GLOBAL DEFAULT    3 init_module
    40: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND printk
    41: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND __aeabi_unwind_cpp_pr1
含义
Num 符号的索引号(从0开始)
Value 符号的内存地址(VMA),0表示该符号的地址尚未确定
Size 符号大小(字节)
Type 符号类型,常见的有:
NOTYPE - 未指定类型
OBJECT - 数据对象
FUNC - 函数或可执行代码
SECTION - 关联到节区
FILE - 源文件名
Bind 符号绑定属性:
LOCAL - 局部符号(static)
GLOBAL - 全局符号(extern)
WEAK - 弱引用符号
Vis 可见性:
DEFAULT - 默认可见规则
PROTECTED - 保护可见性
HIDDEN - 隐藏符号
Ndx 关联的SECTION索引,特殊值包括:
ABS - 绝对值(不重定位)
UND - 未定义(外部引用)
COM - 未初始化的公共块
Name 符号名

这里解释一下Ndx,当它为数值时,表示当前符号属于Num这一行对应数值的符号,通常类型是SECTION。例如以上面为例:

20: 00000000    32 FUNC    LOCAL  DEFAULT    3 simplest_drv_init

我们知道simplest_drv_init是模块初始化函数,它被放到了一个特殊的段里面:初始化段。它的Nxd是3,对应为:

3: 00000000     0 SECTION LOCAL  DEFAULT    3 .init.text

这行类型为SECTION,也就是一个段,名称为.init.text,正是内核模块初始化段的名字。

内核符号导出与模块加载

​ 在编译,链接内核模块时如果符号属于外部符号,则Nxd会被标记为未定义(UND),且绑定属性Bind为GLOBAL,在加载内核模块时,就会搜索/proc/kallsysms,以获得它的信息(地址、类型、可见性)。如果未搜索到,则模块加载会失败。

​ 以上面的simplest_drv.ko为例,查看其源码:

#include <linux/module.h>

//加载内核模块后的入口函数
static int __init simplest_drv_init(void)
{
    printk(KERN_INFO "Simplest driver init\n");
    return 0;
}

//卸载内核模块后的清理函数
static void __exit simplest_drv_exit(void)
{
    printk(KERN_INFO "Simplest driver exit\n");
}

module_init(simplest_drv_init);
module_exit(simplest_drv_exit);

可以看出,它使用了一个外部函数(也就是一个符号)printk,对应readelf输出的这一行:

40: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND printk

Bind为GLOBAL说明是一个全局符号,Ndx为UND说明此符号不在该elf文件(simplest_drv.ko)里面。如果在运行对应内核的设备上执行:

grep "\<printk\>" /proc/kallsyms
801ff718 T printk

同理也可以找到另一个外部符号:

grep "\<__aeabi_unwind_cpp_pr1\>" /proc/kallsyms
8011236c T __aeabi_unwind_cpp_pr1

因此此内核模块在加载时,符号搜索会成功,可以正常加载进来。如果我修改该内核模块源码:

int method_add(int a, int b); //引入外部函数

//加载内核模块后的入口函数
static int __init simplest_drv_init(void)
{
    printk(KERN_INFO "Simplest driver init\n");
    printk(KERN_INFO "call method_add result:%d\n", method_add(1,2)); //调用外部函数
    return 0;
}

添加了两行,引入一个外部的函数,并调用它。编译,重新插入新的内核模块:

insmod simplest_drv.ko 
[11323.976697] simplest_drv: Unknown symbol method_add (err 0)
insmod: ERROR: could not insert module simplest_drv.ko: Unknown symbol in module

此时会提示无法识别method_add这个符号,插入失败。如果要让这个模块插入成功,则需要另一个模块导出这个这个符号。例如:

#include <linux/module.h>


int method_add(int a, int b)
{
    return a + b;
}
EXPORT_SYMBOL(method_add);

static int __init lowermodule_init(void)
{
    printk(KERN_INFO "lowermodule driver init\n");
    return 0;
}

static void __exit lowermodule_exit(void)
{
    printk(KERN_INFO "lowermodule driver exit\n");
}

module_init(lowermodule_init);
module_exit(lowermodule_exit);

MODULE_LICENSE("GPL");
``
编译这个内核模块,并先插入它。
```shell
insmod lower_module.ko

查看符号是否导出:

grep "method_add" /proc/kallsyms
7f008064 r __kcrctab_method_add [lower_module]
7f00805c r __ksymtab_method_add [lower_module]
7f0080db r __kstrtab_method_add [lower_module]
7f008000 T method_add   [lower_module]

符号已出现在/proc/kallsyms中,此时:

insmod simplest_drv.ko
[11778.305071] simplest_drv: no symbol version for method_add
[11778.318088] Simplest driver init
[11778.324599] call method_add result:3

插入内核模块simplest_drv.ko也成功了。

内核模块的依赖和依赖的自动处理

依赖

接着上面的例子,使用lsmod查看当前加载的内核模块:

lsmod 
Module                  Size  Used by
simplest_drv            1012  0
lower_module            1325  1 simplest_drv

可以看到lower_module这个内核模块Used by另一个内核模块simplest_drv,如果你先去移除lower_module会提示:

rmmod lower_module    
rmmod: ERROR: Module lower_module is in use by: simplest_drv

只有一个内核模块不被任何模块依赖时(需要使用里面的符号),才允许移除它。

依赖的自动处理

如果内核模块之间的依赖关系复杂,插入顶层的内核模块时,需要不断的尝试先插入其依赖的模块,这个工作会非常恐怖,所以早有人遇到这种困扰,提供了一个依照内核模块的依赖关系自动插入内核模块的工具modprobe

不过使用modprobe有一些前置条件。modprobe它依赖一个数据库工作,这个所谓的数据库通常是位于/lib/modules/$(uname -r)/下的modules.depmodules.dep.bin

modules.dep是文本形式的依赖规则,格式为:

extra/lower_module.ko:
extra/upper_module.ko: extra/lower_module.ko

这个格式和Makefile的目标,依赖的格式是一样的:

模块:依赖模块1 依赖模块2 ...

多个依赖模块间以空格隔开。它们的base dir就是/lib/modules/$(uname -r)/modules.dep.bin是其二进制形式,前者便于人阅读,后者通常给程序使用,处理速度快。

modules.dep和``modules.dep.bin是通过执行命令depmod生成的,它会扫描/lib/modules/$(uname -r)/下的所有ko文件,生成依赖关系。所以要想modprobe`能正常工作,需要:

  • 拷贝相关的内核模块至/lib/modules/$(uname -r)/
  • 执行depmod -a扫描内核模块生成或更新依赖数据库文件modules.depmodules.dep.bin
  • 执行modprobe xxxx,自动插入内核模块。注意:xxxx不能带.ko后缀,这点和insmod和rmmod不同。

生成内核模块依赖数据库文件后,还可以执行modinfo xxx查看内核模块详细信息和依赖关系。

自动卸载依赖

执行modprobe xxx是自动解决依赖的插入内核模块,如果想要卸载模块,并且卸载其依赖只需要执行modprobe -r xxx,在这个过程中,如果其依赖的模块还有其他模块在使用,则不卸载(Used by != 0)。

循环依赖

内核模块是不允许循环依赖的,所谓循环依赖就是A依赖B,B依赖A。根据内核模块的加载过程,就知道循环依赖是不允许的。

posted @ 2025-08-12 19:22  thammer  阅读(32)  评论(0)    收藏  举报