前言
内核模块开发的基本规则之一是,模块只能访问已显式导出的符号(函数和数据结构),内核使用EXPORT_SYMBOL宏和EXPORT_SYMBOL_GPL宏来导出符号。
关于EXPORT_SYMBOL宏和EXPORT_SYMBOL_GPL宏请参考:Linux EXPORT_SYMBOL宏详解
即便如此,许多符号也受到限制,因此只有具有GPL兼容许可证的模块才能访问它们。然而,事实证明,有一种现成的解决方法,可以让模块轻松访问它想要的任何符号,那就是使用内核函数kallsyms_lookup_name来获取没有使用EXPORT_SYMBOL宏和EXPORT_SYMBOL_GPL宏来导出符号。
kallsyms_lookup_name它将返回与内核符号表中任何符号相关的地址。
EXPORT_SYMBOL_GPL(kallsyms_lookup_name);
但使用kallsyms_lookup_name方法来获取没有导出内核符号的地址在内核版本5.7被删除,即在内核版本5.7即以上该函数没有被导出。
一、API使用
kallsyms_lookup_name 是一个内核函数,用于通过符号名称查找相应的符号地址。它是内核符号查找机制的一部分,允许内核代码和模块在运行时动态地查找和访问内核符号。
只要符号存在于/proc/kallsyms 文件中,都可以通过kallsyms_lookup_name获取其符号的地址。
内核版本 2.6.33 - 5.7.0 该符号都是导出的:
# cat /proc/kallsyms | grep '\<kallsyms_lookup_name\>'
ffffffffb8558e90 T kallsyms_lookup_name
// linux-4.19.90/kernel/kallsyms.c
/* Lookup the address for this symbol. Returns 0 if not found. */
unsigned long kallsyms_lookup_name(const char *name)
{
char namebuf[KSYM_NAME_LEN];
unsigned long i;
unsigned int off;
for (i = 0, off = 0; i < kallsyms_num_syms; i++) {
off = kallsyms_expand_symbol(off, namebuf, ARRAY_SIZE(namebuf));
if (strcmp(namebuf, name) == 0)
return kallsyms_sym_address(i);
}
return module_kallsyms_lookup_name(name);
}
EXPORT_SYMBOL_GPL(kallsyms_lookup_name);
使用EXPORT_SYMBOL_GPL宏导出kallsyms_lookup_name函数。
然而在 2.6.33 以下和 5.7.0 以上该函数没有使用EXPORT_SYMBOL_GPL导出,比如5.19:
// linux-5.19/include/linux/kallsyms.h
/* Lookup the address for a symbol. Returns 0 if not found. */
unsigned long kallsyms_lookup_name(const char *name);
// linux-5.19/kernel/kallsyms.c
/* Lookup the address for this symbol. Returns 0 if not found. */
unsigned long kallsyms_lookup_name(const char *name)
{
char namebuf[KSYM_NAME_LEN];
unsigned long i;
unsigned int off;
/* Skip the search for empty string. */
if (!*name)
return 0;
for (i = 0, off = 0; i < kallsyms_num_syms; i++) {
off = kallsyms_expand_symbol(off, namebuf, ARRAY_SIZE(namebuf));
if (strcmp(namebuf, name) == 0)
return kallsyms_sym_address(i);
if (cleanup_symbol_name(namebuf) && strcmp(namebuf, name) == 0)
return kallsyms_sym_address(i);
}
return module_kallsyms_lookup_name(name);
}
对于在高版本内核kallsyms_lookup_name不在导出的原因请参考:https://lwn.net/Articles/813350/
虽然没有导出,但还是可以在/proc/kallsym 获取其地址,如下:
# uname -r
5.19.0-46-generic
# cat /proc/kallsyms | grep "\<kallsyms_lookup_name\>"
ffffffffb9bbba60 T kallsyms_lookup_name
因此在 2.6.33 以下和 5.7.0 以上可以用 kprobe 来获取该函数的地址:
#include <linux/version.h>
#include <linux/kallsyms.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 7, 0) || LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 33)
#include <linux/kprobes.h>
static unsigned long (*kallsyms_lookup_name_sym)(const char *name);
static int _kallsyms_lookup_kprobe(struct kprobe *p, struct pt_regs *regs)
{
return 0;
}
unsigned long get_kallsyms_func(void)
{
struct kprobe probe;
int ret;
unsigned long addr;
memset(&probe, 0, sizeof(probe));
probe.pre_handler = _kallsyms_lookup_kprobe;
probe.symbol_name = "kallsyms_lookup_name";
ret = register_kprobe(&probe);
if (ret)
return 0;
addr = (unsigned long)probe.addr;
unregister_kprobe(&probe);
return addr;
}
unsigned long generic_kallsyms_lookup_name(const char *name)
{
/* singleton */
if (!kallsyms_lookup_name_sym) {
kallsyms_lookup_name_sym = (void *)get_kallsyms_func();
if(!kallsyms_lookup_name_sym)
return 0;
}
return kallsyms_lookup_name_sym(name);
}
#else
unsigned long generic_kallsyms_lookup_name(const char *name)
{
return kallsyms_lookup_name(name);
}
#endif
这样generic_kallsyms_lookup_name在所有内核版本都可以完成kallsyms_lookup_name的功能。
二、源码解析
2.1 kallsyms_lookup_name
extern const unsigned long kallsyms_num_syms
__attribute__((weak, section(".rodata")));
/* Lookup the address for this symbol. Returns 0 if not found. */
unsigned long kallsyms_lookup_name(const char *name)
{
char namebuf[KSYM_NAME_LEN];
unsigned long i;
unsigned int off;
for (i = 0, off = 0; i < kallsyms_num_syms; i++) {
off = kallsyms_expand_symbol(off, namebuf, ARRAY_SIZE(namebuf));
if (strcmp(namebuf, name) == 0)
return kallsyms_sym_address(i);
}
return module_kallsyms_lookup_name(name);
}
EXPORT_SYMBOL_GPL(kallsyms_lookup_name);
首先定义了一个名为 namebuf 的字符数组,用于存储扩展后的符号名称。KSYM_NAME_LEN 是一个常量,表示符号名称的最大长度。
接下来,函数通过一个循环来遍历符号表中的每个符号。kallsyms_num_syms 是一个表示符号表中符号数量的变量。
在循环中,使用 kallsyms_expand_symbol 函数对符号进行解压缩,将解压后的符号名称存储在 namebuf 中。
然后,函数将解压后的符号名称与传入的 name 进行比较。如果找到匹配的符号名称,函数返回相应符号的地址,即通过 kallsyms_sym_address 函数获取的地址。
如果循环结束后仍然没有找到匹配的符号,函数调用 module_kallsyms_lookup_name 函数,用于在内核模块中查找符号。
最后,通过 EXPORT_SYMBOL_GPL 宏,将 kallsyms_lookup_name 函数导出为内核符号,以便其他模块或代码可以引用和使用该函数。
2.2 kallsyms_expand_symbol
(1)
extern const u8 kallsyms_names[] __weak;
extern const u8 kallsyms_token_table[] __weak;
extern const u16 kallsyms_token_index[] __weak;
/*
* Expand a compressed symbol data into the resulting uncompressed string,
* if uncompressed string is too long (>= maxlen), it will be truncated,
* given the offset to where the symbol is in the compressed stream.
*/
static unsigned int kallsyms_expand_symbol(unsigned int off,
char *result, size_t maxlen)
{
int len, skipped_first = 0;
const u8 *tptr, *data;
/* Get the compressed symbol length from the first symbol byte. */
data = &kallsyms_names[off];
len = *data;
data++;
/*
* Update the offset to return the offset for the next symbol on
* the compressed stream.
*/
off += len + 1;
/*
* For every byte on the compressed symbol data, copy the table
* entry for that byte.
*/
while (len) {
tptr = &kallsyms_token_table[kallsyms_token_index[*data]];
data++;
len--;
while (*tptr) {
if (skipped_first) {
if (maxlen <= 1)
goto tail;
*result = *tptr;
result++;
maxlen--;
} else
skipped_first = 1;
tptr++;
}
}
tail:
if (maxlen)
*result = '\0';
/* Return to offset to the next symbol. */
return off;
}
用于将压缩的符号数据展开为未压缩的字符串形式。
kallsyms_names数组保存每一个内核符号名字被压缩后的字节编码。存储的格式是 len + data,data是压缩后的字符编码。
函数的参数和功能如下:
off:指定符号在压缩流中的偏移量。
result:存储展开后的符号字符串的缓冲区。
maxlen:缓冲区的最大长度,用于避免字符串溢出。
函数首先从压缩流中获取符号的压缩长度,并将其存储在变量 len 中。然后,更新偏移量 off,使其指向下一个符号在压缩流中的位置。
接下来,函数开始解压缩符号数据。它逐字节遍历压缩的符号数据,根据每个字节在符号表中的索引,从符号表中获取相应的字符,并将其复制到结果缓冲区中。
在解压缩过程中,函数会跳过第一个字符,并将 skipped_first 设置为 1,这是为了处理一种特殊情况,即如果结果缓冲区的长度不足以容纳完整的符号,则只复制符号的一部分。
当解压缩完成后,函数检查剩余的缓冲区空间。如果缓冲区还有剩余空间,将在末尾添加空字符 ‘\0’,以确保结果字符串以 null 结尾。
最后,函数返回下一个符号在压缩流中的偏移量,以便在下次解压缩时使用。
(2)
extern const u8 kallsyms_token_table[] __weak;
extern const u16 kallsyms_token_index[] __weak;
kallsyms_token_table 和 kallsyms_token_index 数组在 Linux 内核的符号查找机制中用于符号展开过程。
这些数组的作用是提供压缩的符号数据与对应的未压缩字符之间的映射关系,以高效地将压缩的符号展开为原始的字符串形式。
以下是对这两个数组的简要说明:
kallsyms_token_table:该数组包含了用作压缩符号表示中的标记(token)的字符表。每个标记表示一系列在符号名称中常见的字符序列。数组中的条目被组织成一种能够高效查找标记索引的方式。
kallsyms_token_index:该数组用作对 kallsyms_token_table 的索引。它将压缩符号数据中的每个字节值映射到对应的标记表条目。在符号展开过程中,通过这个索引可以快速获取压缩数据中的字节对应的字符序列。
在符号展开过程中,kallsyms_expand_symbol 函数逐字节遍历压缩的符号数据。对于每个字节,它使用 kallsyms_token_index 数组获取对应的 kallsyms_token_table 中的索引。然后,从标记表中获取相应的字符序列,并将其复制到结果缓冲区中。
通过使用标记和索引,符号展开过程避免了在压缩的符号数据中多次存储重复的字符。相反,它使用较短的标记表示常见字符序列,从而实现了对符号名称的更高效压缩和解压缩。
kallsyms_token_table 和 kallsyms_token_index 数组在 Linux 内核中的符号压缩和展开机制中起着关键作用,通过优化符号名称的存储和查找,提高了效率。
(3)
符号数据通常是通过编译器和链接器生成的。这些符号数据包括函数名、变量名以及其他在编译和链接过程中产生的符号,然后保存在内核符号表(kernel symbol table)。
压缩的符号数据来源通常是内核符号表(kernel symbol table)。内核符号表包含了各种内核函数、变量和其他符号的信息,例如函数名、变量名以及其对应的地址等。
内核符号表中的符号信息通过特定的压缩算法进行压缩,以减小其大小并节省存储空间。
在编译内核时,可以选择启用内核符号表的生成。通过特定的编译选项,例如 CONFIG_KALLSYMS,可以将内核符号信息保留在编译后的内核镜像中。这样,在运行时,可以通过相应的机制(例如 /proc/kallsyms 文件)读取内核符号表,并将符号信息保存在内存中。
# cat /boot/config-4.19.90-23.8.v2101.ky10.x86_64 | grep CONFIG_KALLSYMS
CONFIG_KALLSYMS=y
压缩的符号数据是通过对内核符号表中的符号信息进行压缩算法处理而得到的。这样做的目的是减小符号数据的大小,节省内存空间和存储空间。在压缩过程中,符号的名称和其他相关信息被编码为压缩格式,以便在需要时进行解压缩并恢复出原始的符号信息。
关于符号数据的压缩,Linux内核中使用了一种简单的压缩算法,将符号数据进行压缩以节省内存空间。这种压缩算法使用了基于标记的方法,其中kallsyms_token_table和kallsyms_token_index数组用于解压缩过程。
在内核构建过程中,符号数据首先被收集,并将其压缩为一个压缩流。压缩的符号数据流中的每个字节都对应于kallsyms_token_table中的一个标记,或者表示一个特定的字符。通过解压缩过程,这些压缩的符号数据将被展开为原始的符号字符串。
kallsyms_expand_symbol 函数中的代码片段处理的就是这样的压缩的符号数据,通过解压缩算法和相关的表格,将其还原为未压缩的字符串形式,以便进行进一步的处理和使用。
备注:
具体的压缩过程在内核的构建脚本中实现,而不是在内核源码中:
// linux-4.19.90/scripts/kallsyms.c
/* replace a given token in all the valid symbols. Use the sampled symbols
* to update the counts */
static void compress_symbols(unsigned char *str, int idx)
{
unsigned int i, len, size;
unsigned char *p1, *p2;
for (i = 0; i < table_cnt; i++) {
len = table[i].len;
p1 = table[i].sym;
/* find the token on the symbol */
p2 = find_token(p1, len, str);
if (!p2) continue;
/* decrease the counts for this symbol's tokens */
forget_symbol(table[i].sym, len);
size = len;
do {
*p2 = idx;
p2++;
size -= (p2 - p1);
memmove(p2, p2 + 1, size);
p1 = p2;
len--;
if (size < 2) break;
/* find the token on the symbol */
p2 = find_token(p1, size, str);
} while (p2);
table[i].len = len;
/* increase the counts for this symbol's new tokens */
learn_symbol(table[i].sym, len);
}
}
随着内核的发展,可能会采用更为复杂的压缩算法,以提高压缩率和解压缩效率。例如,使用字典压缩算法,如LZ77或LZ78变种,通过建立符号字典并对重复的符号序列进行替换来实现压缩。这些算法通常会结合其他技术,如霍夫曼编码或算术编码,进一步提高压缩效果。
2.3 kallsyms_sym_address
extern const int kallsyms_offsets[] __weak;
extern const unsigned long kallsyms_relative_base
__attribute__((weak, section(".rodata")));
static unsigned long kallsyms_sym_address(int idx)
{
if (!IS_ENABLED(CONFIG_KALLSYMS_BASE_RELATIVE))
return kallsyms_addresses[idx];
/* values are unsigned offsets if --absolute-percpu is not in effect */
if (!IS_ENABLED(CONFIG_KALLSYMS_ABSOLUTE_PERCPU))
return kallsyms_relative_base + (u32)kallsyms_offsets[idx];
/* ...otherwise, positive offsets are absolute values */
if (kallsyms_offsets[idx] >= 0)
return kallsyms_offsets[idx];
/* ...and negative offsets are relative to kallsyms_relative_base - 1 */
return kallsyms_relative_base - 1 - kallsyms_offsets[idx];
}
用于在内核中获取符号地址的函数kallsyms_sym_address的实现。根据给定的索引idx,该函数返回对应符号的地址。
该函数的实现基于一些条件编译选项,包括CONFIG_KALLSYMS_BASE_RELATIVE和CONFIG_KALLSYMS_ABSOLUTE_PERCPU。这些选项根据内核的配置状态来确定符号地址的计算方式。
目前我接触的x86_64和arm64 都配置了CONFIG_KALLSYMS_BASE_RELATIVE编译选项。在启用CONFIG_KALLSYMS_BASE_RELATIVE选项时,符号地址以相对于基地址的偏移量形式存储,而不是绝对地址。kallsyms_relative_base表示这个基地址:
kallsyms_relative_base变量作为基地址,用于计算相对符号地址。
kallsyms_offsets 是一个数组,记录着每一个符号相对于kallsyms_relative_base变量的偏移量。
但是x86_64配置了CONFIG_KALLSYMS_ABSOLUTE_PERCPU选项,arm64没有配置CONFIG_KALLSYMS_ABSOLUTE_PERCPU选项。
CONFIG_KALLSYMS_ABSOLUTE_PERCPU 选项与 kallsyms 特性中的 per-CPU 符号处理相关。Per-CPU 符号是针对系统中每个 CPU 单独存在的特殊符号。这些符号通常用于存储特定于每个 CPU 的数据或状态。
当启用 CONFIG_KALLSYMS_ABSOLUTE_PERCPU 时,意味着在 kallsyms 机制中将 per-CPU 符号视为绝对值。正的 per-CPU 符号的偏移量被视为绝对地址,而负的偏移量被视为相对于特定基地址(kallsyms_relative_base - 1)的相对地址。
另一方面,当未启用 CONFIG_KALLSYMS_ABSOLUTE_PERCPU 时,per-CPU 符号被视为相对于基地址(kallsyms_relative_base)的无符号偏移量。基地址相对的方法可以更紧凑地存储 per-CPU 符号在符号表中的表示。
2.3.1 x86_64
# cat /etc/os-release
NAME="Kylin Linux Advanced Server"
# uname -r
4.19.90-23.8.v2101.ky10.x86_64
# lscpu
架构: x86_64
# cat /boot/config-4.19.90-23.8.v2101.ky10.x86_64 | grep CONFIG_KALLSYMS_BASE_RELATIVE
CONFIG_KALLSYMS_BASE_RELATIVE=y
# cat /boot/config-4.19.90-23.8.v2101.ky10.x86_64 | grep CONFIG_KALLSYMS_ABSOLUTE_PERCPU
CONFIG_KALLSYMS_ABSOLUTE_PERCPU=y
static unsigned long kallsyms_sym_address(int idx)
{
/* ...otherwise, positive offsets are absolute values */
if (kallsyms_offsets[idx] >= 0)
return kallsyms_offsets[idx];
/* ...and negative offsets are relative to kallsyms_relative_base - 1 */
return kallsyms_relative_base - 1 - kallsyms_offsets[idx];
}
CONFIG_KALLSYMS_ABSOLUTE_PERCPU启用,表示偏移量包含正负值,函数根据正负值决定地址的计算方式。
如果偏移量kallsyms_offsets[idx]大于等于0,表示偏移量是绝对值,函数返回kallsyms_offsets[idx]作为地址。
正数:表示符号的地址是绝对地址,可以直接作为符号的地址使用。
如果偏移量kallsyms_offsets[idx]小于0,表示偏移量是相对于kallsyms_relative_base - 1的负值,函数返回kallsyms_relative_base - 1 - kallsyms_offsets[idx]作为地址。
负数:表示符号的地址是相对于 kallsyms_relative_base - 1 地址的偏移量。通过将偏移量从 kallsyms_relative_base - 1 中减去,可以计算出符号的实际地址。
CONFIG_KALLSYMS_ABSOLUTE_PERCPU用于控制符号地址在处理器特定数据区(per-CPU)时的解释方式。
在内核中,有一些符号(如变量或函数)可以在每个处理器的特定数据区中有不同的副本。这是为了提高性能和并发性。这些符号在每个处理器的数据区中的地址可能是相对于某个基地址而不是绝对地址。
当启用了CONFIG_KALLSYMS_ABSOLUTE_PERCPU选项时,表示偏移值中的正数偏移是绝对地址,而负数偏移是相对于基地址的偏移量。这意味着符号在每个处理器的数据区中具有不同的绝对地址。
当启用CONFIG_KALLSYMS_ABSOLUTE_PERCPU配置选项时,表示每个处理器特定数据区(per-CPU)的符号具有独立的绝对地址。这意味着每个处理器的符号地址不是相对于共享基地址的,而是每个处理器都有自己独特的绝对地址。
2.3.2 arm64
# cat /etc/os-release
NAME="Kylin Linux Advanced Server"
# uname -r
4.19.90-24.4.v2101.ky10.aarch64
# lscpu
架构: aarch64
# cat /boot/config-4.19.90-24.4.v2101.ky10.aarch64 | grep CONFIG_KALLSYMS_BASE_RELATIVE
CONFIG_KALLSYMS_BASE_RELATIVE=y
# cat /boot/config-4.19.90-24.4.v2101.ky10.aarch64 | grep CONFIG_KALLSYMS_ABSOLUTE_PERCPU
没有CONFIG_KALLSYMS_ABSOLUTE_PERCPU选项。
当未启用CONFIG_KALLSYMS_ABSOLUTE_PERCPU选项时,每个处理器特定数据区的符号通常表示为相对于共享基地址的偏移量。通过偏移量与基地址相加,内核可以计算出实际的每个处理器特定数据区中的符号地址。这种方法通过避免为每个处理器存储单独的绝对地址,节省了内存。
static unsigned long kallsyms_sym_address(int idx)
{
/* values are unsigned offsets if --absolute-percpu is not in effect */
if (!IS_ENABLED(CONFIG_KALLSYMS_ABSOLUTE_PERCPU))
return kallsyms_relative_base + (u32)kallsyms_offsets[idx];
如果CONFIG_KALLSYMS_ABSOLUTE_PERCPU未启用,表示偏移量是无符号的,函数返回kallsyms_relative_base + (u32)kallsyms_offsets[idx],其中kallsyms_relative_base是一个基地址,kallsyms_offsets是一个无符号偏移量数组。
函数通过将基地址(kallsyms_relative_base)与无符号偏移值(kallsyms_offsets[idx])相加来计算相对地址。
2.3.3 CONFIG_KALLSYMS_ABSOLUTE_PERCPU
在内核中启用CONFIG_KALLSYMS_ABSOLUTE_PERCPU选项对性能的影响:
符号查找效率提高: 启用CONFIG_KALLSYMS_ABSOLUTE_PERCPU选项后,每个处理器特定数据区的符号都有自己的绝对地址,而不是相对于基地址的偏移量。这意味着符号查找可以更快速和直接地进行,因为不再需要计算相对地址。这可能会提高符号查找的效率,特别是在需要频繁访问处理器特定数据区的情况下。
内存占用增加: 启用CONFIG_KALLSYMS_ABSOLUTE_PERCPU选项会导致每个处理器特定数据区的符号都需要独立的绝对地址。相对于使用相对地址来表示,这可能会增加内存的占用量,因为需要为每个处理器存储独立的地址。如果系统中有大量的处理器或大量的处理器特定数据区符号,这种额外的内存开销可能是显著的。
启动时间延长: 内核在启动时需要解析符号表并建立符号地址的映射关系。启用CONFIG_KALLSYMS_ABSOLUTE_PERCPU选项后,需要额外的工作来处理每个处理器特定数据区的符号地址。这可能会导致启动时间延长,特别是在有大量处理器或处理器特定数据区符号的系统中。
代码复杂性增加: 启用CONFIG_KALLSYMS_ABSOLUTE_PERCPU选项会引入对处理器特定数据区符号的维护和处理的复杂性。需要确保每个处理器的符号地址在正确的位置,并且符号查找和解释逻辑需要处理符号地址的绝对性和相对性。这可能增加内核代码的复杂性和维护成本。
参考资料
Linux 4.19.90文章来源:https://www.toymoban.com/news/detail-674000.html
https://lwn.net/Articles/813350/
https://blog.csdn.net/weixin_38878510/article/details/113264807文章来源地址https://www.toymoban.com/news/detail-674000.html
到了这里,关于Linux 内核函数kallsyms_lookup_name的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!