深入理解系统调用
一、实验要求
找一个系统调用,系统调用号为学号最后2位相同的系统调用
通过汇编指令触发该系统调用
通过gdb跟踪该系统调用的内核处理过程
重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
二、实验环境搭建
2.1安装开发工具
1 sudo apt install build-essential
2 sudo apt install qemu # install QEMU
3 sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
2.2下载内核源码
1 sudo apt install axel
2 axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
3 xz -d linux-5.4.34.tar.xz
4 tar -xvf linux-5.4.34.tar
5 cd linux-5.4.34
2.3配置内核
1 make defconfig # Default configuration is based on 'x86_64_defconfig'
2 make menuconfig
3 Kernel hacking --->
4 Compile-time checks and compiler options --->
5 [*] Compile the kernel with debug info
6 [*] Provide GDB scripts for kernel debugging
7 [*] Kernel debugging
8 Processor type and features ---->
9 [] Randomize the address of the kernel image (KASLR)
2.4编译运行内核
1 make -j$(nproc) # nproc gives the number of CPU cores/threads available 2 kernel panic 3 qemu-system-x86_64 -kernel arch/x86/boot/bzImage
2.5制作根文件系统
1 首先从https://www.busybox.net下载 busybox源代码解压,解压完成后,跟内核一样先配置编译,并安装。 2 axel -n 20 https://busybox.net/downloads/busybox-1.31.1.tar.bz2 3 tar -jxvf busybox-1.31.1.tar.bz2 4 cd busybox-1.31.1
1 make menuconfig #若出现打不开menuconfig,重启虚拟机即可 2 #记得要编译成静态链接,不⽤动态链接库。 3 Settings ---> 4 [*] Build static binary (no shared libs) 5 #然后编译安装,默认会安装到源码⽬录下的 _install ⽬录中。 6 make -j$(nproc) && make install
制作根文件系统镜像
1 mkdir rootfs
2 cd rootfs
3 cp ../busybox-1.31.1/_install/* ./ -rf
4 mkdir dev proc sys home
5 sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
准备init脚本文件放在根文件系统跟目录下(rootfs/init),添加如下内容到init文件。
1 #!/bin/sh 2 mount -t proc none /proc 3 mount -t sysfs none /sys 4 echo "Wellcome MengningOS!" 5 echo "--------------------" 6 cd home 7 /bin/sh
给init脚本添加可执⾏权限
1 chmod +x init
1 #打包成内存根⽂件系统镜像 2 find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../ rootfs.cpio.gz 3 #测试挂载根⽂件系统,看内核启动完成后是否执⾏init脚本 4 qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz
三、系统调用
所有的操作系统在其内核里都有一些内建的函数,这些函数可以用来完成一些系统级别的功能。Linux系统使用的这样的函数叫做“系统调用”,英文是systemcall。这些函数代表了从用户空间到内核空间的一种转换,例如在用户空间调用open函数,则会在内核空间调用sys_open。一个已经安装的系统的支持的所有的系统调用可以在/usr/include/bits/syscall.h文件里面看到。
系统调用是内核向用户进程提供服务的唯一方法,应用程序调用操作系统提供的功能模块(函数)。用户程序通过系统调用从用户态(user mode)切换到核心态(kernel mode ),从而可以访问相应的资源。这样做的好处是:为用户空间提供了一种硬件的抽象接口,使编程更加容易。有利于系统安全。有利于每个进程度运行在虚拟系统中,接口统一有利于移植。
由于学号后两位是50,系统调用号为50的是—NR—listen。

3.1、函数原型
intlisten(int sockfd, int backlog);
参数说明:
sockfd:套接字的文件描述符,即socket()系统调用返回的fd
backlog:保存客户端请求的队列长度
listen()系统调用是比较简单的,但是涉及了backlog,这个参数是比较复杂的,影响到半连接队列和全连接队列。
3.2、内核实现
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn;
//根据fd获取对应的sock结构体,分析bind()系统调用时已经讨论过
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
if ((unsigned int)backlog > somaxconn)
backlog = somaxconn;
err = security_socket_listen(sock, backlog);
if (!err)
err = sock->ops->listen(sock, backlog);//调用对应协议的listen函数
fput_light(sock->file, fput_needed);
}
return err;
}
和bind()函数一样,既然是通过fd来操作,第一步肯定是要通过fd获取对应的socket结构体,才能操作网络相关的变量和结构体。由之前系统调用的分析,我们知道sock->ops指向的是inet_stream_ops结构体,因此sock->ops->listen指向的便是inet_listen()。
int inet_listen(struct socket *sock, int backlog)
{
struct sock *sk = sock->sk;
unsigned char old_state;
int err;
lock_sock(sk);
err = -EINVAL;
if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
goto out;
old_state = sk->sk_state;
//调用listen只能是处于close或者listen状态(只能修改backlog参数)的连接
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
goto out;
/* Really, if the socket is already in listen state
* we can only allow the backlog to be adjusted.
*/
if (old_state != TCP_LISTEN) {
//快速开启选项,暂不分析
if ((sysctl_tcp_fastopen & TFO_SERVER_ENABLE) != 0 &&
inet_csk(sk)->icsk_accept_queue.fastopenq == NULL) {
if ((sysctl_tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) != 0)
err = fastopen_init_queue(sk, backlog);
else if ((sysctl_tcp_fastopen & TFO_SERVER_WO_SOCKOPT2) != 0)
err = fastopen_init_queue(sk, ((uint)sysctl_tcp_fastopen) >> 16);
else
err = 0;
if (err)
goto out;
tcp_fastopen_init_key_once(true);
}
//初始化socket,包括半连接队列等
err = inet_csk_listen_start(sk, backlog);
if (err)
goto out;
}
sk->sk_max_ack_backlog = backlog;//这个其实设置的就是全连接队列的最大长度
err = 0;
out:
release_sock(sk);
return err;
}
inet_csk_listen_start()主要是两件事:一个是分配半连接队列,另一个是将socket加入listen哈希表中。
int inet_csk_listen_start(struct sock *sk, const int nr_table_entries)
{
struct inet_sock *inet = inet_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
//根据nr_table_entries(和用户设置的backlog参数相关)分配半连接队列
int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);
if (rc != 0)
return rc;
sk->sk_max_ack_backlog = 0;
sk->sk_ack_backlog = 0;
inet_csk_delack_init(sk);
sk->sk_state = TCP_LISTEN;//宣告socket进入listen状态
/* 其实在调用listen()之前调用bind()系统调用时已经调用过sk->sk_prot->get_port一次,
* 这里再次调用是因为bind与listen之间并不是原子操作,用户可能在此修改过连接的一些属性
* 比如,sk->reuse,sk->sk_reuseport等,所以再次检查端口保证该端口可用。*/
if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
inet->inet_sport = htons(inet->inet_num);//设置源端口
sk_dst_reset(sk);
sk->sk_prot->hash(sk);//将处于listen状态的连接根据端口号放入listen哈希表中
return 0;
}
sk->sk_state = TCP_CLOSE;//失败了,socket状态置为close初始值
__reqsk_queue_destroy(&icsk->icsk_accept_queue);
return -EADDRINUSE;
}
最后便是加入listen哈希表
void inet_hash(struct sock *sk)
{
if (sk->sk_state != TCP_CLOSE) {
local_bh_disable();
__inet_hash(sk);
local_bh_enable();
}
}
static void __inet_hash(struct sock *sk)
{
struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
struct inet_listen_hashbucket *ilb;
if (sk->sk_state != TCP_LISTEN) {
__inet_hash_nolisten(sk, NULL);
return;
}
WARN_ON(!sk_unhashed(sk));
//根据端口号,获取对应的哈希表
ilb = &hashinfo->listening_hash[inet_sk_listen_hashfn(sk)];
spin_lock(&ilb->lock);
__sk_nulls_add_node_rcu(sk, &ilb->head);//将socket放入listen链表中
sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1);
spin_unlock(&ilb->lock);
}
所以listen()就两件事,根据用户设置的backlog计算真正的请求队列长度,然后分配半连接队列。还有就是再次确认端口可用,然后将该连接加入listen哈希表。
部分结构图关系如下:

四、系统调用总结
程序执行系统调用大致可归结为以下几个步骤:
1、程序调用libc 库的封装函数。
2、调用软中断int 0x80 进入内核。
3、在内核中首先执行system_call 函数(首先将系统调用号(eax)和可以用到的所有CPU寄存器保存到相应的堆栈中(由SAVE_ALL完成),接着根据系统调用号在系统调用表中查找到对应的系统调用服务例程。
4、执行该服务例程。
5、执行完毕后,转入ret_from_sys_call 例程,从系统调用返回
系统调用由内核分配的一个编号唯一标识(系统调用号)。
所有的系统调用都由一处中枢代码处理,根据调用编号和一个静态表,将调用分派到具体的函数。传递的参数也是由中枢代码处理,这样参数的传递独立于实际的系统调用。从用户态到内核态,以及调用分派和参数传递,都是由汇编语言代码实现的。
为容许用户态和内核态之间的切换,用户进程必须通过一条专用的机器指令,引起处理器/内核对该进程的关注,这需要 C 标准库的协助。内核也必须提供一个例程,来满足切换请求并执行相关操作。该例程不能在用户空间中实现,因为其中需要执行普通应用程序不允许执行的命令。

浙公网安备 33010602011771号