Socket與系統調用深度分析


Socket與系統調用深度分析

實驗環境:Linux-5.0.1 內核 32位系統的MenuOS

本文主要解決兩個問題

  • 用戶態如何通過中斷進入socket的系統調用
  • socket抽象層如何通過多態的機制,來支持不同的傳輸層的協議。也就是socket作為父類,TCP/UDP為子類,父類指向子類對象,實現多態。

主要問題有下圖的紅色字體標出

#include<socket.h>
int main(int argc, char *argv[])
{  ...
   socket(...)
   ...
   return 0;
}

該函數會調用socket,socket內核提供給我們的函數,要通過系統調用來使用。

調用流程

那么,在應用程序內,調用一個系統調用的流程是怎樣的呢?

我們以一個假設的系統調用 xyz 為例,介紹一次系統調用的所有環節。

如上圖,系統調用執行的流程如下:

  1. 應用程序 代碼調用系統調用( xyz ),該函數是一個包裝系統調用的 庫函數
  2. 庫函數 ( xyz )負責准備向內核傳遞的參數,並觸發 軟中斷 以切換到內核;
  3. CPU軟中斷 打斷后,執行 中斷處理函數 ,即 系統調用處理函數 ( system_call);
  4. 系統調用處理函數 調用 系統調用服務例程 ( sys_xyz ),真正開始處理該系統調用;

所以如果要找到socket的系統調用,關鍵就是找到socket的system_call入口,在Linux-5.0.1 內核中

https://github.com/mengning/linux/blob/master/arch/x86/entry/syscalls/syscall_32.tbl 從中可以發現socket的系統調用位102號

那么跟蹤程序如果可以跟蹤到socketcall,就可以觀察到系統調用,下面通過gdb調試qume中的linux源代碼來跟蹤系統調用。

跟蹤socketcall系統調用

#啟動qemu 加載內核,不加-S,因為跟蹤的系統調用,不是啟動過程
qemu -kernel ../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img -append  nokaslr -s 
#打開一個新的終端
gdb
file ~/linux-5.0.1/vmlinux
b sys_socketcall   
target remote:1234
c
#在qume中輸入
replyhi

可以看到程序停止在_sys_socketcall,該韓式紙SYSCALL_DEFINE2,也即是系統調用后,通過軟中斷,跳轉到了該函數,查看該函數源碼如下,gdb中的call = 1 ,正是傳遞到swich case語句中

SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
	unsigned long a[AUDITSC_ARGS];
	unsigned long a0, a1;
	int err;
	unsigned int len;

	if (call < 1 || call > SYS_SENDMMSG)
		return -EINVAL;
	call = array_index_nospec(call, SYS_SENDMMSG + 1);

	len = nargs[call];
	if (len > sizeof(a))
		return -EINVAL;

	/* copy_from_user should be SMP safe. */
	if (copy_from_user(a, args, len))
		return -EFAULT;

	err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
	if (err)
		return err;

	a0 = a[0];
	a1 = a[1];

	switch (call) {            //根據call的不同之,從而執行不同的系統系統調用 當前call = 1 也就是執行
	case SYS_SOCKET: 						// 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;
	case SYS_GETSOCKNAME:
		err =
		    __sys_getsockname(a0, (struct sockaddr __user *)a1,
				      (int __user *)a[2]);
		break;
	case SYS_GETPEERNAME:
		err =
		    __sys_getpeername(a0, (struct sockaddr __user *)a1,
				      (int __user *)a[2]);
		break;
	case SYS_SOCKETPAIR:
		err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]);
		break;
	case SYS_SEND:
		err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
				   NULL, 0);
		break;
	case SYS_SENDTO:
		err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
				   (struct sockaddr __user *)a[4], a[5]);
		break;
	case SYS_RECV:
		err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
				     NULL, NULL);
		break;
	case SYS_RECVFROM:
		err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
				     (struct sockaddr __user *)a[4],
				     (int __user *)a[5]);
		break;
	case SYS_SHUTDOWN:
		err = __sys_shutdown(a0, a1);
		break;
	case SYS_SETSOCKOPT:
		err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3],
				       a[4]);
		break;
	case SYS_GETSOCKOPT:
		err =
		    __sys_getsockopt(a0, a1, a[2], (char __user *)a[3],
				     (int __user *)a[4]);
		break;
	case SYS_SENDMSG:
		err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1,
				    a[2], true);
		break;
	case SYS_SENDMMSG:
		err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2],
				     a[3], true);
		break;
	case SYS_RECVMSG:
		err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1,
				    a[2], true);
		break;
	case SYS_RECVMMSG:
		if (IS_ENABLED(CONFIG_64BIT) || !IS_ENABLED(CONFIG_64BIT_TIME))
			err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
					     a[2], a[3],
					     (struct __kernel_timespec __user *)a[4],
					     NULL);
		else
			err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
					     a[2], a[3], NULL,
					     (struct old_timespec32 __user *)a[4]);
		break;
	case SYS_ACCEPT4:
		err = __sys_accept4(a0, (struct sockaddr __user *)a1,
				    (int __user *)a[2], a[3]);
		break;
	default:
		err = -EINVAL;
		break;
	}
	return err;
}

在linuxnet的lab3中查看replyhi源碼,可以發現其實就是基本的socket - bind - listen - accept 過程這就不詳細列出,結合上面的_sys_socketcall,可以猜想到會調用多次_sys_socketcall,進入switch case語句中。

可以看到,調用了4次,分別call為 1 2 4 5,也對用服務器開始動作 socket - bind - listen -accept

客戶端類似,這里不做演示。

socket抽象層的多態的機制

從上上面的SYSCALL_DEFINE2中可以看到__sys_socket()函數

SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args){
、、、
switch (call) {
	case SYS_SOCKET:
		err = __sys_socket(a0, a1, a[2]);
		break;
、、、
}
//進入該函數
int __sys_socket(int family, int type, int protocol)
{
	int retval;
	struct socket *sock;  // socket位關鍵的數據結構
	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));
}
<net.h>
struct socket {
	socket_state		state;
	short			type;
	unsigned long		flags;
	struct socket_wq	*wq;
	struct file		*file;
	struct sock		*sk;
	const struct proto_ops	*ops;  // ***
};

其中最為核心的就是struct proto_ops,查看它的定義可以看到

<net.h>
struct proto_ops {
	int		family;
	struct module	*owner;
	int		(*release)   (struct socket *sock);
	int		(*bind)	     (struct socket *sock,
				      struct sockaddr *myaddr,
				      int sockaddr_len);
	int		(*connect)   (struct socket *sock,
				      struct sockaddr *vaddr,
				      int sockaddr_len, int flags);
	int		(*socketpair)(struct socket *sock1,
				      struct socket *sock2);
	int		(*accept)    (struct socket *sock,
				      struct socket *newsock, int flags, bool kern);
  //...

可以這個結構體中有很多的函數指針,查看SYSCALL_DEFINE2中的__sys_connect

int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
	struct socket *sock;
	struct sockaddr_storage address;
	int err, fput_needed;

	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (!sock)
		goto out;
	err = move_addr_to_kernel(uservaddr, addrlen, &address);
	if (err < 0)
		goto out_put;

	err =
	    security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
	if (err)
		goto out_put;

	err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
				 sock->file->f_flags)   //*** 

可以看到有一個sock->ops->connect函數指針的調用,我們這是調用的是TCP,查看ipv4/tcp_ipv4.c

struct proto tcp_prot = {
	.name			= "TCP",
	.owner			= THIS_MODULE,
	.close			= tcp_close,
	.pre_connect		= tcp_v4_pre_connect,
	.connect		= tcp_v4_connect,
	.disconnect		= tcp_disconnect,
	.accept			= inet_csk_accept,
	.ioctl			= tcp_ioctl,
	.init			= tcp_v4_init_sock,
  //...

這就是tcp中對對相應函數指針實現的初始化。調用時就會通過這個數據結構查到相應的函數。

不同的協議,就痛過不同的數據結構,寫入不同的函數指針,從而實現了多態的機制。

參考資料

https://www.cnblogs.com/fasionchan/p/9431784.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM