深入理解系统调用
一、实验要求
- 找一个系统调用,系统调用号为学号最后2位相同的系统调用
- 通过汇编指令触发该系统调用
- 通过gdb跟踪该系统调用的内核处理过程
- 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
二、配置实验环境
步骤一、下载内核和开发工具
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
步骤二、解压Linux内核,并配置
#解压 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 #shift+y确定勾选 [*] 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 # 此时应该不能正常运行
成功后 :

步骤四、安装并编译busybox制作根文件系统
cd .. 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
步骤五、制作根文件系统
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/
步骤六、创建init脚本文件
touch init vim init #!/bin/sh mount -t proc none /proc mount -t sysfs none /sys echo "Wellcome MyOS!" echo "--------------------" cd home /bin/sh 给init脚本添加执行权限chmod +x init
将init文件放在rootfs目录下,并授予其运行权限
chmod +x init
步骤七、打包系统镜像,测试文件系统
#打包成内存根⽂件系统镜像 find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../ rootfs.cpio.gz #测试挂载根⽂件系统,看内核启动完成后是否执⾏init脚本 cd .. qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz
出现以下界面,表明init脚本已经运行了,到这里环境已经搭建完成。

三、系统调用实验
我的学号位数是08,在64位调用表里可以查到对应的系统调用函数是__x64_sys_lseek

触发系统调用的test.c程序如下:
#include <stdio.h> #include<stdlib.h> #include<sys/types.h> #include<sys/stat.h> #include <fcntl.h> int main(int argc,char *argv[]) { int fd; //fd= creat(argv[1],0755); //if (fd<0) // printf("error!"); asm volatile( "movl %1,%%ebx\n\t" //系统调⽤传递第⼀个参数使⽤EBX寄存器,为argv[1] "movl $0755,%%ecx\n\t"//系统调⽤传递第二个参数使⽤ECX寄存器,为0755 "movl $0x8,%%eax\n\t"//使⽤%eax传递系统调⽤号8,⽤16进制为0x8 "int $0x80\n\t" //触发系统调⽤ "movl %%eax,%0\n\t" //通过EAX寄存器返回系统调⽤值 :"=m"(fd) :"b"(argv[1]) ); printf("%d\n",fd); fclose(fd); return 0; }
在gdb中加断点__ia32_sys_creat,运行test.c编译成的可执行程序test,在断点处停止,观察堆栈情况。

"int $0x80\n\t"指令触发系统调用后,先执行entry_INT80_compat函数,该函数对现场进行保存,之后调用do_int80_syscall_32函数,由用户态切换至内核态,再调用do_syscall_32_irqs_on函数,在其内调用__ia32_sys_creat函数完成creat系统调用的功能,之后切换回用户态,entry_INT80_compat函数恢复现场。

运行test 2.txt命令成功创建了2.txt文件。
Linux内核中⼤约定义了四五百个系统调⽤,这时内核如何知道⽤户态进程希望调⽤的是哪个系统调⽤呢?内核通过给每个系统调⽤⼀个编号来区分,即系统调⽤号。内核实现了很多不同的系统调⽤,⽤户态进程必须指明需要执⾏哪个系统调⽤,这需要使⽤EAX寄存器传递⼀个名为系统调⽤号的参数。除了系统调⽤号外,系统调⽤也可能需要传递参数,在32位x86体系结构下普通的函数调⽤是通过将参数压栈的⽅式传递的。系统调⽤从⽤户态切换到内核态,在⽤户态和内核态这两种执⾏模式下使⽤的是不同的堆栈,即进程的⽤户态堆栈和进程的内核态堆栈,传递参数⽅法⽆法通过参数压栈的⽅式,⽽是通过寄存器传递参数的⽅式。寄存器传递参数的个数是有限制的,⽽且每个参数的⻓度不能超过寄存器的⻓度,32位x86体系结构下寄存器的⻓度最⼤32位。除了EAX⽤于传递系统调⽤号外,参数按顺序赋值给EBX、ECX、EDX、ESI、EDI、EBP,参数的个数不能超过6个,即上述6个寄存器。如果超过6个就把某⼀个寄存器作为指针,指向内存,就可以通过内存来传递更多的参数。以上就是32位x86体系结构下系统调⽤的参数传递⽅式。
由于压栈的⽅式需要读写内存,函数调⽤速度较慢,64位x86体系结构下普通的函数调⽤和系统调⽤都是通过寄存器传递参数,RDI、RSI、RDX、RCX、R8、R9这6个寄存器⽤作函数/系统调⽤参数传递,依次对应第 1 参数到第 6 个参数。
浙公网安备 33010602011771号