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#include <string.h> #include #include #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. 系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
参考: https://www.cntofu.com/book/104/SysCall/syscall-2.md
在内核初始化过程中,完成了系统调用入口的初始化 --->
执行./test文件时候, 触发syscall指令, 处理器从用户模式切换到内核模式,
执行entry_SYSCALL_64
( arch/x86/entry/entry_64.s ),完成系统调用执行前的所需准备(保护现场) --->
entry_SYSCALL_64
切换至内核堆栈,在堆栈中存通用目的寄存器, 老的堆栈,代码段, 标志位等 --->
进入do_syscall_64函数,根据系统调用号,找到对应的函数, 并将返回值保存在regs的ax中 --->
通过syscall_return_slowpath, 在调用结束时,执行该函数,进行返回 --->
系统调用结束和,回到entry_SYSCALL_64, 系统调用将用户程序的返回结果放置在通用目的寄存器rax
中,
因此在系统调用处理完成其工作后,将寄存器的值入栈
并进行恢复现场,之后退出系统调用,转换到用户态
查看mprotect对应的源码(linux-5.4.34/mm/mprotect.c),其对应的入口函数为__x64_sys_mprotect,
整个过程主要函数调用顺序: entry_SYSCALL_64() ---> do_syscall_64 ---> syscall_return_slowpath
4.0 系统调用的入口点在entry_SYSCALL_64处,函数对应源码在arch/x86/entry/entry_64.s文件中
主要用于保存和恢复现场ENTRY(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指令用于保护和恢复现场, 如将当前CPU对应的相关寄存器保存起来 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 */ 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 */
...
/*
* We are on the trampoline stack. All regs except RDI are live.
* We can do future final exit work right here.
*/
STACKLEAK_ERASE_NOCLOBBER
SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi
popq %rdi
popq %rsp
USERGS_SYSRET64
END(entry_SYSCALL_64)