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文件。