diff --git a/articles/20230731-linux-kernel-lib-intro.md b/articles/20230731-linux-kernel-lib-intro.md new file mode 100644 index 0000000000000000000000000000000000000000..9553fd7aa3d720840ea2bc2c52312f3288c40666 --- /dev/null +++ b/articles/20230731-linux-kernel-lib-intro.md @@ -0,0 +1,393 @@ +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces urls refs]
+> Author: Jingqing3948 <2351290287@qq.com>
+> Date: 2023/07/31
+> Revisor: Falcon
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Sponsor: PLCT Lab, ISCAS + +# Linux 内核库函数(kernel libc)简介 + +## 简介 + +本文主要介绍了实验盘中 Linux 内核源码下的 lib 库,包括其目录结构、基本功能和测试方法。为简单起见,以下简称 Linux 内核函数库为 kernel libc。请注意,这里并非用户态的 klibc 函数库。 + +## kernel libc 是什么 + +在此先声明一下我们指的 Linux 内核函数库,这个是 Linux kernel 源码中的 C 库。 + +获取 kernel libc 库的两种方法: + +1. 实验盘文件:进入 `Linux Lab` 后,可以在 `src/linux-stable/lib` 文件夹下找到。 +2. 官网下载解压后,得到 `linux[-v]` 文件夹,然后在其下的 `lib` 文件夹中可以找到。[官网下载链接:https://www.kernel.org/][001] + +## kernel libc 目录结构 + +![1689861637506](images/riscv-klibc/20230731-kernel-libc-file-structure.jpg) + +通过 chatgpt 总结分类: + +1. 内核功能相关文件 + + - 内核自旋锁、互斥锁、读写锁等锁相关的自测文件 + - locking-selftest-wlock-hardirq.h + - locking-selftest-wlock-softirq.h + - locking-selftest-mutex.h + - locking-selftest-rlock.h + - locking-selftest-rlock-hardirq.h + - locking-selftest-rlock-softirq.h + - locking-selftest-rsem.h + - locking-selftest-rtmutex.h + - locking-selftest-softirq.h + - locking-selftest-spin.h + - locking-selftest-spin-hardirq.h + - locking-selftest-spin-softirq.h + - locking-selftest.c + + - 早期 cpio 文件支持 + + - earlycpio.c + + - 故障注入(及其拷贝)用于模拟故障的发生 + + - fault-inject.c + + - fault-inject-usercopy.c + + - Undefined Behavior Sanitizer 相关的文件 + + - ubsan.c + + - Kconfig.ubsan + + - ubsan.h + + - 用于 Kernel Address SANitizer 的文件 + + - kasan 相关文件 + + - 内核 Fence 机制相关文件 + + - kfence + + - 内核 Concurrency Sanitizer 相关文件 + + - kcsan + +2. 文件系统和配置相关文件: + + - 用于文件系统或其他模块的文件 + - 842 + - 用于异常处理的外部表相关文件 + - extable.c + - 用于内核启动配置的文件 + - bootconfig.c + - 用于处理命令行参数的文件 + - cmdline.c + - cmdline_kunit.c + - 生成 CRC 表的文件 + - gen_crc*.c + - KUnit 测试框架相关文件 + - Kconfig + - kunit + - 用于内核后期初始化的文件 + - late_init.c + - 用于内核热补丁的相关文件 + - livepatch + - OID 注册表相关文件 + - oid_registry.c + - build_OID_registry + +3. 硬件和驱动相关文件 + + - Flattened Device Tree (FDT) 相关文件 + - fdt_addresses.c + - fdt.c + - fdt_empty_tree.c + - fdt_ro.c + - fdt_rw.c + - 用于逻辑设备的文件 + - logic_*.c + - PCI I/O 映射相关文件 + - pci_iomap.c + - 用于硬件或数据结构的文件 + - bch.c + - bsearch.c + - btree.c + +4. 网络和协议相关文件: + + - 用于审计相关功能的文件 + - audit.c + - 非屏蔽中断回溯相关文件 + - nmi_backtrace.c + - 用于文本搜索的文件 + - textsearch.c + - Virtual Dynamic Shared Object 相关文件 + - vdso + +5. 数据结构和算法相关文件 + + - 关联数组相关文件 + - assoc_array.c + - 基数树相关文件 + - radix-tree.c + - 位域测试文件 + - bitfield_kunit.c + - 用于内核功能测试的文件 + - 一些以 `test_` 开头的文件 + +6. 其他文件: + + - 密码学相关文件 + - crypto + - 用于构建 ID 的文件 + - buildid.c + - 数学库相关文件 + - math + - 内存相关文件 + - memcat_p.c + - UUID 相关文件 + - uuid.c + - zlib 压缩库相关文件 + - zlib_* + - 用于输出函数的文件 + - vsprintf.c + +## kernel libc 配置与编译 + +kernel libc 下包含各种库函数的实现,为了方便稍后理解如何添加新的库函数,这里简单分析一下其配置和编译管理方式。 + +`lib/` 目录下包含几类文件,一类是源码,还有一类是 Kconfig,再有一类是 Makefile,后面两个用于配置和编译相关的控制,本节主要分析它们。 + +### kernel libc 配置方式 + +Kconfig 文件是一种用于配置选项的脚本文件。它定义了内核 libc 的各个配置选项,包括库函数的开启或关闭,功能的选择等。这样的配置方式可以让开发者根据需求进行灵活的定制,避免在编译时包含不必要的函数,从而减小库的体积。 + +Kconfig 文件的组织通常是由顶层的 Kconfig 文件引用其他子目录下的 Kconfig 文件,逐层组织整个配置树。开发者通过 `make menuconfig` 命令可以进入一个文本式的配置界面,从而通过交互式的方式选择需要开启或关闭的功能,这些选择将在生成配置文件 `.config` 中体现。 + +### kernel libc 编译方式 + + +Makefile 中配置 kernel libc 文件的编译方式,主要分为两个部分:编译选项,编译控制部分。 + +编译选项:`obj-y ` 格式设定导出文件; + +编译控制:通过声明一个与此模块相关联的控制变量,使得可以在 Kconfig 中通过对此变量赋值的方式控制是否编译此模块。 + +## 如何添加新的 kernel libc 库函数 + +从上过程中可以分析得到:添加一个 kernel libc 文件需要的步骤: + +1. 编写 c 文件,在要导出的函数结尾加:`EXPORT_SYMBOL(xxx);` 来在内核中导出符号供其他文件可以访问; +2. 在 Makefile 中添加 `obj-y` 设定如何将 C 源文件编译成目标文件,以及 `obj-$(CONFIG_XXX) += xxx.o` 使得这一符号可以在 Kconfig 文件中被找到; +3. 在 Kconfig 文件中特定 menu 层级内添加该 config 变量,设定变量类型(如常见为 bool 类型,可以在 menuconfig 中回车勾选)以及提示信息 `tristate`; +4. 在 `make kernel-menuconfig` 配置中打开对应选项; +5. `make kernel` 重新编译; +6. 之后就可以运行使用该模块了。 + +## 如何测试新的 kernel libc 库函数 + + +整体的测试流程如下: + +- 在 lib 库里编写相应的 test_xxx.c 文件。 +- 在 lib/makefile 里添加编译时的配置。 +- 在 lib/Kconfig 里添加配置。 +- 在 menuconfig 里启动这个 test 测试用例。 +- 如果未全部通过则继续修改库函数,直至通过测试。 + +下面我们以 printf 函数的测试为例展开分析。 + +首先查看 vsprintf.c, vsprintf 函数可以把输出存到字符缓冲中: + +```c +// src/linux-stable/lib/vsprintf.c:2707 + +/** + * vsnprintf - Format a string and place it in a buffer + * ... + * If you're not already dealing with a va_list consider using snprintf(). + */ +int vsnprintf(char *buf, size_t size, const char *fmt, va_list args); +EXPORT_SYMBOL(vsnprintf); +``` + +然后摘取 test_printf.c 中的一部分:test string 部分分析。首先封装了 do_test 和 test 函数: + +```c +// src/linux-stable/lib/test_printf.c:41 + +do_test(int bufsize, const char *expect, int elen, + const char *fmt, va_list ap) +{ + va_list aq; + int ret, written; + + total_tests++; + + memset(alloced_buffer, FILL_CHAR, BUF_SIZE + 2*PAD_SIZE); + va_copy(aq, ap); + ret = vsnprintf(test_buffer, bufsize, fmt, aq); + va_end(aq); + + if (ret != elen) {// 输入长度是否和预期长度匹配 + pr_warn("vsnprintf(buf, %d, \"%s\", ...) returned %d, expected %d\n", + bufsize, fmt, ret, elen); + return 1; + } + + if (memchr_inv(alloced_buffer, FILL_CHAR, PAD_SIZE)) {// 写入范围是否超出了 PAD_SIZE + pr_warn("vsnprintf(buf, %d, \"%s\", ...) wrote before buffer\n", bufsize, fmt); + return 1; + } + + if (!bufsize) {// bufsize==0 但是有输入信息 + if (memchr_inv(test_buffer, FILL_CHAR, BUF_SIZE + PAD_SIZE)) { + pr_warn("vsnprintf(buf, 0, \"%s\", ...) wrote to buffer\n", + fmt); + return 1; + } + return 0; + } + + written = min(bufsize-1, elen); + if (test_buffer[written]) {// 校验结尾是否是、0 + pr_warn("vsnprintf(buf, %d, \"%s\", ...) did not nul-terminate buffer\n", + bufsize, fmt); + return 1; + } + + if (memchr_inv(test_buffer + written + 1, FILL_CHAR, BUF_SIZE + PAD_SIZE - (written + 1))) {// 是否是在 PAD_SIZE 后面终止的 + pr_warn("vsnprintf(buf, %d, \"%s\", ...) wrote beyond the nul-terminator\n", + bufsize, fmt); + return 1; + } + + if (memcmp(test_buffer, expect, written)) {// 比较一下写入缓冲区的数据和预期是否一样,assert + pr_warn("vsnprintf(buf, %d, \"%s\", ...) wrote '%s', expected '%.*s'\n", + bufsize, fmt, test_buffer, written, expect); + return 1; + } + return 0;// 以上问题都没有碰到,成功 +} +``` + +```c +// src/linux-stable/lib/test_printf.c:95 + +static void __printf(3, 4) __init +__test(const char *expect, int elen, const char *fmt, ...) +{ + va_list ap; + int rand; + char *p; + + if (elen >= BUF_SIZE) {// 长度超出缓冲区,直接不存了 + pr_err("error in test suite: expected output length %d too long. Format was '%s'.\n", + elen, fmt); + failed_tests++; + return; + } + + va_start(ap, fmt); + + failed_tests += do_test(BUF_SIZE, expect, elen, fmt, ap);// 统计多少用例没有通过 + rand = 1 + prandom_u32_max(elen+1); + /* Since elen < BUF_SIZE, we have 1 <= rand <= BUF_SIZE. */ + failed_tests += do_test(rand, expect, elen, fmt, ap);// 随机缓冲区长度 + failed_tests += do_test(0, expect, elen, fmt, ap);// 0 缓冲区长度 + + p = kvasprintf(GFP_KERNEL, fmt, ap); + if (p) { + total_tests++; + if (memcmp(p, expect, elen+1)) { + pr_warn("kvasprintf(..., \"%s\", ...) returned '%s', expected '%s'\n", + fmt, p, expect); + failed_tests++; + } + kfree(p); + } + va_end(ap); +} +``` + +然后在具体测试用例中传入参数交给 test。比如下例是测试打印字符串。 + +```c +// src/linux-stable/lib/test_printf.c:183 + +static void __init +test_string(void) +{ + test("", "%s%.0s", "", "123");// 参数 1 是预期输出,参数 2 是占位符组合,后面变长参数是传入占位符的数据 + test("ABCD|abc|123", "%s|%.3s|%.*s", "ABCD", "abcdef", 3, "123456"); + test("1 | 2|3 | 4|5 ", "%-3s|%3s|%-*s|%*s|%*s", "1", "2", 3, "3", 3, "4", -3, "5"); + test("1234 ", "%-10.4s", "123456"); + test(" 1234", "%10.4s", "123456"); + test(" ", "%4.*s", -5, "123456"); + test("123456", "%.s", "123456"); + test("a||", "%.s|%.0s|%.*s", "a", "b", 0, "c"); + test("a | | ", "%-3.s|%-3.0s|%-3.*s", "a", "b", 0, "c"); +} +``` + +比如 `vsprintf.c` 中的控制变量声明: + +```makefile +// src/linux-stable/lib/Makefile:83 + +obj-$(CONFIG_TEST_PRINTF) += test_printf.o +``` + +根据 CONFIG_TEST_PRINTF 变量来判断是否编译出 test_printf 的目标文件。 + +然后在 lib/KConfig.debug 里可以看到 test 文件设定在 menuconfig 的什么位置下。 + +```shell +menu "Kernel hacking" +config TEST_PRINTF + tristate "Test printf() family of functions at runtime" +``` + +在 linux-lab 目录下执行 `make kernel-menuconfig`,可以看到 printf 对应的测试项: + +``` +kernel-hacking + -> Kernel Testing and Coverage + -> Run time Testing + -> Test printf() family of functions at runtime +``` + +根据其 menu 设定,这个测试文件模块被添加到了 `Run time Testing` 也就是运行时自检部分中,只要执行 `make boot` 运行时就会自动运行此测试文件输出测试结果。 + +我们将其启用: + +![image-20230720214745198](images/riscv-klibc/20230731-kernel-libc-menuconfig.jpg) + +也就是说我们在 menuconfig 里启用了 "Test printf() family of functions at runtime" 这一项,则相当于启用了 TEST_PRINTF 这个变量,则 makefile 中 CONFIG_TEST_PRINTF 这个变量也会被启用,然后编译链接的时候就会添加 test_printf.c 生成的目标文件,并在运行内核的时候一并运行。 + +完成配置后,`make kernel` 重新编译内核,并 `make boot` 运行,可以看到打印的调试信息中出现了: + +```shell +test_printf: loaded. +crng possibly not yet initialized. plain 'p' buffer contains "(____ptrval____)" +test_printf: crng possibly not yet initialized. plain 'p' buffer contains "(____ptrval____)" +test_printf: crng possibly not yet initialized. plain 'p' buffer contains "(____ptrval____)" +test_printf: crng possibly not yet initialized. plain 'p' buffer contains "(____ptrval____)" +test_printf: all 416 tests passed +``` + +![image-20230720215808605](images/riscv-klibc/20230731-kernel-libc-test-vsprint-example.jpg) + +这一段说明测试模块顺利启动并通过测试。 + +## 总结 + +本文主要分析了 kernel libc 库的目录结构并介绍了如何添加和测试新的库函数。 + +对于 Linux kernel 库的使用,本文只做了一个简单尝试。后续可能展开尝试 kernel libc 库的具体用法,以及继续对 kernel libc 库进行分析。 + +## 参考资料 +- [https://www.kernel.org][001] + +[001]: https://www.kernel.org/ diff --git a/articles/images/riscv-klibc/20230731-kernel-libc-file-structure.jpg b/articles/images/riscv-klibc/20230731-kernel-libc-file-structure.jpg new file mode 100644 index 0000000000000000000000000000000000000000..03537663d4926422a538475b21d1ef3269d417a8 Binary files /dev/null and b/articles/images/riscv-klibc/20230731-kernel-libc-file-structure.jpg differ diff --git a/articles/images/riscv-klibc/20230731-kernel-libc-menuconfig.jpg b/articles/images/riscv-klibc/20230731-kernel-libc-menuconfig.jpg new file mode 100644 index 0000000000000000000000000000000000000000..90a8d9144932c180ebfb8a62eee5d62071fa988c Binary files /dev/null and b/articles/images/riscv-klibc/20230731-kernel-libc-menuconfig.jpg differ diff --git a/articles/images/riscv-klibc/20230731-kernel-libc-test-vsprint-example.jpg b/articles/images/riscv-klibc/20230731-kernel-libc-test-vsprint-example.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b0b43c64607dc56d45927e8421d26d13477e58c4 Binary files /dev/null and b/articles/images/riscv-klibc/20230731-kernel-libc-test-vsprint-example.jpg differ