calibre[原创]IA32上Linux内核中断机制分析
文章标题:[原创]IA32上Linux内核中断机制分析顶部 albcamus 发布于:2005-11-2120:57 [楼主][原创]IA32上Linux内核中断机制分析
文章作者:albcamus(albcamus@whitecell)
信息来源:邪恶八进制信息安全团队()
本文章首发whitecell(www.whitecell)后由原作者友情提交到邪恶八进制信息安全团队技术论坛转载请注明文章作者
中断是计算机与外界联系的唯一途径。本文将分析在IA-32体系结构上的Linux内核对待中断系统的处理,针对的是2.6内核,引用的代码则具体则是2.6.14的。
一。几个相关概念的澄清
1,中断信号:
在电路级别来说,中断就是输送到CPU的INTR引脚上的电平信号。
i tools2,可编程中断控制器(PIC,ProgrammableInterruptController):
PIC是在计算机外部设备与CPU之间的芯片,它负责把自己接收到的外部中断信号,提交给CPU。在80386中,PIC是两片i8259A芯片级联;在Pentium以及后来的CPU中,集成了一个叫做高级可编程中断控制器(AdvancedProgrammableInterruptController)的PIC。如果你想用IA32处理器搭建SMP系统,则APIC是必不可少的。
3,中断向量与中断号:
中断向量是Intel从IA-32CPU角度看到的中断信号划分;中断号则是Linux系统对外部中断的号码分配。当外设把中断信号递送给PIC时,与之关联的是一个“中断号”(每个中断号对应一条中断线,从软件的角度来看,这两个术语可以混用);当PIC把这个中断信号发送给CPU时,与之关联的是一个“中断向量”。
在IA-32体系结构中,所有的异常和不可屏蔽中断(Non-MaskableInterrupt)的中断向量都是Intel预先定义的,软件无法更改;可屏蔽中断的中断向量可以通过编程来更改。在Linux上,0号中断(也就是时钟中断)对应的中断向量是0x20,也就是十进制的32。
4,异常(Exception)
顾名思义,异常是指CPU检测到了某种不正常的情形出现。CPU产生的异常是不可屏蔽的(eflags寄存器的IF位对异常不起作用),根据异常处理程序返回时,是否需要重新执行引发异常的那条指令,又可以把异常分为3种:
1)故障(Fault)。故障是比较轻微的异常,返回时重新执行引发故障的那条指令。
2)陷阱(Trap)。陷阱处理返回时,不重新执行引发陷阱的那条指令。
3)中止(Abort)。中止是严重的异常,将导致任务的中止而不会返回。
还有一种是程序产生的异常,如INT3指令、BOUND指令等。CPU把这种异常当作是陷阱来处理。
5,中断描述表IDT
异常与中断发生时,都需要到IDT中查找相关信息,以找到对应的处理程序以及其他动作。需要注意的是,保护模式下发生权限提升时,中断穿越
的是中断门,而异常穿越的是陷阱门。二者的区别是:当CPU穿越中断门时,是自动关中断的;而穿越异常门则不会。
二。重要数据结构与函数
在系统引导期间,需要初试化中断处理(asm/i386/kernel/entry.S):
422#defineBUILD_INTERRUPT(name,nr)\
羚羊的英文423ENTRY(name)\
424pushl$nr-256;\#这里得到一个负数,因为正数留给系统调用
425SAVE_ALL\#保存寄存器
426movl%esp,%eax;\
427callsmp_/**/name;\
428jmpret_from_intr;
其中,SAVE_ALL宏就是用来保存寄存器的。
内核书籍中经常提到的中断上下文,指的是内核正在运行中断服务程序或softirq,无法代表当前进程的情形。中断上下文没有自己专有的堆栈,相反,它借用被中断进程的内核堆栈──IA-32上的Linux默认这个堆栈只有8k大小,而且很可能在处理中断的过程中又被另一个中断源中断。因此如果你自己编
写中断处理程序,递归层次太深或者函数局部变量太大,都有可能导致栈溢出。(i386有一个4KStacks补丁,如果编译时打开该选项,则中断上下文使用独立的栈,而不占用被中断进程的。)
在include/linux/irq.h文件中,定义了一个中断描述数组iqr_desc[NR_IRQS],每一个中断向量都与它的一个元素相关联:
70typedefstructirq_desc{
71hw_irq_controller*handler;
72void*handler_data;
73structirqaction*action;/*IRQactionlist*/
74unsignedintstatus;/*IRQstatus*/
75unsignedintdepth;/*nestedirqdisables*/
76unsignedintirq_count;/*Fordetectingbrokeninterrupts*/
77unsignedintirqs_unhandled;
78spinlock_tlock;
xen79#ifdefined(CONFIG_GENERIC_PENDING_IRQ)||defined(CONFIG_IRQBALANCE)
80unsignedintmove_irq;/*Flagneedtore-targetintrdest*/
81#endif
82}____cacheline_alignedirq_desc_t;/*告诉GCC与CPU的L1告诉缓存对齐*/
83
84externirq_desc_tirq_desc[NR_IRQS];
当一个中断发生时,内核的处理是这样的(arch/i386/kernel/entry.S):
416common_interrupt:
417SAVE_ALL
418movl%esp,%eax
419calldo_IRQ
420jmpret_from_intr
SAVE_ALL宏定义在entry.S中,负责保存寄存器,再将%esp寄存器移送到%eax中,调用do_IRQ()函数(arch/i386/kernel/irq.c):
/*
*do_IRQ()函数负责处理所有的外部设备中断(处理器间中断由它们各自
*的处理函数来处理
*/
fastcallunsignedintdo_IRQ(structpt_regs*regs)
{
/*highbitsudinret_from_code*/
intirq=regs->orig_eax&0xff;
/*i386上如果定义了CONFIG_4KSTAKS,就申请独立的栈,而不占用被中断进程的*/
#ifdefCONFIG_4KSTACKSmattel
unionirq_ctx*curctx,*irqctx;
u32*isp;
#endif
irq_enter();
#ifdefCONFIG_DEBUG_STACKOVERFLOW
/*检查堆栈溢出的代码,此处省去。*/
#endif
#ifdefCONFIG_4KSTACKS
curctx=(unionirq_ctx*)current_thread_info();
irqctx=hardirq_ctx[smp_processor_id()];
/*
*这是我们切换到中断栈的地方。然而,如果我们已经在使用
中断栈(也
*就是说,我们这次是中断了一个中断处理程序),我们就不切换栈,而
*是继续使用当前的栈(此时,“当前的栈”是一个中断栈)
*/
if(curctx!=irqctx){
intarg1,arg2,ebx;
/*buildthestackframeontheIRQstack*/
isp=(u32*)((char*)irqctx+sizeof(*irqctx));
irqctx->tinfo.task=curctx->tinfo.task;
irqctx->tinfo.previous_esp=current_stack_pointer;
asmvolatile(
"xchgl%%ebx,%%esp\n"
"call__do_IRQ\n"
"movl%%ebx,%%esp\n"
weighting:"=a"(arg1),"=d"(arg2),"=b"(ebx)
:"0"(irq),"1"(regs),"2"(isp)
:"memory","cc","ecx"
);
}el
#endif
__do_IRQ(irq,regs);//真正的中断处理
irq_exit();/*如果需要,处理softirq。注意,这里有两种可能不需要处理softirq:1,local_softirq_pending为假;2,我们刚刚是中断了一个中断,嵌套中断没有最终返回之前,softirq是不能处理的。*/
return1;
}
注意,fastcall是在include/asm-i386/linkage.h中定义的宏,它指导GCC连接时把fastcall修饰的函数的前三个参数用寄存器传递。另外一个类似的宏asmlinkage则告诉GCC不要用寄存器传递参数,asmlinkage和fastcall不能共存。
上面的do_IRQ()函数调用的__do_IRQ()代码如下(arch/i386/kernel/irq.c)
fastcallunsignedint__do_IRQ(unsignedintirq,structpt_regs*regs)
{
irq_desc_t*desc=irq_desc+irq;/*找到在irq_desc数组中的位置*/
structirqaction*action;/*取得相应的irqaction结构*/
unsignedintstatus;
kstat_this_cpu.irqs[irq]++;
if(CHECK_IRQ_PER_CPU(desc->status)){
irqreturn_taction_ret;
/*
*因为irq_desc[]数组中,每个CPU占一个元素,这里的desc就是本CPU
*数据,所以此处不需要加锁。
*/
desc->handler->ack(irq);
action_ret=handle_IRQ_event(irq,regs,desc->action);
desc->handler->end(irq);
return1;
}
spin_lock(&desc->lock);
desc->handler->ack(irq);/*给i8259A或APIC应答信号*/
/*
*REPLAYiswhenLinuxrendsanIRQthatwasdroppedearlier
*WAITINGisudbyprobetomarkirqsthatarebeingtested
*/
status=desc->status&~(IRQ_REPLAY|IRQ_WAITING);
status|=IRQ_PENDING;/*we_want_tohandleit*/
/*
*IftheIRQisdisabledforwhateverreason,wecannot
*utheactionwehave.
*/
action=NULL;
if(likely(!(status&(IRQ_DISABLED|IRQ_INPROGRESS)))){/*判断该IRQ是否是被禁止的,或者是已经在其他CPU上被处理*/
action=desc->action;
status&=~IRQ_PENDING;/*我们将处理它*/
status|=IRQ_INPROGRESS;/*置位IRQ_INPROGRESS,以便其他CPU注意*/
}
desc->status=status;
/*
*如果该IRQ没有处理函数,或者被禁止了,及早离开。
*因为我们置位了PENDING,如果别的CPU正在处理该IRQ的
*另一个实例,它就会小心些。
*/
if(unlikely(!action))
gotoout;
pritzker/*
*Edgetriggeredinterruptsneedtoremember
*pendingevents.
*Thisappliestoanyhwinterruptsthatallowacond
*instanceofthesameirqtoarrivewhileweareindo_
IRQ
*orinthehandler.Butthecodehereonlyhandlesthe_cond_
*instanceoftheirq,notthethirdorfourth.Soitismostly
*ufulforirqhardwarethatdoesnotmaskcleanlyinan
*SMPenvironment.
*/
for(;;){
irqreturn_taction_ret;
spin_unlock(&desc->lock);
action_ret=handle_IRQ_event(irq,regs,action);
wikspin_lock(&desc->lock);
if(!noirqdebug)
note_interrupt(irq,desc,action_ret,regs);
if(likely(!(desc->status&IRQ_PENDING)))
break;
cucudesc->status&=~IRQ_PENDING;
}
desc->status&=~IRQ_INPROGRESS;
out:
/*
*->end()用来处理那些由于别的CPU正在运行其处理程序而被禁止的中断
*/
desc->handler->end(irq);
spin_unlock(&desc->lock);
return1;
}
infer三。中断机制在SMP系统上的变化
当intel考虑如何在IA-32上架构SMP时,原来的中断控制器i8259A就显得力不从心了。在SMP上,必须考虑外部设备来的中断信号如何传递给某个合适的CPU问题,必须考虑IPI(Inter-PercossorInterrupt,处理器间中断)问题。Intel自Pentium之后,在CPU中集成了APIC,在SMP上,主板上有一个(至少一个,有的主板有多个IO-APIC,用来更好的分发中断信号)全局的APIC,它负责从外设接收中断信号,再分发到CPU上,这个全局的APIC被称作IO-APIC。
SMP的中断机制如下图所示:
SMP系统中的中断分发示意图
=800)window.open('../images/10_2_2f2ff1e5e1256c9.png');"onload="if(this.width>'800')this.width='800';if(this.height>'800')this.height='800';">
图1:SMP系统中的中断分发示意图
在系统引导的时候,通过tup_IO_APIC()函数(arch/i386/kernel/io_apic.c)对IO-APIC进行初试化;每个CPU被激活成为online状态的时候,通过tup_local_APIC()函数(arch/kernel/i386/apic.c)对本地APIC进行初试化。
在SMP系统上,Linux除了处理CPU异常、外部设备中断之外,还要处理处理器间中断。当一个CPU想对另一个CPU发送中断信号时,就在自己的本地APIC的ICR寄存器(InterruptCommandRegister,中断命令寄存器)中存放其中断向量,和目标CPU拥有的本地APIC的标识,触发中断。IPI中断信号经由APIC总线传递到目标APIC,那个收到中断的APIC就向自己所属的CPU发送一个中断。
Linux针对IA32的SMP系统定义了五种IPI:
1,CALL_FUNCTION_VECTOR。发往自己除外的所有CPU,强制它们执行指定的函数;
2,RESCHEDULE_VECTOR。使被中断的CPU重新调度;
3,INVLIDATE_TLB_VECTOR。使被中断的CPU废弃自己的TLB缓存内容。
4,ERROR_APIC_VECTOR。
5,SPUROUS_APIC_VECTOR。
在IA-32体系结构中,SMP的高速缓存一致性(CacheCoherence)问题是通过一种叫做总线监视(Buswatching,也叫Snoopying)的硬件技术来解决的。每当某个CPU或DMA控制器改写了某块内存区域的内容(这总是要通过总线来进行的,所以逃不过总线监视),别的CPU就会自动
废弃缓存了该内存区域的Cache。然而对TLB的情况则有所不同(为什么不同?Intel的手册说TLB也可以对软件透明,这里有点疑惑),Linux内核中,每个CPU在改变了页表的时候,都需要给其它所有运行着与该页表有关的任务的CPU发送IPI,使它们废弃自己的TLB内容。
参考:
UnderstandingtheLinuxKernel,2nd
IA-32IntelArchitectureSoftwareDeveloper’sManual,Volume3:SystemProgrammingGuide
WSS(WhitecellSecuritySystems),一个非营利性民间技术组织,致力于各种系统安全技术的研究。坚持传统的hacker精神,追求技术的精纯。
WSS主页:www.whitecell/
WSS论坛:www.whitecell/forums/(c)Copyleft2003-2007,EvilOctalSecurityTeam.
ThisfileisdecompiledbyanunregisteredversionofChmDecompiler.
Regsiteredversiondoesnotshowthismessage.
YoucandownloadChmDecompilerat:/