Byeo

bind system call 1 (__sys_bind) 본문

프로그래밍 (Programming)/네트워크 스택

bind system call 1 (__sys_bind)

BKlee 2024. 4. 27. 15:49
반응형

BSD socket API에서 socket을 생성한 뒤에는 server-side 측에서 bind를 실행합니다. bind는 나의 ip:port를 socket과 연결하는 역할을 수행하죠. 이 번에는 BSD socket API중에서 bind를 분석해보려고 합니다.

 

이전 포스트 (socket system call)

https://byeo.tistory.com/entry/socket-system-call

 

socket system call 1 (syscall_socket ~ __sock_create)

int socket(int domain, int type, int protocol); socket 시스템 콜의 동작을 한 번 들여다봅니다! https://man7.org/linux/man-pages/man2/socket.2.html linux 5.15 기준입니다. 1. syscall_64.tbl 이전 포스트에서 리눅스 시스템 콜

byeo.tistory.com

 

1. __sys_bind

system call을 호출해서 __sys_* 함수까지 어떻게 불리는지는 지난번 __sys_socket에서 짧게 다뤘었습니다. 따라서 해당 부분은 스킵하겠습니다.

 

// net/socket.c/__sys_bind() :1671

/*
 *	Bind a name to a socket. Nothing much to do here since it's
 *	the protocol's responsibility to handle the local address.
 *
 *	We move the socket address to kernel space before we call
 *	the protocol layer (having also checked the address is ok).
 */
 
int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
	struct socket *sock;
	struct sockaddr_storage address;
	int err, fput_needed;

	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (sock) {
		err = move_addr_to_kernel(umyaddr, addrlen, &address);
		if (!err) {
			err = security_socket_bind(sock,
						   (struct sockaddr *)&address,
						   addrlen);
			if (!err)
				err = sock->ops->bind(sock,
						      (struct sockaddr *)
						      &address, addrlen);
		}
		fput_light(sock->file, fput_needed);
	}
	return err;
}

SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
	return __sys_bind(fd, umyaddr, addrlen);
}

 

위 함수는 __sys_bind 함수입니다. 여기서 먼저 sturct sockaddr 함수의 구조체를 파악해보고 가도록 하죠.

 

1) struct sockaddr

// include/linux/socket.h :26

typedef __kernel_sa_family_t	sa_family_t;

struct sockaddr {
	sa_family_t	sa_family;	/* address family, AF_xxx	*/
	char		sa_data[14];	/* 14 bytes of protocol address	*/
};

 

__kernel_sa_family_t는 아래에 정의되어 있습니다.

// include/uapi/linux/socket.h :10
typedef unsigned short __kernel_sa_family_t;

 

결국 sockaddr은 2 bytes short (sa_family)와 14 bytes의 character (sa_data)로 구성된 것을 볼 수 있습니다.

 

여기서 우리는 C 프로그래밍을 할 때, 보통 IPv4의 경우에는 sockaddr_in으로 인자를 넘겨줍니다. 그러면 sockaddr_in과 sockaddr이 어떤 관계에 있는지 한 번 분석해봅시다.

1-1) struct sockaddr_in

       struct sockaddr_in {
           sa_family_t     sin_family;     /* AF_INET */
           in_port_t       sin_port;       /* Port number */
           struct in_addr  sin_addr;       /* IPv4 address */
       };

출처: https://man7.org/linux/man-pages/man3/sockaddr.3type.html

 

sa_family_t: sa_family_t는 아까 위에서 typedef __kernel_sa_family_t sa_family_t;로 확인했었죠.

 

in_port_t: 그 다음, in_port_t는 어떤 type일까요?

// usr/include/netinet/in.h :122

/* Type to represent a port.  */
typedef uint16_t in_port_t;

 

2 bytes 값입니다. 우리는 port가 2 bytes라는 것을 알고있지요.

 

struct in_addr: 마지막으로 in_addr을 봅시다.

// include/uapi/linux/in.h :88

/* Internet address. */
struct in_addr {
	__be32	s_addr;
};

 

in_addr도 4 bytes 변수입니다. __be32는 유추컨대 big endian 32 bit겠죠? 보통 different machine끼리 통신할 때는 big endian을 기준으로 잡는다고 합니다. socket programming을 할 때 우리가 hton* 를 호출해야 하는 이유이죠.

 

1-2) (struct sockaddr*) &sockaddr_in

그러면 sockaddr_in이 sockaddr로 변환할 때 어떻게 들어가게 될까요?

struct sockaddr_in {
   sa_family_t     sin_family;     /* AF_INET */
   in_port_t       sin_port;       /* Port number */
   struct in_addr  sin_addr;       /* IPv4 address */
};

struct sockaddr {
	sa_family_t	sa_family;	/* address family, AF_xxx	*/
	char		sa_data[14];	/* 14 bytes of protocol address	*/
};


// user space
struct sockaddr_in address;
address.sin_family = AF_INET; 
address.sin_addr.s_addr = INADDR_ANY /* the localhost*/ ; 
address.sin_port = htons(8080); 
bind(fd, (struct sockaddr *)&address, sizeof(address))

 

sin_family 는 2 bytes 값으로서 그대로 AF_INET이 들어가게 될 것입니다.

in_port_t는 2 bytes, in_addr는 4bytes입니다. 따라서 sa_data에는 차례로 총 6 bytes가 복사될 것입니다.

 

그러면 sa_data는 왜 14bytes나 필요할까요? 

궁금해서 찾아보니 사실 별 의미 없다고 하네요. glibc [각주:1]

 

16 bytes의 IPv6는 어떻게 처리하나요?

IPv6는 주소 자체가 16 bytes입니다. 그러면 14 bytes로 어떻게 처리하는 걸까요? 이는 __kernel_sockaddr_storage를 통해서 해결한다고 합니다. [각주:2]

 

 

2) sockfd_lookup_light

// net/socket.c/sockfd_lookup_light() :545

static struct socket *sockfd_lookup_light(int fd, int *err, int *fput_needed)
{
	struct fd f = fdget(fd);
	struct socket *sock;

	*err = -EBADF;
	if (f.file) {
		sock = sock_from_file(f.file);
		if (likely(sock)) {
			*fput_needed = f.flags & FDPUT_FPUT;
			return sock;
		}
		*err = -ENOTSOCK;
		fdput(f);
	}
	return NULL;
}

 

fd는 사용자가 입력한 fd를, err과 fput_needed는 초기화되지 않은 임의의 값으로서 넘겨집니다. 

 

여기서 struct fd f= fdget(fd);를 가장 먼저 실행하는데, 이는 내부로 들어가면 __fdget, __fget_light, __fget_light를 순차적으로 호출하며, files_lookup_fd_raw를 통해서 fdt table에서 fd를 찾아 반환 struct file을 반환합니다. 이는 fdget의 __to_fd에 의해서 struct fd로 변환됩니다.

 

즉, fdget 함수를 통해서 fd에 연결된 struct fd를 가져오는 것으로 보이고, struct fd 내에는 우리가 scoekt system call때 봤었던 struct file이 존재합니다.

// include/linux/file.h :36

struct fd {
	struct file *file;
	unsigned int flags;
};

 

2-1) sock_from_file

// net/socket.c/sock_from_file() :496

/**
 *	sock_from_file - Return the &socket bounded to @file.
 *	@file: file
 *
 *	On failure returns %NULL.
 */
 
struct socket *sock_from_file(struct file *file)
{
	if (file->f_op == &socket_file_ops)
		return file->private_data;	/* set in sock_map_fd */

	return NULL;
}

sock_from_file은 file 구조체에서 file->private_data를 반환합니다. 우리는 socket system call을 분석할 때, private_data는 struct socket임을 확인했었습니다. 따라서 struct socket*이 반환됩니다.

 

2-2) return

// net/socket.c/sock_fd_lookup_light() :552


static struct socket *sockfd_lookup_light(int fd, int *err, int *fput_needed)
{

...
		sock = sock_from_file(f.file);
		if (likely(sock)) {
			*fput_needed = f.flags & FDPUT_FPUT;
            	return sock;
       	 }

 

정상적인 상황이라면 sock은 있을 것입니다. 따라서 *fput_needed에 값을 수정하고 sock을 return합니다.

 

fput_needed는 f.flags에서 FDPUT_FPUT bit가 set되어있는지 여부를 저장합니다. 이는 추후에 fd에 대한 reference count를 관리할 때 쓰이는 것으로 보입니다.

 

3) move_addr_to_kernel

umyaddr은 userspace에서 생성된 구조체에 대한 포인터입니다. 따라서 user 영역에 존재하는 값들이죠. 이를 kernel로 가져오려면 copy_from_user등과 같은 함수를 사용해야 함을 시스템콜 추가해보기에서 확인했었습니다.

 

코드는 간단해보입니다.

// net/socket.c/move_addr_to_kernel() :242

/**
 *	move_addr_to_kernel	-	copy a socket address into kernel space
 *	@uaddr: Address in user space
 *	@kaddr: Address in kernel space
 *	@ulen: Length in user space
 *
 *	The address is copied into kernel space. If the provided address is
 *	too long an error code of -EINVAL is returned. If the copy gives
 *	invalid addresses -EFAULT is returned. On a success 0 is returned.
 */

int move_addr_to_kernel(void __user *uaddr, int ulen, struct sockaddr_storage *kaddr)
{
	if (ulen < 0 || ulen > sizeof(struct sockaddr_storage))
		return -EINVAL;
	if (ulen == 0)
		return 0;
	if (copy_from_user(kaddr, uaddr, ulen))
		return -EFAULT;
	return audit_sockaddr(ulen, kaddr);
}

 

먼저 조건을 체크합니다. ulen은 사용자가 직접 넘겨준 구조체의 길이입니다. 우리가 bind system call을 사용할 때 3번째 인자에 해당하는 값이죠. 

 

bind(fd, (struct sockaddr *)&address, sizeof(address));

 

kernel은 bind system call을 처리할 때, struct sockaddr*로 캐스팅해서 받습니다. 즉, kernel 입장에서는 이 address라는 변수가 sockaddr_in일지 sockaddr_in6일지 모르게 되는거죠. 그렇다고 kernel 내부에서 copy할 때 sizeof(sturct sockaddr)을 마음대로 할 수는 없으니 길이값이 필요합니다.

 

copy_from_user는 struct sockaddr* uaddr의 값들을 struct sockaddr_storage 타입의 kaddr에 ulen만큼 복사합니다. 이제 user의 값이 kernel로 넘겨진 순간이죠.

 

3-1) struct sockaddr_storage *kaddr

여기서 새로운 type이 하나 등장합니다. struct sockaddr_storage는 뭘까요?

 

// include/linux/socket.h :42

#define sockaddr_storage __kernel_sockaddr_storage

 

// include/uapi/linux/socket.h :12

/*
 * The definition uses anonymous union and struct in order to control the
 * default alignment.
 */
struct __kernel_sockaddr_storage {
	union {
		struct {
			__kernel_sa_family_t	ss_family; /* address family */
			/* Following field(s) are implementation specific */
			char __data[_K_SS_MAXSIZE - sizeof(unsigned short)];
				/* space to achieve desired size, */
				/* _SS_MAXSIZE value minus size of ss_family */
		};
		void *__align; /* implementation specific desired alignment */
	};
};

 

임의의 Address Family를 모두 수용하기 위해서 만들어진 충분한 공간입니다. 앞서 16 bytes의 IPv6는 어떻게 처리하나요? 에서 나왔던 구조체인데요, 여기는 char __data[_K_SS_MAXSIZE(128) - 2] 만큼의 공간이 있어서 충분해보입니다.

 

왜 128로 정해졌나요?

와 같은 궁금증이 생겼었는데, https://datatracker.ietf.org/doc/html/rfc2553 에서 다루고있습니다.

 

 

3) sock->ops->bind

sock->ops는 우리가 socket system call에서 inet_stream_ops가 등록되었음을 확인했었습니다. 이 부분부터는 다음 포스트에서 다루겠습니다.

 

 

 

현재까지의 구조도

반응형
Comments