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;
}

 

posted @ 2024-04-15 15:20  秦舒云  阅读(739)  评论(0编辑  收藏  举报