1. 程式人生 > >從零開始—Socket系統呼叫和多型封裝

從零開始—Socket系統呼叫和多型封裝

1 重新搭建實驗環境

前面都是用實驗樓環境做的實驗,偷的懶總是要還的,這一次重灌環境前後花了十幾個小時,踩了無數的坑。

1.1 Ubuntu和LINUX核心的區別

Ubuntu是基於LINUX核心編寫的一個作業系統。LINUX核心定義了一些基本的系統功能,Ubuntu在核心之上加入了圖形介面,包管理等功能,優化了人機互動。本次實驗,要求使用LINUX核心5.0以上,所以,在下載安裝完Ubuntu系統後,需要對核心進行更新。

$ uname -a

上面這個指令會顯示Ubuntu當前的核心版本,我們可以通過它來觀察核心的升級是否成功。

1.2 從零開始

下載安裝Ubuntu

首先到Ubuntu官網上下載一個Ubuntu映象,但是太慢了,我們可以在國內的映象網站上去下載。指路網易映象。
下載完成後,在VMware虛擬機器中進行系統安裝,沒什麼可說的。

  • 設定超級管理員
    新裝的系統沒有超級管理員,所以需要先設定一個。執行下面的命令,按照提示要求完成管理員註冊。
sudo passwd root
  • 設定共享資料夾
    為了方便VMware內虛擬主機和我們的主機進行互動,可以設定一個共享資料夾
    首先將虛擬主機關機,然後在虛擬機器設定=》選項卡中設定共享資料夾。安裝VMwareTools,在VMware選單欄,點選“重新安裝VMwareTools”。虛擬主機內會出現資源管理器,裡面有下載好的壓縮包,將它拷貝到桌面上解壓。然後執行VMware底部彈出的建議命令,完成安裝。檢視共享資料夾。共享資料夾的位置在/mnt/hfgs/share/

更換國內源

國外的資源下載速度實在太慢,所以在開始工作之前,建議先更換成國內映象,指路科大映象。

  • 備份原始源
$ sudo cp /etc/apt/sources.list /etc/apt/sources_backup.list
  • 修改配置檔案
$ sudo gedit /etc/apt/sources.list

把從網上找到的資源列表複製拷貝過來,點選資源管理器右上角的save按鈕

  • 更新源
$ sudo apt-get update

下載編譯LINUX5.0核心

先下載5.0以上linux核心。

  • 解壓核心檔案
$ xz -d linux-5.0.1.tar.xz
$ tar -xvf linux-5.0.1.tar
  • 安裝依賴
$ sudo apt-get install build-essential
$ sudo apt-get install libelf-dev
$ sudo apt-get install libncurses-dev
$ sudo apt-get install flex
$ sudo apt-get install bison
$ sudo apt-get install libssl-dev
  • 配置核心
$ cd /linux/5.0.1
$ sudo cp /boot/config-5.0.23-generic -r .config
$ sudo make oldconfig
$ sudo make localmodconfig
$ make menuconfig

在彈出的圖形化介面中配置
kernel hacking -> compile-time and compiler options 勾選 [*] compiler the kernel with debug info

  • 編譯核心
$ sudo make
$ sudo make modules_install
# 更新
$ sudo make install
  • 重啟虛擬機器
    檢視核心版本是否已經是5.0.1
$ uname -a

1.3 搭建實驗環境

  • 安裝qemu模擬命令,載入linux核心
$ sudo apt install qemu
$ qemu-sysem-x86_64 -kernel linux-5.0.1/arch/x86_64/boot/bzIamge
  • 剩餘的部分主要是配置qemu環境,把寫好的replyhi網路聊天程式整合到qemu中,和上一次實驗內容相同,不再重複演示。

2 Socket系統呼叫分析

按照實驗要求,我們分為兩個方向來研究Socket系統呼叫。實驗指出,核心將系統呼叫作為一個特殊中斷來處理,因此首先我們對這一點進行驗證;其次我們將探究,對於不同的協議,Socket系統呼叫原始碼中是如何封裝協議細節的,是否使用了實驗提到的“多型”機制,怎麼實現的。

2.1 系統呼叫的中斷實現

修改Makefile

為探究64位程式中socket的系統呼叫行為,我們首先需要對上一節使用到的Makefile進行修改

#
# Makefile for linuxnet/lab3
#
# ... 省略前文

rootfs:
        gcc -o init linktable.c menu.c main.c -m64 -static -lpthread
        find init | cpio -o -Hnewc |gzip -9 > ../../rootfs.img
        qemu-system-x86_64 -kernel ../../linux-5.0.1/arch/x86/boot/bzImage -initrd ../../rootfs.img -append nokaslr -s -S 

# ...省略後文

在編譯指令gcc那一行,將編譯選項由-m32改為-m64
執行指令

$ make rootfs

我們得到了新的64位可執行檔案init

GDB除錯

使用GDB除錯init,在socket函式前打上斷點。

$ gdb init
$ (gdb) break socket

開啟彙編視窗,檢視程式碼執行情況

$ (gdb) layout asm

可以看到,程式在socket函式入口處停下,下一條彙編指令是一個syscall的系統呼叫。

反彙編init

init進行反彙編

$ objdump -d init > init_ASM.txt

檢視init_ASM.txt檔案,在第104553行找到socket對應的系統呼叫。

證明對於socket api的呼叫是通過socketcall這個特殊中斷來實現的。

syscall的具體實現

利用同樣的辦法,我們按照上一節的方法啟動qemu進行遠端除錯,設定如下斷點:

$ (gdb) break sys_socketcall

跟蹤到一個關鍵函式:SYSCALL_DEFINE2(),它位於linux-5.0.1/net/socket.c之中。
關鍵程式碼如下:

switch (call) {
    case SYS_SOCKET:
        err = __sys_socket(a0, a1, a[2]);
        break;
    case SYS_BIND:
        err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
        break;
    case SYS_CONNECT:
        err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
        break;
    case SYS_LISTEN:
        err = __sys_listen(a0, a1);
        break;
    case SYS_ACCEPT:
        err = __sys_accept4(a0, (struct sockaddr __user *)a1,
                    (int __user *)a[2], 0);
        break;
    // ... 省略其餘部分
    }

可見,每次socket都會呼叫同一個函式,通過傳入的call值不同,在分支語句中執行對應的系統服務例程。

2.2 Socket封裝網路協議的多型機制

__sys_socket()為例,其原始碼位於同一檔案下,也是C語言實現的:

int __sys_socket(int family, int type, int protocol)
{
    int retval;
    struct socket *sock;
    int flags;

    /* Check the SOCK_* constants for consistency.  */
    BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
    BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
    BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
    BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);

    flags = type & ~SOCK_TYPE_MASK;
    if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
        return -EINVAL;
    type &= SOCK_TYPE_MASK;

    if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
        flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)
        return retval;

    return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

注意到函式的傳入引數中有一個protocol變數,它用來指定傳入的協議是多少。對於系統底層來說,不同的protocol值對應不同的協議型別,而對於socket通訊來說,它只負責從高層接受這個欄位值,然後交付更底層的函式,在這裡,呼叫到的sock_create程式碼如下:

int __sock_create(struct net *net, int family, int type, int protocol,
             struct socket **res, int kern)
{
    int err;
    struct socket *sock;
    const struct net_proto_family *pf;

    /*
     *      Check protocol is in range
     */
    if (family < 0 || family >= NPROTO)
        return -EAFNOSUPPORT;
    if (type < 0 || type >= SOCK_MAX)
        return -EINVAL;

    /* Compatibility.

       This uglymoron is moved from INET layer to here to avoid
       deadlock in module load.
     */
    if (family == PF_INET && type == SOCK_PACKET) {
        pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)\n",
                 current->comm);
        family = PF_PACKET;
    }

    err = security_socket_create(family, type, protocol, kern);
    if (err)
        return err;
    // 省略後文

可以發現這個函式仍然不是最底層的函式,它根據情況繼續呼叫security_socket_creat(),或者返回協議錯誤資訊。

從程式碼上來看,Socket封裝協議細節,使用到的應該是名為socket的結構體,在__sys_bind()等函式中,協議欄位作為地址長度被傳入,說明對於socket來說是通過判斷協議欄位長度來區分ipv4和ipv6兩種不同協議的。在socket結構體中,有一個名為sk_family的欄位,通過它的取值不同來判斷這個socket是使用ipv4還是ipv6。可以從socket.c中的程式碼印證這一點:

/* This routine returns the IP overhead imposed by a socket i.e.
 * the length of the underlying IP header, depending on whether
 * this is an IPv4 or IPv6 socket and the length from IP options turned
 * on at the socket. Assumes that the caller has a lock on the socket.
 */
u32 kernel_sock_ip_overhead(struct sock *sk)
{
    struct inet_sock *inet;
    struct ip_options_rcu *opt;
    u32 overhead = 0;
#if IS_ENABLED(CONFIG_IPV6)
    struct ipv6_pinfo *np;
    struct ipv6_txoptions *optv6 = NULL;
#endif /* IS_ENABLED(CONFIG_IPV6) */

    if (!sk)
        return overhead;

    switch (sk->sk_family) {
    case AF_INET:
        inet = inet_sk(sk);
        overhead += sizeof(struct iphdr);
        opt = rcu_dereference_protected(inet->inet_opt,
                        sock_owned_by_user(sk));
        if (opt)
            overhead += opt->opt.optlen;
        return overhead;
#if IS_ENABLED(CONFIG_IPV6)
    case AF_INET6:
        np = inet6_sk(sk);
        overhead += sizeof(struct ipv6hdr);
        if (np)
            optv6 = rcu_dereference_protected(np->opt,
                              sock_owned_by_user(sk));
        if (optv6)
            overhead += (optv6->opt_flen + optv6->opt_nflen);
        return overhead;
#endif /* IS_ENABLED(CONFIG_IPV6) */
    default: /* Returns 0 overhead if the socket is not ipv4 or ipv6 */
        return overhead;
    }
}
EXPORT_SYMBOL(kernel_sock_ip_overhead);

綜上所述,socket實現了協議封裝的多型,它通過結構體的形式,用協議欄位的長度作為劃分協議的依據,以此將ipv4和ipv6區分開來。而對於呼叫這些函式和api的高層來說,不管自己是什麼協議都呼叫同樣的函式