深入理解系统调用
实验内容
- 找一个系统调用,系统调用号为学号最后2位相同的系统调用
- 通过汇编指令触发该系统调用
- 通过gdb跟踪该系统调用的内核处理过程
- 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
实验步骤
1.环境配置
安装开发工具
sudo apt install build-essential sudo apt install qemu # install QEMU sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
下载内核源代码
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
# 打开debug相关选项
Kernel hacking --->
Compile-time checks and compiler options --->
[*] Compile the kernel with debug info
[*] Provide GDB scripts for kernel debugging
[*] Kernel debugging
# 关闭KASLR,否则会导致打断点失败
Processor type and features ---->
[] Randomize the address of the kernel image (KASLR)
编译内核
make -j$(nproc) # nproc gives the number of CPU cores/threads available # 测试⼀下内核能不能正常加载运⾏,因为没有⽂件系统终会kernel panic qemu-system-x86_64 -kernel arch/x86/boot/bzImage # 此时应该不能正常运行
2.制作内存根文件系统
下载 busybox源代码解压,编译并安装
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
#记得要编译成静态链接,不⽤动态链接库。
Settings --->
[*] Build static binary (no shared libs)
#然后编译安装,默认会安装到源码⽬录下的 _install ⽬录中。
make -j$(nproc) && 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脚本文件放在根文件系统跟目录下(rootfs/init),这样内核才能加载启动文件从而启动
touch init
添加如下内容到init文件
#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys echo "Wellcome TestOS!" echo "--------------------" cd home /bin/sh
给init文件加上执行权限
sudo chmod +x init
接着回到linux-5.4.34目录下,输入以下命令打包成内存根文件系统镜像
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
可以看到内核成功启动。
3.查看系统调用
打开linux内核下arch/x86/entry/syscalls/syscall_64.tbl文件,查看系统调用列表。选择07号系统调用,对应的系统调用为poll。

查阅资料得知,poll()函数功能:在指定时间内轮询一定数量的文件描述符,来测试其中是否有就绪者
poll的原型如下:
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
其中各个参数的介绍如下:
1.fds——是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读,可写和异常等事件。pollfd结构体如下所示:
struct pollfd
{
int fd; //文件描述符
short events; //注册的事件
short revents; //实际发生的事件,由内核来修改
}
至于poll支持哪些事件类型,大家可自行搜索,此处不再介绍。
2.nfds——指定被监听事件集合fds的大小
3.timeout——指定poll的超时值,当timeout为-1时poll调用将永远阻塞,直至某个事件发生,而当timeout为0时,poll调用将立即返回。
使用汇编调用系统调用
创建test.c,输入代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int fd;
char* filename="test.txt";
int key_val;
int ret;
struct pollfd *key_fds;//定义一个pollfd结构体key_fds
fd = open(filename, O_RDWR);
if (fd < 0)//小于0说明没有成功
{
printf("error, can't open %s\n", filename);
return 0;
}
if(argc !=1)
{
printf("Usage : %s ",argv[0]);
return 0;
}
key_fds ->fd = fd;
key_fds->events = POLLIN;
//ret = poll(key_fds, 1, 5000);
int b1 = 1;
int b2 = 5000;
asm volatile(
"movq %3, %%rdx\n\t"
"movq %2, %%rsi\n\t"
"movq %1, %%rdi\n\t"
"movl $0x07, %%eax\n\t"
"syscall\n\t"
"movq %%rax, %0\n\t"
:"=m"(ret)
:"b"(key_fds),"c"(b1),"d"(b2)
);
if(!ret)
{
printf("time out\n");
}
else
{
if(key_fds->revents==POLLIN)
{
read(fd, &key_val, 1);
printf("test succeed\n");
}
}
return 0;
}
使用下面命令将test.c进行静态编译
gcc -o test test.c -static
将形成的可执行文件放到rootfs/home/目录下,然后重新打包rootfs文件夹
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
接下来使用gdb进行调试。
执行
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
再打开一个终端,在对应的系统调用入口处打好断点后,执行test,并在gdb调试中使用bt查看当前堆栈

系统调用的入口在entry_SYSCALL_64(),查看源码
ENTRY(entry_SYSCALL_64)
UNWIND_HINT_EMPTY
/*
* Interrupts are off on entry.
* We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
* it is too small to ever cause noticeable irq latency.
*/
swapgs
/* tss.sp2 is scratch space. */
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
在中断或异常处理的entry代码处, 会执行SWAPGS切换到kernel GS, GS.base 是存储了中断stack 的地址。
然后调用了do_syscall_64,代码如下
GLOBAL(entry_SYSCALL_64_after_hwframe)
pushq %rax /* pt_regs->orig_ax */
PUSH_AND_CLEAR_REGS rax=$-ENOSYS
TRACE_IRQS_OFF
/* IRQs are off. */
movq %rax, %rdi
movq %rsp, %rsi
call do_syscall_64 /* returns with IRQs disabled */
先将rax中的值入栈保存,然后通过rdi,rsi进行传参,其中rdi传递的是系统调用号,rsi传递的是pt_regs
函数do_syscall_64()的代码如下
#ifdef CONFIG_X86_64
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
struct thread_info *ti;
enter_from_user_mode();
local_irq_enable();
ti = current_thread_info();
if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
nr = syscall_trace_enter(regs);
if (likely(nr < NR_syscalls)) {
nr = array_index_nospec(nr, NR_syscalls);
regs->ax = sys_call_table[nr](regs);
#ifdef CONFIG_X86_X32_ABI
} else if (likely((nr & __X32_SYSCALL_BIT) &&
(nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
X32_NR_syscalls);
regs->ax = x32_sys_call_table[nr](regs);
#endif
}
syscall_return_slowpath(regs);
}
在该函数中,通过传入的参数nr找到相应的系统调用,返回值保存在regs->ax中。系统调用结束后,执行syscall_return_slowpath进行返回。
然后在gdb单步调试中,我们可以看到从syscall_return_slowpath返回后,开始恢复现场。主要是将之前保存在栈中的寄存器的值,重新恢复到原来的寄存器中,一次系统调用完成。



浙公网安备 33010602011771号