#181 Linux 内核分析 之五:system_call中断处理过程的简要分析

说明

欧长坤 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 这学期学校恰好有操作系统的课程,上个学习就开始寻思研究研究Linux内核代码,恰好MOOC有这个课程,遂选了此课。

一、准备工作

本周的实验是第四周实验的自然延伸。同样的,它也并不难。 我们可以在http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/syscalls/syscall_32.tbl中查看系统调用号。

如果你上周没有看过我的文章,你可以看看这里。我使用了systeminfo这样一个系统调用。

二、实验过程:跟踪系统调用

我们使用了sysinfo这个库函数API。 下面的三张实验图显示了我们已经成功的使用gdb调试跟踪到了sysinfo这个系统调用(函数名为sys_sysinfo),但是随后使用next命令继续调试时,显然无法再继续调试了,因为不能直接使用gdb来对使用汇编代码编写的system_call进行调试和追踪。这里有一篇文章介绍了如何使用gdb来调试汇编代码,我就不继续展开了:http://www.doc88.com/p-0781911176267.html

三、system_call过程分析

不说废话,老师上课对system_call的分析过程非常清楚,伪代码抓住了重点,我们很容易就能够画出下面的流程图:

接下来我们逐行分析系统调用处理过程的汇编伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.macro INTERRUPT_RETURN  ; 中断返回
iret
.endm
.macro SAVE_ALL ; 保护现场
...
.macro RESTORE_INT_REGS
...
.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
restore_all:
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
work_notifysig:
... ; deal with pending signals
END(work_pending)

可以看到无论是中断返回(ret_from_intr) ,还是系统调用返回,都使用了 work_pendingresume_userspace

对于宏SAVE_ALL来说,这条语句会把将寄存器的值压入堆栈当中,压入堆栈的顺序对应struct pt_regs,出栈时,这些值传递到struct pt_regs的成员,实现从汇编代码向C程序传递参数。struct pt_regs可以在arch/x86/include/asm/ptrace.h中查看。

而接下来为何直接就跳到了sys_call_talbe呢,这里伪代码里面没有说明清楚情况,我们来看真实代码片段:

1
2
3
4
5
6
7
8
9
    SAVE_ALL 
GET_THREAD_INFO(%ebp)
# system call tracing in operation / emulation
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(NR_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)

首先,GET_THREAD_INFO 宏可以获得当前进程的thread_info结构的地址,获取当前进程的信息。而jnz syscall_trace_entry比较testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)的结果不为零的时候跳转。如此可对用户态进程传递过来的系统调用号的合法性进行检查。如果不合法则跳转到syscall_badsys标记的命令处。

所以,比较结果大于或者等于最大的系统调用号的时候跳转,合法则跳转到相应系统调用号所对应的服务例程当中,也就是在sys_call_table表中找到了相应的函数入口点。由于sys_call_table表的表项占4字节,因此获得服务例程指针的具体方法是将由eax保存的系统调用号乘以4再与sys_call_table表的基址相加。 然后,进入到系统调用表查找到系统调用服务程序的入口函数的地址,再进行跳转。 这样便完成了对sys_call_table的进入。

接下来伪代码马上就来到了syscall_exit,真实代码情况也是如此,在执行完syscall_call后,已经没有比较在继续处理了,因此马上接触并退出系统调用。 但是这个退出的过程就变得非常的复杂了。

syscall_exit开始到irq_return的真实代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
syscall_exit:  
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY)
TRACE_IRQS_OFF
movl TI_flags(%ebp), %ecx
testl $_TIF_ALLWORK_MASK, %ecx # current->work
jne syscall_exit_work
restore_all:
TRACE_IRQS_IRET
restore_all_notrace:
movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS
movb PT_OLDSS(%esp), %ah
movb PT_CS(%esp), %al
andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
CFI_REMEMBER_STATE
je ldt_ss # returning to user-space with LDT SS
restore_nocheck:
RESTORE_REGS 4 # skip orig_eax/error_code
irq_return:
INTERRUPT_RETURN

这段处理过程就涉及到内核中断返回时候涉及到得一些非常繁杂的细节了,比如work_pending等其它细节工作,我们想要彻底弄清楚,在这个篇幅里显然是不够的,因此我也不再继续赘述。值得注意的事情是work_pending后回涉及到处理进程信号量的问题,这里涉及到了很多进程调度的内容,我们在以后的关于进程部分的课程作业中再详细谈及它吧。

完成这些其余的工作之后,最终来到了irq_return,在这里使用宏 INTERRUPT_RETURN 实际上就是iret指令,恢复现场,最终完成了系统调用中断处理的返回。

四、总结

好了,我们来总结一下:

1、用户态到内核态需要int 0x80进行中断,只有生成了中断向量后才可以切换状态;

2、中断处理让CPU停止当前工作转为执行系统内核中预设的一些任务,因此必须要对当前CPU执行的任务进行执行现场的保护工作,并对一些其他杂七杂八的工作进行检查,完成调用后,再进行检查,才能执行iret返回。

3、系统内部调用涉及CPU架构等内容,不同的CPU对于系统调用的汇编具体代码是不一样的。

如果我的文章对你起到了帮助,你可以选择金额不限的捐助,帮助我写出更多的文章。