Linux驱动适配不同内核时,由于内核版本的不同,有些函数可能没有,或者在高版本中函数已经变化了,比如增删了一些参数。
一般情况我们处理方式是在使用这些函数时,通过宏来判断当前的内核版本,根据版本来决定怎么正确的使用函数,比如:
#if LINUX_VERSION_CODE < KERNEL_VERSION(5,12,0)
generic_fillattr(inode, stat);
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(5,12,0) && LINUX_VERSION_CODE < KERNEL_VERSION(6,3,0)
generic_fillattr(mnt_userns, inode, stat);
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(6,3,0) && LINUX_VERSION_CODE < KERNEL_VERSION(6,6,0)
generic_fillattr(idmap, inode, stat);
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(6,6,0)
generic_fillattr(idmap,request_mask,inode, stat);
#endif
这种我们需要准确知道generic_fillattr
这个函数在哪个内核版本怎么变化了。但是Linux的内核有那么多,要找到从哪个版本开始变化的,很多时候需要巨大的工作量。
Linux的驱动适配,我们都需要在对应适配的内核中去编译一次,基于这个原理,我们可以在编译时,通过测试脚本,来确定使用的函数在当前内核中是否存在,以及是怎么样的形式存在。
低版本内核中,接受三个参数:
int vfs_getattr(struct vfsmount *mnt, struct dentry *dentry, struct kstat *stat)
{
struct inode *inode = dentry->d_inode;
int retval;
retval = security_inode_getattr(mnt, dentry);
if (retval)
return retval;
if (inode->i_op->getattr)
return inode->i_op->getattr(mnt, dentry, stat);
generic_fillattr(inode, stat);
return 0;
}
高一点的版本中,mnt和dentry参数直接变成了path结构体,而只接受2个参数:
int vfs_getattr(struct path *path, struct kstat *stat)
{
struct inode *inode = path->dentry->d_inode;
int retval;
retval = security_inode_getattr(path->mnt, path->dentry);
if (retval)
return retval;
if (inode->i_op->getattr)
return inode->i_op->getattr(path->mnt, path->dentry, stat);
generic_fillattr(inode, stat);
return 0;
}
更高的版本中,多传入了2个参数,提供了额外的功能,允许调用者对文件属性获取操作进行更细粒度的控制,
request_mask:用于指定要获取的文件属性的掩码,即希望获取哪些属性的位掩码。
query_flags:用于指定其他查询标志,例如是否忽略安全性检查等。
int vfs_getattr(const struct path *path, struct kstat *stat,
u32 request_mask, unsigned int query_flags)
{
int retval;
retval = security_inode_getattr(path);
if (retval)
return retval;
return vfs_getattr_nosec(path, stat, request_mask, query_flags);
}
我们创建一个名为generate-kernel-function的脚本,包括以下函数:
prefix="KERNEL_API_"
while [ "${1:0:1}" = "-" ] ; do
case "$1" in
-t)
shift
tmpdir=$1
;;
-o)
shift
outfile=$1
;;
esac
shift
done
run() {
( $@ || return 1 ) >/dev/null 2>&1
}
do_test() {
local var="$1"
local val="$2"
[ -n "$var" ]
var="${prefix}$var"
local src="$tmp_dir/test.c"
local obj="$tmp_dir/test.o"
cat > "$src"
if ( run gcc -o "$obj" -c "$src" ) ; then
out "#define $var ${val:-1}"
elif [ -z "$val" ] ; then
out "#undef $var"
else
out "// not $var $val"
fi
}
do_test 这个函数接受两个参数:
$1:变量名,用于指定要测试的条件。
$2:值,用于指定测试条件的预期值。
函数首先将这两个参数保存到本地变量 var 和 val 中。然后,它检查是否提供了变量名 $var,并在必要时添加前缀。接着,它定义了两个本地变量 src 和 obj,用于指定测试代码的源文件和目标文件的路径。
接下来,函数使用 cat 命令将标准输入中的内容输出到文件 $src 中,以生成测试代码。
接着,函数调用 c_run 函数编译源文件 $src 并生成目标文件 $obj。如果编译成功,则输出 #define $var ${val:-1},其中 ${val:-1} 表示如果 $val 未定义,则使用默认值 1。如果编译失败且 $val 未定义,则输出 #undef $var,表示未定义该变量。否则,输出 // not $var $val,表示未达到预期值。
do_test "VFS_GETATTR_ARGS" 2 <<END
#include
int test(struct path path, struct kstat exec_file_stat, u32 request_mask, unsigned int query_flags) {
return vfs_getattr(&path, &exec_file_stat);
}
END
do_test "VFS_GETATTR_ARGS" 3 <<END
#include
int test(struct vfsmount *path, struct dentry *dentry, struct kstat exec_file_stat) {
return vfs_getattr(path, dentry, &exec_file_stat);
}
END
do_test "VFS_GETATTR_ARGS" 4 <<END
#include
int test(struct path path, struct kstat exec_file_stat, u32 request_mask, unsigned int query_flags) {
return vfs_getattr(&path, &exec_file_stat, 0, 0);
}
END
在编译的Makefile文件中,我们可以通过:
generate-kernel-function -t `pwd`/tmp -o kernel-function.h
调用测试函数,并根据测试的结果生成宏,并输出到的一个头文件中,而在实际使用vfs_getattr函数的地方,我们通过判断哪个宏存在,就可以正确的调用对应的函数:
#if (2 == KERNEL_API_VFS_GETATTR_ARGS)
#define our_vfs_getattr(path, exec_file_stat) vfs_getattr(path, exec_file_stat)
#elif (3 == KERNEL_API_VFS_GETATTR_ARGS)
#define our_vfs_getattr(path, exec_file_stat) vfs_getattr((path)->mnt, (path)->dentry, exec_file_stat)
#elif (4 == KERNEL_API_VFS_GETATTR_ARGS)
#define our_vfs_getattr(path, exec_file_stat) vfs_getattr(path, exec_file_stat, 0, 0)
#else
#error The number of arguments to vfs_getattr is neither 2 nor 3 nor 4
#endif
static int get_exec_eigenvalue(const char* exe_path, uint64_t* eigenvalue) {
struct kstat exec_file_stat;
our_nameidata_t nd;
if (our_nd_from_name(exe_path, 0, &nd) == 0) {
if (our_vfs_getattr(&(nd.path), &exec_file_stat)) {
}
do_test "HAVE_PATH_LOOKUP" <<END
#include
#include
void test(const char *path) {
struct nameidata nd;
path_lookup(path, LOOKUP_FOLLOW, &nd);
}
END
static void add_mntpoint(const char *mnt, const char *fs, const char *options)
{
int rc;
our_nameidata_t nd;
if(!should_track_mountpoint(fs)) {
return;
}
#ifdef KERNEL_API_HAVE_PATH_LOOKUP
rc = path_lookup(mnt, LOOKUP_FOLLOW | LOOKUP_DIRECTORY, &nd);
#else
rc = kern_path(mnt, LOOKUP_FOLLOW | LOOKUP_DIRECTORY, &(nd.path));
#endif
}
通过这种方式,我们就可以不必关心函数是在哪个版本变化的,而只需在遇到编译不过的内核时,到这个版本的内核源码中查询这个函数的定义,根据定义来写好测试函数,在编译时动态的就适配这个函数。