32 位的应用程序在 64 位内核中的 ioctl 问题

工作中的一个项目涉及到了内核模块和用户程序两部分,之间通过 ioctl() 进行通讯,现在需要 64 位(x86_64)的 Linux,所使用内核版本是固定的,2.6.9。

首先,直接编译运行以后,发现一个错误:kernel: ioctl32(${APP_NAME}:${PID}): Unknown cmd fd(6) cmd(c01844f01){00} arg(0816ed60) on ${file}。Google 了一下,明白这是由于用户程序是 32 位而内核是 64 位的缘故。问题的缘由是知道了,得开始找解决方法了。刚开始还以为是内核模块中自定义的 ioctl()(通过 struct file_operations 定义) 返回了错误,后来在其中加上了调试信息,发现根本就没有进到函数中去。

后来找到了 Linux 64Bit 下的 ioctl和compat_ioctl ioctl32 Unknown cmd fd 这篇文章,虽然也说的是 2.6,但我在 The Linux Cross Reference 2.6.9 的 struct file_operations 中根本就没有找到 compat_ioctl(),进一步发现这是在 2.6.11 中加入的。虽然没能解决问题,但至少大致了解了问题出现真正的原因。

但还是不明白为什么会没有进到我自己的函数中去,在 LXR 中寻找“ioctl32”,最后在 fs/compat.c 中找到了“unknown cmd”,其实调用的是compat_sys_ioctl(),看下来,大概知道,整个内核空间中对于 ioctl()request 是全局唯一的,在这里是 cmd。每一个 cmd 对应一个处理器,其类型是 typedef int (*ioctl_trans_handler_t)(unsigned int fd, unsigned int cmd,unsigned long arg, struct file *),当对应的处理器没有找到的时候,就会打出“unknown cmd”错误。但当时并不明白这个处理器是干什么用的。所以认为是命令在用户程序和内核中不一样所导致的。那这个命令是如何产生的呢?通常都会调用 _IOR_IOW_IOWR 等宏所产生的,其中最后一个是一个结构,宏展开的时候会获取这个结构的大小作为命令的一部分。而项目的结构中有指针和 time_t,而命令在用户程序以及内核中都要使用,并且一个是 32 位,一个是 64 位,结构体大小肯定不一样,最后所获得的命令也必然不同。所以这时候在调用 _IOWR 生成命令值的时候,将最后一个结构体换成另外一个,并保证其大小在 32 位和 64 位下是一样的。但即使这样,仍然出现相同的错误。

接着 Google,找到的信息都是零零散散,最后找到了 32bit ioctl support for 64bit kernels,比较系统的介绍了同样的问题。简单来说,因为应用程序是 32 位,内核(包括内核模块)是 64 位,而 32 位和 64 位的很多数据类型大小是不同的,比如 long 和指针类型,当应用程序需要和内核打交道的时候(比如系统调用,比如本文提到的 ioctl),就需要将数据转换一下,才不会导致数据错误。一般的系统调用内核已经对其参数进行了转换,而 ioctl 是和自定义的内核模块通讯,所以相应的转换就必须自行处理。而前面提到的“handler”就是自定义的转换函数,并且需要和命令对应起来。那么就需要在模块初始化的时候使用 register_ioctl32_conversion(unsigned int cmd, ioctl_trans_handler_t handler) 进行注册,退出的时候调用 unregister_ioctl32_conversion(unsigned int cmd) 将其注销。

在处理器中,数据转换如下:

  1. #if defined CONFIG_COMPAT
  2. int my_handler(unsigned int fd, unsigned int cmd,
  3.                unsigned long arg, struct file *) {
  4.  
  5.     struct myarg_32bit *arg32 = (struct myarg_32bit *)arg;
  6.     struct myarg arg64;
  7.     mm_segment_t old_fs;
  8.     int err;
  9.  
  10.     /* 32 -> 64*/
  11.     /* for each field in myarg */
  12.     err = 0;
  13.     err |= get_user(arg64.field1, &arg32->field1);
  14.     ...
  15.  
  16.     if (err) return -EFAULT;
  17.     old_fs = get_fs();
  18.     set_fs(KERNEL_DS); /* tell kernel to accept arguments in kernel space */
  19.  
  20.     /*
  21.      * 接着调用真正的 ioctl,最终会调到 file_operations 中的 .ioctl,
  22.      * 也就是我们自己的处理函数
  23.      */
  24.     err = sys_ioctl(file, cmd, (unsigned long)&arg64)
  25.  
  26.     set_fs(old_fs);
  27.  
  28.     /* 64 -> 32 */
  29.     /* for each field in myarg */
  30.     err |= put_user(arg64.field1, &arg32->field1);
  31.     ...
  32.  
  33.     return err;
  34. }
  35. #endif

原来文档中提到的函数原型比较老,建议直接查看内核源码,保证函数参数都正确。内核中也有一些例子,比如 arch/x86_64/ia32/ia32_ioctl.c,可以作为参考。

最后总结一下步骤:

  • ioctl() 中的命令代码使用 _IOWR 等宏生成,必须要保证在 32 位和 64 位下相同,这样才能根据命令找到对应的处理器,并且在真正的处理函数中才能正确处理。
  • 必须要注册处理器,先 32 -> 64,再调用 sys_ioctl(),再 64 -> 32。

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <blockquote>
  • You can use BBCode tags in the text.
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>. Beside the tag style "<foo>" it is also possible to use "[foo]".
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
eight times equals 32
Solve this math question and enter the solution with digits. E.g. for "two plus four = ?" enter "6".