使用 gdb 跟踪分析一个系统调用内核函数
一、实验过程
1.给MenuOS增加命令:geiuid
然后进入menu,运行make rootfs
,结果如下:
2.使用gdb跟踪系统调用内核函数sys_getuid16
步骤如下:
//shell1中启动内核
cd ~/LinuxKernel
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -S -s
// shell2中使用gdb调试
cd ~/LinuxKernel
gdb
file linux-3.18.6/vmlinux
target remote:1234
b start_kernel
c
b sys_getuid16 //在sys_getuid16处设置断点,启动MenuOS后执行getuid命令,getuid命令仅执行一半
c
// 然后在qemu中输入getuid
运行结果如下
继续运行以下步骤
list
s
s
finish
然后在system_call处设置一个断点,但是还是停在了sys_getuid16处,system_call 不是正常的函数,是一段特殊的汇编代码,gdb还不支持跟踪system_call。
二、分析system_call开始到iret结束并得到流程图
系统调用在内核代码中的处理过程
1)系统调用在内核代码中的初始化
在init/main.c文件中start_kernel函数执行下面的语句,完成初始化:
trap_init();
在arch/x86/kernel/traps.c中有具体的初始化步骤:
#ifdef CONFIG_X86_32
set_system_trap_gate(SYSCALL_VECTOR, &system_call);
set_bit(SYSCALL_VECTOR,used_vectors);
#endif
2)系统调用的工作机制
system call在系统启动过程中初始化好了之后,一旦用户执行:int 0x80立马调到arch/x86/kernel/entry_32.S: ENTRY(system_call)
,开始执行。
在系统调用返回之前,有可能发生进程调度;当前进程也有可能会有进程间通讯的信号需要处理。
arch/x86/kernel/entry_32.S :
ENTRY(system_call)
...
SAVE_ALL //保存现场
...
syscall_call:
call *sys_call_table(,%eax,4) //调用系统调用服务程序
syscall_after_call:
movl %eax, PT_EAX(%esp) #store the return value
syscall_exit:
...
jne syscall_exit_work //进程调度时机
restore_all:
TRACE_IRQS_IRET
...
irq_return:
INTERRUPT_RETURN //到这里,整个system_call 执行完毕
上面的汇编代码是对entry_32.S代码的简化,仅仅只是摘录的系统调用最精要的汇编语句。其实汇编执行过程比这个还用复杂得多,为了更方便理解,进一步删减entry_32.S汇编代码,得到如下system_call 伪代码:
// asm pseudo code
//系统调用处理过程的汇编伪代码
.macro INTERRUPT_RETURN
iret
.endm
.macro SAVE_ALL
...
.endm
ENTRY(system_call)
SAVE_ALL
syscall_call:
call *sys_call_table(,%eax,4) //执行系统调用的服务程序
movl %eax,PT_EAX(%esp) #store the return value
syscall_exit:
testl $_TIF_ALLWORK_MASK, %ecx // current->work
jne syscall_exit_work //1. 有进程需要调度,2. 有信号需要处理···
restore_call:
RESTORE_INT_REGS
irq_return:
INTERRUPT_RETURN //中断返回
ENDPROC(system_call)
syscall_exit_work:
testl $_TIF_WORK_SYSCALL_EXIT, %ecx
jz work_pending
END(syscall_exit_work)
work_pending:
testb $_TIF_NEED_RESCHED, %cl //判断是否需要进程调度
jz work_notifysig
work_resched:
call schedule //执行进程调度
jz restore_all //返回restore_all
work_notifysig:
... //deal with pending signals
END(work_pending)
根据上面的伪代码,我们可以清晰的画出从system_call 到iret之间的处理过程的流程图:
三、问题与总结
-
从system_call执行过程可以看出,系统调用其实也是一种中断,软中断。执行过程包括中断响应,也是就是执行系统调用服务程序,执行完毕后中断返回。但是在执行系统调用服务程序的时候,可能会发生进程调度和信号处理,使得整个system_call 汇编程序变得复杂起来。但是,总体而言,执行过程:中断响应,保存现场,执行中断服务例程,恢复现场,中断退出。
-
内核抽象成很多中断处理过程的一个集合,提供内核的服务线程当成一般的线程理解。
-
linux的系统调用过程:用户程序→C库(即API):INT 0x80 →system_call→系统调用服务例程→内核程序。我们常说的用户API其实就是系统提供的C库。
系统调用是通过软中断指令 INT 0x80 实现的,而这条INT 0x80指令就被封装在C库的函数中。软中断和我们常说的硬中断不同之处在于,软中断是由指令触发的,而不是由硬件外设引起的。INT 0x80 这条指令的执行会让系统跳转到一个预设的内核空间地址,它指向系统调用处理程序,即system_call函数。系统调用处理程序system_call 并不是系统调用服务例程,系统调用服务例程是对一个具体的系统调用的内核实现函数,而系统调用处理程序是在执行系统调用服务例程之前的一个引导过程,是针对INT 0x80这条指令,面向所有的系统调用的。简单来讲,执行任何系统调用,都是先通过调用C库中的函数,这个函数里面就会有软中断 INT 0x80 语句,然后转到执行系统调用处理程序 system_call ,system_call 再根据具体的系统调用号转到执行具体的系统调用服务例程。