linux系统调用流程
由于系统调用都是从调用中断开始的,所以我们还是从中断讲起.
关于中断: Intel386认识两种事件类:异常(exception)与中断(interrupt)。两者都会强制性创建
一个进程或任务。中断能在任何不可预料的时间发生,来响应硬件的信号,是硬中断;
而异常是由指令执行而产生的,是软中断。
386能辨认两种中断来源:可屏蔽中断和不可屏蔽中断。并能辨认两种异常来源:处理器检测
异常(Processor detected exceptions)和程序异常(programmed exceptions)。
每一个中断和异常都有一个号码(中断号),(中断号*4)就是中断处理程序的入口,(中断号*4+2)就是相应
代码段(cs)首地址.不可屏蔽中断和处理器检测异常都已经被安排在0到31的矢量表中了,
可屏蔽中断的矢量地址由硬件决定,外部中断控制器在中断认可时钟周期时将矢量地址放到总线上。
任何在32到255范围内的矢量,都可以作为可屏蔽中断和程序异常用。
以下是所有可能的中断和异常的列表:
0 Divide error
1 Debug exception
3 NMI interrupt
4 INTO-detected overflow
5 Bound range exceeded
6 Invalid opcode
7 coprocessor not available
8 double fault
9 coprocessor segment overrun
10 invalid task state segment
11 segment not present
12 stack fault
13 general protection
14 page fault
15 reserved
16 coprocessor error
17-31 reserved
32-255 maskable interrupt
中断的调用是通过中断号来实现的.
系统初始化时要初始化中断描述表(idt).所以我们从中断向量表(idt)的设置开始我们的征程.
在linux/init/main.c中有start_kernel()函数如下:
asmlinkage void start_kernel(void)
{ ....
....
trap_init(); /*trap_init()是设置idt的函数
....
}
在linux/arch/i386/traps.c之中有trap_init()如下:
void trap_init(void)
{ ....
....
set_call_gate(&default_ldt,lcall7);
set_trap_gate(0,÷_error);
set_trap_gate(1,&debug);
....
set_system_gate(5,&bounds);
set_trap_gate(7,&device_not_available);
....
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
for (i=18;i<48;i++)
set_trap_gate(i,&reserved);
set_system_gate(0x80,&system_call);
....
}
set_trap_gate与set_system_gate都是宏.它们可以在system.h之中找到如下.
#define set_intr_gate(n,addr) \\
_set_gate(&idt[n],14,0,addr)
#define set_trap_gate(n,addr) \\
_set_gate(&idt[n],15,0,addr)
#define set_system_gate(n,addr) \\
_set_gate(&idt[n],15,3,addr)
#define set_call_gate(a,addr) \\
_set_gate(a,12,3,addr)
其中的_set_gate也在同一文件中,如下.
#define _set_gate(gate_addr,type,dpl,addr) \\
__asm__ __volatile__ ("movw %%dx,%%ax\\n\\t" \\
"movw %2,%%dx\\n\\t" \\
"movl %%eax,%0\\n\\t" \\
"movl %%edx,%1" \\
:"=m" (*((long *) (gate_addr))), \\
"=m" (*(1+(long *) (gate_addr))) \\
:"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \\
"d" ((char *) (addr)),"a" (KERNEL_CS << 16) \\
:"ax","dx")
我们看到这里是用汇编写的.为了效率的原因,系统调用在底层大都是用汇编写的.
问题:由于linux用的汇编于IBM/PC 的汇编不同,再对内管理还不熟系所以对_set_gate
中的第2,3两个参数用法不了解.
trap.c中完成的是idt的初始化部分.我们看到在trap_init()中,有一句
set_system_gate(0x80,&system_call);
既把0x80作为system_call的入口了.这个system_call是一个入口的标号.
有关system_call的内容在linux/arch/i386/entry.S中,如下.
ENTRY(system_call)
pushl %eax # save orig_eax
/*NUM1 SAVE_ALL
#ifdef __SMP__
/*NUM2 ENTER_KERNEL
#endif
movl $-ENOSYS,EAX(%esp)
cmpl $(NR_syscalls),%eax
jae ret_from_sys_call
/*NUM3 movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax
testl %eax,%eax
je ret_from_sys_call
#ifdef __SMP__
GET_PROCESSOR_OFFSET(%edx)
movl SYMBOL_NAME(current_set)(,%edx),%ebx
#else
movl SYMBOL_NAME(current_set),%ebx
#endif
andl $~CF_MASK,EFLAGS(%esp) # clear carry - assume no errors
movl %db6,%edx
movl %edx,dbgreg6(%ebx) # save current hardware debugging status
testb $0x20,flags(%ebx) # PF_TRACESYS
jne 1f
/*NUM4 call *%eax
movl %eax,EAX(%esp) # save the return value
/*NUM5 jmp ret_from_sys_call
ALIGN
又是奇怪的汇编,但没关系.分析嘛,又不是自己写!
让我来分析其中的主要部分(有标号NUM1-5)
NUM1:
SAVE_ALL是一个宏.它的功能是保护现场,保存所有必要信息.
它的宏展开也在entry.S中,如下.
#define SAVE_ALL \\
cld; \\
push %gs; \\
push %fs; \\
push %es; \\
push %ds; \\
pushl %eax; \\
pushl %ebp; \\
pushl %edi; \\
pushl %esi; \\
pushl %edx; \\
pushl %ecx; \\
pushl %ebx; \\
movl $(KERNEL_DS),%edx; \\
mov %dx,%ds; \\
mov %dx,%es; \\
movl $(USER_DS),%edx; \\
mov %dx,%fs;
NUM2:
ENTER_KERNEL也是一个宏,它的功能是进入核心态,也在entry.S中有定义,这里就不详细说明了.
NUM3:
movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax是非常有内容的一句.
SYSMBOL_NAME的宏定义在linux/linkage.h之中,有如下.
#define SYMBOL_NAME_STR(X) #X
sys_call_table是很重要的一部分,它是不同系统调用的入口表,定义在entry.S中,如下
....
.data
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_setup) /* 0 */
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write).
.....
.long SYMBOL_NAME(sys_vm86)
.space (NR_syscalls-166)*4
这里可能不明白的是ENTRY这个宏,在上面我们也看到了有ENTRY(system_call)嘛.
猜测是一个入口标号.让我们来看一看这个宏(在linux/linkage.h中):
#define ENTRY(name) \\
.globl SYMBOL_NAME(name); \\
ALIGN; \\
SYMBOL_NAME_LABEL(name)
哦,我已经知道SYMBOL_NAME了,SYSBOL_NAME_LABEL又是什么呢?也很简单,如下:
#define SYMBOL_NAME_LABEL(X) X##:
大致是这个样子,在linkage.h中有SYSBOL_NAME_LABEL的好几种形式,只有细微差别.
知道了sys_call_table是什么,再来看看SYMBOL_NAME(sys_fork)之中地sys_fork.
sys_fork是一个函数名(表示地址).
那么movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax是将%eax寄存器中的中断号
乘4来索引sys_call_table.(要乘4是因为sys_call_table 中每一项是.long型的).
这样在%eax中就存放了相应的程序入口地址.
NUM4:
call *%eax ; 这就清楚了,转入相应的程序入
NUM5:
jmp ret_from_sys_call 在entry.S中有如下:
.globl ret_from_sys_call
ret_from_sys_call:
...
ret_from_sys_call是系统调用结束后执行的代码部分,我想到最后再分析.
对ENTRY(SYSTEM_CALL)就说到这里.ENTRY(SYSTEM_CALL),也就是int 0x80h(由上面讲到过的
set_system_gate(0x80,&system_call)设置,是所有系统调用的统一入口,只有进入了之后才
分别根据传进来的系统调用号(在%eax中)来进入不同处理函数入口的.
那么,到了现在当程序员还是无法只通过简单的函数调用来实现系统调用.因为有太多琐碎的
工作要做(譬如说置%eax,怎么传参数,etc).
下面我们要看系统调用的另一重要部分:
从编程员角度看,我们希望只写一个譬如说fork()函数就能完成创建一个进程的全部工作,可以
做到吗?答案当然是肯定的.这就要借助于_syscallx这样的一组宏.它们定义在asm/unistd.h
之中,其中x表示这个系统调用带参数的个数.如下:
static inline _syscall0(int,idle)
static inline _syscall0(int,fork)
static inline _syscall2(int,clone,unsigned long,flags,char *,esp)
static inline _syscall0(int,pause)
static inline _syscall0(int,setup)
....
static inline _syscall1(int,close,int,fd)
static inline _syscall1(int,_exit,int,exitcode)
static inline _syscall3(pid_t,waitpid,pid_t,pid,int *,wait_stat,int,options)
_syscallx的宏展开如下:
#define _syscall0(type,name) \\
type name(void) \\
{ \\
long __res; \\
__asm__ volatile ("int $0x80" \\
: "=a" (__res) \\
: "0" (__NR_##name)); \\
if (__res >= 0) \\
return (type) __res; \\
errno = -__res; \\
return -1; \\
}
....
#define _syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, \\
type5,arg5) \\
type name (type1 arg1,type2 arg2,type3 arg3,type4 arg4,type5 arg5) \\
{ \\
long __res; \\
__asm__ volatile ("int $0x80" \\
: "=a" (__res) \\
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \\
"d" ((long)(arg3)),"S" ((long)(arg4)),"D" ((long)(arg5))); \\
if (__res>=0) \\
return (type) __res; \\
errno=-__res; \\
return -1; \\
}
_syscallx最多能带5个参数,所以最多是_syscall5,如上.
在_syscallx的宏定义中,我们看到:
int $0x80 ;这就是系统调用的入口了,
剩余的部分一定是在设置各自不同处理函数的入口与参数了.
果其然, "b" ((long)(arg1)),"c" ((long)(arg2))之类就是将参数放到不同寄存器中了.
那么__NR_##name又是什么呢?
__NR_##name是对应的不同的系统调用的号码,权且叫它系统调用号吧.定义在unistd.h中,如下:
#define __NR_setup 0 /* used only by init, to get system going */
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
#define __NR_creat 8
#define __NR_link 9
#define __NR_unlink 10
....
将系统调用号放入%eax中,留于在ENTRY(system_call)之中用(上面提到过的).
要利用系统调用要把unistd.h 包含进去.
最后的收尾工作:
由ret_from_sys_call来完成,如下.
ret_from_sys_call:
cmpl $0,SYMBOL_NAME(intr_count)
jne 2f
9: movl SYMBOL_NAME(bh_mask),%eax
andl SYMBOL_NAME(bh_active),%eax
jne handle_bottom_half
#ifdef __SMP__
cmpb $(NO_PROC_ID), SYMBOL_NAME(saved_active_kernel_processor)
jne 2f
#endif
movl EFLAGS(%esp),%eax # check VM86 flag: CS/SS are
testl $(VM_MASK),%eax # different then
jne 1f
cmpw $(KERNEL_CS),CS(%esp) # was old code segment supervisor ?
je 2f
1: sti
orl $(IF_MASK),%eax # these just try to make sure
andl $~NT_MASK,%eax # the program doesn\'t do anything
movl %eax,EFLAGS(%esp) # stupid
cmpl $0,SYMBOL_NAME(need_resched)
jne reschedule
#ifdef __SMP__
GET_PROCESSOR_OFFSET(%eax)
movl SYMBOL_NAME(current_set)(,%eax), %eax
#else
movl SYMBOL_NAME(current_set),%eax
#endif
cmpl SYMBOL_NAME(task),%eax # task[0] cannot have signals
je 2f
movl blocked(%eax),%ecx
movl %ecx,%ebx # save blocked in %ebx for signal handling
notl %ecx
andl signal(%eax),%ecx
jne signal_return
2: RESTORE_ALL
ALIGN
在系统调用返回时:
<a>.先检查有没有中断请求,如果有则用bottom_half机制处理它们,具体如下:
movl SYMBOL_NAME(bh_mask),%eax
andl SYMBOL_NAME(bh_active),%eax
jne handle_bottom_half
而handle_bottom_half这种机制在分析定时器时已经清楚了,这里只是用汇编来实现的,如下:
handle_bottom_half:
incl SYMBOL_NAME(intr_count)
call SYMBOL_NAME(do_bottom_half)
decl SYMBOL_NAME(intr_count)
jmp 9f
ALIGN
分析过定时器后,看到这一段必然发出会心的微笑了,我不在分析了.
<b>.检查need_resched决定是否要再进行调度
cmpl $0,SYMBOL_NAME(need_resched)
jne reschedule
....
reschedule:
pushl $ret_from_sys_call
jmp SYMBOL_NAME(schedule)
先将ret_from_sys_call压入栈,然后跳转到schedule函数(一个负责进程调度的函数,在定时期
分析中以有谈到).
<c>.检查是否有信号(signal),如下.如果有就转入信号处理函数(handle_signal-->do-->signal)
后再返回.
andl signal(%eax),%ecx
jne signal_return
<d>.最后一个RESTRORE_ALL就回到过去.我也不在展开RESTORE_ALL的宏了(也在entry.S中).
现在,系统调用的基本流程已经清楚了,让我再来总结一下:
<1>.当编程人员写了一个系统调用函数时,通过_syscallx宏,进入了int 0x80h;
并且以经将参数都放好了,__NR_##name对应了相应的系统调用号.
<2>.进入了int 0x80h 后,如上面的分析通过系统调用号查到sys_call_table中响应的处理函数
<3>.执行处理函数后,int 0x80h 负责调用ret_from_sys_call,返回到系统调用前的地方.
posted on 2018-10-30 16:46 blogernice 阅读(568) 评论(0) 收藏 举报
浙公网安备 33010602011771号