Linux的64位操作系统对32位程序的兼容

最近在调试一个关于OpenVPN的程序,由于是远程支持的因此一些很奇怪的现象根本不好找切入点,比如OpenVPN客户端连接服务器正常,虚拟IP地址也已经分配了,tap设备已经打开并没有抱错,,然而打开的tap设备不是tap0而是” “,也就是什么都没有,连个空格都不是,这是怎么回事呢? 为了问题简化,将引起问题的代码从OpenVPN中切出来,得到一个纯粹打开tap设备的代码:int main(int argc, char *argv[]){

struct ifreq ifr; int fd, err; char *clonedev = “/dev/net/tun”; if( (fd = open(clonedev , O_RDWR)) < 0 ) { perror(“Opening /dev/net/tun”); return fd; } memset(&ifr, 0, sizeof(ifr)); ifr.ifr_flags |= IFF_TUN;//或者IFF_TAP printf(“1:%s\n”, ifr.ifr_name); if( (err = ioctl(fd, TUNSETIFF, &ifr)) < 0 ) { perror(“ioctl(TUNSETIFF)”); close(fd); return err; } printf(“2:%s\n”, ifr.ifr_name); return fd;}编译为test执行后,发现第二次打印出”tun0″,正常,然后将此程序拷贝给远程的问题机器,却没有打印”tun0″。很多奇怪的问题都和系统相关,于是问到了对方的系统版本,由uname -a得到,发现其实它是一个64位的系统,于是安装了一个64位的Red Hat,版本是:2.6.9-78.EL x86_64 GNU/Linux。运行的test是一个在32位系统上编译的程序。由于linux的64位内核对32位程序提供了兼容服务,且x86-64体系也对32位的指令集和寄存器提供了最底层的兼容,想象而言不该出此问题的,在64位系统上检查到了/lib/libc以及/lib/ld-linux等32位的系统库和链接器就更加坚定了“问题不该有”的观念–64位系统兼容32位程序的简单性需要N多层次的支持,机器指令兼容了,操作系统层和编译器就不必再操心指令,操作系统只需要提供系统服务的兼容即可,编译器几乎什么都不需要做,再往上就是系统库了,比如glibc就需要提供两套,为32位程序和64位程序分别提供服务。然而虽然“问题不该有”,事实是问题确实出现了,机器指令是兼容的,操作系统也是兼容的,而系统中确实也有两套libc和ld,那么问题出在哪里呢? 十有八九是tun的驱动有问题,于是在drivers/net/tun.c的tun_chr_ioctl这个字符设备的ioctl函数中加入dump_stack()调用,编译之,insmod之,然后再次执行test,通过dmesg查看日志,以下是Call Trace:<ffffffffa02c65b9>{:tun:tun_chr_ioctl+0} <ffffffffa02c65dd>{:tun:tun_chr_ioctl+36} <ffffffff8019c341>{chrdev_open+952} <ffffffff801a7c86>{sys_ioctl+1006} <ffffffff8012b355>{dev_ifsioc+228} <ffffffff801c65a4>{compat_sys_ioctl+379} <ffffffff801279f7>{sysenter_do_call+27}其中有一个dev_ifsioc很令人好奇,难道执行流不是通过sys_ioctl直接路由到tun_chr_ioctl的吗?为何还要有一个dev_ifsioc?最后只好看2.6.9内核的代码了。 搜索到了以下一行:HANDLE_IOCTL(TUNSETIFF, dev_ifsioc)HANDLE_IOCTL的定义:#define HANDLE_IOCTL(cmd,handler) { (cmd), (ioctl_trans_handler_t)(handler) }, 这明明是想构造一个ioctl_trans数组:struct ioctl_trans { unsigned long cmd; ioctl_trans_handler_t handler; struct ioctl_trans *next;};这个数组提供了内核层次系统调用的64位向32位的兼容性,整个系统所有需要提供兼容性的系统调用都会注册一个ioctl_trans,由此可见dev_ifsioc实际处理了TUNSETIFF这个ioctl命令。64位上的32位程序发起的ioctl系统调用被操作系统路由到了compat_sys_ioctl(具体原因一会儿说):asmlinkage long compat_sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg){ … t = ioctl32_hash_table[ioctl32_hash (cmd)];

while (t && t->cmd != cmd) t = t->next; if (t) { if (t->handler) { lock_kernel(); error = t->handler(fd, cmd, arg, filp); //对于TUNSETIFF而言,这里调用dev_ifsioc unlock_kernel(); up_read(&ioctl32_sem); } else { up_read(&ioctl32_sem); error = sys_ioctl(fd, cmd, arg); } } …}dev_ifsioc的实现如下,它只要提供“兼容性”服务,比如统一64位和32位的数据类型等:static int dev_ifsioc(unsigned int fd, unsigned int cmd, unsigned long arg){ struct ifreq ifr; struct ifreq32 __user *uifr32; … mm_segment_t old_fs; int err; uifr32 = compat_ptr(arg); //转换64位的unsigned long数据类型到32位的地址 … switch (cmd) { case SIOCSIFMAP: …//不是我们关注的TUNSETIFF default: //对于TUNSETIFF,掉入了default,顺利从uifr32所代表的32位地址处拷贝了ifr结构到内核 if (copy_from_user(&ifr, uifr32, sizeof(*uifr32))) return -EFAULT; break; } old_fs = get_fs(); set_fs (KERNEL_DS); err = sys_ioctl (fd, cmd, (unsigned long)&ifr); //1 set_fs (old_fs); if (!err) { switch (cmd) { //后面的case明显没有TUNSETIFF case SIOCGIFFLAGS: case SIOCGIFMETRIC: case SIOCGIFMTU: case SIOCGIFMEM: case SIOCGIFHWADDR: case SIOCGIFINDEX: case SIOCGIFADDR: case SIOCGIFBRDADDR: case SIOCGIFDSTADDR: case SIOCGIFNETMASK: case SIOCGIFTXQLEN: if (copy_to_user(uifr32, &ifr, sizeof(*uifr32))) return -EFAULT; break; case SIOCGIFMAP: …//不是我们关注的TUNSETIFF } } return err;}注意“1”处的sys_ioctl调用使用的ifr的地址调用sys_ioctl,而ifr的地址显然只是一个中间变量,它存储在发起系统调用的进程的内核栈上,明显是一个内核栈地址,由此可见,即使sys_ioctl将执行流路由到了tun_chr_ioctl,而tun_chr_ioctl正确地将信息拷贝到了它的参数arg,数据也仅仅填充到了内核栈上,而不是真正的用户进程的地址。如果需要真正将数据拷贝到用户进程空间,我们需要在后面的switch中加一个case,这个case即TUNSETIFF,这样结果就正确了。这明显是一个内核的bug,不知道哪个家伙加了HANDLE_IOCTL(TUNSETIFF, dev_ifsioc)这么一行,却忘记了在dev_ifsioc中处理TUNSETIFF,这几乎可以肯定不是一个人加的,有时间翻一下patchs确认一下。

有事者,事竟成;破釜沉舟,百二秦关终归楚;苦心人,

Linux的64位操作系统对32位程序的兼容

相关文章:

你感兴趣的文章:

标签云: