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的高層來說,不管自己是什么協議都調用同樣的函數。