一、 搭建Linux内核调试环境
本次实验的目录架构,一个主目录LinuxK,其包含3个文件夹:linux-5.4.34内核文件夹,busybox文件夹和rootfs文件夹。
安装开发工具
sudo apt install build-essential sudo apt install qemu # install QEMU sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
下载内核源码
# 在linuxK目录下 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
配置内核选项
# 在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)
编译运行内核
#在linux-5.4.34目录下 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目录下 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
注意:由于与gcc兼容性问题,不要去官网下载busybox,可用在https://github.com/mirror/busybox 下载 busybox。
make menuconfig #记得要编译成静态链接,不⽤动态链接库。 Settings ---> [*] Build static binary (no shared libs) #然后编译安装,默认会安装到源码⽬录下的 _install ⽬录中 make -j$(nproc) make install
#制作内存根文件系统镜像 # 在linuxK目录下 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),添加如下内容到init⽂件中
#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys echo "Wellcome MyOS!" echo "--------------------" cd home /bin/sh
#在 rootfs 目录下 给init脚本添加可执⾏权限
chome +x init
# 在 rootfs目录下,打包成内存根⽂件系统镜像 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 # 纯命令行下启动虚拟机 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-systemx86_64”命令强⾏关闭虚拟机。此时不可关闭QEMU及终端。再打开另一个终端(建议在linux5.4.34目录下打开),输入如下命令:
gdb vmlinux
(gdb) target remote:1234
(gdb) b start_kernel
c、 bt、 list、 next、 step....
系统调用概述
现代cpu通常有多种特权级别,一般来说特权级总共有4个,编号从Ring 0(最高特权)到Ring 3(最低特权),在Linux上之用到Ring 0和RIng 3,用户态对应Ring 3,内核态对应Ring 0。普通应用程序运行在用户态下,其诸多操作都受到限制,而系统调用是运行在内核态的,操作系统一般是通过中断来从用户态切换到内核态的。
中断一般有两个属性,一个是中断号,一个是中断处理程序。不同的中断有不同的中断号,每个中断号都对应了一个中断处理程序。在内核中有一个叫中断向量表的数组来映射这个关系。当中断到来时,cpu会暂停正在执行的代码,根据中断号去中断向量表找出对应的中断处理程序并调用。中断处理程序执行完成后,会继续执行之前的代码。中断分为硬件中断和软件中断,我们这里说的是软件中断,软件中断通常是一条指令,使用这条指令用户可以手动触发某个中断。例如在i386下,对应的指令是int,在int指令后指定对应的中断号,如int 0x80代表你调用第0x80号的中断处理程序。
对于每个系统调用都有一个系统调用号,在触发中断之前,会将系统调用号放入到一个固定的寄存器,0x80对应的中断处理程序会读取该寄存器的值,然后决定执行哪个系统调用的代码。
操作系统通过系统调用为运行于其上的进程提供服务。当用户态进程发起一个系统调用, CPU 将切换到 内核态 并开始执行一个 内核函数 。 内核函数负责响应应用程序的要求,例如操作文件、进行网络通讯或者申请内存资源等。
调用流程
在应用程序内,调用一个系统调用的流程是怎样的呢?我们以一个假设的系统调用 xyz 为例,介绍一次系统调用的所有环节。
如上图,系统调用执行的流程如下:
1.应用程序 代码调用系统调用( xyz ),该函数是一个包装系统调用的 库函数 ;
2.库函数 ( xyz )负责准备向内核传递的参数,并触发 软中断 以切换到内核;
3.CPU被 软中断 打断后,执行 中断处理函数 ,即 系统调用处理函数 ( system_call);
4.系统调用处理函数 调用 系统调用服务例程 ( sys_xyz ),真正开始处理该系统调用;
执行态切换
应用程序 ( application program )与 库函数 ( libc )之间, 系统调用处理函数 ( system call handler )与 系统调用服务例程 ( system call service routine )之间, 均是普通函数调用。 而 库函数 与 系统调用处理函数 之间,由于涉及用户态与内核态的切换,要复杂一些。Linux 通过 软中断 实现从 用户态 到 内核态 的切换。 用户态 与 内核态 是独立的执行流,因此在切换时,需要准备 执行栈 并保存 寄存器 。内核实现了很多不同的系统调用(提供不同功能),而 系统调用处理函数 只有一个。 因此,用户进程必须传递一个参数用于区分,这便是 系统调用号 ( system call number )。 在 Linux 中, 系统调用号 一般通过 eax 寄存器 来传递。
总结起来, 执行态切换 过程如下:
1.应用程序 在 用户态 准备好调用参数,执行 int 指令触发 软中断 ,中断号为 0x80 ;
2.CPU 被软中断打断后,执行对应的 中断处理函数 ,这时便已进入 内核态 ;
3.系统调用处理函数 准备 内核执行栈 ,并保存所有 寄存器 (一般用汇编语言实现);
4.系统调用处理函数 根据 系统调用号 调用对应的 C 函数—— 系统调用服务例程 ;
5.系统调用处理函数 准备 返回值 并从 内核栈 中恢复 寄存器 ;
6.系统调用处理函数 执行 ret 指令切换回 用户态 ;
查看系统调用表
打开内核源码目录linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl,可以看到12号系统调用是__x64_sys_brk。
汇编调用程序:
汇编结果:
重新生成文件系统:
结果:
(gdb) b __x64_sys_brk Breakpoint 1 at 0xffffffff81199a40: file mm/mmap.c, line 187. (gdb) c Continuing. Breakpoint 1, __x64_sys_brk (regs=0xffffc900001b7f58) at mm/mmap.c:187 187 SYSCALL_DEFINE1(brk, unsigned long, brk) (gdb) n do_syscall_64 (nr=18446612682188181624, regs=0xffffc900001b7f58) at arch/x86/entry/common.c:300 300 syscall_return_slowpath(regs); (gdb) n 301 } (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:184 184 movq RCX(%rsp), %rcx (gdb) n 185 movq RIP(%rsp), %r11 (gdb) n 187 cmpq %rcx, %r11 /* SYSRET requires RCX == RIP */ (gdb) n 188 jne swapgs_restore_regs_and_return_to_usermode (gdb) n 205 shl $(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx (gdb) n 206 sar $(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx (gdb) n 210 cmpq %rcx, %r11 (gdb) n 211 jne swapgs_restore_regs_and_return_to_usermode (gdb) n 213 cmpq $__USER_CS, CS(%rsp) /* CS must match SYSRET */ (gdb) n 214 jne swapgs_restore_regs_and_return_to_usermode (gdb) n 216 movq R11(%rsp), %r11 (gdb) n 217 cmpq %r11, EFLAGS(%rsp) /* R11 == RFLAGS */ (gdb) n 218 jne swapgs_restore_regs_and_return_to_usermode (gdb) n 238 testq $(X86_EFLAGS_RF|X86_EFLAGS_TF), %r11 (gdb) n 239 jnz swapgs_restore_regs_and_return_to_usermode (gdb) n 243 cmpq $__USER_DS, SS(%rsp) /* SS must match SYSRET */ (gdb) n 244 jne swapgs_restore_regs_and_return_to_usermode (gdb) n 253 POP_REGS pop_rdi=0 skip_r11rcx=1 (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:259 259 movq %rsp, %rdi (gdb) n 260 movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:262 262 pushq RSP-RDI(%rdi) /* RSP */ (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:263 263 pushq (%rdi) /* RDI */ (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:271 271 SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi (gdb) n 273 popq %rdi (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:274 274 popq %rsp (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:275 275 USERGS_SYSRET64 (gdb) n 0x0000000000475349 in ?? ()