深入理解系统调用
1. 制作根文件系统,借助BusyBox 构建极简内存根⽂件系统,提供基本的⽤户态可执⾏程序
1.0 下载编译Linux内核
# 安装 sudo apt install build-essential sudo apt install qemu # install QEMU sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev # 重新下载linux内核源码 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 # 此时应该不能正常运行
1.1 下载 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
1.2 安装后,开始制作
make menuconfig # 注意编译成静态链接; 如果运行不成功,可尝试重启虚拟机 Settings ---> [*] Build static binary (no shared libs) #然后编译安装,默认会安装到源码⽬录下的 _install ⽬录中。 make -j$(nproc) && make install
1.3 制作内存根文件系统镜像
cd ../
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/
1.4 准备init脚本文件放在根文件系统根目录下(rootfs/init)
添加如下内容到init文件
#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys echo "Wellcome TestOS!" echo "--------------------" cd home /bin/sh
#给init脚本添加可执行权限 chmod +x init #打包成内存根文件系统镜像 find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz #测试挂载根文件系统,看内核启动完成后是否执行init脚本 qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz
重新启动虚拟机,可以看到成功执行了init脚本
2. 查看linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl下系统调用表,10号系统调用为 mprotect; 对应函数为__x64_sys_mprotect
该系统调用的作用: mprotect(const void *start, size_t len, int prot)函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值
注意: 所指定的内存区间必须包含整个页, 即区间地址必须和整个系统页大小对齐,且区间长度必须是页大小的整数倍
3. 通过汇编指令触发该系统调用
创建test.c文件,进行编码
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/mman.h> #define PAGESIZE 4096 int main(void) { char *p; char c; /* Allocate a buffer; it will have the default protection of PROT_READ|PROT_WRITE. */ p = malloc(1024+PAGESIZE-1); if (!p) { printf("Couldn’t malloc(1024)"); } p = (char *)(((int) p + PAGESIZE-1) & ~(PAGESIZE-1)); c = p[666]; /* Read; ok */ p[666] = 42; /* Write; ok */ /* Mark the buffer read-only. */ int res; // asm volatile( "mov $0x2, %%edx\n\t" // 将第3个参数放入 edx 寄存器, 0x2为PROT_READ "mov $0x400, %%esi\n\t" // 将第二个参数放入 esi 寄存器 "mov %1, %%rdi\n\t" // 将第一个参数放入 rdi 寄存器 "mov $0x0a, %%eax\n\t" // mprotect 的系统调用号为10,将其放入 eax 寄存器 "syscall\n\t" // 触发系统调用 "mov %%rax, %0\n\t" // 将函数处理结果返回给 res 变量 :"=m"(res) :"b"(p) ); printf("The return value is : %d \n", res); if (res) { printf("Couldn’t mprotect\n"); } //c = p[666]; /* Read; ok */ //p[666] = 42; /* Write; program dies on SIGSEGV */ return 0; }
先运行test文件,查看是否成功, 结果为0,表示成功
静态编译该文件得到可执行文件, 并将其放入rootfs/home/目录下,然后重新打包rootfs文件夹:
gcc -o test test.c -static
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
3. 通过gdb跟踪该系统调用的内核处理过程
运行命令qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s
重新打开一个终端,在__x64_sys_mprotect处设置断点
断点之后,在qemu模拟器执行 ./test,之后会在该入口点处停止,查看系统调用栈(断点处乱码,gdb错误,暂时无法跟踪)
4. 系统调用过程分析
4.0 系统调用
⼀般每个系统调⽤对应⼀个系统调⽤的封装例程,函数库再⽤这些封装例程定义出给程序员调⽤的API,这些API也就是系统调用的封装,
在系统调用过程中,用户程序可以通过系统调用来请求内核服务,从而会由用户态转为内核态。
4.1 系统调用初始化
在内核初始化过程中,完成了系统调用的初始化,这个过程主要包括中断初始化和系统调用初始化。
初始化过程: start_kernel --> trap_init --> cpu_init --> syscall_init
系统调用对应的中断是软中断,向量号为0x80
在中断初始化时候,会通过软件中断的处理程序system_call(entry_SYSCALL_64)与0x80关联,
便于之后执行系统调用时候,通过int &0x80跳转到system_call中断处理程序入口处
而系统调用中,还需要根据系统调用号查找对应的系统调用处理函数,因此需要系统调用的初始化,
主要就是系统调用入口和系统调用表的初始化
trap_init函数中调用了cpu_init(arch/x86/kernel/cpu/common.c)函数,该函数会调用
调用相同源码文件中的syscall_init
完成per-cpu
状态初始化,该函数执行系统调用入口的初始化。
4.2 系统调用的执行过程
- int $0x80或syscall指令触发, 切换到内核态,并从MSR寄存器找到中断函数入口
- 开始执⾏system_call(entry_INT80_32或entry_SYSCALL_64)汇编代码
- 通过swapgs 和压栈动作保存现场
- do_syscall_64, 在eax寄存器中获取到系统调用号, 根据系统调用号执行对应的系统调用处理函数
- Linux 内核由一个特殊的表system call table。 系统调用表是Linux内核源码文件 arch/x86/entry/syscall_64.c中定义的数组sys_call_table的对应
- 该函数会在sys_call_table数组中找到对应的系统调用处理函数, 并将返回值保存在regs的ax中
- syscall_return_slowpath在调用结束时执行,进行返回, 准备恢复现场,此时可以调用schedul函数做进程调度
- 回到entry_SYSCALL_64,恢复现场
- 系统调用返回iret/sysret, 退出系统调用,回到用户态
- 继续执行int $0x80或syscall指令的下一条指令
查看mprotect对应的源码(linux-5.4.34/mm/mprotect.c),其对应的入口函数为__x64_sys_mprotect,
整个过程主要函数调用顺序: entry_SYSCALL_64() ---> do_syscall_64 ---> syscall_return_slowpath
4.3 系统调用的入口点在entry_SYSCALL_64处,函数对应源码在arch/x86/entry/entry_64.s文件中
主要用于保存和恢复现场ENTRY(entry_SYSCALL_64)