环境:Linux 0.11 / Linux 3.4.2
参考书籍:Linux内核完全剖析基于0.11内核-赵炯
一、Linux中断机制
硬件中断信号源
在早期80x86的PC机器上的,其采用了两片8259A中断控制芯片,可以发出IRQ0~IRQ15中断请求信号。
对于ARM上则是由中断控制器(NVIC/ GIC)发出中断请求信号。
1.1 Linux中断分类
Linux中断可以分为两类硬中断和软中断:
① 硬中断:对于早期PC主机上类似于8259A的中断控制芯片发出的中断 /
ARM中断控制器发出的中断 /
② 软中断:CPU自行保留的中断 /
系统调用异常 /
1.2 Linux中断号
Linux内核中每个中断都是由0~255之前的一个数字标识,称为中断号。早期的PC电脑中断号分配:
(1) int0~int31由Inter公司自行保留。
(2) Linux系统中int32~int47被分配为8259A对应的IRQ0~IRQ15。
(3) int 128被设置为系统调用(system_call)。
1.3 Linux中断工作流程
以80x86为例子:
① 首先将寄存器值入栈:原ss、原esp、eflags、cs、eip。
ss为堆栈的段地址
esp为堆栈的栈顶地址
eflags为标志寄存器
cs为代码段寄存器、ip为指令帧寄存器两个寄存器共同指示了要读取的指令的地址
② 如果有出错码则将出错码error_code压入栈,无出错码则不压栈。
③ 将当前函数的返回值入栈(保证中断执行后能正常返回)。
④ 将ebx、ecx等寄存器的值压入栈。
⑤ 在执行中断处理函数前最后把error_code和 函数的返回值入栈,并将sp指针加上8个字节返回正常位置。(是为了向中断处理函数进行传参)。
异常处理堆栈变化示意图
⑥ 出栈函数返回值。
⑦ 返回所有入栈的寄存器值。
二、中断的代码实现过程
1.Linux中断代码结构
中断前的处理过程,中断的回复过程 | 中断的执行过程 | |
硬件中断的处理过程 | asm.s | trap.c |
软中断及系统调用的处理过程 | system_call.s | fork.c signal.c sys.c |
2.硬件中断处理过程分析
① asm.s文件
在Linux 0.11内核中,asm.s文件主要涉及对intel保留的中断int0~int16的说明(实现执行中断前的现场保护,和执行完中断的现场恢复的过程),int17~int31保留使用。具体的中中断定义如下图。
Linux 中断/异常分为有错误码和无错误码。
代码分析以无中断码的do_divide_error为例,内容已经注释在代码中。
/*
* linux/kernel/asm.s
*
* (C) 1991 Linus Torvalds
*/
/*
* asm.s contains the low-level code for most hardware faults.
* page_exception is handled by the mm, so that isn't here. This
* file also handles (hopefully) fpu-exceptions due to TS-bit, as
* the fpu must be properly saved/resored. This hasn't been tested.
*/
.globl _divide_error,_debug,_nmi,_int3,_overflow,_bounds,_invalid_op
.globl _double_fault,_coprocessor_segment_overrun
.globl _invalid_TSS,_segment_not_present,_stack_segment
.globl _general_protection,_coprocessor_error,_irq13,_reserved
//.global定义了一些全局函数的声明,函数的具体实现在traps.c文件中。
//在0.11内核中全局函数打印了当前正在执行中断/异常的类型。
//int0--divide_error是处理处理被零除错的情况
//无中断码
_divide_error:
pushl $_do_divide_error//把do_divide_error函数地址入栈,此时esp的内容为函数的地址
no_error_code://无中断码中断
xchgl %eax,(%esp) //利用xchgl指令将eax寄存器的地址和esp内容交换,eax值入栈,此时eax保存函数的地址
pushl %ebx //寄存器ebx值入栈
pushl %ecx //寄存器ecx入栈
pushl %edx //寄存器edx入栈
pushl %edi //寄存器edi入栈
pushl %esi //寄存器esi入栈
pushl %ebp //寄存器ebp入栈
push %ds //寄存器ds入栈,16位寄存器入栈高位也占4个字节.
push %es //寄存器es入栈
push %fs //寄存器fs入栈
pushl $0 # "error code" //无错误码,将0入栈
lea 44(%esp),%edx //将esp指针的地址+44字节处的内容存入edx寄存器中,从前面的异常处理堆栈变化示意图可以看出是将esp0地址处的内容,也就是将中断函数返回的地址存入edx
pushl %edx //将edx存入栈
movl $0x10,%edx //初始化段寄存器ds,es,fs,加载内核的数据段选择符
mov %dx,%ds
mov %dx,%es
mov %dx,%fs
call *%eax //eax保存的是do_divide_error函数的地址,此代码是调用eax内容地址的函数
addl $8,%esp //弹出最后入栈的两个参数值为39行和41行(参数要传递给调用的C程序),让堆栈指针指向fs处。
pop %fs //恢复寄存器的值
pop %es
pop %ds
popl %ebp
popl %esi
popl %edi
popl %edx
popl %ecx
popl %ebx
popl %eax
iret //中断返回
_debug://这是一个中断
pushl $_do_int3 # _do_debug
jmp no_error_code//跳转
_nmi:
pushl $_do_nmi
jmp no_error_code
_int3:
pushl $_do_int3
jmp no_error_code
_overflow:
pushl $_do_overflow
jmp no_error_code
_bounds:
pushl $_do_bounds
jmp no_error_code
_invalid_op:
pushl $_do_invalid_op
jmp no_error_code
_coprocessor_segment_overrun:
pushl $_do_coprocessor_segment_overrun
jmp no_error_code
_reserved:
pushl $_do_reserved
jmp no_error_code
_irq13:
pushl %eax
xorb %al,%al
outb %al,$0xF0
movb $0x20,%al
outb %al,$0x20
jmp 1f
1: jmp 1f
1: outb %al,$0xA0
popl %eax
jmp _coprocessor_error
_double_fault:
pushl $_do_double_fault
error_code://有中断码中断
xchgl %eax,4(%esp) # error code <-> %eax
xchgl %ebx,(%esp) # &function <-> %ebx
pushl %ecx
pushl %edx
pushl %edi
pushl %esi
pushl %ebp
push %ds
push %es
push %fs
pushl %eax # error code
lea 44(%esp),%eax # offset
pushl %eax
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
call *%ebx
addl $8,%esp
pop %fs
pop %es
pop %ds
popl %ebp
popl %esi
popl %edi
popl %edx
popl %ecx
popl %ebx
popl %eax
iret
_invalid_TSS:
pushl $_do_invalid_TSS
jmp error_code
_segment_not_present:
pushl $_do_segment_not_present
jmp error_code
_stack_segment:
pushl $_do_stack_segment
jmp error_code
_general_protection:
pushl $_do_general_protection
jmp error_code
② traps.c文件
traps.c文件主要描述在处理硬件中断/异常的时候asm.s文件中调用的c程序函数的具体定义。
die()函数用于在中断显示详细错误信息。
trap_init()函数是中断/异常初始化函数:其中的关键为两个宏定义都是用来初始化中断向量表。
① set_trap_gate 初始化中断向量表,并设置特权级为0
② set_system_gate 初始化中断向量表,并设置特权级为3
/*
* linux/kernel/traps.c
*
* (C) 1991 Linus Torvalds
*/
/*
* 'Traps.c' handles hardware traps and faults after we have saved some
* state in 'asm.s'. Currently mostly a debugging-aid, will be extended
* to mainly kill the offending process (probably by giving it a signal,
* but possibly by killing it outright if necessary).
*/
#include <string.h>
#include <linux/head.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/system.h>
#include <asm/segment.h>
#include <asm/io.h>
#define get_seg_byte(seg,addr) ({ \
register char __res; \
__asm__("push %%fs;mov %%ax,%%fs;movb %%fs:%2,%%al;pop %%fs" \
:"=a" (__res):"0" (seg),"m" (*(addr))); \
__res;})
#define get_seg_long(seg,addr) ({ \
register unsigned long __res; \
__asm__("push %%fs;mov %%ax,%%fs;movl %%fs:%2,%%eax;pop %%fs" \
:"=a" (__res):"0" (seg),"m" (*(addr))); \
__res;})
#define _fs() ({ \
register unsigned short __res; \
__asm__("mov %%fs,%%ax":"=a" (__res):); \
__res;})
int do_exit(long code);
void page_exception(void);
void divide_error(void);
void debug(void);
void nmi(void);
void int3(void);
void overflow(void);
void bounds(void);
void invalid_op(void);
void device_not_available(void);
void double_fault(void);
void coprocessor_segment_overrun(void);
void invalid_TSS(void);
void segment_not_present(void);
void stack_segment(void);
void general_protection(void);
void page_fault(void);
void coprocessor_error(void);
void reserved(void);
void parallel_interrupt(void);
void irq13(void);
//esp_ptr 段指针
//nr 出错的段号
//总的来说die函数就是用来打印错误信息
static void die(char * str,long esp_ptr,long nr)
{
long * esp = (long *) esp_ptr;
int i;
//以下基本在打印栈信息
printk("%s: %04x\n\r",str,nr&0xffff);
printk("EIP:\t%04x:%p\nEFLAGS:\t%p\nESP:\t%04x:%p\n",
esp[1],esp[0],esp[2],esp[4],esp[3]);
printk("fs: %04x\n",_fs());
printk("base: %p, limit: %p\n",get_base(current->ldt[1]),get_limit(0x17));
if (esp[4] == 0x17) {
printk("Stack: ");
for (i=0;i<4;i++)
printk("%p ",get_seg_long(0x17,i+(long *)esp[3]));
printk("\n");
}
str(i);
printk("Pid: %d, process nr: %d\n\r",current->pid,0xffff & i);
for(i=0;i<10;i++)
printk("%02x ",0xff & get_seg_byte(esp[1],(i+(char *)esp[0])));
printk("\n\r");
do_exit(11);//退出中断 /* play segment exception */
}
void do_double_fault(long esp, long error_code)
{
die("double fault",esp,error_code);
}
void do_general_protection(long esp, long error_code)
{
die("general protection",esp,error_code);
}
//asm.s调用的第一个函数在这里
void do_divide_error(long esp, long error_code)
{
die("divide error",esp,error_code);
}
void do_int3(long * esp, long error_code,
long fs,long es,long ds,
long ebp,long esi,long edi,
long edx,long ecx,long ebx,long eax)
{
int tr;
__asm__("str %%ax":"=a" (tr):"0" (0));
printk("eax\t\tebx\t\tecx\t\tedx\n\r%8x\t%8x\t%8x\t%8x\n\r",
eax,ebx,ecx,edx);
printk("esi\t\tedi\t\tebp\t\tesp\n\r%8x\t%8x\t%8x\t%8x\n\r",
esi,edi,ebp,(long) esp);
printk("\n\rds\tes\tfs\ttr\n\r%4x\t%4x\t%4x\t%4x\n\r",
ds,es,fs,tr);
printk("EIP: %8x CS: %4x EFLAGS: %8x\n\r",esp[0],esp[1],esp[2]);
}
void do_nmi(long esp, long error_code)
{
die("nmi",esp,error_code);
}
void do_debug(long esp, long error_code)
{
die("debug",esp,error_code);
}
void do_overflow(long esp, long error_code)
{
die("overflow",esp,error_code);
}
void do_bounds(long esp, long error_code)
{
die("bounds",esp,error_code);
}
void do_invalid_op(long esp, long error_code)
{
die("invalid operand",esp,error_code);
}
void do_device_not_available(long esp, long error_code)
{
die("device not available",esp,error_code);
}
void do_coprocessor_segment_overrun(long esp, long error_code)
{
die("coprocessor segment overrun",esp,error_code);
}
void do_invalid_TSS(long esp,long error_code)
{
die("invalid TSS",esp,error_code);
}
void do_segment_not_present(long esp,long error_code)
{
die("segment not present",esp,error_code);
}
void do_stack_segment(long esp,long error_code)
{
die("stack segment",esp,error_code);
}
void do_coprocessor_error(long esp, long error_code)
{
if (last_task_used_math != current)
return;
die("coprocessor error",esp,error_code);
}
void do_reserved(long esp, long error_code)
{
die("reserved (15,17-47) error",esp,error_code);
}
//中断的初始化函数
//set_trap_gate 优先级较低 只能由用户程序来调用
//set_system_gate 优先级很高 能由系统和用户所有的程序调用
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error);//如果被除数是0就会产生这个中断
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);
}
3.软中断处理过程分析
① system_call.s文件
在system_call.s文件中主要实现了
(1)系统调用中断(0x80)入口处理程序
(2)信号检测处理过程
(3)sys_execve和sys_fork系统调用
(4)int 16协处理器处理过程/ int 7 设备不存在处理过程 / int 32 时钟中断处理过程 / int 46 硬盘中断处理过程 / int 38 软盘中断处理过程
初学Linux内核,先了解系统调用处理过程:
在Linux 0.11内核中,在执行中断号为(0x80)的中断处理函数中会识别存放于eax寄存器中的功能号,处理函数会根据功能号在system_call_table中执行内核提供的不同系统调用功能。
所有实现系统调用的函数,内核按功能号的顺序定义在include/linux/sys.h中system_call_table如下:
//定义系统调用的sys_call_table
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid };
系统调用的代码如下所示:
.align 2 //2^2=4字节对齐
bad_sys_call: //错误的系统调用
movl $-1,%eax //将-1存入eax寄存器
iret //返回
.align 2 //4字节对齐
reschedule: //重新执行调度程序
pushl $ret_from_sys_call //将当前进程执行的地址存入栈
jmp _schedule //调度
/**************************************/
//int 0x80 Linux 0.11系统调用的入口处理函数
//eax寄存器中存放调用号
.align 2 //4字节对齐
_system_call:
cmpl $nr_system_calls-1,%eax //比较调用号是否超出范围,如果超出范围,则在ea中写入-1
ja bad_sys_call
push %ds //保存原段寄存器
push %es
push %fs
//系统调用最多可以传递三个参数,亦可以无参数。参数的传递方式是通过栈传递的,第一个参数ebx,第二个参数ecx,
//第三个参数edx,寄存器的入栈顺序是GCC GNU规定的
//在文件include/unistd.h中133~183行存放中三种系统调用的宏定义
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es //ds,es段寄存器指向内核数据段
movl $0x17,%edx # fs points to local data space
mov %dx,%fs //fs段寄存器指向用户数据段
call _sys_call_table(,%eax,4) //调用对应的系统调用,计算方法sys_call_table地址 + 4*ax(调用号
//)位置的系统调用
pushl %eax //将系统调用返回值存入栈
//40~44行是检查当前任务的运行状态,如果不在就绪状态(state不等于0)就执行任务调度程序
//如果时间片等于0(counter=0)也执行任务调度程序
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 3f
cmpw $0x0f,CS(%esp) # was old code segment supervisor ?
jne 3f
cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ?
jne 3f
movl signal(%eax),%ebx
movl blocked(%eax),%ecx
notl %ecx
andl %ebx,%ecx
bsfl %ecx,%ecx
je 3f
btrl %ecx,%ebx
movl %ebx,signal(%eax)
incl %ecx
pushl %ecx
call _do_signal
popl %eax
3: popl %eax
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret
系统调用的执行流程