一、 实验要求:
- 找一个系统调用,系统调用号为学号最后2位相同的系统调用
- 通过汇编指令触发该系统调用
- 通过gdb跟踪该系统调用的内核处理过程
- 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
二、实验环境:
-
虚拟机版本:VMware Workstation 15
-
操作系统版本:Ubuntu19.10
-
三、实验前的准备工作
-
-
配置内核选项
-
* 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
-
-
电脑加电启动⾸先由bootloader加载内核,内核紧接着需要挂载内存根⽂件系统,其中包含必要的设备驱动和⼯具, bootloader加载根⽂件系统到内存中,内核会将其挂载到根⽬录/下,然后运⾏根⽂件系统中init脚本执⾏⼀些启动任务,最后才挂载真正的磁盘根⽂件系统。
-
我们这⾥为了简化实验环境,仅制作内存根⽂件系统。这⾥借助BusyBox 构建极简内存根⽂件系统,提供基本的⽤户态可执⾏程序。
-
具体过程
-
# 下载busybox • ⾸先从https://www.busybox.net下载 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 # 安装busybox • 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/ // 打包成内存根⽂件系统镜像 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
注意:这里由于老师示范与我自己安装的很多文件夹的目录有所不同,所以,在运行上述命令时,要考虑到这一点,在有关于路径的问题上自己斟酌。
- 跟踪调试linux内核的基本方法
- 纯命令⾏下启动虚拟机
- qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
- ⽤以上命令先启动,然后可以看到虚拟机⼀启动就暂停了。加-nographic -append "console=ttyS0"参数启动不会弹出QEMU虚拟机窗⼝,可以在纯命令⾏下启动虚拟机,此时可以通过“killall qemu-system-x86_64”命令强⾏关闭虚拟机。
-
- cd linux-5.4.34/ - gdb vmlinux - (gdb) target remote:1234 - (gdb) b start_kernel - c、 bt、 list、 next、 step....
- 至此,在正式开始实验前的准备工作已经完毕。我们有了一个可以加载指定文件系统镜像的简单虚拟机。
四、正式实验:
- 一些简单的背景知识
-
-
-
具体来说,在Linux中通过执⾏int $0x80或syscall指令来触发系统调⽤的执⾏,其中这条int$0x80汇编指令是产⽣中断向量为128的编程异常(trap)。
-
另外Intel处理器中还引⼊了sysenter指令(快速系统调⽤),因为Intel专⽤AMD并不⽀持,在此不再详述。我们只关注int指令和syscall指令触发的系统调⽤,进⼊内核后,开始执⾏对应的中断服务程序entry_INT80_32或entry_SYSCALL_64
-
-
-
-
内核实现了很多不同的系统调⽤,⽤户态进程必须指明需要执⾏哪个系统调⽤,这需要使⽤EAX寄存器传递⼀个名为系统调⽤号的参数。除了系统调⽤号外,系统调⽤也可能需要传递参数
-
- 本人学号最后两位是87,所以选定如下系统调用
87 common unlink __x64_sys_unlink //功能描述: //从文件系统中删除一个名称。如果名称是文件的最后一个连接,并且没有其它进程将文件打开,名称对应的文件会实际被删除。
-
int main() { asm volatile( "movl $0,%%edi\n\t" "movl $0x57,%%eax\n\t"//transfer the number of syscall "syscall\n\t"//syscall ); return 0; }
- 因为这里的根文件系统是静态连接,所以gcc编译时需要有-static
gcc -o _87syscall _87_test.c -static
- 因为rootf有变化,需要重新生成内存根文件镜像
-
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
- 重新挂载镜像,然后运行qemu
-
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
- 接下来,设置断点,运行程序。并查看调用。
- 系统在初始化过程中没有捕获断点,推测在初始化过程总没有使用该系统调用。于是运行所写的程序,成功捕获。
- 用命令,可以看到,具体的调用栈,依次为entry_SYSCALL_64,do_syscall_64,__x64_sys_unlink.
- 可以看到unlink系统调用实际上执行do_unlinkat,恰巧在系统调用表中找到unlinkat调用,猜测unlink调用已经被淘汰?
- 逐步单步调试
五、分析与总结
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 */
swapgs指令,快速保存一些寄存器里的参数,然后再在栈中构造一各pt_regs结构用以保存当前的相关信息。
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 */ TRACE_IRQS_IRETQ /* we're about to change IF */
接着执行了do_syscall_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); }
根据系统调用号,执行相关的系统调用。在调用完成后,
syscall_return_slowpath(regs);
- 上面这个命令返回
__visible inline void syscall_return_slowpath(struct pt_regs *regs) { struct thread_info *ti = current_thread_info(); u32 cached_flags = READ_ONCE(ti->flags); CT_WARN_ON(ct_state() != CONTEXT_KERNEL); if (IS_ENABLED(CONFIG_PROVE_LOCKING) && WARN(irqs_disabled(), "syscall %ld left IRQs disabled", regs->orig_ax)) local_irq_enable(); rseq_syscall(regs);
- 再向后的函数看的不是很明白,但是结合上面单步调试的两张截图,可以看出,在这之后将保存在栈中的相关数据回复的寄存器中,使得调用前的相关数据得以回复,继续执行。