diff --git a/articles/20230731-riscv-klibc-linux-kernel-libc-intro.md b/articles/20230731-riscv-klibc-linux-kernel-libc-intro.md new file mode 100644 index 0000000000000000000000000000000000000000..fe754a3c7e0036f530cbe301c86005894c9db774 --- /dev/null +++ b/articles/20230731-riscv-klibc-linux-kernel-libc-intro.md @@ -0,0 +1,429 @@ +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc1 - [spaces pangu autocorrect]
+> 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 库,包括其架构、基本功能、测试方法。后续第二篇文章还会引入 klibc 在 Linux 官网上单独下载的内核库的用法和介绍。 + +## kernel libc 是什么 + +创立目的:为嵌入式,启动准备的 Linux 内核精简小型 c 库。 + +他具有:轻量,高效,可靠,尽可能减少对 os 和硬件的依赖,启动快等特点。 + +在此先声明一下我们指的 kernel libc 库,这个是 Linux kernel 中的 libc 库,而不是直接下载 klibc 库得到的,笔者和吴老师讨论后决定:**用 kernel libc 指代 Linux kernel 中的 c 库,用 klibc 指代单独下载的 klibc 库。** + +获取 kernel libc 库的两种方法: + +1. 实验盘文件:进入 linux-lab 后,可以在 `src/linux-stable/lib` 文件夹下找到。s +2. https://www.kernel.org/ 官网下载解压后,得到 `linux[-v]` 文件夹,其下的 `lib` 文件夹中可以找到。 + +## kernel libc vs glibc + +glibc 通用库相较 kernel libc 体积较大且功能齐全。kernel libc 在其轻量的特点上也付出了一些代价,貌似在优化方面做的并不如 glibc 好。但是能精简代码这点在嵌入式领域比较重要。 + +## 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 + +## 新库函数的测试 + +首先熟悉一下测试流程。 + +在添加完一个库函数后,编写相应的测试文件,并不是通过直接编译运行此 test 文件来执行的,而是通过编辑 Makefile 文件,Kconfig 文件。以及在 kernel-menuconfig 里面,启动相应的测试文件,这样在 `make boot` 阶段即可执行。因此整体的测试流程如下: + +- 在 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 + * @buf: The buffer to place the result into + * @size: The size of the buffer, including the trailing null space + * @fmt: The format string to use + * @args: Arguments for the format string + * + * This function generally follows C99 vsnprintf, but has some + * extensions and a few limitations: + * + * - ``%n`` is unsupported + * - ``%p*`` is handled by pointer() + * + * See pointer() or Documentation/core-api/printk-formats.rst for more + * extensive description. + * + * **Please update the documentation in both places when making changes** + * + * The return value is the number of characters which would + * be generated for the given input, excluding the trailing + * '\0', as per ISO C99. If you want to have the exact + * number of characters written into @buf as return value + * (not including the trailing '\0'), use vscnprintf(). If the + * return is greater than or equal to @size, the resulting + * string is truncated. + * + * 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); + + /* + * Every fmt+args is subjected to four tests: Three where we + * tell vsnprintf varying buffer sizes (plenty, not quite + * enough and 0), and then we also test that kvasprintf would + * be able to print it as expected. + */ + 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"); + /* + * POSIX and C99 say that a negative precision (which is only + * possible to pass via a * argument) should be treated as if + * the precision wasn't present, and that if the precision is + * omitted (as in %.s), the precision should be taken to be + * 0. However, the kernel's printf behave exactly opposite, + * treating a negative precision as 0 and treating an omitted + * precision specifier as if no precision was given. + * + * These test cases document the current behaviour; should + * anyone ever feel the need to follow the standards more + * closely, this can be revisited. + */ + 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"); +} +``` + +然后在 Makefile 里有添加这么一行: + +```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 + +我们将其启用: + +![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 内核库如何编译、运行、测试。 + +同样,我们也可以借由这个过程反推出:如果我们要新添加一个库函数,添加流程以及如何对其展开测试。 + +- 首先,在 lib 库文件夹里编写相应文件; +- 在 lib/Makefile 里以大致为 `obj-$(CONFIG_NAME) += XXX.o` 的形式把这个模块定义为 CONFIG_NAME,让 Kconfig 可见; +- 在 lib/Kconfig 里规定 CONFIG_NAME 的数据类型(如可选的 bool 型),所属菜单使得开发者使用时知道从 menuconfig 的什么位置去启用等; +- 在 menuconfig 中勾选启用对应模块,保存 .config 文件后 `make kernel` 重新编译,这样下次启动后该模块就会同时被启用; +- 测试文件的编写:基本流程同上,编写相应文件,在 lib/Kconfig.debug 里添加测试文件的信息,通常以 test_xxx 格式命名,添加于 `make kernel-menuconfig` -> kernel-hacking -> Kernel Testing and Coverage -> Run time Testing 目录下; +- 下次编译后 `make boot` 启动时就会自动运行 test 文件完成启动时的测试。 + +对于 Linux kernel 库的使用,本文只做了一个简单尝试。后续可能展开尝试 kernel libc 库的具体用法,以及继续对 kernel libc 库进行分析。 + +## 参考资料 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