- Today
- Total
Byeo
bind system call 1 (__sys_bind) 본문
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
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가 등록되었음을 확인했었습니다. 이 부분부터는 다음 포스트에서 다루겠습니다.
현재까지의 구조도
'프로그래밍 (Programming) > 네트워크 스택' 카테고리의 다른 글
bind system call 3 (__inet_bind - 2) (0) | 2024.05.01 |
---|---|
bind system call 2 (__inet_bind) (0) | 2024.04.28 |
BSD Socket API (0) | 2024.04.17 |
socket system call 4 (sock_map_fd) (0) | 2024.04.14 |
socket system call 3 (tcp_v4_init_sock) (0) | 2024.04.06 |