LINUX中断描述符初始化
@CopyLeft by ICANTH , I Can do ANy THing that I CAN THink ! ~
Author : WenHui , WuHan University , 2012-6-4
硬件产生中断之后,需要通过门描述符来寻找中断的处理程序入口。门描述符和段描述符一样,8个字节。门描述符大体分为:段偏移、段选择子以及DPL。段选择子用于在GDT中寻找到门段基址,DPL用于控制当前进程中断或异常访问权限。当发生中断时,将门描述符所指向的段基地放入%cs,将段偏移放入%eip,转入相应服务。
门描述符结构如下:
任务门描述符 :用于在发生中断时调度相应进程
中断门描述符 :描述中断处理程序所在段选择子和段内偏移值。据此修改EIP及关闭中断Eflags的IT标识位
陷阱门描述符 :与中断门描述符一样,但不关中断
中断描述符表IDT( Interrupt Descriptor Table )用于存放256个门描述符,对应256个中断向量。其中IA32规定前0~31号固定向量用于异常。寄存器 idtr 为门描述符表IDT的物理地址。根据向量寻找基对应门描述符,并将中断或异常程序加载过程下图所示:
LINUX初始化中断描述符表分两步:初步初始化,以及最终初始化。初步初始化在head.S文件: http://os1a.cs.columbia.edu/lxr/source/arch/x86/kernel/head_32.S
中断描述符表初步初始化
1)声明256个门描述符的IDT表空间。
701
idt_descr:
702
.word IDT_ENTRIES*8-1 # idt contains 256 entries
703
.long idt_table
2)设置指向IDT表地址的寄存器 ldtr 。
425
lgdt early_gdt_descr
426
lidt idt_descr
427
ljmp $(__KERNEL_CS),$1f
428
1: movl $(__KERNEL_DS),%eax # reload all the segment registers
2)初始化256个门描述符。对于每个门描述符,段选择子都指向内核段,段偏移都指向igore_int函数,该函数只打印一句话:“哥们,别急,俺还没被真正初始化勒!~”。
490
/*
491
* setup_idt
492
*
493
* sets up a idt with 256 entries pointing to
494
* ignore_int, interrupt gates. It doesn't actually load
495
* idt - that can be done only after paging has been enabled
496
* and the kernel moved to PAGE_OFFSET. Interrupts
497
* are enabled elsewhere, when we can be relatively
498
* sure everything is ok.
499
*
500
* Warning: %esi is live across this function.
501
*/
502
setup_idt:
503
lea ignore_int,%edx
504
movl $(__KERNEL_CS << 16),%eax
505
movw %dx,%ax /* selector = 0x0010 = cs */
506
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
507
508
lea idt_table,%edi
509
mov $256,%ecx
510
rp_sidt:
511
movl %eax,(%edi)
512
movl %edx,4(%edi)
513
addl $8,%edi
514
dec %ecx
515
jne rp_sidt
中断描述符表最终初始化
中断描述符表最终初始化分为两部分:异常和中断,分别在函数trap_init(void)和init_IRQ(void)中实现,都由系统初始化入口函数start_kernel()所调用。对于特定异常,其处理异常函数预先已经设定好;但对于特定异步中断,由于需要捕获设备I/O中断的程序数目不定,所以得采用特定数据结构来对irq及其action进行描述。
trap_init: http://os1a.cs.columbia.edu/lxr/source/arch/x86/kernel/traps.c#830
do_IRQ:
1 )异常部分最终初始化
由于IA32规定异常所对应的固定向量,所以直接调用set_trap_gate()、set_intr_gate()、set_system_gate()、set_system_intr_gate()、set_task_gate()、set_intr_gate_ist()设置异常处理函数、初始相应异常。而这些函数,都只是_set_gate宏的封装而已。异常处理函数由宏定义DO_ERROR生成。其调用关系如下:
在trap_init(void)函数中,调用在set_intr_gate_ist()设置“stack exception”的12号中断门描述符。set_intr_gate_ist是设置中断描述符函数_set_gate的一层封装,给_set_gate传入异常处理函数stack_segment作为门描述符的偏移地址,传入 __KERNEL_CS 作为段选择子,传入GATE_INTERRUPT表示中断门描述符类型,传入DPL = 0表示只能由内核态访问、而不能由用户调用。
_set_gate函数中,其调用pack_gate()组装成一个门描述符格式,并调用write_idt_entry写入IDT表中相应描述符。stack_segment函数压入do_stack_segment函数地址并跳转到error_code汇编代码进行处理。具体代码(/arch/x86/include/asm/desc.h)如下:
830
void
__init
trap_init
(void)
831
{
857
set_intr_gate_ist
(12, &
stack_segment
,
STACKFAULT_STACK
);
898
x86_init
.
irqs
.
trap_init
(); /* 初始化该平台x86的特定设备 */
899
}
383
static inline void
set_intr_gate_ist
(int
n
, void *
addr
, unsigned
ist
)
384
{
385
BUG_ON
((unsigned)
n
> 0xFF);
386
_set_gate
(
n
,
GATE_INTERRUPT
,
addr
, 0,
ist
,
__KERNEL_CS
);
387
}
312
static inline void
_set_gate
(int
gate
, unsigned
type
, void *
addr
,
313
unsigned
dpl
, unsigned
ist
, unsigned
seg
)
314
{
315
gate_desc
s
;
316
pack_gate
(&
s
,
type
, (unsigned long)
addr
,
dpl
,
ist
,
seg
);
317
/*
318
* does not need to be atomic because it is only done once at
319
* setup time
320
*/
321
write_idt_entry
(
idt_table
,
gate
, &
s
);
322
}
064
static inline void
pack_gate
(
gate_desc
*
gate
, unsigned char
type
,
065
unsigned long
base
, unsigned
dpl
, unsigned
flags
,
066
unsigned short
seg
)
067
{
068
gate
->
a
= (
seg
<< 16) | (
base
& 0xffff);
069
gate
->
b
= (
base
& 0xffff0000) |
070
(((0x80 |
type
| (
dpl
<< 5)) & 0xff) << 8);
071
}
115
static inline void
native_write_idt_entry
(
gate_desc
*
idt
, int
entry
,
116
const
gate_desc
*
gate
)
117
{
118
memcpy
(&
idt
[
entry
],
gate
, sizeof(*
gate
));
119
}
然异常12号处理者stack_segment,何许人也?在/arch/x86/kernel/entry_32.S中定义如下:
1014
ENTRY(stack_segment)
1015
RING0_EC_FRAME
1016
pushl $do_stack_segment
1017
CFI_ADJUST_CFA_OFFSET 4
1018
jmp error_code
1019
CFI_ENDPROC
1020
END(stack_segment)
有的异常,例如stack_segment,系统由硬件产生错误码并压入栈中。另外一些异常,系统将不产生异常,例如overflow,为了统一中断、产生错误码的异常和不产生错误码的异常的栈内存布局,故“人为地”pushl $0。
958
ENTRY(overflow)
959
RING0_INT_FRAME
960
pushl $0
961
CFI_ADJUST_CFA_OFFSET 4
962
pushl $do_overflow
963
CFI_ADJUST_CFA_OFFSET 4
964
jmp error_code
965
CFI_ENDPROC
966
END(overflow)
在error_code中,首先保存中断寄存器上下文等,其次调用do_stack_segment进行处理,最后调用ret_from_exception进行善后工作,代码如下:
1291
error_code:
1292
/* the function address is in %gs's slot on the stack */
1293
pushl %fs
1294
CFI_ADJUST_CFA_OFFSET 4
1295
/*CFI_REL_OFFSET fs, 0*/
1296
pushl %es
1297
CFI_ADJUST_CFA_OFFSET 4
1298
/*CFI_REL_OFFSET es, 0*/
1299
pushl %ds
1300
CFI_ADJUST_CFA_OFFSET 4
1301
/*CFI_REL_OFFSET ds, 0*/
1302
pushl %eax
1303
CFI_ADJUST_CFA_OFFSET 4
1304
CFI_REL_OFFSET eax, 0
1305
pushl %ebp
1306
CFI_ADJUST_CFA_OFFSET 4
1307
CFI_REL_OFFSET ebp, 0
1308
pushl %edi
1309
CFI_ADJUST_CFA_OFFSET 4
1310
CFI_REL_OFFSET edi, 0
1311
pushl %esi
1312
CFI_ADJUST_CFA_OFFSET 4
1313
CFI_REL_OFFSET esi, 0
1314
pushl %edx
1315
CFI_ADJUST_CFA_OFFSET 4
1316
CFI_REL_OFFSET edx, 0
1317
pushl %ecx
1318
CFI_ADJUST_CFA_OFFSET 4
1319
CFI_REL_OFFSET ecx, 0
1320
pushl %ebx
1321
CFI_ADJUST_CFA_OFFSET 4
1322
CFI_REL_OFFSET ebx, 0
1323
cld
1324
movl $(__KERNEL_PERCPU), %ecx
1325
movl %ecx, %fs
1326
UNWIND_ESPFIX_STACK /* 用于处理系统栈不是32位的情况 */
1327
GS_TO_REG %ecx /* 把%gs存入%ecx中 */
1328
movl PT_GS(%esp), %edi # get the function address
1329
movl PT_ORIG_EAX(%esp), %edx # get the error code
1330
movl $-1, PT_ORIG_EAX(%esp) # no syscall to restart
1331
REG_TO_PTGS %ecx /* 把%gs的值存入栈的%gs中,即处理代码指针位置 */
1332
SET_KERNEL_GS %ecx
1333
movl $(__USER_DS), %ecx
1334
movl %ecx, %ds
1335
movl %ecx, %es
1336
TRACE_IRQS_OFF
1337
movl %esp,%eax # pt_regs pointer
1338
call *%edi
1339
jmp ret_from_exception
1340
CFI_ENDPROC
1341
END(page_fault)
在error_code中执行在1338行call *edi之前,栈的内存布局如下图。
在error_code处理“栈异常”处理时,call *edi = call do_stack_segment,do_stack_segment函数由DO_ERROR宏生成。error_code给do_stack_segment函数传入regs参数时,由ABI规范规定,eax为第一个参数 struct pt_regs regs。pt_regs与栈内存布局相同,其结构体如下:
021
struct
pt_regs
{
022
long
ebx
;
023
long
ecx
;
024
long
edx
;
025
long
esi
;
026
long
edi
;
027
long
ebp
;
028
long
eax
;
029
int
xds
;
030
int
xes
;
031
int
xfs
;
032
int
xgs
; /* 对应%gs,对于异常而言是处理器代码 */
033
long
orig_eax
; /* 对应于内存布局中的error code或vector */
034
long
eip
;
035
int
xcs
;
036
long
eflags
;
037
long
esp
;
038
int
xss
;
039
};
stack_segment函数是“ 栈异常 ”的异常处理函数,由 DO_ERROR宏 产生:
215
#ifdef CONFIG_X86_32
216
DO_ERROR
(12,
SIGBUS
, "stack segment",
stack_segment
)
217
#endif
183
#define
DO_ERROR
(
trapnr
,
signr
,
str
,
name
) \
184
dotraplinkage
void
do_
##name(struct
pt_regs
*
regs
, long
error_code
) \
185
{ \
186
if (
notify_die
(
DIE_TRAP
,
str
,
regs
,
error_code
,
trapnr
,
signr
) \
187
==
NOTIFY_STOP
) \
188
return; \
189
conditional_sti
(
regs
); \
190
do_trap
(
trapnr
,
signr
,
str
,
regs
,
error_code
,
NULL
); \
191
}
可见,do_stack_segment最后调用do_trap进行异常处理。
2 )中断描述符表中断部分最终初始化
由start_kernel()调用init_IRQ(void)进行中断最终初始化。其操作流程如下:
1) 将CPU0 IRQ0…IRQ15的vectors设置为0…15。对于CPU0的vecotr_irq而言,若其IRQ是PIC中断,则其vector早已由PIC固定,所以设置其IRQ0…IRQ15的vector为0…15。若是I/O APIC,由于其vector分配是可由OS动态设置,所以vector 0…15可能会被重新分配;
2) 本质上调用native_init_IRQ()函数对irq_desc、以及中断部分门描述符进行初始化,并针对CONFIG_4KSTACKS配置、协处理器模拟浮点运算等进行配置;
a、 调用init_ISA_irqs()初始化8259A可断控制器,并对相应中断请求线IRQ进行初始化、使其对应中断控制器irq_desc的操作函数为8259A操作接口函数;
b、 调用apic_intr_init()函数针对采取I/O APIC中断处理器的情况,对APIC中断处理器进行初始化工作;
c、 将调用set_intr_gate为系统中每根中断请求线IRQ地应的中断向量号设置了相应的中断门描述门, 其中断处理函数定义在interrupt数组中 ;
d、 在PC平台下set_irq(2, &rq2)对从8259A中断控制器的第三根IRQ请求做特殊处理;
e、 irq_ctx_init函数用于在配置CONFIG_4KSTACK的情况下配置当前CPU的中断栈相关项。LINUX内核在未配置CONFIG_4KSTACK时,共享所中断进程的内核栈,内核栈为两页,即8K。在2.6版本时,增加了CONFIG_4KSTACK选项,将栈大小从两页减小至一页,为了应对栈的减少,故中断处理程序拥有自己的中断处理程序线,为原先共享栈的一半,即4K,每个CPU拥有一个中断栈。
125
void
__init
init_IRQ
(void)
126
{
127
int
i
;
128
129
/*
130
* On cpu 0, Assign IRQ0_VECTOR..IRQ15_VECTOR's to IRQ 0..15.
131
* If these IRQ's are handled by legacy interrupt-controllers like PIC,
132
* then this configuration will likely be static after the boot. If
133
* these IRQ's are handled by more mordern controllers like IO-APIC,
134
* then this vector space can be freed and re-used dynamically as the
135
* irq's migrate etc.
136
*/
137
for (
i
= 0;
i
<
legacy_pic
->
nr_legacy_irqs
;
i
++)
138
per_cpu
(vector_irq, 0)[
IRQ0_VECTOR
+
i
] =
i
;
/* intr_init()本质上调用native_init_IRQ()初始化。用x86_init.irqs.intr_init()函数对irq_desc、以及中断部分门描述符进行初始化。x86_init是
x86_init_ops
类型,集成针对x86特定平台的各种初始化工作,包含初始化PCI总线、中断、异常等,其中irqs就是针对中断进行初始化操作。*/
140
x86_init
.
irqs
.
intr_init
();
141
}
/arch/x86/include/asm/x86_init.h
115
/**
116
* struct x86_init_ops - functions for platform specific setup
117
*
118
*/
119
struct
x86_init_ops
{
122
struct
x86_init_irqs
irqs
;
124
struct
x86_init_paging
paging
;
125
struct
x86_init_timers
timers
;
127
struct
x86_init_pci
pci
;
128
};
047
/**
048
* struct x86_init_irqs - platform specific interrupt setup
049
* @pre_vector_init: init code to run before interrupt vectors
050
* are set up.
051
* @intr_init: interrupt init code
052
* @trap_init: platform specific trap setup
053
*/
054
struct
x86_init_irqs
{
055
void (*
pre_vector_init
)(void);
056
void (*
intr_init
)(void);
057
void (*
trap_init
)(void);
058
};
/arch/x86/kernel/x86_init.c
029
/*
030
* The platform setup functions are preset with the default functions
031
* for standard PC hardware.
032
*/
033
struct
x86_init_ops
x86_init
__initdata
= {
051
.
irqs
= {
052
.
pre_vector_init
=
init_ISA_irqs
,
053
.
intr_init
=
native_init_IRQ
,
054
.
trap_init
=
x86_init_noop
,
055
},
082
};
235
void
__init
native_init_IRQ
(void)
236
{
237
int
i
;
238
239
/* Execute any quirks before the call gates are initialised: */
/* 调用init_ISA_irqs()初始化irq_desc[0…NR_IRQS] = {.status = IRQ_DISABLED,
.action = NULLL, .depth = 1}。并且对于irq0 … 15,
将其handler = &i8259A_irq_type*/
240
x86_init
.
irqs
.
pre_vector_init
();
241
242
apic_intr_init
();
243
244
/*
245
* Cover the whole vector space, no vector can escape
246
* us. (some of these will be overridden and become
247
* 'special' SMP interrupts)
248
*/
/*
调用set_intr_gate()为每根IRQ对应的中断向量号设置了相应的中断门描述符 */
249
for (
i
=
FIRST_EXTERNAL_VECTOR
;
i
<
NR_VECTORS
;
i
++) {
250
/* IA32_SYSCALL_VECTOR could be used in trap_init already. */
251
if (!
test_bit
(
i
,
used_vectors
))
252
set_intr_gate
(
i
,
interrupt
[
i
-
FIRST_EXTERNAL_VECTOR
]);
253
}
/* 系统若非I/O APIC,则IRQ3用于8259A从片的级联,故进行特殊处理 */
255
if (!
acpi_ioapic
)
256
setup_irq
(2, &
irq2
);
257
258
#ifdef CONFIG_X86_32
259
/*
260
* External FPU? Set up irq13 if so, for
261
* original braindamaged IBM FERR coupling.
262
*/
/*
在处理器集成协处理器,而该协处理器没有浮点处理单元的情况下设置针对浮点计算异常的处理函数fpu_irq,该函数用软件模拟的方法来进行浮点数运算 */
263
if (
boot_cpu_data
.
hard_math
&& !
cpu_has_fpu
)
264
setup_irq
(
FPU_IRQ
, &
fpu_irq
);
/* 在内核选中CONFIG_4KSTACKS时,为系统中每一CPU设置中断、异常处理函数所需的内核态栈;在没有选中该选项的情况下,irq_ctx_init是个空语句 */
266
irq_ctx_init
(
smp_processor_id
());
267
#endif
268
}
101
void
__init
init_ISA_irqs
(void)
102
{
103
int
i
;
/*
若存在LOCAL_APIC,则对Local APIC模块进行设置 */
105
#if
defined
(CONFIG_X86_64) ||
defined
(CONFIG_X86_LOCAL_APIC)
106
init_bsp_APIC
();
107
#endif
/*
初始化中断控制器8259A */
108
legacy_pic
->
init
(0);
109
110
/*
111
* 16 old-style INTA-cycle interrupts:
112
*/
/*
系统中断老式处理方式是由两片8259A级联而成,每片可有8个IRQ,故两片最多初始化16个IRQ,但由于IRQ 2用于级联,故实际仅有15个有效IRQ。 */
113
for (
i
= 0;
i
<
legacy_pic
->
nr_legacy_irqs
;
i
++) {
114
struct
irq_desc
*
desc
=
irq_to_desc
(
i
);
115
116
desc
->
status
=
IRQ_DISABLED
;
117
desc
->
action
=
NULL
;
118
desc
->
depth
= 1;
119
120
set_irq_chip_and_handler_name
(
i
, &
i8259A_chip
,
121
handle_level_irq
, "XT");
122
}
123
}
interrupt 数组
interrupt数组符号由汇编代码定义,其源码如下:
/arch/x86/kernel/entry_32.S
819
/*
820
* Build the entry stubs and pointer table with some assembler magic.
821
* We pack 7 stubs into a single 32-byte chunk, which will fit in a
822
* single cache line on all modern x86 implementations.
823
*/
824
.section .init.rodata,"a"
825
ENTRY(interrupt)
826
.text
827
.p2align 5
828
.p2align CONFIG_X86_L1_CACHE_SHIFT
829
ENTRY(irq_entries_start)
830
RING0_INT_FRAME
831
vector=FIRST_EXTERNAL_VECTOR /* =0x20,0~31号内部中断 */
832
.rept (NR_VECTORS-FIRST_EXTERNAL_VECTOR+6)/7
833
.balign 32 /* 32字节对齐 */
834
.rept 7
835
.if vector < NR_VECTORS
836
.if vector <> FIRST_EXTERNAL_VECTOR
/* 按照CFA规则修改前一个offset,以达4字节对齐。CFA标准(Call Frame Information), help a debugger create a reliable backtrace through functions. */
837
CFI_ADJUST_CFA_OFFSET -4
838
.endif
839
1: pushl $(~vector+0x80) /* Note: always in signed byte range ,[-256 ~ -1] */
840
CFI_ADJUST_CFA_OFFSET 4 /* 按CFA规则4字节对齐 */
841
.if ((vector-FIRST_EXTERNAL_VECTOR)%7) <> 6
842
jmp 2f
843
.endif
844
.previous
845
.long 1b
846
.text
847
vector=vector+1
848
.endif
849
.endr /* end of rep 7 */
850
2: jmp common_interrupt
851
.endr /* end of rep (NR_VECTORS – FIRST_...) */
852
END(irq_entries_start)
853
854
.previous
855
END(interrupt)
ENTRY(interrupt)通过伪指令.rept、.endr,将在代码段产生(NR_VECTORS - FIRST_EXTERNAL_VECTOR)个跳转到common_interrupt的汇编代码片段,起始地址是irq_entries_start;在数据段产生一个中断数组的符号interrupt,用于记录产生代码段中每个中断向量处理的汇编代码片段地址,在C语言中将interrupt符号作为中断数组变量导入:
132
extern void (*
__initconst
interrupt
[
NR_VECTORS
-
FIRST_EXTERNAL_VECTOR
])(void);
ENTRY(interrupt)编译之后,所生成的代码段和数据段内存布局如下:
ENTRIY(interrupt)汇编代码段主要由两个rept构成,外层rept循环(NR_VECTORS-FIRST_EXTERNAL_VECTOR+6)/7次,而每次内层rept循环7次,内层循环所产生的代码以32字节对齐,内层rept循环产生的代码如上图irq_entries_start中以粗黑方框表示。
以两层rept循环生成jmp common_interrupt的目的在于:
在内循环内,前6次循环产生的代码指令为:push和short jmp,而第7次产生的代码指令为:push和long jmp。push占2字节,short jmp占2字节,long jmp占5字节,故采取此种方式内层rept循环7次产生的代码大小为:6 * (2 + 2) + 2 + 5 = 31 字节。而外层循环以32字节对齐,相比于老版本每次都为push和long jmp而言,所以利用short jmp节省了内存开销。参见: http://didat.sprg.uniroma2.it/la/docs/la12-10.pdf
每个中断门描述符在将vector压入后都跳转到common_interrupt进行处理。common_interrupt在保存中断现场之后,跳转到do_IRQ进行中断函数处理,最后调用iret_from_intr进行中断返回、恢复中断上下文。
863
common_interrupt:
864
addl $-0x80,(%esp) /* Adjust vector into the [-256,-1] range */
865
SAVE_ALL /* 宏定义,负责完成宏定义中断现场的保护工作 */
866
TRACE_IRQS_OFF
867
movl %esp,%eax
868
call do_IRQ
869
jmp ret_from_intr
870
ENDPROC(common_interrupt)
195
.macro SAVE_ALL
196
cld /* 清除系统标志寄存器EFLAGS中的方向标志位DF,使%si、%di寄存器的值在每次字符串指令操作后自动+1,使自字符串从低地址到高地址方向处理 */
197
PUSH_GS
198
pushl %fs
199
CFI_ADJUST_CFA_OFFSET 4
200
/*CFI_REL_OFFSET fs, 0;*/
201
pushl %es
202
CFI_ADJUST_CFA_OFFSET 4
203
/*CFI_REL_OFFSET es, 0;*/
204
pushl %ds
205
CFI_ADJUST_CFA_OFFSET 4
206
/*CFI_REL_OFFSET ds, 0;*/
207
pushl %eax
208
CFI_ADJUST_CFA_OFFSET 4
209
CFI_REL_OFFSET eax, 0
210
pushl %ebp
211
CFI_ADJUST_CFA_OFFSET 4
212
CFI_REL_OFFSET ebp, 0
213
pushl %edi
214
CFI_ADJUST_CFA_OFFSET 4
215
CFI_REL_OFFSET edi, 0
216
pushl %esi
217
CFI_ADJUST_CFA_OFFSET 4
218
CFI_REL_OFFSET esi, 0
219
pushl %edx
220
CFI_ADJUST_CFA_OFFSET 4
221
CFI_REL_OFFSET edx, 0
222
pushl %ecx
223
CFI_ADJUST_CFA_OFFSET 4
224
CFI_REL_OFFSET ecx, 0
225
pushl %ebx
226
CFI_ADJUST_CFA_OFFSET 4
227
CFI_REL_OFFSET ebx, 0
228
movl $(__USER_DS), %edx
229
movl %edx, %ds
230
movl %edx, %es
231
movl $(__KERNEL_PERCPU), %edx
232
movl %edx, %fs
233
SET_KERNEL_GS %edx
234
.endm
common_interrupt在调用 do_IRQ 之前中断栈内存布局如下:

