3.5.4. 内核是如何导出符号的¶
符号是什么东西?我们为什么需要导出符号呢?内核模块如何导出符号呢?其他模块又是如何找到这些符号的呢?
这是这一小节讨论的知识,实际上,符号指的就是内核模块中使用EXPORT_SYMBOL声明的函数和变量。当模块被装入内核后,它所导出的符号都会记录在公共内核符号表中。在使用命令insmod加载模块后,模块就被连接到了内核,因此可以访问内核的共用符号。
通常情况下我们无需导出任何符号,但是如果其他模块想要从我们这个模块中获取某些方便的时候,
就可以考虑使用导出符号为其提供服务。这被称为模块层叠技术。
例如msdos文件系统依赖于由fat模块导出的符号;USB输入设备模块层叠在usbcore和input模块之上。
也就是我们可以将模块分为多个层,通过简化每一层来实现复杂的项目。
modprobe是一个处理层叠模块的工具,它的功能相当于多次使用insmod,除了装入指定模块外还同时装入指定模块所依赖的其他模块。该命令在前面的“内核模块常用命令”中也有提及。
当我们要导出模块的时候,可以使用下面的宏
EXPORT_SYMBOL(name)
EXPORT_SYMBOL_GPL(name) //name为我们要导出的标志
符号必须在模块文件的全局部分导出,不能在函数中使用,_GPL使得导出的模块只能被GPL许可的模块使用。
编译我们的模块时,这两个宏会被拓展为一个特殊变量的声明,存放在ELF文件中。
具体也就是存放在ELF文件的符号表中:
st_name: 是符号名称在符号名称字符串表中的索引值
st_value: 是符号所在的内存地址
st_size: 是符号大小
st_info: 是符号类型和绑定信息
st_shndx: 表示符号所在section
当ELF的符号表被加载到内核后,会执行simplify_symbols来遍历整个ELF文件符号表。
根据st_shndx找到符号所在的section和st_value中符号在section中的偏移得到真正的内存地址。并最终将符号内存地址,符号名称指针存储到内核符号表中。
simplify_symbols函数原型如下:
simplify_symbols函数 (内核源码/kernel/module.c)¶
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75/********************************************************************************************
* @brief simplify_symbols()
* @note 在模块加载过程中简化模块的符号表(内部函数)
* @param mod 指向 module 结构体的指针,该结构体代表了一个内核模块
* @param info 指向 load_info 结构体的指针,该结构体包含了模块加载时的详细信息
* @return 成功时返回0,失败时返回负数(ret:错误码)
********************************************************************************************/
static int simplify_symbols(struct module *mod, const struct load_info *info)
{
Elf_Shdr *symsec = &info->sechdrs[info->index.sym];
Elf_Sym *sym = (void *)symsec->sh_addr;
unsigned long secbase;
unsigned int i;
int ret = 0;
const struct kernel_symbol *ksym;
for (i = 1; i < symsec->sh_size / sizeof(Elf_Sym); i++) {
const char *name = info->strtab + sym[i].st_name;
switch (sym[i].st_shndx) {
case SHN_COMMON:
/* Ignore common symbols */
if (!strncmp(name, "__gnu_lto", 9))
break;
/* We compiled with -fno-common. These are not
supposed to happen. */
pr_debug("Common symbol: %s\n", name);
pr_warn("%s: please compile with -fno-common\n",
mod->name);
ret = -ENOEXEC;
break;
case SHN_ABS:
/* Don't need to do anything */
pr_debug("Absolute symbol: 0x%08lx\n",
(long)sym[i].st_value);
break;
case SHN_LIVEPATCH:
/* Livepatch symbols are resolved by livepatch */
break;
case SHN_UNDEF:
ksym = resolve_symbol_wait(mod, info, name);
/* Ok if resolved. */
if (ksym && !IS_ERR(ksym)) {
sym[i].st_value = kernel_symbol_value(ksym);
break;
}
/* Ok if weak or ignored. */
if (!ksym &&
(ELF_ST_BIND(sym[i].st_info) == STB_WEAK ||
ignore_undef_symbol(info->hdr->e_machine, name)))
break;
ret = PTR_ERR(ksym) ?: -ENOENT;
pr_warn("%s: Unknown symbol %s (err %d)\n",
mod->name, name, ret);
break;
default:
/* Divert to percpu allocation if a percpu var. */
if (sym[i].st_shndx == info->index.pcpu)
secbase = (unsigned long)mod_percpu(mod);
else
secbase = info->sechdrs[sym[i].st_shndx].sh_addr;
sym[i].st_value += secbase;
break;
}
}
return ret;
}
内核导出的符号表结构有两个字段,一个是符号在内存中的地址,一个是符号名称指针,
符号名称被放在了 __ksymtab_strings 这个section中,以 EXPORT_SYMBOL 举例,符号会被放到名为 ___ksymtab 的section中。这个结构体我们要注意,它构成的表是导出符号表而不是通常意义上的符号表 。
kernel_symbol结构体(内核源码/include/linux/export.h)¶
1
2
3
4
5struct kernel_symbol {
unsigned long value;
const char *name;
const char *namespace;
};
kernel_symbol结构体成员¶
成员
描述
value
符号的虚拟地址。在内核中,这通常是函数或变量在内存中的地址。对于内核符号,这个地址是相对于内核的虚拟地址空间的。
name
符号的名称
namespace
符号的命名空间。在内核中,命名空间用于区分不同类型的符号。
其他的内核模块在寻找符号的时候会调用resolve_symbol_wait去内核和其他模块中通过符号名称寻址目标符号,resolve_symbol_wait会调用resolve_symbol,进而调用 find_symbol。找到了符号之后,把符号的实际地址赋值给符号表 sym[i].st_value = ksym->value。
注意
find_symbol 函数是 Linux 内核中用于搜索特定符号的函数。这个函数在内核模块加载、符号解析和依赖管理中扮演着重要角色。当你需要找到一个符号的地址、所有者、CRC 校验和、许可证信息等时,可以使用这个函数。
find_symbol函数(内核源码/kernel/module.c)¶
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57/************************************************************************************
* @brief find_symbol()
* @note 用于搜索特定符号的函数
* @param name 要搜索的符号名称,这个名称应该与内核中的一个符号完全匹配
* @param owner 指向指向 module 结构体的指针的指针
* @param crc 设置为指向该符号的 CRC 校验和,如果不关心CRC校验和,设置为 NULL
* @param license 设置为指向该符号的许可证信息,如果你不关心许可证信息,设置为 NULL
* @param gplok 指定是否允许在 GPL 兼容的模块中搜索符号
* @param warn 指定如果找不到符号是否应该发出警告
* @return 成功找到返回该符号的地址,没有找到返回 NULL
************************************************************************************/
static const struct kernel_symbol *find_symbol(const char *name,
struct module **owner,
const s32 **crc,
enum mod_license *license,
bool gplok,
bool warn)
{
struct find_symbol_arg fsa;
fsa.name = name;
fsa.gplok = gplok;
fsa.warn = warn;
if (each_symbol_section(find_exported_symbol_in_section, &fsa)) {
if (owner)
*owner = fsa.owner;
if (crc)
*crc = fsa.crc;
if (license)
*license = fsa.license;
return fsa.sym;
}
pr_debug("Failed to find symbol %s\n", name);
return NULL;
}
/************************************************************************************
* @brief __symbol_get()
* @note 用于根据符号名称获取其对应的内核地址(内部函数)
* @param symbol 要查找的符号的名称。这个名称应该与内核中的一个符号完全匹配。
* @return 返回值是一个 void 指针,指向找到的符号的地址。符号不存在返回 NULL
************************************************************************************/
void *__symbol_get(const char *symbol)
{
struct module *owner;
const struct kernel_symbol *sym;
preempt_disable();
sym = find_symbol(symbol, &owner, NULL, NULL, true, true);
if (sym && strong_try_module_get(owner))
sym = NULL;
preempt_enable();
return sym ? (void *)kernel_symbol_value(sym) : NULL;
}
EXPORT_SYMBOL_GPL(__symbol_get);
注解
第25行:在each_symbol_section中,去查找了两个地方,一个是内核的导出符号表,即我们在将内核符号是如何导出的时候定义的全局变量,一个是遍历已经加载的内核模块,查找动作是在each_symbol_in_section中完成的。
第50行:__symbol_get函数调用了find_symbol函数。
第57行:导出符号标志。
至此符号查找完毕,最后将所有section借助ELF文件的重定向表进行重定向,就能使用该符号了。
到这里内核就完成了内核模块的加载/卸载以及符号导出,感兴趣的读者可以查阅内核源码目录下/kernel/module.c文件。