学号118
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
实验过程
内核编译
下载Linux Kernel 5.0.1 source code,然后设置编译选项
make i386_defconfig
make manuconfig
然后在menuconfig中选择编译内核时添加调试信息
然后执行
qemu-system-i386 -kernel arch/x86/boot/bzImage -initrd ../rootfs.img -s -S -append nokaslr
QEMU将会停在启动界面,等待调试。
gdb调试工具
打开gdb,加载符号表,选择远程连接,设置断点。
不过这里加载符号表时出错
解决办法:手动修改。
在start_kernel函数设置断点,开始调试。
调试过程
在内核启动过程中,会初始化sys_call_table,完成这一工作的是trap_init函数。
内核对系统调用函数是通过宏定义产生的函数名,time系统调用号为13,实现time系统调用处理程序的代码位于/kernel/time.c函数中,
COMPAT_SYSCALL_DEFINE1(time, old_time32_t __user *, tloc)
{
old_time32_t i;
i = (old_time32_t)ktime_get_real_seconds();
if (tloc) {
if (put_user(i,tloc))
return -EFAULT;
}
force_successful_syscall_return();
return i;
}
总结
对于一个完成的系统调用过程:
- 用户应用中的代码为通用寄存器赋值(系统调用号、系统调用参数)
- 处理器执行到int 0x80指令或者sysenter指令
- 发现中断向量号为128,用128在IDT中找到中断描述符,取出段选择符、段偏移量,找到GDT中的段基地址,然后段基地址+段偏移量找到system_call的入口。这里用到的数据结构的地址的初始化都是在系统启动时,由位于
start_kernel
函数中的trap_init
等初始化函数完成的。 - 跳转到
system_call
的地址。实际上到这一步就跳到了内核中的一个汇编代码文件,在x86_64中对应为arch/x86/entry/entry_64.S - 该汇编文件中的指令将处理器从用户模式转向内核模式,然后开始执行系统调用入口
entry_SYSCALL_64
entry_SYSCALL_64
中的汇编代码将栈指针指向内核栈,并且在内核栈保存通用寄存器的值、旧的栈(用户栈)和用户代码段地址、标志位等等。entry_SYSCALL_64
检查位于rax
寄存器中的系统调用号,在sys_call_table
中寻找对应的处理函数地址,然后执行它。这段代码往往是别的内核文件中的C代码,比如write
系统调用的实现位于fs/read_write.c- 如果系统调用号出错,那就从系统调用中退出
- 在系统调用结束之后,使用
sysretq
指令,恢复调用之前的处理器现场,包括之前的栈、通用寄存器值、标志位等,然后将系统调用处理程序的返回值放入eax
寄存器供用户程序使用,然后从entry_SYSCALL_64
中退出。