嵌入式Hacker (es-hacker)

Embedded bsp developer enjoys thinking and hacking opensource and develop boards(NanoPi, LicheePi, RPi...)

0%

Linux内核 / 基础组件 / 模块机制快速入门 (1)

哈喽,我是杰克吴,继续记录我的学习心得。

一、关于兴趣的几点思考

1. 享受不是兴趣,愿意付出才是:

  • 兴趣很容易跟享受混淆。享受是被动的,无需付出;而兴趣则要求你甘愿为了这件事情付出努力。

2.任何事情,接触皮毛的时候不要谈兴趣:

  • 在我开始公众号写文章之前,只是粗浅地觉得这个事不难我可以尝试一下,而事实上,持续写作的难度和意义超乎大多数人的想象。

  • 任何事情,先做到 60 分,再谈是否喜欢

3. 兴趣和爱好不太一样:

  • 区别在于你是否需要且愿意通过刻意练习以收获这个兴趣,以及这件事是否能给你带来持续的成就感。

  • 吃喝玩乐(旅游,逛街,买买买)是爱好,不是兴趣。纯粹的看电影是爱好,但是认真地写影评(经历了思考与分享)则算是兴趣。表面看上去都是同一件事,但是不同人会发展成不一样的结果

  • 最开始时可能只是爱好,但是随着你的持续思考和投入,可能会发展为你的理想职业

4. 兴趣可以带有功利性:

  • 那些看似功利的标准(例如高考、面试),存在很多偏差的部分,但不可否认,在绝大多数情况下,它们提供了较为高效和正确的努力方向

  • 把自己热爱的事情用来挣钱,非常好。只凭自己的兴致去做,确实会有更多愉悦,但这也是最廉价、最轻易的喜欢了,问题是,你很难真正做得好。你真的喜欢这个事,你会主动争取做好,赢得市场才会给你带来更长久的愉悦感


二、模块机制快速入门 (1)

目录:

1
1. 内核模块的使用
2
2. 内核模块的文件格式
3
3. EXPORT_SYMBOL 是如何实现符号导出的?
4
4. 相关参考

基于 Linux-4.14 + Arm-v7。

1. 内核模块的使用

最简单的内核模块:

1
#include <linux/init.h>
2
#include <linux/module.h>
3
4
static char *name = "embedded hacker";
5
module_param(name, charp, S_IRUGO);    // 指定模块可以接收的参数
6
7
static void print_hello(void)
8
{
9
    printk(KERN_INFO "Hello World, %s\n", name);
10
}
11
12
static int __init hello_init(void)
13
{
14
    printk(KERN_INFO "Hello World init\n");
15
    print_hello();
16
    return 0;
17
}
18
module_init(hello_init);
19
20
static void __exit hello_exit(void)
21
{
22
    printk(KERN_INFO "Hello World exit\n ");
23
}
24
module_exit(hello_exit);
25
26
EXPORT_SYMBOL(print_hello);   // 导出符号 print_hello
27
MODULE_AUTHOR("es-hacker");   // 指定作者
28
MODULE_LICENSE("GPL v2");     // 指定 license
29
MODULE_DESCRIPTION("A simple Hello World Module");  // 指定模块的描述信息
30
MODULE_ALIAS("a simplest module");  // 指定模块的别名

运行效果:

1
$ insmod hello.ko   // 加载模块
2
Hello World init    // 加载模块时,module_init() 里的函数被调用
3
Hello World, embedded hacker
4
5
$ rmmod hello       // 卸载模块
6
Hello World exit    // 卸载模块时,module_exit() 里的函数被调用
7
8
$ insmod hello.ko name=Jack // 指定模块参数
9
Hello World init
10
Hello World, Jack
11
12
$ rmmod hello
13
Hello World exit

到此,内核模块的使用方法就介绍完毕了,非常简单易用。

接下来是痛苦的部分:探索一下背后的实现机制

2. 内核模块的文件格式

可以用 file 命令确定一个文件的格式:

1
$ file hello.ko 
2
hello.ko: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), BuildID[sha1]=2feb2cb1328c0a9113658d6e90ac20d7e4c56384, not stripped

内核模块的格式为 ELF ( Executable and Linkable Format ):

目前不需要全面了解 ELF 文件格式的所有技术细节,只需要结合 Linux 源码中定义的 ELF 相关数据结构,简单了解一下 ELF 的构造即可。

静态的 ELF 文件视图总体上可分为 3 部分

  • 头部的 ELF header;

  • 中间的 Section;

  • 尾部的 Section header table

1) ELF header 部分:

作用:描述整个 ELF 文件。

组成:Linux 内核里的数据结构定义如下,注释部分为内核模块机制相关的的成员。

1
typedef struct elf32_hdr{
2
  unsigned char	e_ident[EI_NIDENT];
3
4
  /* 文件类型 */
5
  Elf32_Half	e_type;
6
  Elf32_Half	e_machine;
7
  Elf32_Word	e_version;
8
9
  /* Entry point */
10
  Elf32_Addr	e_entry;
11
  Elf32_Off	e_phoff;
12
13
  /* Section header table 在文件中的偏移量 */
14
  Elf32_Off	e_shoff;
15
  Elf32_Word	e_flags;
16
  Elf32_Half	e_ehsize;
17
  Elf32_Half	e_phentsize;
18
  Elf32_Half	e_phnum;
19
20
  /* Section header table 中 entry 的大小 */
21
  Elf32_Half	e_shentsize;
22
23
  /* Section header table 中有多少个 entry */
24
  Elf32_Half	e_shnum;
25
  Elf32_Half	e_shstrndx;
26
} Elf32_Ehdr;

实践:

1
2
$ # readelf hello.ko -h       # [-h|--file-header]
3
ELF Header:
4
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
5
  Class:                             ELF32
6
  Data:                              2's complement, little endian
7
  Version:                           1 (current)
8
  OS/ABI:                            UNIX - System V
9
  ABI Version:                       0
10
  Type:                              REL (Relocatable file)
11
  Machine:                           ARM
12
  Version:                           0x1
13
  Entry point address:               0x0
14
  Start of program headers:          0 (bytes into file)
15
  Start of section headers:          59648 (bytes into file)
16
  Flags:                             0x5000000, Version5 EABI
17
  Size of this header:               52 (bytes)
18
  Size of program headers:           0 (bytes)
19
  Number of program headers:         0
20
  Size of section headers:           40 (bytes)
21
  Number of section headers:         52
22
  Section header string table index: 51

2) Section 部分:

作用:对应人们常说的各种数据段、代码段等,术语是 section。

组成:ELF 文件的主体,位于文件视图中间部分的一个连续区域中。但是当模块被内核加载时,会根据各自属性被重新分配到新的内存区域。

3) Section header table 部分:

作用:每一个条目(术语叫 entry) 就是一个 Section header,负责描述 Section;

组成:由若干个 Section header entry 组成,Linux 内核里的数据结构定义如下 (注释部分为内核模块机制相关的的成员):

1
typedef struct elf32_shdr {
2
  Elf32_Word	sh_name;
3
  Elf32_Word	sh_type;
4
  Elf32_Word	sh_flags;
5
6
  /* 对应的 section 在内存中的实际地址。初始值为0,当模块被内核加载时,会被修改为 section 在内存中的实际地址 */
7
  Elf32_Addr	sh_addr;
8
9
  /* section 在文件视图中的偏移量 */
10
  Elf32_Off	sh_offset;
11
12
  /* section 在文件视图中的大小 */
13
  Elf32_Word	sh_size;
14
  Elf32_Word	sh_link;
15
  Elf32_Word	sh_info;
16
  Elf32_Word	sh_addralign;
17
  Elf32_Word	sh_entsize;
18
} Elf32_Shdr;

实践:

1
$ readelf hello.ko -S     # [-S|--section-headers|--sections]
2
There are 52 section headers, starting at offset 0xe900:
3
4
Section Headers:
5
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
6
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
7
  [ 1] .note.gnu.build-i NOTE            00000000 000034 000024 00   A  0   0  4
8
  [ 2] .text             PROGBITS        00000000 000058 000000 00  AX  0   0  1
9
  [...]
10
  [ 5] .init.text        PROGBITS        00000000 000070 00001c 00  AX  0   0  4
11
  [...]
12
  [ 7] .exit.text        PROGBITS        00000000 00008c 00000c 00  AX  0   0  4
13
  [...]
14
  [ 9] __ksymtab         PROGBITS        00000000 000098 000008 00   A  0   0  4
15
  [...]
16
  [25] __ksymtab_strings PROGBITS        00000000 0001f1 00000c 00   A  0   0  1
17
  [26] __param           PROGBITS        00000000 000200 000014 00   A  0   0  4
18
  [27] .rel__param       REL             00000000 00b9e4 000020 08   I 49  26  4
19
  [28] __versions        PROGBITS        00000000 000214 000100 00   A  0   0  4
20
  [29] .data             PROGBITS        00000000 000314 000004 00  WA  0   0  4
21
  [...]
22
  [48] .ARM.attributes   ARM_ATTRIBUTES  00000000 00b21a 000031 00      0   0  1
23
  [49] .symtab           SYMTAB          00000000 00b24c 000520 10     50  75  4
24
  [50] .strtab           STRTAB          00000000 00b76c 0001cd 00      0   0  1
25
  [51] .shstrtab         STRTAB          00000000 00e6e4 00021b 00      0   0  1
26
27
Key to Flags:
28
  W (write), A (alloc), X (execute), M (merge), S (strings)
29
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
30
  O (extra OS processing required) o (OS specific), p (processor specific)

这里只截取模块加载相关的部分 section header,现在有个初步印象就好,后续使用到了相关的 secition header,再做进一步的研究分析。

内核模块自身并不会使用到上述数据结构 (elf32_hdr、elf32_shdr),它们是给内核模块加载器在加载模块时使用的。

3. EXPORT_SYMBOL() 是如何实现符号导出的?

EXPORT_SYMBOL() 系列宏用来向外界导出一个符号。内核和内核模块通过符号表的形式向外部世界导出符号的相关信息。

为什么要导出符号?

  • 如果没有独立存在的内核模块,作为单一的 Linux 内核映像,就没必要导出符号了。对于静态编译链接而成的内核映像而言,所有的符号引用都会在静态链接阶段完成。

  • 有了内核模块之后,独立编译链接的内核模块要使用到内核提供的基础设施(即调用内核函数,例如 printk)的话,就必须要解决符号引用问题 (unresolved symbol)。

  • 可以用 nm 命令来查看一个模块中出现的未定义符号:

1
$ nm hello.o -u         # [-u|--undefined-only]
2
         U __aeabi_unwind_cpp_pr0
3
         U param_ops_charp
4
         U printk
5
         U __this_module
  • 处理 unresolved symbol 问题的本质是在模块加载期间找到该符号在内存中的实际地址。

从全局上看,EXPORT_SYMBOL 的完整实现包括 3 部分:

  • EXPORT_SYMBOL 的定义部分

  • 链接脚本链接器部分

  • 使用导出符号部分

EXPORT_SYMBOL 的定义:

1
// include/linux/export.h
2
#define EXPORT_SYMBOL(sym) __EXPORT_SYMBOL(sym, "")
3
4
/* For every exported symbol, place a struct in the __ksymtab section */
5
#define ___EXPORT_SYMBOL(sym, sec)					\
6
	extern typeof(sym) sym;						\
7
	__CRC_SYMBOL(sym, sec)						\
8
	static const char __kstrtab_##sym[]				\
9
	__attribute__((section("__ksymtab_strings"), aligned(1)))	\
10
	= VMLINUX_SYMBOL_STR(sym);					\
11
	static const struct kernel_symbol __ksymtab_##sym		\
12
	__used								\
13
	__attribute__((section("___ksymtab" sec "+" #sym), used))	\
14
	= { (unsigned long)&sym, __kstrtab_##sym }

以 hello.ko 为例,EXPORT_SYMBOL(print_hello) 本质上就是定义了 2 个变量:

1
static const char __kstrtab_print_hello[] = "print_hello"
2
3
static const struct kernel_symbol __ksymtab_print_hello = {
4
  (unsigned long)&print_hello,
5
  __kstrtab_print_hello,
6
};
  • 变量1: char []

    • 用于保存符号名;
    • 被放置在名为 “__ksymtab_strings” 的 section 中;
  • 变量2: struct kernel_symbol

    • 用于保存符号名与地址;
    • 被放置在名为 “___ksymtab+print_hello” 的 section 中;

根据 scripts/module-common.lds 里的定义:

1
SECTIONS {
2
  [...]
3
	__ksymtab		0 : { *(SORT(___ksymtab+*)) }
4
  [...]
5
}

_ksymtab+print_hello” 会被转换为 “ksymtab”,这样就跟我们用 readelf hello.ko -S 查看到的 section 对应上了。

为了让内核可以通过上述 __ksymtab section 找到被导出的符号,链接器必须导出 section 的地址

1
include/asm-generic/vmlinux.lds.h
2
3
	/* Kernel symbol table: Normal symbols */			\
4
	__ksymtab         : AT(ADDR(__ksymtab) - LOAD_OFFSET) {		\
5
		VMLINUX_SYMBOL(__start___ksymtab) = .;			\
6
		KEEP(*(SORT(___ksymtab+*)))				\
7
		VMLINUX_SYMBOL(__stop___ksymtab) = .;			\
8
	}	
9
10
	/* Kernel symbol table: strings */				\
11
	__ksymtab_strings : AT(ADDR(__ksymtab_strings) - LOAD_OFFSET) {	\
12
		*(__ksymtab_strings)					\
13
	}

在 kernel/module.c 中,可以看到下列声明:

1
/* Provided by the linker */
2
extern const struct kernel_symbol __start___ksymtab[];
3
extern const struct kernel_symbol __stop___ksymtab[];
4
[...]

这些变量会在内核或者内核模块查找某个符号时被使用。

EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 导出符号的可见性

从这里开始重头戏模块加载的分析了,鉴于大多数人的注意力无法在一篇文章里上集中太久,更多的内容将放在后面的文章里。建议大家可以先自行阅读相关书籍,不是自己理解到的东西是消化不了的。

4. 相关参考

  • Linux 设备驱动开发详解,第 4 章节

  • 深入 Linux 设备驱动程序内核机制,第 1 章节

  • 深入 Linux 内核架构,第 7 章节

  • 深入理解 Linux 内核,第20 章节、附录2

5. 更多值得关注的知识点

  • 模块的加载

  • 模块的参数传递机制

  • 模块之间的依赖关系

  • 模块中的版本控制机制


三、思考技术,也思考人生

要学习技术,更要学习如何生活

你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。

嵌入式系统 (Linux、RTOS、OpenWrt、Android) 和 开源软件 感兴趣,关注公众号:嵌入式Hacker

觉得文章对你有价值,不妨点个 在看和赞

这是一篇有趣的文章吗?

欢迎关注我的其它发布渠道