分类: LINUX
2006-06-05 22:09:29
中断处理和系统调用
中断处理分为硬件中断和软件异常,什么是硬件中断,磁盘,I/O设备等硬件发出数据交换请求就叫硬件中断,所以需要相应的处理程序;什么是软件异常,顾名思义软件出错误了,比如程序在一个地方被0除的时候就会产生一个错误,叫软件异常,当然也需要相应的处理程序了;
系统调用是因为应用程序需要调用内核的功能内核给用户程序留的接口。
下面的trap_init(kernel/trap.c)函数用于安装相应中断号的中断处理函数;
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error);
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
for
(i=17;i<48;i++)
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13);
outb_p(inb_p(0x21)&0xfb,0x21);
outb(inb_p(0xA1)&0xdf,0xA1);
set_trap_gate(39,¶llel_interrupt);
}
硬件中断和软件异常的处理程序在主要在汇编文件asm.s中,比如divide_error这个过程为:
_divide_error:
pushl
$_do_divide_error
no_error_code:
xchgl
%eax,(%esp)
pushl
%ebx
pushl
%ecx
pushl
%edx
pushl
%edi
pushl
%esi
pushl
%ebp
push
%ds
push
%es
push
%fs
pushl
$0 # "error code"
lea
44(%esp),%edx
pushl
%edx
movl
$0x10,%edx
mov
%dx,%ds
mov
%dx,%es
mov
%dx,%fs
call
*%eax
addl
$8,%esp
pop
%fs
pop
%es
pop
%ds
popl
%ebp
popl
%esi
popl
%edi
popl
%edx
popl
%ecx
popl
%ebx
popl
%eax
iret
汇编代码do_divide_error也是一个函数,所以0除的真正处理函数还是用C来实现的,do_divide_error(kernel/trap.c)的原型为:
void do_divide_error(long esp, long error_code)
{
die("divide
error",esp,error_code);
}
调用die函数,给出一些信息然后死机,但是我们对函数的参数进行一些分析会发现很有意思。
call *%eax 这个语句就是调用do_divide_error这个函数执行。eax中放入函数的地址是通过:
xchgl %eax,(%esp) 这条语句实现的,因为在这之前,函数的地址刚好被压入堆栈,esp指向的正是函数的地址;
那么do_divide_error函数的参数esp是什么呢?通过:
lea
44(%esp),%edx
pushl
%edx
44(%esp)表示44+esp为引发该中断的指令的地址压入堆栈时的esp0指针的位置。
执行 call *%eax 这个语句时堆栈的状态:
|
原ss |
||||
中断返回地址 |
|||||
原eflags |
|||||
中断返回地址 |
ßesp0 |
||||
eip |
|||||
ßesp1 |
|||||
ebx |
|||||
ecx |
|||||
edx |
|||||
44 |
|||||
esi |
|||||
ebp |
|||||
|
ds |
||||
|
es |
||||
|
ßesp2 |
||||
Error_code |
|||||
Esp0 |
|||||
|
中断调用没有出错号的情景
所以do_divide_error(long
esp, long error_code)的esp是引发异常的地址,error_code就是异常码。
下面函数sched_init(kernel/sched.c)用于安装中断号0x80的系统调用函数处理函数system_call。
void sched_init(void)
{
int i;
struct
desc_struct * p;
if
(sizeof(struct sigaction) != 16)
panic("Struct
sigaction MUST be 16 bytes");
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
p =
gdt+2+FIRST_TSS_ENTRY;
for(i=1;i
task[i]
= NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
/* Clear NT, so that we won't have troubles with that
later on */
__asm__("pushfl
; andl $0xffffbfff,(%esp) ; popfl");
ltr(0);
lldt(0);
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH
& 0xff , 0x40); /* LSB */
outb(LATCH
>> 8 , 0x40); /* MSB */
set_intr_gate(0x20,&timer_interrupt);
outb(inb_p(0x21)&~0x01,0x21);
set_system_gate(0x80,&system_call);
}
而system_call这个函数在kernel/system_call.s中实现,原型为:
reschedule:
pushl $ret_from_sys_call
jmp
_schedule
.align 2
system_call:
cmpl
$nr_system_calls-1,%eax
ja
bad_sys_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
movl
$0x10,%edx # set up ds,es to
kernel space
mov %dx,%ds
mov %dx,%es
movl
$0x17,%edx # fs points to
local data space
mov %dx,%fs
call
_sys_call_table(,%eax,4)
pushl %eax
movl
_current,%eax
cmpl
$0,state(%eax) # state
jne
reschedule
cmpl
$0,counter(%eax) # counter
je
reschedule
ret_from_sys_call:
movl
_current,%eax # task[0] cannot
have signals
cmpl
_task,%eax
je
cmpw $0x
jne
cmpw
$0x17,OLDSS(%esp) # was stack
segment = 0x17 ?
jne
movl
signal(%eax),%ebx
movl
blocked(%eax),%ecx
notl %ecx
andl
%ebx,%ecx
bsfl
%ecx,%ecx
je
btrl
%ecx,%ebx
movl
%ebx,signal(%eax)
incl %ecx
pushl %ecx
call
_do_signal
popl %eax
3: popl %eax
在程序的开始处,首先比较EAX中的功能号是不是有效.
然后保存会用到的寄存器.
LINUX默认把ds,es作为内核数据段,0x10为内核数据段的选择符;fs用于用户数据段,0x
接着通过一个地址跳转表sys_call_table调用相应系统调用的C函数。在C函数返回后,保存(push)了调用返回值(eax中的值)。
接下来,该程序查看执行本次调用进程的状态。如果由于上面C函数的操作或者其他情况而使进程的状态从执行态变成了其它状态,或者由于时间片已经用完(counter==0),则调用进程调度函数schedule()(jmp _schedule).由于在执行“jmp _schedule”之前已经把返回地址ret_from_sys_call入栈,因此在执行完schedule()后最终会返回到ret_from_sys_call处继续执行。
从ret_from_sys_call标号开始处的代码执行一些系统调用后的处理工作。
该段汇编代码调用sys_call_table+
%eax *4处的函数执行,sys_call_table是一个函数表在include/linux/sys.h文件中;
extern int sys_setup();
extern int sys_exit();
extern int sys_fork();
extern int sys_read();
…………………………
extern int sys_setreuid();
extern int sys_setregid();
fn_ptr sys_call_table[]
= { sys_setup, sys_exit, sys_fork, sys_read,
……………………………………………………………..
sys_setreuid,sys_setregid };
其中fn_ptr(include/linux/sched.h)是一个函数指针,定义为:
typedef int (*fn_ptr)();
下面我们来实现一个自己的系统调用。
在用户空间调用的函数原型为:
Int getval(int val);
则我们在include/unistd.h文件中要包括该文件,并且通过
_syscall1(int,getval,int,val)
实现该函数,注意上面的函数后面没有“;”这个符号。
_syscall1这个宏表示有一个参数的系统调用,定义在include/unistd.h文件中:
#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
:
"=a" (__res) \
:
"0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \
return
(type) __res; \
errno = -__res; \
return -1; \
}
我们还要在include/unistd.h文件中加上函数的功能号
#define __NR_getval 72
我们在include/linux/sys.h 还要加上
extern int sys_ getval();
在sys_call_table的最后加上函数sys_ getval。
然后在内核的某个文件(比如kernel/sched.c)中实现:
int sys_ getval(
long val)
{
return (int)val;
}
下面测试我们的系统调用的文件:
/* file name:test.c
/*
#define __LIBRARY__
#include
#define __NR_getval
72
_syscall1(int,getval,int,val)
main()
{
printf(“our
value=%d”,getval(10));
}