深入理解系统调用
一、实验要求
- 找一个系统调用,系统调用号为学号最后2位相同的系统调用
- 通过汇编指令触发该系统调用
- 通过gdb跟踪该系统调用的内核处理过程
- 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
笔者学号最后2位为43,查阅得知为accept()函数。

二、实验环境搭建
1. 配置内核选项并编译
sudo apt install axel
axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
xz -d linux-5.4.34.tar.xz
tar -xvf linux-5.4.34.tar
cd linux-5.4.34
make defconfig # Default configuration is based on 'x86_64_defconfig'
make menuconfig
make -j4
qemu-system-x86_64 -kernel arch/x86/boot/bzImage
2. 构造根文件并提供可执行程序
axel -n 20 https://busybox.net/downloads/busybox-1.31.1.tar.bz2
tar -jxvf busybox-1.31.1.tar.bz2
cd busybox-1.31.1
make menuconfig
make -j4 && make install
mkdir rootfs
cd rootfs
cp ../busybox-1.31.1/_install/* ./ -rf
mkdir dev proc sys home
sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
给init脚本添加权限
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
echo "Wellcome MengningOS!"
echo "--------------------"
cd home
/bin/sh
chmod +x init
打包根文件并测试
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage-initrd rootfs.cpio.gz
三、编写系统调用程序
编写程序 accept_43.c,代码如下:
int main() {
asm volatile(
"movl $0x2B,%eax\n\t"
"syscall\n\t"
);
printf("Hello accept");
return 0;
}
用gcc静态编译,生成可执行文件,重新执行
gcc -o test_mincore test_mincore.c -static
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz
启动qemu观察结果。
四、跟踪accept()内核处理过程
重启 qemu
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S –s
启动 gdb
cd linux-5.4.34/ gdb vmlinux (gdb) target remote:1234 (gdb) b __x64_sys_accept
命令中在accept系统调用处打上断点,在qemu中运行调用函数,程序会停在断点等待调试。
在gdb中,可以查看堆栈信息(bt命令),查看对应代码(l命令)。由下图可知,accept是通过syscall来进行的系统调用,系统调用的入口为 entry_SYSCALL_64 。

从终端信息可知:系统先调用entry_SYSCALL_64 (),其次是do_syscall_64(),最后才是__x64_sys_accept() 。
使用n命令进行单步执行,可以在gdb中查看保存现场信息的步骤:
(gdb) n do_syscall_64 (nr=140730116323824, regs=0xffffc900001b7f58) at arch/x86/entry/common.c:300 300 syscall_return_slowpath(regs); (gdb) n 301 } (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:184 184 movq RCX(%rsp), %rcx (gdb) n 185 movq RIP(%rsp), %r11 (gdb) n 187 cmpq %rcx, %r11 /* SYSRET requires RCX == RIP */ (gdb) n 188 jne swapgs_restore_regs_and_return_to_usermode (gdb) n 205 shl $(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx (gdb) n 206 sar $(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx (gdb) n 210 cmpq %rcx, %r11 (gdb) n 211 jne swapgs_restore_regs_and_return_to_usermode (gdb) n 213 cmpq $__USER_CS, CS(%rsp) /* CS must match SYSRET */ (gdb) n 214 jne swapgs_restore_regs_and_return_to_usermode (gdb) n 216 movq R11(%rsp), %r11 (gdb) n 217 cmpq %r11, EFLAGS(%rsp) /* R11 == RFLAGS */ (gdb) n 218 jne swapgs_restore_regs_and_return_to_usermode (gdb) n 238 testq $(X86_EFLAGS_RF|X86_EFLAGS_TF), %r11 (gdb) n 239 jnz swapgs_restore_regs_and_return_to_usermode (gdb) n 243 cmpq $__USER_DS, SS(%rsp) /* SS must match SYSRET */ (gdb) n 244 jne swapgs_restore_regs_and_return_to_usermode (gdb) n 253 POP_REGS pop_rdi=0 skip_r11rcx=1 (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:259 259 movq %rsp, %rdi (gdb) n 260 movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:262 262 pushq RSP-RDI(%rdi) /* RSP */ (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:263 263 pushq (%rdi) /* RDI */ (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:271 271 SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi (gdb) n 273 popq %rdi (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:274 274 popq %rsp (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:275 275 USERGS_SYSRET64 (gdb) n 0x0000000000448b97 in ?? () (gdb) c Continuing. (gdb)
五、系统调用分析
sysenter和syscall都借助CPU内部的MSR寄存器来查找系统调⽤处理⼊⼝,可断处理的思路,压栈关键寄存器、保存现场、恢复现场,最后系统调⽤返回。swapgs指令以类似快照的⽅式将保存现场和恢复现场时的CPU寄存器也通过CPU内部的存储器快速保存和恢复,加快了系统调⽤。进入内核态后,swapgs进行现场的保存,并完成寄存器的压栈。调用do_do_syscall_64,从中断向量表中获得系统调用号并对函数进行调用。
accept() 函数模型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:套接字的文件描述符,socket()系统调用返回的文件描述符fd
addr:指向存放地址信息的结构体的首地址
addrlen:存放地址信息的结构体的大小,其实也就是sizof(struct sockaddr)
可以看出,bind(),connect(),以及accept()的参数都是一致的。
内核实现:
SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr, int __user *, upeer_addrlen) { return sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0); }
最终调用的是SYSCALL_DEFINE4 。
函数执行完后回到do_do_syscall_64,进行syscall_return_slowpath(regs)的调用,prepare_exit_to_usermode(regs)准备返回用户态。再次回到entry_64.S中,进行最后的现场恢复和堆栈的切换。回到用户态程序,继续执行下一条指令,至此,系统调用过程结束。

浙公网安备 33010602011771号