感谢支持
我们一直在努力

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确认一下。

1.结构体ioctl_trans:
struct ioctl_trans {
    unsigned long cmd;
    ioctl_trans_handler_t handler;
    struct ioctl_trans *next;
};
该结构体提供了一个粘合层,用户可以动态注册一个ioctl_trans以便其提供64位和32位的粘合:
extern int register_ioctl32_conversion(unsigned int cmd,
                ioctl_trans_handler_t handler);
extern int unregister_ioctl32_conversion(unsigned int cmd);
整个系统的ioctl_trans连接成一个哈希表,放在ioctl32_hash_table变量中。每一个ioctl_trans的handler都是一个回调函数,在其中将64位的数据和32位的数据类型进行统一,统一成64位可以正确识别和处理的,以防在后续的64位代码中出错,比如一个32位的signed int为-1,需要将之转化成64个1而不是32个0加上32个1。
2.一套完整的系统调用:
如果不这样的话,32位程序的系统调用如何被路由到通过ioctl_trans们进行粘合的代码就成了问题,要知道x86-64已经不使用int 0x80作为触发系统调用的机制了,而使用syscall指令来触发。那么原来的32位程序都是用int 0x80来触发的,这下怎么办?办法就是仍然保留0x80号中断号,将其处理程序设置成ia32_syscall,它在ia32_sys_call_table中找具体的系统调用处理函数,具体在arch/x86_64/ia32/ia32entry.S中:
ENTRY(ia32_syscall)
        CFI_STARTPROC
        swapgs
        sti
        movl %eax,%eax
        pushq %rax
        cld
        SAVE_ARGS 0,0,1
        GET_THREAD_INFO(%r10)
        testl $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),threadinfo_flags(%r10)
        jnz ia32_tracesys
ia32_do_syscall:
        cmpl $(IA32_NR_syscalls),%eax
        jae  ia32_badsys
        IA32_ARG_FIXUP
        call *ia32_sys_call_table(,%rax,8) # xxx: rip relative

ia32_sys_call_table:
        .quad sys_restart_syscall
        .quad sys_exit
        .quad stub32_fork
        .quad sys_read

    .quad compat_sys_ioctl

在arch/x86_64/kernel/traps.c的trap_init函数中将ia32_syscall设置成0x80号中断的处理程序:
set_system_gate(IA32_SYSCALL_VECTOR, ia32_syscall);//#define IA32_SYSCALL_VECTOR 0x80
那么使用sysenter的怎么办呢? 这是通过在exec的时候由内核检测到其是32位程序是动态将处理代码map到gate处的,要知道x86-64也不使用sysenter机制进行系统调用。那64位的x86-64怎么系统调用呢?在arch/x86-64/kernel/entry.S中有ENTRY(system_call)这个标志,在arch/x86_64/kernel/setup64.c中的syscall_init函数中有以下一行:
wrmsrl(MSR_LSTAR, system_call);
可见64位的x86-64是通过一个MSR寄存器来保存系统调用处理地址的,而不再是通过中断。至于说机器如何处理这个信息以及这个寄存器如何影响系统运行,这已经到x86-64体系的cpu实现硬件问题了,和本文的linux系统的要旨无关,此处简略(再说不简略也不行啊,我也不会啊)。
3.总结
由于硬件指令的兼容,32位的程序在用户态不受任何影响的运行,由于内核保留了0x80号中断作为32位程序的系统调用服务,因此32位程序可以安全触发0x80号中断使用系统调用,由于内核为0x80中断安排了另一套全新的系统调用表,因此可以安全地转换数据类型成一致的64位类型,再加上应用级别提供了两套c库,可以使64位和32位程序链接不同的库。因此linux的64-32兼容搞得非常好。
     为了看一下在x86-64上64位程序和32位程序是如何执行系统调用的,写一个最简单的测试程序:
#include <sys/types.h>
#include <unistd.h>
int main()
{
        getpid();
}
之所以选择getpid是因为它没有参数,最简单,将之在Red Hat 32位机器上按照如下命令行编译:
gcc test.c -o test-32 -g
然后再将之在64位机器上同样方式编译,只是可执行文件名字变为test-64。接下来首先gdb test-32:
(gdb) b main

(gdb) r

(gdb) b getpid
Breakpoint 2 at 0xf7f3d430
(gdb) disassemble  0xf7f3d430 0xf7f3d43a
0xf7f3d430 <getpid+0>:  mov    $0x14,%eax    #0x14是20,正是getpid的系统调用号
0xf7f3d435 <getpid+5>:  int    $0x80         #32位程序以int 0x80触发系统调用
0xf7f3d437 <getpid+7>:  ret   
0xf7f3d438 <getpid+8>:  nop   
0xf7f3d439 <getpid+9>:  nop   
End of assembler dump.
(gdb)  
结果全部在,可见即使在64位机器上,32位程序仍然使用int 0x80触发系统调用,在内核中已经注册了0x80的中断处理函数。接下来再试一下64位的程序如何触发系统调用,执行gdb test-64:
(gdb) b main

(gdb) r

(gdb) b getpid
Breakpoint 2 at 0x32fbf90f40
(gdb) disassemble 0x32fbf90f40 0x32fbf90f70
Dump of assembler code from 0x32fbf90f40 to 0x32fbf90f70:
0x00000032fbf90f40 <getpid+0>:  mov    %fs:0x94,%edx
0x00000032fbf90f48 <getpid+8>:  test   %edx,%edx
0x00000032fbf90f4a <getpid+10>: mov    %edx,%eax
0x00000032fbf90f4c <getpid+12>: jle    0x32fbf90f50 <getpid+16>
0x00000032fbf90f4e <getpid+14>: repz retq
0x00000032fbf90f50 <getpid+16>: jne    0x32fbf90f5e <getpid+30>
0x00000032fbf90f52 <getpid+18>: mov    %fs:0x90,%eax
0x00000032fbf90f5a <getpid+26>: test   %eax,%eax
0x00000032fbf90f5c <getpid+28>: jne    0x32fbf90f4e <getpid+14>
0x00000032fbf90f5e <getpid+30>: mov    $0x27,%eax #系统调用号装入eax
0x00000032fbf90f63 <getpid+35>: syscall         #执行系统调用
0x00000032fbf90f65 <getpid+37>: test   %edx,%edx
0x00000032fbf90f67 <getpid+39>: jne    0x32fbf90f4e <getpid+14>
0x00000032fbf90f69 <getpid+41>: mov    %eax,%fs:0x90
值得注意的是,在2.6.9内核的x86-64机器上,getpid和32位机器的getpid系统调用号有所不同,在64位上是39号,定义在include/asm-x86_64/unistd.h:
#define __NR_getpid                             39
__SYSCALL(__NR_getpid, sys_getpid)
而刚才看到过,32位兼容的getpid的系统调用号为20,定义在arch/x86_64/ia32/ia32entry.S中:
ia32_sys_call_table:

    .quad sys_getpid                /* 20 */


PS:千万不要觉得test.c很简单然后就stepi单指令跟踪哦,因为这会涉及到一大堆跳转,如果你不明白链接的知识,不了解GOT和PIC的话,那就麻烦大了,因此还是直接在getpid处下断比较直观,如果你想顺便把代码重定位和GOT等玩意儿搞了的话,也可以试一下,反正在调试器面前,整个地址空间都会暴露,想看什么都行,当然,要学会让/proc/<pid>/maps等文件帮忙哦。

赞(0) 打赏
转载请注明出处:服务器评测 » Linux的64位操作系统对32位程序的兼容
分享到: 更多 (0)

听说打赏我的人,都进福布斯排行榜啦!

支付宝扫一扫打赏

微信扫一扫打赏