歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> Linux教程

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確認一下。

Copyright © Linux教程網 All Rights Reserved