compat_ioctl - Linux 64位内核(arm64)驱动兼容32位应用程序(armhf)的ioctl接口
最近,公司来了一次硬件升级,开发平台从全志T3(armhf)升级到全志T527(arm64),平台迁移后,想直接使用原来动态库和应用程序从而减少开发量,用户态大部分接口都运行正常,唯独ioctl接口无法调用成功。
如果要成功移植要做到以下几点:
1. 驱动要同时实现 unlocked_ioctl 和 compat_ioctl。
struct file_operations
{
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
}__randomize_layout;
从名字就可以猜测,compat_ioctl就是发生兼容性的场景下要调用的函数,事实也确实如此。
当应用层是32位程序,内核及架构是32位程序,那么驱动的unlocked_ioctl函数被调用。
当应用层是32位程序,内核及架构是64位程序,那么驱动的compat_ioctl函数被调用。
当应用层是64位程序,内核及架构是64位程序,那么驱动的unlocked_ioctl函数被调用。
那为什么一个ioctl()系统调用需要在驱动里面实现2个对应的函数呢?
我们可以看到unlocked_ioctl 和 compat_ioctl这2个函数的最后一个参数是 unsigned long类型的,long类型在不同的架构下面的长度是不同的,在32位平台下是4字节,64位平台下就是8个字节,当32位的应用程序使用ioctl系统调用时,传了4个字节的参数,到驱动中,应该是8个字节,这样就产生了不兼容,为了不影响64位的应用程序,就提供了2个接口来实现。当compat_ioctl被调用时,这个参数就会自动扩展成8个字节的数据,这是比较简单从场景,compat_ioctl可以和unlocked_ioctl使用同样的实现。
还有一种更复杂的场景,那就是用户态ioctl的最后一个参数是一个地址,在驱动中使用copt_from_user去取数据,显然,这种情况下,传来的地址是不能直接使用的,用户态传来的32位的地址对于64位的内核来说,就是个错误的地址,那么就需要一个转换函数,那就是 compat_ptr 。
函数定义在:include/linux/compat.h
/*
* A pointer passed in from user mode. This should not
* be used for syscall parameters, just declare them
* as pointers because the syscall entry code will have
* appropriately converted them already.
*/
/*
* 从用户态传入一个指针。这是不能用作系统调用参数的,仅仅是个声明,
* 系统调用的入口代码会妥善的转换好他。
*/
#ifndef compat_ptr
static inline void __user *compat_ptr(compat_uptr_t uptr)
{
return (void __user *)(unsigned long)uptr;
}
#endif
static inline compat_uptr_t ptr_to_compat(void __user *uptr)
{
return (u32)(unsigned long)uptr;
}
我们看看官方是如何介绍他的,ioctl based interfaces — The Linux Kernel documentation
32-bit compat mode¶
In order to support 32-bit user space running on a 64-bit machine, each subsystem or driver that implements an ioctl callback handler must also implement the corresponding compat_ioctl handler.
As long as all the rules for data structures are followed, this is as easy as setting the .compat_ioctl pointer to a helper function such as compat_ptr_ioctl() or blkdev_compat_ptr_ioctl().
compat_ptr()
On the s390 architecture, 31-bit user space has ambiguous representations for data pointers, with the upper bit being ignored. When running such a process in compat mode, the compat_ptr() helper must be used to clear the upper bit of a compat_uptr_t and turn it into a valid 64-bit pointer. On other architectures, this macro only performs a cast to a void __user * pointer.
In an compat_ioctl() callback, the last argument is an unsigned long, which can be interpreted as either a pointer or a scalar depending on the command. If it is a scalar, then compat_ptr() must not be used, to ensure that the 64-bit kernel behaves the same way as a 32-bit kernel for arguments with the upper bit set.
The compat_ptr_ioctl() helper can be used in place of a custom compat_ioctl file operation for drivers that only take arguments that are pointers to compatible data structures.
翻译一下:
32位兼容模式
为了支持64位机器上的32位用户空间代码,每个实现了ioctl回调函数的驱动和子系统必需对应的 compat_ioctl 。
只要数据结构遵循一定的规则,这就很容易操作。将.compat_ioctl指针直接赋值为 compat_ptr_ioctl() 或者 blkdev_compat_ptr_ioctl() 这样的辅助函数就可以了。
compat_ptr()
在 s390架构中, 31-bit user space has ambiguous representations for data pointers, with the upper bit being ignored. When running such a process in compat mode, the compat_ptr() helper must be used to clear the upper bit of a compat_uptr_t and turn it into a valid 64-bit pointer.
在其他架构中,这个宏仅仅是将其转换为void __user * 指针.
在compat_ioctl()回调中,最后一个参数是unsigned long的,根据命令的实际情况,它可能被解释为一个指针或者一个普通变量。如果是一个变量,那么compat_ptr()就不能被使用,这样就确保64-bit kernel和 32-bit kernel表现是相同的。
compat_ptr_ioctl()辅助函数被使用为一个定制的compat_ioctl file operation,仅仅将指针转换成兼容的数据结构。
什么意思呢?意思就是当ioctl的最后一个参数被当成一个值来使用的时候,是不需要使用 compat_ptr转换的,只有用作指针的时候才需要转换。当用作指针的时候,这个指针就可以被转换为兼容的地址。
当最后一个参数用作指针的时候,可以偷个懒,直接使用 compat_ptr_ioctl 用作 compat_ioctl的回调函数就行了。
我们来看下定义 fs/ioctl.c
/**
* compat_ptr_ioctl - generic implementation of .compat_ioctl file operation
*
* This is not normally called as a function, but instead set in struct
* file_operations as
*
* .compat_ioctl = compat_ptr_ioctl,
*
* On most architectures, the compat_ptr_ioctl() just passes all arguments
* to the corresponding ->ioctl handler. The exception is arch/s390, where
* compat_ptr() clears the top bit of a 32-bit pointer value, so user space
* pointers to the second 2GB alias the first 2GB, as is the case for
* native 32-bit s390 user space.
*
* The compat_ptr_ioctl() function must therefore be used only with ioctl
* functions that either ignore the argument or pass a pointer to a
* compatible data type.
*
* If any ioctl command handled by fops->unlocked_ioctl passes a plain
* integer instead of a pointer, or any of the passed data types
* is incompatible between 32-bit and 64-bit architectures, a proper
* handler is required instead of compat_ptr_ioctl.
*/
long compat_ptr_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
if (!file->f_op->unlocked_ioctl)
return -ENOIOCTLCMD;
return file->f_op->unlocked_ioctl(file, cmd, (unsigned long)compat_ptr(arg));
}
EXPORT_SYMBOL(compat_ptr_ioctl);
可以看到,就是直接转换了一下,就调用unlocded_ioctl了。
原理搞清楚了,具体怎么用呢,要根据ioctl的第三个参数的用途来决定
如果只用作数值,那么用一样的函数就行了:
static const struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = xxx_ioctl,
.compat_ioctl = xxx_ioctl,
};
如果只用作指针,那么内核提供了偷懒函数:
static const struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = xxx_ioctl,
.compat_ioctl = compat_ptr_ioctl,
};
如果有时候用作指针,有时候用作数值,那就需要分开写了:
static const struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = xxx_ioctl,
.compat_ioctl = xxx_ioctl_compat,
};
static long xxx_ioctl_compat(struct file *file, unsigned int cmd, unsigned long arg)
{
unsigned long arg64;
int ret = 0;
switch(cmd)
{
case CMD_A:
unsigned long a = arg; //arg被当作一个值在内核中使用
break;
case CMD_B:
arg64 = (unsigned long)compat_ptr(arg);
//arg被当作一个指针,需要内核态和用户态根据地址拷贝数据。下面的例子传了一个结构体struct_a_t的地址
ret = copy_from_user(&sturct_a,(struct struct_a_t *)arg64,sizeof(struct struct_a_t));
ret = copy_to_user( ptr_to_compat(arg64), &struct_a, sizeof(struct_a_t) );
break;
default:
break;
}
return ret;
}