From 72ec9bf5ae974b16aa9ac7c19a13cd686a84fa32 Mon Sep 17 00:00:00 2001
From: Jingqing3948 <2351290287@qq.com>
Date: Tue, 8 Aug 2023 23:24:07 +0800
Subject: [PATCH] add 0230808-riscv-klibc-function-analysis-1-v1-pr (after
using gpt to optimise format)
---
...808-riscv-klibc-str-function-analysis-1.md | 344 ++++++++++++++++++
1 file changed, 344 insertions(+)
create mode 100644 articles/20230808-riscv-klibc-str-function-analysis-1.md
diff --git a/articles/20230808-riscv-klibc-str-function-analysis-1.md b/articles/20230808-riscv-klibc-str-function-analysis-1.md
new file mode 100644
index 0000000..5df8189
--- /dev/null
+++ b/articles/20230808-riscv-klibc-str-function-analysis-1.md
@@ -0,0 +1,344 @@
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc2 - [tounix spaces codeinline]
+> Author: Jingqing3948 <2351290287@qq.com>
+> Date: 2023/08/02
+> Revisor: Falcon
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Sponsor: PLCT Lab, ISCAS
+
+# kernel libc 库分析之 str_1
+
+## 前言
+
+本文主要是展开分析 lib 库里的 str 类函数的一部分,以及对其的一些展开测试。
+
+## 列表及功能介绍
+
+```shell
+├── fdt_strerror.c
+├── kstrtox.c # kstrtox 主要是字符串 -> 整数转换相关
+├── kstrtox.h
+├── kunit
+│ ├── string-stream.c # 字符串流操作
+│ ├── string-stream.h
+│ └── string-stream-test.c
+├── strcat_kunit.c # 测试文件
+├── string.c # 字符串的一些基础操作
+├── string_helpers.c # 字符串的辅助处理函数,如获取大小、编码解码等
+├── strncpy_from_user.c # 从用户空间获取字符串的处理函数
+├── strnlen_user.c
+├── strscpy_kunit.c
+├── test_fortify
+│ ├── write_overflow-strcpy.c # 这一系列主要是先校验是否溢出,再决定是否调用函数
+│ ├── write_overflow-strcpy-lit.c
+│ ├── write_overflow-strlcpy.c
+│ ├── write_overflow-strlcpy-src.c
+│ ├── write_overflow-strncpy.c
+│ ├── write_overflow-strncpy-src.c
+│ ├── write_overflow-strscpy.c
+├── test-kstrtox.c
+├── test_string.c
+├── test-string_helpers.c
+├── ucs2_string.c # 处理通用字符集 UCS2 的字符串函数
+```
+
+因为篇幅问题本文只对其中的一部分展开分析,主要为 str 基础函数部分如 strcpy strcmp 等。
+
+本文主要是对 string 中的基础函数展开分析,以及其他文件中对应函数的对比扩展。
+
+由于大部分函数内容都存在于 string.c 中,少部分在其他文件中呈现,因此为了使结构不过于混乱,**本文规定:除非特殊声明,否则所有函数内容均属于 `lib/string.c` 和 `lib/test_string.c`.**
+
+## 字符串、内存比较函数优化
+
+字符串和内存的比较。
+
+| 函数名 | 参数 | 返回值 | 功能 |
+|-------------|----------------------------------------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------|
+| strncasecmp | const char *s1, const char *s2, size_t len | int | 比较两个字符串,忽视大小写区别。len 是最长判断范围。返回值为 0 则表示一致。采用 unsigned char 存储每一位要比较的字符,更能确保比较时字符串行为一致且正确 |
+| strcasecmp | const char *s1, const char *s2 | int | 重载的函数,没有给定判断长度限制,因此不用判断:当字符串结束的时候,长度是否小于 len |
+| strcmp | const char *cs, const char *ct | int | 逐位比较两个字符串是否每一位完全一致。如果第一个不一致位 cs>ct 返回 1,否则返回 -1;一致返回 0 |
+| strncmp | const char *cs, const char *ct, size_t count | int | 只比较指定长度 |
+| memcmp | const void *cs, const void *ct, size_t count | int | 优化后的指定内存空间比较 |
+| bcmp | const void *a, const void *b, size_t len | int | 调用了 memcmp,返回 0 表示一致,非 0 不一致。可以在 bcmp 中加一些操作比如对返回值的判断 |
+
+memcmp 所作优化部分代码如下:
+
+```c
+// src/linux-stable/lib/string.c:765
+
+#ifdef CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS
+ if (count >= sizeof(unsigned long)) {
+ const unsigned long *u1 = cs;
+ const unsigned long *u2 = ct;
+ do {
+ if (get_unaligned(u1) != get_unaligned(u2))
+ break;
+ u1++;
+ u2++;
+ count -= sizeof(unsigned long);
+ } while (count >= sizeof(unsigned long));
+ cs = u1;
+ ct = u2;
+ }
+#endif
+```
+
+如果定义了 `CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS`,就逐字比较。否则(或者需要比较的长度大小<1 字)就逐位比较。
+
+## 字符串、内存复制函数与优化
+
+复制 src 字符串到 dest 位置。
+
+| 函数名 | 参数 | 返回值 | 功能 |
+|---------|---------------------------------------------------------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| strcpy | char *dest, const char *src | char * | 一位一位赋值直到到达字符串结尾。后面会介绍优化版的 strcpy,可以按块复制 |
+| strncpy | char *dest, const char *src | char * | 复制指定长度。给 while 多加了一个判定结束的条件:`ount==0` |
+| strlcpy | char *dest, const char *src, size_t size | size_t | 把字符串指定长度复制给**缓冲区**,先判断 size 和 strlen 谁小,用小的作为复制长度调用 `memcpy`。
和 strncpy 的区别在于:strncpy 是指定复制 count 位过去,不管缓冲区是否溢出;`strlcpy` 是传入了 dest 的 size,来判断复制多少位可以不溢出,更安全。 |
+| strscpy | char *dest, const char *src, size_t count | size_t | 优化版的 `strcpy`。返回值标识是否复制成功,成功返回 0 |
+| stpcpy | char *\_\_restrict\_\_ dest, const char *\_\_restrict\_\_ src | char * | 复制结束后,让 dest 指向自己这个字符串的结尾 \\0 |
+| memcpy | void *dest, const void *src, size_t count | void * | 逐位复制 |
+
+strscpy 所作优化部分代码如下:
+
+1. count 不合法,即为 0 或超出 INT_MAX:返回错误码 -E2BIG。
+2. 如果启用了 `CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS`,而且 src 和 dest 都对齐了,则设置 max 限定复制的最大字节数以免越过页边界。每次复制的块大小是 `unsigned long` 32bits,如果 max >= 32bits 就复制一块过去,直到 max < 32bits,这时候开始逐位复制。
+
+```c
+// src/linux-stable/lib/string.c:200
+
+while (max >= sizeof(unsigned long)) {
+ unsigned long c, data;
+
+ c = read_word_at_a_time(src+res);
+ if (has_zero(c, &data, &constants)) {
+ data = prep_zero_mask(c, data, &constants);
+ data = create_zero_mask(data);
+ *(unsigned long *)(dest+res) = c & zero_bytemask(data);
+ return res + find_zero(data);
+ }
+ *(unsigned long *)(dest+res) = c;
+ res += sizeof(unsigned long);
+ count -= sizeof(unsigned long);
+ max -= sizeof(unsigned long);
+}
+```
+
+3. 读一个字长度出来,如果里面有 0(说明这个字中间终止了),则只复制到 0 位。截取 0 位以前的部分再补一个 0 位,复制给 dest。否则复制一整个 `unsigned long` 过去,然后开启下一轮复制循环。
+
+4. 跳出 3 的循环后,继续逐位复制到结尾。
+
+**lib/strncpy_from_user.c**:从用户空间拷贝字符串到内核空间。函数列表如下:
+
+| 函数名 | 参数 | 返回值 | 功能 |
+|----------------------|---------------------------------------------------------------------------|--------|-----------------------------------------------------------------------------------------------------------|
+| do_strncpy_from_user | char *dst, const char __user *src, unsigned long count, unsigned long max | long | 这里的 `strcpy` 和 `lib/string.c` 做的优化有异曲同工之妙,主要也是对齐后逐位进行处理。复制成功返回复制字节数。 |
+| strncpy_from_user | char *dst, const char __user *src, long count | long | `do_strncpy_from_user` 的包装类。先校验内存访问是否安全且在内核允许范围内,然后再决定是否内存访问 |
+
+do_strncpy_from_user 所作优化部分思路如下:
+
+- 首先了解一下如何从用户空间拷贝数据:`unsafe_get_user`,实际上是一个宏。不安全的主要原因是其没有做权限检查。调用方法:`unsafe_get_user(c, (unsigned long __user *)(src), byte_at_a_time);` 从 `src` 处获取 `unsigned long` 型数值存到 `char c` 里(这里是宏定义而不是函数,所以可以实现赋值),如果失败了跳转到 `byte_at_a_time` 处。参考 [unsafe_put_user 的解释][001]
+- 如果 max >= unsigned long 的长度,也就是剩下需要拷贝的字符数量不够 32 位,跳转到 `byte_at_a_time` 处逐个字节复制。
+- 如果当前这一个 unsigned long 的长度里出现了 0 位,说明里面出现了字符串的终止位,通过设置掩码后赋值的方式赋值 0 位前的部分,结束复制。
+- 如果这一个 unsigned long 长度里没有 0 位,则说明字符串没有终止,直接复制一个 unsigned long 的长度后 `max -= sizeof(unsigned long);` 继续下一轮复制。
+- byte_at_a_time:循环获取 1 字节长度复制。复制完成后跳出循环;获取失败返回 `-EFAULT`。
+
+## 字符串、内存搜索函数的优化
+
+| 函数名 | 参数 | 返回值 | 功能 |
+|------------|----------------------------------------|--------|-----------------------------------------------------------------------------------------------|
+| strchr | const char *s, int c | char * | 逐位搜索 s 字符串里面有没有 c 字符。返回对应位置的指针,没有返回空指针 NULL。也可以搜索空字符 |
+| strchrnul | const char *s, int c | char * | 未找到不返回 NULL,而是返回指向字符串结尾的 \0 的指针。这样可以借助 `strchr` 来找字符串的结尾位置 |
+| strnchrnul | const char *s, size_t count, int c | char * | 限定一个长度范围内搜索 c。失败返回第一个超出的字符的指针 |
+| strrchr | const char *s, int c | char * | 找到最后一个匹配 c 的指针 |
+| strnchr | const char *s, size_t count, int c | char * | 限定在前 cnt 范围里面找,失败返回 NULL |
+| memchr | const void *s, int c, size_t n | void * | 类似 strchr |
+| memchr_inv | const void *start, int c, size_t bytes | void * | 优化版本的 memchr,找第一个不匹配的字符位置 |
+
+memchr_inv 所作优化部分思路如下:
+
+- 首先写了一个 `static void *check_bytes8(const u8 *start, u8 value, unsigned int bytes)`,这个就是在 bytes 范围内遍历查找,低效,用于处理尾部数据。
+- 如果要寻找长度<16B,就直接调用 check_bytes8 处理即可。
+
+```c
+// src/linux-stable/lib/string.c:934
+
+ value64 = value;
+#if defined(CONFIG_ARCH_HAS_FAST_MULTIPLIER) && BITS_PER_LONG == 64
+ value64 *= 0x0101010101010101ULL;
+#elif defined(CONFIG_ARCH_HAS_FAST_MULTIPLIER)
+ value64 *= 0x01010101;
+ value64 |= value64 << 32;
+#else
+ value64 |= value64 << 8;
+ value64 |= value64 << 16;
+ value64 |= value64 << 32;
+#endif
+```
+
+- 比如 value 是 0xEB,扩展为 64 位后就变为 0xEBEBEBEB. 这样可以同时进行比较提高效率。
+
+```c
+// src/linux-stable/lib/string.c:946
+
+prefix = (unsigned long)start % 8;
+if (prefix) {
+ u8 *r;
+
+ prefix = 8 - prefix;
+ r = check_bytes8(start, value, prefix);
+ if (r)
+ return r;
+ start += prefix;
+ bytes -= prefix;
+}
+```
+
+- 这里检查一下前 8 位前缀有无对齐,没有对齐就先比较一下这 8 位再将其对齐。
+
+> chatGPT: 让我们假设 start 是一个指向内存区域的指针,并且当前的 u8 类型是一个无符号 8 位整数。我们来举一个具体数字的例子来说明 prefix 是如何影响对齐的。
+> 假设 start 指针指向如下的内存区域,每个字节用十六进制表示:
+
+```
+ 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D
++----+----+----+----+----+----+----+----+----+----+----+----+----+----+
+| AB | CD | EF | 12 | 34 | 56 | 78 | 9A | BC | DE | F0 | 11 | 11 | 33 |
++----+----+----+----+----+----+----+----+----+----+----+----+----+----+
+ ^
+ |
+ start
+```
+
+> 在这个例子中,start 指针指向内存的位置 start[0],即 AB(十六进制)。我们假设每个字节都是不同的,因此在这个区域内没有重复的字节值。
+> 现在,我们来计算 prefix,即指针 start 相对于 8 字节对齐的偏移量:
+
+```c
+prefix = (unsigned long)start % 8
+ = (unsigned long)0x00 % 8
+ = 0
+```
+
+> 由于 start 指针的值是 0x00,它已经是 8 字节对齐的,prefix 的值是 0。这意味着在此例中,start 指针已经对齐了,没有前缀需要处理。
+> 但是,如果 start 指针的值不是 8 的倍数,那么 prefix 就会有一个非零值,表示指针在 8 字节对齐时,需要向前调整的字节数。通过调用 check_bytes8 函数来查找不等于给定值的字节时,我们会处理这个前缀部分。
+
+- start 想要比较完并跳转到下一个 8 字节对齐位置,需要往前比较并跳转 `8-prefix` 个长度。比较完后,start 指针 + 这个长度,剩余要比较的长度 bytes - 这个长度。处理完之后我们现在就对齐好了,可以开始更高效的查询方式了。
+
+```c
+// src/linux-stable/lib/string.c:958
+
+words = bytes / 8;
+
+ while (words) {
+ if (*(u64 *)start != value64)
+ return check_bytes8(start, value, 8);
+ start += 8;
+ words--;
+ }
+```
+
+- 每次比较 8 个字节 64 位,提高效率。words 部分比较完了,就剩下结尾不够 8 字节长度的待比较部分了。
+
+- 结尾部分再调用 `check_bytes8(start, value, bytes%8)` 即可完成比较。
+
+## 字符串长度计算函数的优化
+
+| 函数名 | 参数 | 返回值 | 功能 |
+|---------|-----------------------------|--------|--------------------------|
+| strlen | const char *s | size_t | 逐位自增计算长度到结尾为止 |
+| strnlen | const char *s, size_t count | size_t | 限定一定长度范围内获取 len |
+
+**lib/strlen_from_user.c**:用户空间里 str 相关函数。
+
+| 函数名 | 参数 | 返回值 | 功能 |
+|-----------------|----------------------------------------------------------------|--------|------------------------|
+| do_strnlen_user | const char __user *src, unsigned long count, unsigned long max | long | 优化版计算 strlen 的函数 |
+
+do_strnlen_user 所作优化部分思路如下:
+
+首先通过此方式对齐:
+
+```c
+// lib/strlen_from_user.c:33
+
+align = (sizeof(unsigned long) - 1) & (unsigned long)src;
+src -= align;
+max += align;
+```
+
+然后采用类似 `strcpy` 部分的优化方法,每次获取一个 `unsigned long` 的长度并通过掩码截断。如果获取长度 > 用户指定 count 长度,返回 count+1。
+
+`long strnlen_user(const char __user *str, long count)`:比起 `do_strlen_user`,首先确定 count 是否合法和用户空间范围等。
+
+这个函数基本不会被使用,因为其他线程任何时候都可以直接修改用户空间字符串,一般用 `strcpy_from_user` 直接获取字符串拷贝来代替此函数。
+
+## 其他字符串、内存操作函数
+
+| 函数名 | 参数 | 返回值 | 功能 |
+|-----------------------------------------------|--------------------------------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| strcat | char *dest, const char *src | char * | 将 src 字符串拼接到 dest 结尾处,返回 dest 首指针 |
+| strncat | char *dest, const char *src, size_t count | char * | 限定一定的拼接长度 |
+| strlcat | char *dest, const char *src, size_t count | size_t | 校验内存缓冲区的大小,拼接更安全 |
+| strspn | const char *s, const char *accept | size_t | 找找 s 字符串里面有多少个开头连续的字符是 accept 字符串里出现过的,比如 accept 是 abcd, s 是 aabcdefgaa, 那么就出现过 5 次。
函数写的很简单,两层循环遍历扫描,对于每一个 s 中的字符,遍历 accept 看是否是 accept 中的一个字符,如果是 `cnt++`,如果不是直接返回 cnt,最终返回 cnt。但是这里是不是可以用一个缓冲数组存 `*accept` 对应字符,这样也不用每次到 accept 中访问指定地址中的内容? |
+| strcspn | const char *s, const char *reject | size_t | 和 accept 相反,看开头多少个字符是未出现在 reject 中的并计数 |
+| strpbrk | const char *cs, const char *ct | char * | 查找 cs 中第一次出现 ct 中字符的位置 |
+| strsep | char **s, const char *ct | char * | 借助 `strpbrk` 找到第一次出现 ct 中字符的 cs 的位置,将其字符替换成空字符;并返回该字符位置后面的一个字符的指针。 |
+| memset / memset16 /
memset32 / memset64 | void *s, int c, size_t count | void * | 从 s 开始,赋值 count 个字符 c。返回 s 头指针也就是最开始传入的时候 s 所指向的地址
不同的函数名表示以不同的长度为单位进行赋值 |
+| memmove | void *dest, const void *src, size_t count | void * | 如果 dest 在 src 左边,则采用 `memcpy` 的逐位复制方法。否则采用从尾部倒过来复制的方法。为了防止 dest 在 src 右边出现重叠部分从左到右复制会影响到后面的复制流程 |
+| memscan | void *addr, int c, size_t size | void * | 找到 c 第一次出现的地址,或者超出 size 的第一个字节的地址 |
+| strstr | const char *s1, const char *s2 | char * | 找到 s1 中第一次出现 s2 子串的位置。遍历 s1 进行 `memcmp(s1, s2, length_of_s2)` 查找 |
+| strnstr | const char *s1, const char *s2, size_t len | char * | 限定搜索一定长度范围 |
+
+## 自测模块
+
+主要包括 `test-string.c`, `test_strcpy.c`, `test_fortify/write_overflow-strcpy_*.c`
+
+### test_string.c
+
+这个测试模块位于:
+
+```
+menu Kernel Hacking
+-> menu "Kernel Testing and Coverage"
+ -> if RUNTIME_TESTING_MENU ("Runtime Testing")
+ -> config STRING_SELFTEST ("Test string functions at runtime") 里。
+```
+
+编译运行后,输出信息里有这么一句,和 `test_string.c` 里相对应:
+
+```shell
+String selftests succeeded
+```
+
+在此仅摘取一部分测试案例说明其思路。
+
+| 测试函数名 | 思路 |
+|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
+| memset16_selftest memset32_selftest memset64_selftest | 开辟一整块空间全部赋值为 0xa1,遍历选取其中的一段赋值为 0xb2,遍历检查是否正常赋值。 |
+| strchr_selftest | 测试搜索字符串中存在的字符、不存在的字符、搜索 \0 字符是否正确返回位置或 NULL,失败则用不同的测试码代表出现错误的具体哪一种情况 |
+
+### 其他相关测试模块
+
+`strcpy` 部分通过 `lib/test_strcpy.c` 函数测试其功能,该函数通过给定不同的 dest 缓冲区大小和 count 规定复制字符数,测试结果是否符合预期。
+
+`strcpy` 部分通过 `lib/test_fortify/write_overflow-strcpy-*.c` 测试溢出的边界情况,如 `strcpy(small, large)`, `strncpy(small, large_src, sizeof(small) + 1)`。
+
+## 总结
+
+本文截取了一部分功能较为基础的 str 文件先进行分析。
+
+分析重点主要还是在其中的一些优化手段上。其中有一部分在这篇文章中有简单分析过其应用:[articles/20230617-riscv-klibc-opt-summary.md (gitee.com)][002]
+
+其中的优化内容主要包括:
+
+1. `memcmp`:在开启高效对齐的前提下逐字比较,结尾部分逐位比较,效率高于逐位比较。
+2. `strscpy`:在开启高效对齐且 dest src 地址对齐的前提下逐字复制,结尾部分逐位复制。
+3. `strnlen`:类似,先通过判断字符串起始位置计算如何对齐后逐块计算长度。
+4. `memchr_inv`:待查找值扩展为 64 位,这样查找时更为高效。如果当前指针位置没有对齐,先逐位比较几次直至对齐在字节开始处(地址 %8 == 0),然后一次比较 8 个字节,如果在这 8 个字节中发现了要找的字符,或剩余字节数不够 8 个了,再逐位比较其中的位置。
+
+在下一篇文章中将继续展开分析 str 相关函数及其中所做优化,如 str 和整数转换,编码解码等,在下一篇文章中展开叙述。
+
+## 参考资料
+
+- [Articles/736348][001]
+- [tinylab/riscv-linux/blob/master/articles/20230617-riscv-klibc-opt-summary.md][002]
+
+[001]: https://lwn.net/Articles/736348/
+[002]: https://gitee.com/tinylab/riscv-linux/blob/master/articles/20230617-riscv-klibc-opt-summary.md
--
Gitee