Linux 驱动开发之内核模块分析4(基于Linux6.6)---符号表导出分析
前言
Linux内核头文件提供了一个方便的方法用来管理符号的对模块外部的可见性,因此减少了命名空间的污染(命名空间的名称可能会与内核其他地方定义的名称冲突),并且适当信息隐藏。 如果你的模块需要输出符号给其他模块使用,应当使用下面的宏定义:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name); //只适用于包含GPL许可权的模块;
这两个宏均用于将给定的符号导出到模块外. _GPL版本的宏定义只能使符号对GPL许可的模块可用。 符号必须在模块文件的全局部分导出,不能在函数中导出,这是因为上述这两个宏将被扩展成一个特殊用途的声明,而该变量必须是全局的。这个变量存储于模块的一个特殊的可执行部分(一个"ELF段" ),在装载时,内核通过这个段来寻找模块导出的变量。
一、宏定义EXPORT_SYMBOL分析
2.1、__EXPORT_SYMBOL
include/linux/export.h
/*
* Allow symbol exports to be disabled completely so that C code may
* be reused in other execution contexts such as the UEFI stub or the
* decompressor.
*/
#define __EXPORT_SYMBOL(sym, sec, ns)
#elif defined(CONFIG_TRIM_UNUSED_KSYMS)
#include <generated/autoksyms.h>
/*
* For fine grained build dependencies, we want to tell the build system
* about each possible exported symbol even if they're not actually exported.
* We use a symbol pattern __ksym_marker_<symbol> that the build system filters
* from the $(NM) output (see scripts/gen_ksymdeps.sh). These symbols are
* discarded in the final link stage.
*/
#define __ksym_marker(sym) \
static int __ksym_marker_##sym[0] __section(".discard.ksym") __used
#define __EXPORT_SYMBOL(sym, sec, ns) \
__ksym_marker(sym); \
__cond_export_sym(sym, sec, ns, __is_defined(__KSYM_##sym))
#define __cond_export_sym(sym, sec, ns, conf) \
___cond_export_sym(sym, sec, ns, conf)
#define ___cond_export_sym(sym, sec, ns, enabled) \
__cond_export_sym_##enabled(sym, sec, ns)
#define __cond_export_sym_1(sym, sec, ns) ___EXPORT_SYMBOL(sym, sec, ns)
#ifdef __GENKSYMS__
#define __cond_export_sym_0(sym, sec, ns) __GENKSYMS_EXPORT_SYMBOL(sym)
#else
#define __cond_export_sym_0(sym, sec, ns) /* nothing */
#endif
#else
#define __EXPORT_SYMBOL(sym, sec, ns) ___EXPORT_SYMBOL(sym, sec, ns)
分析:
1)#运算符,##运算符
通常在宏定义中使用#来创建字符串 #abc就表示字符串”abc”等。
##运算符称为预处理器的粘合剂,用来替换粘合两个不同的符号,
如:#define xName (n) x##n
则xName(4) 则变为x4
2)gcc的 __attribute__ 属性:
__attribute__((section(“section_name”)))的作用是将指定的函数或变量放入到名为”section_name”的段中。
__attribute__属性添加可以在函数或变量定义的时候直接加入在定义语句中。
如:
int myvar__attribute__((section("mydata"))) = 0;
表示定义了整形变量myvar=0;并且将该变量存放到名为”mydata”的section中
关于gcc_attribute详解可以参考:http://blog.sina.com.cn/s/blog_661314940100qujt.html
2.2、EXPORT_SYMBOL的作用是什么?
作用和用途
-
符号导出:
- 在 Linux 内核中,模块通常是相互独立的,但有时模块之间需要共享代码或数据。
EXPORT_SYMBOL
允许一个模块中的函数或变量可以被其他模块引用和调用。 - 使用
EXPORT_SYMBOL
导出的符号可以在内核的其他部分或其他内核模块中访问。
- 在 Linux 内核中,模块通常是相互独立的,但有时模块之间需要共享代码或数据。
-
内核模块间的相互调用:
- 当某个模块定义了一个函数或全局变量,并希望其他模块能够调用或访问它时,就需要使用
EXPORT_SYMBOL
来导出该符号。 - 这对于内核模块间的协作非常重要,尤其是在驱动程序或其他核心功能中,不同模块之间的代码共享非常常见。
- 当某个模块定义了一个函数或全局变量,并希望其他模块能够调用或访问它时,就需要使用
示例
假设你有一个内核模块 module_a.c
,其中定义了一个函数 foo()
,你希望其他模块也能调用它。你可以这样做:
module_a.c
#include <linux/module.h>
#include <linux/kernel.h>
void foo(void) {
printk(KERN_INFO "foo function called\n");
}
EXPORT_SYMBOL(foo); // 导出 foo 函数,使其他模块可以调用
module_b.c
#include <linux/module.h>
#include <linux/kernel.h>
extern void foo(void); // 声明外部函数
static int __init mod_b_init(void) {
printk(KERN_INFO "module_b loaded\n");
foo(); // 调用 module_a 中的 foo 函数
return 0;
}
static void __exit mod_b_exit(void) {
printk(KERN_INFO "module_b unloaded\n");
}
module_init(mod_b_init);
module_exit(mod_b_exit);
MODULE_LICENSE("GPL");
在 module_b.c
中,你通过 extern
声明了 foo()
函数,后续可以调用 module_a.c
中导出的 foo()
函数。EXPORT_SYMBOL
在 module_a.c
中将 foo()
导出,允许其他模块访问它。
EXPORT_SYMBOL
与 EXPORT_SYMBOL_GPL
-
EXPORT_SYMBOL
: 将符号导出为可以被所有模块访问的符号,适用于任何模块,包括闭源模块。 -
EXPORT_SYMBOL_GPL
: 这个宏的作用和EXPORT_SYMBOL
类似,但它有一个附加的限制——仅允许 GPL 许可的模块访问该符号。也就是说,只有那些以 GPL 或兼容许可发布的模块,才可以访问该符号。使用
EXPORT_SYMBOL_GPL
是为了在内核中确保只有符合 GPL 许可的代码可以访问特定的内核符号,从而避免闭源模块直接使用内核中的 GPL 代码。
二、 EXPORT_SYMBOL使用方法
EXPORT_SYMBOL
是 Linux 内核模块开发中的一个非常重要的宏,用于将符号(函数、变量等)从一个内核模块导出,使得其他模块能够访问这些符号。下面将详细介绍 EXPORT_SYMBOL
的使用方法。
1. 基本语法
EXPORT_SYMBOL(symbol);
symbol
是你希望导出的符号(通常是函数或变量)。通过此宏,符号将会被公开,其他模块就能访问这个符号。
2. 使用场景
- 函数导出:如果你在一个内核模块中实现了一个函数,想让其他模块也能调用这个函数,就需要使用
EXPORT_SYMBOL
将其导出。 - 变量导出:如果你有一个全局变量,其他模块可能需要访问该变量,那么就需要将它导出。
3. 示例:导出函数
假设我们有两个模块:module_a
和 module_b
。module_a
中有一个函数 foo
,我们希望 module_b
能够调用 foo
。
module_a.c
#include <linux/module.h>
#include <linux/kernel.h>
void foo(void) {
printk(KERN_INFO "foo function called\n");
}
EXPORT_SYMBOL(foo); // 导出 foo 函数,使得其他模块可以调用
在 module_a.c
中,我们定义了一个函数 foo
,并通过 EXPORT_SYMBOL(foo)
将其导出。这意味着其他模块可以调用 foo()
函数。
module_b.c
#include <linux/module.h>
#include <linux/kernel.h>
extern void foo(void); // 声明 foo 函数
static int __init mod_b_init(void) {
printk(KERN_INFO "module_b loaded\n");
foo(); // 调用 module_a 中的 foo 函数
return 0;
}
static void __exit mod_b_exit(void) {
printk(KERN_INFO "module_b unloaded\n");
}
module_init(mod_b_init);
module_exit(mod_b_exit);
MODULE_LICENSE("GPL");
在 module_b.c
中,首先通过 extern
声明了外部函数 foo
,然后在初始化函数 mod_b_init
中调用了 foo()
。由于 foo
在 module_a.c
中通过 EXPORT_SYMBOL
导出,因此 module_b
可以成功调用。
4. 使用 EXPORT_SYMBOL_GPL
EXPORT_SYMBOL_GPL
是一个与 EXPORT_SYMBOL
类似的宏,但它有一个附加限制:只有在 GPL 许可下的模块才能访问该符号。
EXPORT_SYMBOL
可以被任何模块(无论是否是 GPL 许可的模块)访问。EXPORT_SYMBOL_GPL
仅允许 GPL 许可的模块访问该符号。这对于确保闭源模块不会直接使用内核中 GPL 代码的部分有帮助。
使用 EXPORT_SYMBOL_GPL
示例
假设你希望导出的符号仅允许 GPL 模块使用,你可以使用 EXPORT_SYMBOL_GPL
。
#include <linux/module.h>
#include <linux/kernel.h>
void foo(void) {
printk(KERN_INFO "foo function called\n");
}
EXPORT_SYMBOL_GPL(foo); // 仅允许 GPL 模块访问 foo
5. 符号的访问权限
- 使用
EXPORT_SYMBOL
可以让任何内核模块访问被导出的符号。 - 使用
EXPORT_SYMBOL_GPL
只能让 GPL 许可的内核模块访问该符号。如果其他非 GPL 模块尝试访问这个符号,编译器会给出错误。