前面都是用實驗樓環境作的實驗,偷的懶老是要還的,這一次重裝環境先後花了十幾個小時,踩了無數的坑。linux
Ubuntu是基於LINUX內核編寫的一個操做系統。LINUX內核定義了一些基本的系統功能,Ubuntu在內核之上加入了圖形界面,包管理等功能,優化了人機交互。本次實驗,要求使用LINUX內核5.0以上,因此,在下載安裝完Ubuntu系統後,須要對內核進行更新。shell
$ uname -a
上面這個指令會顯示Ubuntu當前的內核版本,咱們能夠經過它來觀察內核的升級是否成功。ubuntu
首先到Ubuntu官網上下載一個Ubuntu鏡像,可是太慢了,咱們能夠在國內的鏡像網站上去下載。指路網易鏡像。
下載完成後,在VMware虛擬機中進行系統安裝,沒什麼可說的。api
sudo passwd root
/mnt/hfgs/share/
。國外的資源下載速度實在太慢,因此在開始工做以前,建議先更換成國內鏡像,指路科大鏡像。網絡
$ sudo cp /etc/apt/sources.list /etc/apt/sources_backup.list
$ sudo gedit /etc/apt/sources.list
把從網上找到的資源列表複製拷貝過來,點擊資源管理器右上角的save按鈕app
$ sudo apt-get update
先下載5.0以上linux內核。socket
$ 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
$ uname -a
$ sudo apt install qemu $ qemu-sysem-x86_64 -kernel linux-5.0.1/arch/x86_64/boot/bzIamge
按照實驗要求,咱們分爲兩個方向來研究Socket系統調用。實驗指出,內核將系統調用做爲一個特殊中斷來處理,所以首先咱們對這一點進行驗證;其次咱們將探究,對於不一樣的協議,Socket系統調用源碼中是如何封裝協議細節的,是否使用了實驗提到的「多態」機制,怎麼實現的。flex
爲探究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調試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這個特殊中斷來實現的。
利用一樣的辦法,咱們按照上一節的方法啓動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值不一樣,在分支語句中執行對應的系統服務例程。
以__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的高層來講,無論本身是什麼協議都調用一樣的函數。