1.如何处理处理器内部异常
1.1.论述
1.1.1.异常分类
1.错误
异常被处理后,再次执行触发异常指令
2.陷阱
异常被处理后,执行触发异步指令之后指令
3.终止
异常被处理后,终止运行
1.1.2.IDT向量号
向量号0~20目前已经固定分配给异常。向量号2除外。
向量号21~31目前属于Intel保留区间。无法使用。
向量号32~255属于用户自定义区间,可供处理外部中断。
1.1.3.异常表
向量号 | 助记符 | 描述 | 异常类型 | 错误码 | 触发源 |
0 | #DE | 除法错误 | 错误 | No | DIV或IDIV指令 |
1 | #DB | 调试异常 | 错误/陷阱 | No | 仅供Intel处理器使用 |
3 | #BP | 断点异常 | 陷阱 | No | INT 3指令 |
4 | #OF | 溢出异常 | 陷阱 | No | INTO指令 |
5 | #BR | 越界异常 | 错误 | No | BOUND指令 |
6 | #UD | 无效/未定义的机器码 | 错误 | No | UD2指令或保留的机器码 |
7 | #NM | 设备异常(FPU不存在) | 错误 | No | 浮点指令WAIT/FWAIT指令 |
8 | #DF | 双重错误 | 终止 | Yes(Zero) | 任何异常,NMI中断或INTR中断 |
9 | 协处理器段越界(保留) | 错误 | No | 浮点指令 | |
10 | #TS | 无效的TSS段 | 错误 | Yes | 访问TSS段或任务切换 |
11 | #NP | 段不存在 | 错误 | Yes | 加载段寄存器或访问系统段 |
12 | #SS | SS段错误 | 错误 | Yes | 栈操作或加载栈段寄存器SS |
13 | #GP | 通用保护性异常 | 错误 | Yes | 任何内存引用和保护检测 |
14 | #PF | 页错误 | 错误 | Yes | 任何内存引用 |
16 | #MF | x87 FPU错误(计算错误) | 错误 | No | x87 FPU浮点指令或WAIT/FWAIT指令 |
17 | #AC | 对齐检测 | 错误 | Yes(Zero) | 引用内存中的任何数据 |
18 | #MC | 机器检测 | 终止 | No | 如有错误码,其与CPU类型有关 |
19 | #XM | SIMD浮点异常 | 错误 | No | SSE/SSE2/SSE3浮点指令 |
20 | #VE | 虚拟化异常 | 错误 | No | 违反EPT |
1.1.4.异常处理进入流程
1.处理器捕获到异常时,根据异常向量号从IDT索引出对应的门描述符
2.由门描述符定位到处理程序的位置。
如果向量号索引到一个中断门或陷阱门,处理器会像执行CALL访问调用门一般,去执行异常处理程序。
3.在确定了处理程序的段选择子,段内偏移后。
3.1.如果IDT中索引出的门描述符中IST区域为0
3.1.1.如果异常处理程序的特权级更高,则先完成栈空间切换,再执行流程跳转。
3.1.1.1.处理器从TR找到TSS,从TSS取出对应特权级的栈段选择子和栈指针。用取出信息设置SS,ESP。(IA-32e下不会取出SS,相应的SS被设置NULL段选择子)
3.1.1.2将之前的SS,RSP寄存器值压入栈
3.1.1.3.将RFLAGS,CS,RIP寄存器值压入栈
3.1.1.4.如异常向量号会产生错误码,
3.1.1.4.1.错误码压入栈。
3.1.1.5.如向量号索引到的是中断门
3.1.1.5.1.将RFLAGS中IF标志位复位。IF被复位后,可以起到禁止外部可屏蔽中断的效果。
3.1.2.如果异常处理程序特权级与CS中特权级一致
3.1.2.1.将RFLAGS,CS,RIP寄存器值入栈
3.1.2.2.如异常向量号会产生错误码
3.1.2.2.1.错误码压入栈
3.1.2.3.如向量号索引到的是中断门
3.1.2.3.1.将RFLAGS中IF标志位复位。IF被复位后,可以起到禁止外部可屏蔽中断的效果。
3.2.如IDT中索引出的门描述符中IST区域大于0
3.2.1.处理器从TR找到TSS,从TSS取出对应特权级的栈段选择子和栈指针。用取出信息设置SS,ESP。(IA-32e下不会取出SS,相应的SS被设置NULL段选择子)
3.2.2将之前的SS,RSP寄存器值压入栈
3.2.3.将RFLAGS,CS,RIP寄存器值压入栈
3.2.4.如异常向量号会产生错误码,
3.2.4.1.错误码压入栈。
3.2.5.如向量号索引到的是中断门
3.2.5.1.将RFLAGS中IF标志位复位。IF被复位后,可以起到禁止外部可屏蔽中断的效果。
1.1.5.异常处理离开流程
1.执行IRET
IRET详细解析:
1.异常进入时,若发生了特权级切换(由此引发栈切换)
1.1.出栈得到RIP,CS,RFLAGS,SS,RSP
1.2.设置出栈的RFLAGS到RFLAGS
1.3.设置出栈的SS,RSP到SS,RSP
1.4.设置出栈的RIP,CS到RIP,CS
1.4执行后,就完成了执行流程的跳转返回。
2.异常进入时,若未发生特权级切换
2.1.出栈得到RIP,CS,RFLAGS
2.2.设置出栈的RFLAGS到RFLAGS
2.3.设置出栈的RIP,CS到RIP,CS
2.3执行后,就完成了执行流程的跳转返回。
1.2.实践
1.2.1.异常向量号0处理实践
1.依据中断向量号0得到其在IDT表中对应表项的起始线性地址lpAddr
2.按16字节64位陷阱门描述符格式构造rax,rdx寄存器值
3.用rax设置lpAddr位置低8字节内存,用rdx设置lpAddr位置高8字节内存。
// 设置IDT中对应表项完成IDT表的异常注册
set_trap_gate(0,0,divide_error);
inline void set_trap_gate(unsigned int n,unsigned char ist,void * addr)
{
_set_gate(IDT_Table + n , 0x8F , ist , addr); //P,DPL=0,TYPE=F
}
#define _set_gate(gate_selector_addr, attr, ist, code_addr) \
do \
{ unsigned long __d0,__d1; \
__asm__ __volatile__ ( "movw %%dx, %%ax \n\t" \
"andq $0x7, %%rcx \n\t" \
"addq %4, %%rcx \n\t" \
"shlq $32, %%rcx \n\t" \
"addq %%rcx, %%rax \n\t" \
"xorq %%rcx, %%rcx \n\t" \
"movl %%edx, %%ecx \n\t" \
"shrq $16, %%rcx \n\t" \
"shlq $48, %%rcx \n\t" \
"addq %%rcx, %%rax \n\t" \
"movq %%rax, %0 \n\t" \
"shrq $32, %%rdx \n\t" \
"movq %%rdx, %1 \n\t" \
:"=m"(*((unsigned long *)(gate_selector_addr))) , \
"=m"(*(1 + (unsigned long *)(gate_selector_addr))),"=&a"(__d0),"=&d"(__d1) \
:"i"(attr << 8), \
"3"((unsigned long *)(code_addr)),"2"(0x8 << 16),"c"(ist) \
:"memory" \
); \
}while(0)
值得注意的是上述设置的128位陷阱门描述符的IST区域为0。
完成上述设置后,就可以等待向量号0的异常被触发。
向量号0的异常被触发时,按异常处理进入流程执行(这部分由处理器自动完成)。异常进入流程执行完毕,将按异常的段选择子,段内偏移定位到指定位置进行实际的异常处理。
ENTRY(divide_error)
pushq $0
pushq %rax
leaq do_divide_error(%rip), %rax
xchgq %rax, (%rsp)
error_code:
pushq %rax
movq %es, %rax
pushq %rax
movq %ds, %rax
pushq %rax
xorq %rax, %rax
pushq %rbp
pushq %rdi
pushq %rsi
pushq %rdx
pushq %rcx
pushq %rbx
pushq %r8
pushq %r9
pushq %r10
pushq %r11
pushq %r12
pushq %r13
pushq %r14
pushq %r15
cld
movq ERRCODE(%rsp), %rsi
movq FUNC(%rsp), %rdx
movq $0x10, %rdi
movq %rdi, %ds
movq %rdi, %es
movq %rsp, %rdi
callq *%rdx
jmp ret_from_exception
除0异常被触发时,完成异常进入处理后,从 上述ENTRY(divide_error)处继续执行。
1.继续向栈中压入,
数值0(因为向量号0的异常本身没有错误号,为了使得有错误号的异常,无错误号的异常共用一个退出机制,所以,本身没有错误号的异常,强制压入一个数值作为伪造的错误号)
处理函数段内偏移
rax es ds rbp rdi rsi rdx rcx rbx r8 r9 r10 r11 r12 r13 r14 r15
这一过程可以称为由中断编写者人为保存执行现场
2.从栈中得到伪造的错误号设置到rsi,将处理函数段内偏移设置到rdx,用rsp设置rdi。
借助AT&T汇编寄存器传参知识,我们知道这样为即将跳转执行的处理函数设置好参数。参数1为rdi,参数2为rsi。
3.用0x10设置ds,es
4.通过call转去执行处理函数
void do_divide_error(struct pt_regs * regs,unsigned long error_code)
{
color_printk(RED,BLACK,
"do_divide_error(0),ERROR_CODE:%#018lx,"
"RSP:%#018lx,RIP:%#018lx,CPU:%#018lx\n",
error_code , regs->rsp , regs->rip , SMP_cpu_id());
while(1)
hlt();
}
上述中断处理函数结束时,通过while(1) hlt();让程序保持低功耗运行。如果上述没有while(1)处理。则处理函数结束时,默认执行ret。这样执行流程将转移到前面callq *%rdx的下一条指令。也即jmp ret_from_exception;
ret_from_exception:
ENTRY(ret_from_intr)
movq $-1, %rcx
testq softirq_status(%rip), %rcx check softirq
jnz softirq_handler
GET_CURRENT(%rbx)
movq TSK_PREEMPT(%rbx), %rcx check preempt
cmpq $0, %rcx
jne RESTORE_ALL
movq TSK_FLAGS(%rbx), %rcx try schedule
testq $2, %rcx
jnz reschedule
jmp RESTORE_ALL
RESTORE_ALL:
popq %r15;
popq %r14;
popq %r13;
popq %r12;
popq %r11;
popq %r10;
popq %r9;
popq %r8;
popq %rbx;
popq %rcx;
popq %rdx;
popq %rsi;
popq %rdi;
popq %rbp;
popq %rax;
movq %rax, %ds;
popq %rax;
movq %rax, %es;
popq %rax;
addq $0x10, %rsp;
iretq;
上述处理代码中,在异常处理返回位置添加了软中断机制支持,preempt机制支持,进程调度机制支持。这三个机制不在这里展开讨论。
RESTORE_ALL:处执行的逻辑时,依次出栈以便恢复之前保持的执行现场。
它们是r15 r14 r13 r12 r11 r10 r9 r8 rbx rcx rdx rsi rdi rbp ds es rax,
然后再出栈以便移除之前压入的异常处理函数段内偏移,异常错误码。
最后执行iretq。iretq将执行与异常进入处理完全相反的出栈与恢复操作。
这样,iretq执行完毕后,由于向量号0的性质是错误,所以,我们将再次执行触发除0错误的那条指令。且在执行这条指令的时候,这些寄存器r15 r14 r13 r12 r11 r10 r9 r8 rbx rcx rdx rsi rdi rbp ds es rax,及ss,rsp的值全部都和之前中断发生时候维持一致。
这样我们就完整的讲述了实践上为一个异常向量号设置中断处理,中断发生时的具体处理过程,中断处理后返回的具体过程。
1.2.2.异常向量号13处理实践
1.依据中断向量号13得到其在IDT表中对应表项的起始线性地址lpAddr
2.按16字节64位陷阱门描述符格式构造rax,rdx寄存器值
3.用rax设置lpAddr位置低8字节内存,用rdx设置lpAddr位置高8字节内存。
set_trap_gate(13,0,general_protection);
// 后续部分参考前述
完成上述设置后,就可以等待向量号13的异常被触发。
向量号13的异常被触发时,按异常处理进入流程执行(这部分由处理器自动完成)。异常进入流程执行完毕,将按异常的段选择子,段内偏移定位到指定位置进行实际的异常处理。
ENTRY(general_protection)
pushq %rax
leaq do_general_protection(%rip), %rax
xchgq %rax, (%rsp)
jmp error_code
error_code:
pushq %rax
movq %es, %rax
pushq %rax
movq %ds, %rax
pushq %rax
xorq %rax, %rax
pushq %rbp
pushq %rdi
pushq %rsi
pushq %rdx
pushq %rcx
pushq %rbx
pushq %r8
pushq %r9
pushq %r10
pushq %r11
pushq %r12
pushq %r13
pushq %r14
pushq %r15
cld
movq ERRCODE(%rsp), %rsi
movq FUNC(%rsp), %rdx
movq $0x10, %rdi
movq %rdi, %ds
movq %rdi, %es
movq %rsp, %rdi
callq *%rdx
jmp ret_from_exception
向量号13的异常被触发时,完成异常进入处理后,从 上述ENTRY(general_protection)处继续执行。
1.继续向栈中压入,
因为向量号13的异常本身有错误号,所以这里无需额外压入伪造的错误号
后续部分参考前述
向量号13的异常处理函数(包含错误码解析)
void do_general_protection(struct pt_regs * regs,unsigned long error_code)
{
color_printk(RED,BLACK,
"do_general_protection(13),"
"ERROR_CODE:%#018lx,RSP:%#018lx,RIP:%#018lx,CPU:%#018lx\n",
error_code , regs->rsp , regs->rip , SMP_cpu_id());
if(error_code & 0x01)
color_printk(RED,BLACK,"The exception occurred during delivery of an event external to the program,such as an interrupt or an earlier exception.\n");
if(error_code & 0x02)
color_printk(RED,BLACK,"Refers to a gate descriptor in the IDT;\n");
else
color_printk(RED,BLACK,"Refers to a descriptor in the GDT or the current LDT;\n");
if((error_code & 0x02) == 0)
if(error_code & 0x04)
color_printk(RED,BLACK,"Refers to a segment or gate descriptor in the LDT;\n");
else
color_printk(RED,BLACK,"Refers to a descriptor in the current GDT;\n");
color_printk(RED,BLACK,"Segment Selector Index:%#010x\n",error_code & 0xfff8);
while(1)
hlt();
}
2.如何处理中断
2.1.中断
2.1.1.论述
中断和异常本质上是一类。但是异常是处理器执行指令时,引发的,处理器自动检测到异常,并执行异常进入流程,然后跳转到异常处理,异常处理会先保存环境,再跳转到处理函数进行处理,处理函数执行结束,会恢复环境,并执行异常退出流程(IRET)。
中断和异常的差别是,中断的产生是外部硬件设备产生的。外部硬件设备产生了中断信号,因为计算机系统可能同时连接很多外部设备,那么为了对来自各个外部设备的中断信号进行管理,所以,外部硬件设备产生的中断信号,一般先汇集到控制器。再从控制器传递到处理器。
中断和异常的另一个差别,对于异常,每个向量号对应那个异常都是固定分配好的。但是对于中断,每个可分配向量号对应那个外部中断是可以编程配置的,而非固定的。
处理器收到中断信号后,可以从中断信号取得中断向量号信息。
处理器自身的后续动作就会类似收到异常后的动作,即执行中断进入流程(和异常进入流程一致),然后跳转到中断处理,中断处理会先保存环境,再跳转到处理函数进行处理,处理函数执行结束,会恢复环境,并执行中断退出流程(IRET)。
2.1.2.中断向量表
向量号 | 助记符 | 异常/中断描述 | 异常/中断类型 | 错误码 | 触发源 |
2 | NMI中断 | 中断 | No | 不可屏蔽中断 | |
32~255 | 用户自定义中断 | 中断 | 外部中断或执行INT n指令 |
2.1.2.实践
无论采用何种中断控制器,中断信号初始由何种硬件设备产生,最终达到处理器的中断信号,处理器所需的仅仅是两个信息:
1.来的一个中断需要被处理
2.来的中断的中断向量号
所以,处理器层面针对中断仅仅像对待异常一样:
1.在IDT表中为特定向量号的中断进行注册。
2.中断处理代码里,做类似异常处理代码的保存执行环境,跳转到处理函数,恢复执行环境,中断返回(IRET)。即可。
这里以中断向量号2,32为例来说明处理器层面处理特定中断向量号的具体实践步骤。
2.1.3.中断向量号2处理实践
1.依据中断向量号2得到其在IDT表中对应表项的起始线性地址lpAddr
2.按16字节64位中断门描述符格式构造rax,rdx寄存器值
3.用rax设置lpAddr位置低8字节内存,用rdx设置lpAddr位置高8字节内存。
set_intr_gate(2, 0, nmi);
inline void set_intr_gate(unsigned int n,unsigned char ist,void * addr)
{
_set_gate(IDT_Table + n , 0x8E , ist , addr); //P,DPL=0,TYPE=E
}
#define _set_gate(gate_selector_addr, attr, ist, code_addr) \
do \
{ unsigned long __d0,__d1; \
__asm__ __volatile__ ( "movw %%dx, %%ax \n\t" \
"andq $0x7, %%rcx \n\t" \
"addq %4, %%rcx \n\t" \
"shlq $32, %%rcx \n\t" \
"addq %%rcx, %%rax \n\t" \
"xorq %%rcx, %%rcx \n\t" \
"movl %%edx, %%ecx \n\t" \
"shrq $16, %%rcx \n\t" \
"shlq $48, %%rcx \n\t" \
"addq %%rcx, %%rax \n\t" \
"movq %%rax, %0 \n\t" \
"shrq $32, %%rdx \n\t" \
"movq %%rdx, %1 \n\t" \
:"=m"(*((unsigned long *)(gate_selector_addr))) , \
"=m"(*(1 + (unsigned long *)(gate_selector_addr))),"=&a"(__d0),"=&d"(__d1) \
:"i"(attr << 8), \
"3"((unsigned long *)(code_addr)),"2"(0x8 << 16),"c"(ist) \
:"memory" \
); \
}while(0)
值得注意的是上述设置的128位中断门描述符的IST区域为0。
完成上述设置后,就可以等待向量号2的不可屏蔽外部中断被触发。
向量号2的中断被触发时,按中断处理进入流程执行(这部分由处理器自动完成)。中断进入流程执行完毕,将按中断的段选择子,段内偏移定位到指定位置进行实际的中断处理。
ENTRY(nmi)
pushq %rax
cld;
pushq %rax;
pushq %rax
movq %es, %rax
pushq %rax
movq %ds, %rax
pushq %rax
xorq %rax, %rax
pushq %rbp;
pushq %rdi;
pushq %rsi;
pushq %rdx;
pushq %rcx;
pushq %rbx;
pushq %r8;
pushq %r9;
pushq %r10;
pushq %r11;
pushq %r12;
pushq %r13;
pushq %r14;
pushq %r15;
movq $0x10, %rdx;
movq %rdx, %ds;
movq %rdx, %es;
movq $0, %rsi
movq %rsp, %rdi
callq do_nmi
jmp RESTORE_ALL
nmi中断被触发时,完成中断进入处理后,从 上述ENTRY(nmi)处继续执行。
1.继续向栈中压入,
数值rax(因为nmi本身没有错误号,为了使得中断和异常共用一个退出机制,所以,本身没有错误号的中断,强制压入一个数值作为伪造的错误号)
数值rax(因为所有外部中断共用一个处理函数,所以,这里对于异常来说是存放各自处理函数段内偏移的地方,对外部中断随便指定一个数值即可。这是为了使得中断和异常共用一个退出机制,所以,本身没有各自独立处理函数的中断,强制压入一个数值作为伪造的处理函数段内偏移)
rax es ds rbp rdi rsi rdx rcx rbx r8 r9 r10 r11 r12 r13 r14 r15
这一过程可以称为由中断编写者人为保存执行现场
2.用伪造的错误号(数值0)设置到rsi,用rsp设置rdi。
借助AT&T汇编寄存器传参知识,我们知道这样为即将跳转执行的处理函数设置好参数。参数1为rdi,参数2为rsi。
3.用0x10设置ds,es
4.通过callq转去执行处理函数do_nmi。(这里处理函数是直接指定的)
void do_nmi(struct pt_regs * regs,unsigned long error_code)
{
color_printk(RED,BLACK,"do_nmi(2),ERROR_CODE:%#018lx,RSP:%#018lx,RIP:%#018lx,CPU:%#018lx\n",error_code , regs->rsp , regs->rip , SMP_cpu_id());
while(1)
hlt();
}
上述中断处理函数结束时,通过while(1) hlt();让程序保持低功耗运行。如果上述没有while(1)处理。则处理函数结束时,默认执行ret。这样执行流程将转移到前面callq do_nmi的下一条指令。也即jmp RESTORE_ALL
RESTORE_ALL:
popq %r15;
popq %r14;
popq %r13;
popq %r12;
popq %r11;
popq %r10;
popq %r9;
popq %r8;
popq %rbx;
popq %rcx;
popq %rdx;
popq %rsi;
popq %rdi;
popq %rbp;
popq %rax;
movq %rax, %ds;
popq %rax;
movq %rax, %es;
popq %rax;
addq $0x10, %rsp;
iretq;
RESTORE_ALL:处执行的逻辑时,依次出栈以便恢复之前保持的执行现场。
它们是r15 r14 r13 r12 r11 r10 r9 r8 rbx rcx rdx rsi rdi rbp ds es rax,
然后再出栈以便移除之前压入的中断处理函数段内偏移,中断错误码。
最后执行iretq。iretq将执行与中断进入处理完全相反的出栈与恢复操作。
这样,iretq执行完毕后,由于向量号2的性质是中断,所以,我们继续执行后续指令。在执行后续指令的时候,这些寄存器r15 r14 r13 r12 r11 r10 r9 r8 rbx rcx rdx rsi rdi rbp ds es rax,及ss,rsp的值全部都和之前中断发生时候维持一致。
这样我们就完整的讲述了实践上为一个中断向量号设置中断处理,中断发生时的具体处理过程,中断处理后返回的具体过程。
2.1.4.中断向量号32处理实践
1.依据中断向量号32得到其在IDT表中对应表项的起始线性地址lpAddr
2.按16字节64位中断门描述符格式构造rax,rdx寄存器值
3.用rax设置lpAddr位置低8字节内存,用rdx设置lpAddr位置高8字节内存。
set_intr_gate(32 , 0 , interrupt[0]);
// 后续部分参考前述
完成上述设置后,就可以等待向量号32的中断被触发。
向量号32的中断被触发时,按中断处理进入流程执行(这部分由处理器自动完成)。中断进入流程执行完毕,将按中断描述符的段选择子,段内偏移定位到指定位置进行实际的中断处理。
#define SAVE_ALL \
"cld; \n\t" \
"pushq %rax; \n\t" \
"pushq %rax; \n\t" \
"movq %es, %rax; \n\t" \
"pushq %rax; \n\t" \
"movq %ds, %rax; \n\t" \
"pushq %rax; \n\t" \
"xorq %rax, %rax; \n\t" \
"pushq %rbp; \n\t" \
"pushq %rdi; \n\t" \
"pushq %rsi; \n\t" \
"pushq %rdx; \n\t" \
"pushq %rcx; \n\t" \
"pushq %rbx; \n\t" \
"pushq %r8; \n\t" \
"pushq %r9; \n\t" \
"pushq %r10; \n\t" \
"pushq %r11; \n\t" \
"pushq %r12; \n\t" \
"pushq %r13; \n\t" \
"pushq %r14; \n\t" \
"pushq %r15; \n\t" \
"movq $0x10, %rdx; \n\t" \
"movq %rdx, %ds; \n\t" \
"movq %rdx, %es; \n\t"
#define IRQ_NAME2(nr) nr##_interrupt(void)
#define IRQ_NAME(nr) IRQ_NAME2(IRQ##nr)
#define Build_IRQ(nr) \
void IRQ_NAME(nr); \
__asm__ ( SYMBOL_NAME_STR(IRQ)#nr"_interrupt: \n\t" \
"pushq $0x00 \n\t" \
SAVE_ALL \
"movq %rsp, %rdi \n\t" \
"leaq ret_from_intr(%rip), %rax \n\t" \
"pushq %rax \n\t" \
"movq $"#nr", %rsi \n\t" \
"jmp do_IRQ \n\t");
Build_IRQ(0x20)
void (* interrupt[24])(void)=
{
IRQ0x20_interrupt,
...
};
上述代码实际上是定义了一个名字为IRQ0x20_interrupt的中断处理入口点。这里参考前面的set_intr_gate,IRQ0x20_interrupt是向量号32的外部中断处理的入口点。
向量号32的外部中断被处理器检测到时,完成中断进入处理后,从IRQ0x20_interrupt处继续执行。
1.继续向栈中压入,
数值0(因为向量号32的中断没有错误号,为了使得中断和异常共用一个退出机制,所以,本身没有错误号的中断,强制压入一个数值作为伪造的错误号)
数值rax(因为所有外部中断共用一个处理函数,所以,这里对于异常来说是存放各自处理函数段内偏移的地方,对外部中断随便指定一个数值即可。这是为了使得中断和异常共用一个退出机制,所以,本身没有各自独立处理函数的中断,强制压入一个数值作为伪造的处理函数段内偏移)
rax es ds rbp rdi rsi rdx rcx rbx r8 r9 r10 r11 r12 r13 r14 r15
这一过程可以称为由中断编写者人为保存执行现场
2.用0x20设置到rsi,用rsp设置rdi。
借助AT&T汇编寄存器传参知识,我们知道这样为即将跳转执行的处理函数设置好参数。参数1为rdi,参数2为rsi。
3.用0x10设置ds,es
4.将ret_from_intr的地址入栈存储
4.通过jmp转去执行处理函数do_IRQ。(这里处理函数是直接指定的)
void do_IRQ(struct pt_regs * regs,unsigned long nr)
{
// 通用外部中断处理函数
}
由于所有可屏蔽外部中断共用这一个处理函数,所以上述称为通用外部中断处理函数。参数2传入的是中断向量号。
上述函数执行完毕,由于我们前面是通过jmp跳转到函数的,这里函数执行完毕,会执行ret。ret的行为是出栈得到一个段内偏移,然后执行jmp跳转到这个段内位置继续执行。这里将跳转到ret_from_intr。
ENTRY(ret_from_intr)
movq $-1, %rcx
testq softirq_status(%rip), %rcx check softirq
jnz softirq_handler
GET_CURRENT(%rbx)
movq TSK_PREEMPT(%rbx), %rcx check preempt
cmpq $0, %rcx
jne RESTORE_ALL
movq TSK_FLAGS(%rbx), %rcx try schedule
testq $2, %rcx
jnz reschedule
jmp RESTORE_ALL
对软中断,preempt,schedule暂且不谈。执行RESTORE_ALL。
RESTORE_ALL:处执行的逻辑时,依次出栈以便恢复之前保持的执行现场。
它们是r15 r14 r13 r12 r11 r10 r9 r8 rbx rcx rdx rsi rdi rbp ds es rax,
然后再出栈以便移除之前压入的中断处理函数段内偏移,中断错误码。
最后执行iretq。iretq将执行与中断进入处理完全相反的出栈与恢复操作。
这样,iretq执行完毕后,由于向量号32的性质是中断,所以,我们继续执行后续指令。在执行后续指令的时候,这些寄存器r15 r14 r13 r12 r11 r10 r9 r8 rbx rcx rdx rsi rdi rbp ds es rax,及ss,rsp的值全部都和之前中断发生时候维持一致。
2.1.5.中断控制器
向前面的实践部分,我们做的事情其实是当处理器收到向量号为2,32的中断信号时,如何处理的问题。
但是向量号为2或32的中断代表的具体含义是什么,这个中断的触发和投递方式是怎样的?
这些问题的解答依赖于中断控制器和中断源。
我们常用的中断控制器,要么是8259A,要么是APIC。
8259A是单核时代的主流选择。
APIC是多核时代的主流选择。
2.2.基于8259A处理外部中断
2.2.1.知识背景
中断控制器严格来说,是独立于处理器的可编程设备。处理器编程的最终参考是处理器官方说明文档。中断控制器最终参考也应是中断控制器官方说明文档。
默认部署介绍:
8259A是单核下主流的中断控制器。可以通过引脚连接多个外部设备,并支持8259A级联。默认下,可认为8259A按如下部署。
8259A | PIN | 典型中断请求源 |
主芯片 | IR0 | timer时钟 |
IR1 | 键盘 | |
IR2 | 级联从8259A芯片 | |
IR3 | 串口2 | |
IR4 | 串口1 | |
IR5 | 并口2 | |
IR6 | 软盘驱动器 | |
IR7 | 并口1 | |
从芯片 | IR0 | CMOS RTC实时时钟 |
IR1 | 重定向到主8259A芯片的IR2引脚 | |
IR2 | 保留 | |
IR3 | 保留 | |
IR4 | PS/2鼠标 | |
IR5 | 协处理器 | |
IR6 | SATA主硬盘 | |
IR7 | SATA从硬盘 |
、
中断控制器本身是可编程的。具体如何编程,不同中断控制器以官方说明文档为准。
对8259A,可编程体现在允许程序通过io端口操作访问,8259A芯片内的ICW寄存器组,OCW寄存器组。对这两个寄存器组的设置将决定芯片的行为,对这两个寄存器组的设置也将影响芯片内部寄存器如IRR,ISP,PR,IMR等。
对8259A芯片内部寄存器介绍:
IRR:8个比特位。每个比特位置1表示对应引脚上有中断信号接入。
IMR:8个比特位。每个比特位置1表示对应引脚被屏蔽。
PR:优先级解析。
ISR:8个比特位。每个比特位置1表示对应引脚上的中断信号正在被处理器处理。
对8259A芯片工作流程介绍:
1.从IRR收到的中断请求中选取最高优先级者
2.向CPU发送一个INT信号,并等待INTA回复
处理器方面,每执行完一条指令,都会检测是否收到INT信号。
如收到,
1.暂停执行后续指令。
2.向芯片发一个INTA信号
芯片方面:
1.收到INTA后,将选取的中断请求从IRR复位。将ISR中对应位置位。
处理器方面:
1.继续再向芯片发一个INTA
芯片方面:
1.再次收到INTA后,把选取中断请求的中断向量号发送到数据总线。
如果芯片是AEOI模式,则会复位ISR中对应位。
处理器方面:
1.在数据总线接收中断向量号,收到后,进入中断处理流程。
如芯片是非AEOI模式,处理器在中断处理流程中负责向芯片发EOI。如向量号对应的是从芯片的引脚,则需向从芯片也发EOI。
为啥需要主,从芯片级联?有两个芯片,同时独立与处理器连接不行吗?
因为处理器只有一个引线-INTR可供接收外部中断信号。
编程控制介绍:
主芯片的ICW1(8位寄存器),通过0x20端口交互。一般写入0x11,意思是级联了从芯片,且ICW4有效。
主芯片的ICW2(8位寄存器),通过0x21端口交互。一般写入0x20,意思是主芯片引脚0~7的中断信号关联的向量号是0x20~0x27。
主芯片的ICW3(8位寄存器),通过0x21端口交互。一般写入0x04,意思是主芯片引脚IR2负责级联从芯片。(默认部署下)
主芯片的ICW4(8位寄存器),通过0x21端口交互。一般写入0x01,意思是采用EOI模式。该模式下处理器中断处理中需要向芯片发送EOI。
主芯片的OCW1(8位寄存器),通过端口0x21交互。每个比特位设置为1表示此比特位对应的引脚被屏蔽。
主芯片的OCW2(8位寄存器),通过端口0x20交互。用于优先级控制。采用默认行为时,不设置即可。
主芯片的OCW3(8位寄存器),通过端口0x20交互。用于信号抢占控制。采用默认行为时,不设置即可。
从芯片的ICW1(8位寄存器),通过0xA0端口交互。一般写入0x11,意思是级联了主芯片,且ICW4有效。
从芯片的ICW2(8位寄存器),通过0xA1端口交互。一般写入0x28,意思是从芯片引脚0~7的中断信号关联的向量号是0x28~0x2F。
从芯片的ICW3(8位寄存器),通过0xA1端口交互。一般写入0x02,意思是从芯片级联到主芯片IR2。
从芯片的ICW4(8位寄存器),通过0xA1端口交互。一般写入0x01,意思是采用EOI模式。该模式下处理器中断处理需向从芯片发EOI。(中断来自于从芯片引脚时)
从芯片的OCW1(8位寄存器),通过端口0xA1交互。每个比特位设置为1表示此比特位对应引脚被屏蔽。
从芯片的OCW2(8位寄存器),通过端口0xA0交互。用于优先级控制。采用默认行为时,不设置即可。
从芯片的OCW3(8位寄存器),通过端口0xA0交互。用于信号抢占控制。采用默认行为时,不设置即可。
2.2.2.实践
//8259A-master ICW1-4
io_out8(0x20,0x11);
io_out8(0x21,0x20);
io_out8(0x21,0x04);
io_out8(0x21,0x01);
//8259A-slave ICW1-4
io_out8(0xa0,0x11);
io_out8(0xa1,0x28);
io_out8(0xa1,0x02);
io_out8(0xa1,0x01);
//8259A-M/S OCW1
io_out8(0x21,0xfd);
io_out8(0xa1,0xff);
sti();
上述指令操作的含义是:
1.设置主芯片的ICW1为0x11-级联且ICW4有效
2.设置主芯片的ICW2为0x20-主芯片引脚向量号从0x20起
3.设置主芯片的ICW3为0x04-是主芯片的IR2引脚级联了从芯片
4.设置主芯片的ICW4为0x01-中断处理过程需向主芯片发EOI
5.设置从芯片的ICW1为0x11-级联且ICW4有效
6.设置从芯片的ICW2为0x28-从芯片引脚向量号从0x28起
7.设置从芯片的ICW3为0x02-从芯片级联到了主芯片的IR2引脚
8.设置从芯片的ICW4为0x01-中断处理过程在处理来自从芯片引脚的信号时,需向从芯片发EOI。
9.设置主芯片的OCW1为0xfd-主芯片只有IR1引脚传入的中断信号会被控制器处理。
10.设置从芯片的OCW1为0xff-从芯片所有引脚传入的中断信号都被屏蔽。
void do_IRQ(struct pt_regs * regs,unsigned long nr)
{
xxx
}
当主芯片的IR1引脚接收到中断信号时,处理器会处理0x21外部向量中断。我们知道0x21信号的源头是键盘。具体的外部信号源的信号如何处理,属于设备驱动的范畴。
具体到某个设备如何驱动---(设备驱动,直白一点就是设备本身是可编程,受控的。处理器通过控制设备的寄存器来控制设备的行为,驱动设备)。
某个设备到底该如何控制,控制后的行为表现是什么,需要参考设备的编程手册。设备最基础是电路,然后一般电路之后有一个设备专属的控制器,通过这个控制器发出设备的中断信号给下一级的通用中断控制器。
2.3.基于APIC处理外部中断
2.3.1.知识背景
既然已经有了8259A作为中断控制器,为何还要介绍APIC:
8259A芯片计算机系统只有一个,只能接入到一个cpu。多核时代,无法通过8259A让中断被多个cpu均可处理的效果。
作为对比,APIC拆分为I/O APIC,Local APIC。其中I/O APIC位于系统主板,一般计算机系统只有一个。但Local APIC每个cpu都分配了一个,用于为关联的cpu服务。
这样,可以通过8259A将信号转发到I/O APIC,或I/O APIC自身感应信号。再将信号投递给目标Local APIC。目标Local APIC再通知到cpu。
也可通过8259A直接连到Local APIC,Local APIC再通知cpu。不过这样,8259A只会连接到BSP cpu的Local APIC。依然只有单个cpu可处理中断。
APIC间消息通讯:
除了通过引脚收取信号,APIC设备间还可相互通讯。这些消息通过APIC设备间的总线进行流通。这里的APIC设备包括Local APIC,I/O APIC。
2.3.2.Local APIC背景知识
Local APIC属于cpu内部的可编程设备。
可编程设备通过控制内部寄存器编程硬件行为。
涉及到硬件控制的内容,类似前面介绍8259A提到的。我们要控制硬件,就要了解,硬件有哪些可访问交互的寄存器,访问交互的方法,访问交互后的效果是怎样的。
有些硬件设备还存在启用一说。因为这些硬件设备默认下是不启用的。对待这样的设备控制,先要了解启用这些设备的方法,启用后,再参考其说明手册,获知如何与其交互,交互效果的说明。
Local APIC模式使能控制:
Local APIC模式使能依赖IA_32_APIC_BASE寄存器。
该寄存器位于MSR寄存器组偏移0x1b位置(交互方法)。
含义解释:
编号[0,7]区域存储 保留
编号[8]区域存储 BSP标志
编号[9]区域存储 保留
编号[10]区域存储 支持x2APIC模式下作为模式开关
编号[11]区域存储 APIC开关
编号[12, MAXPHYADDR-1]区域存储 APIC寄存器组PhyBaseAddr(4KB对齐)
编号[MAXPHYADDR, 63]区域存储 保留
如何判断cpu是否支持x2APIC?
CPUID.01h:ECX[21]
Local APIC的可交互寄存器表,及其交互方法(访问手段--访问地址信息/MSR内偏移信息)
访问物理地址 | MSR内偏移 | 名称 | 权限 |
FEE0 0000 | |||
FEE0 0010 | |||
FEE0 0020 | 802 | Local APIC ID | R/W |
FEE0 0030 | 803 | Local APIC Ver | R |
FEE0 0040 | |||
FEE0 0050 | |||
FEE0 0060 | |||
FEE0 0070 | |||
FEE0 0080 | 808 | TPR | R/W |
FEE0 0090 | APR | R | |
FEE0 00A0 | 80A | PPR | R |
FEE0 00B0 | 80B | EOI | W |
FEE0 00C0 | RRD | R | |
FEE0 00D0 | 80D | LDR | R/W |
FEE0 00E0 | DFR | R/W | |
FEE0 00F0 | 80F | SVR | R/W |
FEE0 0100 | 810 | ISR(31:0) | R |
FEE0 0110 | 811 | ISR(63:32) | R |
FEE0 0120 | 812 | ISR(95:64) | R |
FEE0 0130 | 813 | ISR(127:96) | R |
FEE0 0140 | 814 | ISR(159:128) | R |
FEE0 0150 | 815 | ISR(191:160) | R |
FEE0 0160 | 816 | ISR(223:192) | R |
FEE0 0170 | 817 | ISR(255:224) | R |
FEE0 0180 | 818 | TMR(31:0) | R |
FEE0 0190 | 819 | TMR(63:32) | R |
FEE0 01A0 | 81A | TMR(95:64) | R |
FEE0 01B0 | 81B | TMR(127:96) | R |
FEE0 01C0 | 81C | TMR(159:128) | R |
FEE0 01D0 | 81D | TMR(191:160) | R |
FEE0 01E0 | 81E | TMR(223:192) | R |
FEE0 01F0 | 81F | TMR(255:224) | R |
FEE0 0200 | 820 | IRR(31:0) | R |
FEE0 0210 | 821 | IRR(63:32) | R |
FEE0 0220 | 822 | IRR(95:64) | R |
FEE0 0230 | 823 | IRR(127:96) | R |
FEE0 0240 | 824 | IRR(159:128) | R |
FEE0 0250 | 825 | IRR(191:160) | R |
FEE0 0260 | 826 | IRR(223:192) | R |
FEE0 0270 | 827 | IRR(255:224) | R |
FEE0 0280 | 828 | ESR | R |
FEE0 0290~02E0 | |||
FEE0 02F0 | 82F | LVT CMCI | R/W |
FEE0 0300 | 830 | ICR(31:0) | R/W |
FEE0 0310 | 831 | ICR(63:32) | R/W |
FEE0 0320 | 832 | LVT定时 | R/W |
FEE0 0330 | 833 | LVT温度 | R/W |
FEE0 0340 | 834 | LVT性能 | R/W |
FEE0 0350 | 835 | LVT LINT0 | R/W |
FEE0 0360 | 836 | LVT LINT1 | R/W |
FEE0 0370 | 837 | LVT Err | R/W |
FEE0 0380 | 838 | 初始计数(定时器) | R/W |
FEE0 0390 | 839 | 当前计数(定时器) | R |
FEE0 03A0~03D0 | |||
FEE0 03E0 | 83E | 分频配置 | R/W |
83F | SELF IPI | W |
下面对上述涉及到的可交互寄存器,做细化的介绍。
Local APIC Ver:
编号[0,7]区域存储 版本号
编号[8, 15]区域存储 保留
编号[16, 23]区域存储 LVT表项数
编号[24]区域存储 是否支持禁止广播EOI
编号[25, 31]区域存储 保留
Local APIC ID:
解释1:
编号[0, 23]区域存储 保留
编号[24, 27]区域存储 APIC ID
编号[28, 31]区域存储 保留
解释2:
编号[0, 23]区域存储 保留
编号[24, 31]区域存储 APIC ID
解释3:
编号[0, 31]区域存储 x2 APIC ID
对LVT xxx寄存器介绍前的背景介绍:
如果要很好理解每个LVT xxx寄存器的作用,前提需要理解Local APIC能接收并向处理器传递的中断信号的分类。
1.内部I/O 设备
属于这一类型的中断信号的特点是:要么I/O设备直接将中断信号发往对应cpu的LINT0/LINT1引脚,要么I/O设备将中断信号发往8259A控制器,该控制器再将中断信号发到bsp cpu的LINT0/LINT1引脚。
APIC启用下,cpu中断引脚LINT0/LINT1收到的中断信号,交给Local APIC处理后,再最终被cpu所接收和处理。
2.外部I/O 设备
属于这一类型的中断信号的特点是:I/O设备将中断信号发到I/O APIC的输入引脚。I/O APIC以中断消息投递给一个或多个cpu里面的Local APIC。APIC间消息投递走的是专用总线。
3.IPI消息
属于这一类型的中断信号的特点是:消息发送者,消息接收者均是Local APIC。APIC间消息投递头的是专用总线。
4.定时器
属于这一类型的中断信号的特点是:处理器内部定时硬件向Local APIC发送。
5.性能监控
属于这一类型的中断信号的特点是:处理器内部性能监控硬件向Local APIC发送。
6.温度
属于这一类型的中断信号的特点是:处理器内部温度监控硬件向Local APIC发送。
7.内部错误
属于这一类型的中断信号的特点是:APIC监测到收发中断消息时产生错误时产生此中断信号。搭配ESR用于记录错误信息。
LVT 定时寄存器介绍:
当收到定时器类型中断信号,设置行为响应方式,这就是LVT定时器寄存器做的事。
编号[0, 7]区域存储 IDT-NO
编号[8, 11]区域存储 保留
编号[12]区域存储 Deli-State
编号[13, 15]区域存储 保留
编号[16]区域存储 Mask
编号[17, 18]区域存储 Timer Mode
编号[19, 31]区域存储 保留
Time Mode | Name |
00 | One-Shot |
01 | Periodic |
10 | TSC-Deadline |
11 | 保留 |
LVT CMCI寄存器介绍:
编号[0, 7]区域存储 IDT-NO
编号[8, 10]区域存储 Deli-Mode
编号[12]区域存储 Deli-State
编号[13]区域存储 Pin Polarity
编号[14]区域存储 Remote IRR
编号[15]区域存储 保留
编号[16]区域存储 Mask
编号[17, 31]区域存储 保留
Remote IRR:
Local APIC收到中断请求时,置位。收到处理器的EOI时,复位。
Deli-Mode:
Value | Name | Desc |
000 | Fixed | 向量号采用IDT-NO |
010 | SMI | 采用SMI专线传递。IDT-NO区域需设0 |
100 | NMI | 采用NMI专线传递。 |
LVT LINT0:
编号[0, 7]区域存储 IDT-NO
编号[8, 10]区域存储 Deli-Mode
编号[12]区域存储 Deli-State
编号[13]区域存储 Pin Polarity
编号[14]区域存储 Remote IRR
编号[15]区域存储 Trig-Mode
编号[16]区域存储 Mask
编号[17, 31]区域存储 保留
Remote IRR:
Local APIC收到中断请求时,置位。收到处理器的EOI时,复位。
Deli-Mode:
Value | Name | Desc |
000 | Fixed | 向量号采用IDT-NO |
010 | SMI | 采用SMI专线传递。IDT-NO需为0 |
100 | NMI | 采用NMI专线传递。 |
101 | INIT | 采用INIT专线传递。 |
111 | ExtINT | 接收来自8259A信号并中转。向量号从8259A得到。 |
LINT0在Deli-Mode为Fixed下,可设置Trig-Mode:
0 Edge
1 Level
LVT LINT1:
编号[0, 7]区域存储 IDT-NO
编号[8, 10]区域存储 Deli-Mode
编号[12]区域存储 Deli-State
编号[13]区域存储 Pin Polarity
编号[14]区域存储 Remote IRR
编号[15]区域存储 保留
编号[16]区域存储 Mask
编号[17, 31]区域存储 保留
Remote IRR:
Local APIC收到中断请求时,置位。收到处理器的EOI时,复位。
Deli-Mode:
Value | Name | Desc |
000 | Fixed | 向量号采用IDT-NO |
010 | SMI | 采用SMI专线传递。IDT-NO需为0 |
100 | NMI | 采用NMI专线传递。 |
101 | INIT | 采用INIT专线传递。 |
111 | ExtINT | 接收来自8259A信号并中转。向量号从8259A得到。 |
LVT Err:
编号[0, 7]区域存储 IDT-NO
编号[8, 10]区域存储 Deli-Mode
编号[12]区域存储 Deli-State
编号[13]区域存储 Pin Polarity
编号[14]区域存储 Remote IRR
编号[15]区域存储 保留
编号[16]区域存储 Mask
编号[17, 31]区域存储 保留
Remote IRR:
Local APIC收到中断请求时,置位。收到处理器的EOI时,复位。
Deli-Mode:
Value | Name | Desc |
000 | Fixed | 向量号采用IDT-NO |
010 | SMI | 采用SMI专线传递。IDT-NO需为0 |
100 | NMI | 采用NMI专线传递。 |
LVT 性能:
编号[0, 7]区域存储 IDT-NO
编号[8, 10]区域存储 Deli-Mode
编号[12]区域存储 Deli-State
编号[13]区域存储 Pin Polarity
编号[14]区域存储 Remote IRR
编号[15]区域存储 保留
编号[16]区域存储 Mask
编号[17, 31]区域存储 保留
Remote IRR:
Local APIC收到中断请求时,置位。收到处理器的EOI时,复位。
Deli-Mode:
Value | Name | Desc |
000 | Fixed | 向量号采用IDT-NO |
010 | SMI | 采用SMI专线传递。IDT-NO需为0 |
100 | NMI | 采用NMI专线传递。 |
LVT 温度:
编号[0, 7]区域存储 IDT-NO
编号[8, 10]区域存储 Deli-Mode
编号[12]区域存储 Deli-State
编号[13]区域存储 Pin Polarity
编号[14]区域存储 Remote IRR
编号[15]区域存储 保留
编号[16]区域存储 Mask
编号[17, 31]区域存储 保留
Remote IRR:
Local APIC收到中断请求时,置位。收到处理器的EOI时,复位。
Deli-Mode:
Value | Name | Desc |
000 | Fixed | 向量号采用IDT-NO |
010 | SMI | 采用SMI专线传递。IDT-NO需为0 |
100 | NMI | 采用NMI专线传递。 |
ESR,TPR,PPR,CR8不展开解释。具体可以查询APIC用户手册。
IRR,ISR,TMR:
Local APIC的Fixed投递模式下,中断请求可存在IRR,ISR。
IRR共含256个比特位。置位表示向量号中断已被Local APIC接收,尚未处理。
ISR共含256个比特位。置位表示向量号中断正被cpu处理。
TMR共含256个比特位。置位表示向量号中断为电平触发。复位表示边沿触发。
EOI:
Fixed投递模式下,需在中断处理过程含写入EOI的代码。
Local APIC收到EOI消息后, 将复位ISR的最高位。再派送下个中断到cpu。
对于采用电平触发的中断,且关闭了禁止广播EOI。
Local APIC收到EOI后,广播到所有I/O APIC。
SVR:
编号[0, 7]区域存储 伪中断向量号
编号[8]区域存储 APIC软件使能位
编号[9]区域存储 焦点处理器检测
编号[10,11]区域存储 保留
编号[12]区域存储 禁止广播EOI开关
编号[13,31]区域存储 保留
2.3.3.I/O APIC背景知识
背景:
Local APIC每个cpu各自有一个,定位是APIC体系下,直接与cpu交互,管理中断信号。实现多cpu均可进行中断处理目标。
I/O APIC一般位于系统主板上,整个系统只有一个。它的定位是接收外部各个中断源的中断信号,按照指定方式,将中断信号投递给一个或多个Local APIC。这一就实现了多核下对中断的一套处理机制。
硬件驱动:
像之前介绍8259A,Local APIC提到的,为了实现硬件编程,控制硬件行为。我们需要知道该硬件有哪些可以交互的寄存器,这些寄存器的说明是怎样的,与这些寄存器交互的方法是怎样的。像一些硬件还有启用和非启用一说,这样的话,我们还要了解如何启用硬件。
I/O APIC的寄存器交互方法:
PhyAddr | Name | Width | 权限 |
FEC0 0000 | I/O Register Select | 8 | R/W |
FEC0 0010 | I/O Window | 32 | R/W |
FEC0 0040 | EOI Register | 32 | W |
I/O Register Select:
编号[0, 7]区域存储 I/O APIC寄存器地址
编号[8, 31]区域存储 保留
I/O Window:
编号[0, 31]区域存储 Data
EOI:
编号[0, 7]区域存储 No
编号[8, 31]区域存储 保留
采用电平触发模式的中断请求,在中断处理结束时需通过EOI寄存器像I/O APIC发EOI消息。采用No找到匹配的I/O中断定向投递寄存器,向其发EOI。
I/O中断定向投递寄存器收到EOI后,复位远程IRR。
I/O APIC可交互寄存器介绍:
Index | Name | Width | 权限 |
00 | I/O APIC ID | 32 | R/W |
01 | I/O APIC Ver | 32 | R |
02~0F | |||
10~11 | R-T-0 | 64 | R/W |
12~13 | R-T-1 | 64 | R/W |
... | |||
3E~3F | R-T-23 | 64 | R/W |
40~FF |
下面对涉及的一些寄存器做具体化介绍:
I/O APIC ID:
编号[0, 23]区域存储 保留
编号[24, 27]区域存储 I/O APIC ID
编号[28, 31]区域存储 保留
使用I/O APIC前,需向I/O APIC ID写入值,保证此值在APIC设备间唯一性。
I/O APIC Ver:
编号[0, 7]区域存储 I/O APIC Ver
编号[8, 15]区域存储 保留
编号[16, 23]区域存储 可用RTE数
编号[24, 31]区域存储 保留
可用RTE数决定了R-T-X的有效范围。
R-T-X:
编号[0, 7]区域存储 IDT-NO
编号[8, 10]区域存储 Deli-Mode
编号[11]区域存储 Tar-Mode
编号[12]区域存储 Deli-State
编号[13]区域存储 Pin Polarity
编号[14]区域存储 远程IRR
编号[15]区域存储 Trig-Mode
编号[16]区域存储 Mask
编号[17, 55]区域存储 保留
编号[56, 63]区域存储 Deli-Target
Tar-Mode为0时,[56, 59]得到目标Local APIC ID
Tar-Mode为1时,[56, 63]得到目标Local APIC ID
I/O APIC的引脚:
前面我们介绍了I/O APIC的R-T-X。要知道每个R-T-X分别服务于一个I/O APIC的引脚。R-T-X的内容决定了I/O APIC启用下,每个引脚收到中断信号后的行为表现。
Pin | I/O APIC中断源 |
0 | 8259A主芯片 |
1 | 键盘 |
2 | HPET#0,8254定时器0 |
3 | 串行接口2&4 |
4 | 串行接口1&3 |
5 | 并行接口2 |
6 | 软盘驱动器 |
7 | 并行接口1 |
8 | RTC/HPET#1 |
9 | |
10 | |
11 | HPET#2 |
12 | HPET#3,鼠标(PS/2接口) |
13 | FERR#,DMA |
14 | 主SATA接口 |
15 | 从SATA接口 |
16 | PIRQA# |
17 | PIRQB# |
18 | PIRQC# |
19 | PIRQD# |
20 | PIRQE# |
21 | PIRQF# |
22 | PIRQG# |
23 | PIRQH# |
2.3.4.实践上的选择
现在我们知道可供使用的中断控制器有8259A,Local APIC,I/O APIC。
默认情况下,8259A是使能的。Local APIC,I/O APIC是未使能的。
这样,实践上计算机系统处理外部中断有三种选择:
1.仅仅使用8259A
默认下,即是如此。
外部设备中断信号传递到8259A,8259A传递到BSP的cpu。cpu处理中断。
APIC设备全部禁用了。
2.8259A搭配BSP cpu的Local APIC。
需要启用BSP cpu的Local APIC。禁用I/O APIC(默认禁用)。
外部设备中断信号传递到8259A,8259A传递到BSP的LINT0/1。
Local APIC开启下,cpu的LINT0/1收到的中断信号,递交给Local APIC。Local APIC再递交给cpu。
3.I/O APIC搭配Local APIC实现多核均可处理中断。
需要启动每个cpu的Local APIC,启用I/O APIC。禁用8259A。
外部设备中断信号传递到I/O APIC,I/O APIC依据R-T-X配置,处理并转发给指定Local APIC(走总线传输)。Local APIC处理后递交给cpu处理。
控制器的启用和关闭:
1.8259A:
一方面,8259A本身允许编程控制来屏蔽自身所有引脚的中断信号接收。
2.IMCR交互方法:
//enable IMCR
io_out8(0x22,0x70);
io_out8(0x23,0x01);
IMCR可控制BSP cpu接收中断请求的链路。
采用0x00时,BSP cpu接收来自8259A的中断信号。
采用0x01时,BSP cpu接收来自Local APIC的中断信号。
访问ICMR寄存器的方法如上:
先向0x22端口写入0x70
再向0x23端口写入1字节作为寄存器内容。这里是0x01。
3.Local APIC:
// 通过0x1b访问MSR寄存器的IA32_APIC_BASE来开启APIC&x2PAIC
// 这样就完成了Local APIC设备的启用
__asm__ __volatile__( "movq $0x1b, %%rcx \n\t"
"rdmsr \n\t"
"bts $10, %%rax \n\t"
"bts $11, %%rax \n\t"
"wrmsr \n\t"
"movq $0x1b, %%rcx \n\t"
"rdmsr \n\t"
:"=a"(x),"=d"(y)
:
:"memory");
color_printk(WHITE,BLACK,"eax:%#010x,edx:%#010x\t",x,y);
if(x&0xc00)
color_printk(WHITE,BLACK,"xAPIC & x2APIC enabled\n");
通过0x1b访问MSR寄存器组的IA32_APIC_BASE寄存器,在支持APIC,x2APIC下,通过设置该寄存器来启用对应cpu的Local APIC设备。
上述在cpu同时支持APIC,x2APIC下,使能Local APIC,且在x2APIC模式下工作。
// x2APIC模式下,借助0x80f访问MSR寄存器组的SVR寄存器
// 并设置其SVR[8],SVR[12]这样可以在软件层面也保持APIC设备的开启。
// 且开启禁止广播EOI消息功能
__asm__ __volatile__( "movq $0x80f, %%rcx \n\t"
"rdmsr \n\t"
"bts $8, %%rax \n\t"
"bts $12, %%rax\n\t"
"wrmsr \n\t"
"movq $0x80f, %%rcx \n\t"
"rdmsr \n\t"
:"=a"(x),"=d"(y)
:
:"memory");
color_printk(WHITE,BLACK,"eax:%#010x,edx:%#010x\t",x,y);
if(x&0x100)
color_printk(WHITE,BLACK,"SVR[8] enabled\n");
if(x&0x1000)
color_printk(WHITE,BLACK,"SVR[12] enabled\n");
Local APIC设备的开启有两层。上述通过0x80f访问MSR寄存器组的SVR寄存器。并置位SVR[8]来保证软件层面Local APIC也是开启的。这样才使得Local APIC完全开启。
SVR[12]是开启禁止广播EOI消息。
4.I/O APIC:
//get RCBA address
io_out32(0xcf8,0x8000f8f0);
x = io_in32(0xcfc);
color_printk(RED,BLACK,"Get RCBA Address:%#010x\n",x);
x = x & 0xffffc000;
color_printk(RED,BLACK,"Get RCBA Address:%#010x\n",x);
//get OIC address
if(x > 0xfec00000 && x < 0xfee00000)
{
p = (unsigned int *)Phy_To_Virt(x + 0x31feUL);
}
//enable IOAPIC
x = (*p & 0xffffff00) | 0x100;
io_mfence();
*p = x;
io_mfence();
memset(interrupt_desc,0,sizeof(irq_desc_T)*NR_IRQS);
我们已经知道为了启用I/O APIC硬件设备,我们需要将OIC[8]设置为1。访问OIC的方式是通过RCBA得到芯片组配置寄存器物理基地址。将此物理地址+0x31FE得到OIC物理地址。
上述io_out(0xcf8, 0x8000f8f0),x = io_in32(0xcfc)是通过间接寻址得到RCBA寄存器内容。
从RCBA值得到芯片组配置寄存器物理基地址。
从芯片组配置寄存器物理基地址+0x31fe得到OIC寄存器物理地址,转化为线性地址。
通过线性地址访问内存取得OIC内容。将低8位设置为0,将OIC[8]设置为1。这样我们就完成了I/O APIC设备的启用。
2.3.2.实践
我们实践的是采用I/O APIC + Local APIC实现多核下中断处理。
// 访问I/O APIC设备寄存器前期准备
IOAPIC_pagetable_remap();
// I/O APIC设备驱动中与其寄存器交互的方式就是通过
// 物理地址0xfec0 0000,0xfec0 0010,0xfec0 0040来与I/O APIC设备寄存器进行交互的
// 我们访问物理区域,只能先得到此区域的线性地址,再通过线性地址访问。
// 通过线性地址能访问物理地址的前提是此线性地址对应的物理页已经完成了页表注册
void IOAPIC_pagetable_remap()
{
unsigned long * tmp;
// 取得物理地址对应的线性地址。
unsigned char * IOAPIC_addr = (unsigned char *)Phy_To_Virt(0xfec00000);
// 物理地址---这个地址就是线性地址对应物理页的基地址
ioapic_map.physical_address = 0xfec00000;
// I/O Register Select线性地址
ioapic_map.virtual_index_address = IOAPIC_addr;
// I/O Window线性地址
ioapic_map.virtual_data_address = (unsigned int *)(IOAPIC_addr + 0x10);
// EOI线性地址
ioapic_map.virtual_EOI_address = (unsigned int *)(IOAPIC_addr + 0x40);
// 对线性地址进行页表注册
Global_CR3 = Get_gdt();
tmp = Phy_To_Virt(Global_CR3 + (((unsigned long)IOAPIC_addr >> PAGE_GDT_SHIFT) & 0x1ff));
if (*tmp == 0)
{
// PDPT页表若不存在,就动态分配
unsigned long * virtual = kmalloc(PAGE_4K_SIZE,0);
// 构造PML4页表项
set_mpl4t(tmp,mk_mpl4t(Virt_To_Phy(virtual),PAGE_KERNEL_GDT));
}
// 得到PDPT页表项
tmp = Phy_To_Virt((unsigned long *)(*tmp & (~ 0xfffUL)) + (((unsigned long)IOAPIC_addr >> PAGE_1G_SHIFT) & 0x1ff));
if(*tmp == 0)
{
// PDT页表不存在,就动态分配
unsigned long * virtual = kmalloc(PAGE_4K_SIZE,0);
// 构造PDT页表项
set_pdpt(tmp,mk_pdpt(Virt_To_Phy(virtual),PAGE_KERNEL_Dir));
}
// 得到PDT页表项
tmp = Phy_To_Virt((unsigned long *)(*tmp & (~ 0xfffUL)) + (((unsigned long)IOAPIC_addr >> PAGE_2M_SHIFT) & 0x1ff));
// 设置PDT页表项
set_pdt(tmp,mk_pdt(ioapic_map.physical_address,PAGE_KERNEL_Page | PAGE_PWT | PAGE_PCD));
// 每当我们更新了页表,就需要让tlb中所有页表缓存失效。以免地址翻译出错。
flush_tlb();
}
1.由于我们需使用I/O APIC。所以,需要和I/O APIC可编程寄存器交互。交互的前期准备是为访问物理地址0xFEC0 0000,0xFEC0 0010,0xFEC0 0040准备。为了访问物理地址,我们需要找到物理地址对应的线性地址。通过线性地址进行物理内存访问,
线性地址可以进行物理内存访问的前提是,线性地址对应物理页需要经过页表注册。
上述干的就是这个事情。
// 设置cpu收到中断向量号[32, 55]时如何处理
for(i = 32;i < 56;i++)
{
set_intr_gate(i , 0 , interrupt[i - 32]);
}
无论我们采用什么样的中断控制器。最终结果都是处理器收到中断信号,获知其中断向量号,然后进行中断处理。
这部分内容在任何中断控制器下,都是需要的。
上述做的就是,设置号执行命令cpu收到向量号为32~55的中断信号后,后续如何处理。
// 屏蔽8259A
//mask 8259A
color_printk(GREEN,BLACK,"MASK 8259A\n");
io_out8(0x21,0xff);
io_out8(0xa1,0xff);
由于我们要演示的是借助I/O APIC+Local APIC实现多核下中断处理。所以,我们将8259A所有引脚均屏蔽。这样8259A本身不会输出任何中断信号。中断信号在I/O APIC开启下,将达到I/O APIC对应引脚。
//enable IMCR
io_out8(0x22,0x70);
io_out8(0x23,0x01);
由于我们采用APIC向cpu传递中断信号,所以需要设置IMCR为0x01
// Local APIC设备驱动
//init local apic
Local_APIC_init();
void Local_APIC_init()
{
unsigned int x,y;
unsigned int a,b,c,d;
get_cpuid(1,0,&a,&b,&c,&d);
color_printk(WHITE,BLACK,"CPUID\t01,eax:%#010x,ebx:%#010x,ecx:%#010x,edx:%#010x\n",
a,b,c,d);
if((1<<9) & d)
color_printk(WHITE,BLACK,"HW support APIC&xAPIC\t");
else
color_printk(WHITE,BLACK,"HW NO support APIC&xAPIC\t");
if((1<<21) & c)
color_printk(WHITE,BLACK,"HW support x2APIC\n");
else
color_printk(WHITE,BLACK,"HW NO support x2APIC\n");
__asm__ __volatile__( "movq $0x1b, %%rcx \n\t"
"rdmsr \n\t"
"bts $10, %%rax \n\t"
"bts $11, %%rax \n\t"
"wrmsr \n\t"
"movq $0x1b, %%rcx \n\t"
"rdmsr \n\t"
:"=a"(x),"=d"(y)
:
:"memory");
color_printk(WHITE,BLACK,"eax:%#010x,edx:%#010x\t",x,y);
if(x&0xc00)
color_printk(WHITE,BLACK,"xAPIC & x2APIC enabled\n");
__asm__ __volatile__( "movq $0x80f, %%rcx \n\t"
"rdmsr \n\t"
"bts $8, %%rax \n\t"
"bts $12, %%rax\n\t"
"wrmsr \n\t"
"movq $0x80f, %%rcx \n\t"
"rdmsr \n\t"
:"=a"(x),"=d"(y)
:
:"memory");
color_printk(WHITE,BLACK,"eax:%#010x,edx:%#010x\t",x,y);
if(x&0x100)
color_printk(WHITE,BLACK,"SVR[8] enabled\n");
if(x&0x1000)
color_printk(WHITE,BLACK,"SVR[12] enabled\n");
__asm__ __volatile__( "movq $0x802, %%rcx \n\t"
"rdmsr \n\t"
:"=a"(x),"=d"(y)
:
:"memory");
color_printk(WHITE,BLACK,"eax:%#010x,edx:%#010x\tx2APIC ID:%#010x\n",x,y,x);
__asm__ __volatile__( "movq $0x803, %%rcx \n\t"
"rdmsr \n\t"
:"=a"(x),"=d"(y)
:
:"memory");
color_printk(WHITE,BLACK,"local APIC Version:%#010x,Max LVT Entry:%#010x,SVR(Suppress EOI Broadcast):%#04x\t",x & 0xff,(x >> 16 & 0xff) + 1,x >> 24 & 0x1);
if((x & 0xff) < 0x10)
color_printk(WHITE,BLACK,"82489DX discrete APIC\n");
else if( ((x & 0xff) >= 0x10) && ((x & 0xff) <= 0x15) )
color_printk(WHITE,BLACK,"Integrated APIC\n");
__asm__ __volatile__( "movq $0x82f, %%rcx \n\t" //CMCI
"wrmsr \n\t"
"movq $0x832, %%rcx \n\t" //Timer
"wrmsr \n\t"
"movq $0x833, %%rcx \n\t" //Thermal Monitor
"wrmsr \n\t"
"movq $0x834, %%rcx \n\t" //Performance Counter
"wrmsr \n\t"
"movq $0x835, %%rcx \n\t" //LINT0
"wrmsr \n\t"
"movq $0x836, %%rcx \n\t" //LINT1
"wrmsr \n\t"
"movq $0x837, %%rcx \n\t" //Error
"wrmsr \n\t"
:
:"a"(0x10000),"d"(0x00)
:"memory");
color_printk(GREEN,BLACK,"Mask ALL LVT\n");
__asm__ __volatile__( "movq $0x808, %%rcx \n\t"
"rdmsr \n\t"
:"=a"(x),"=d"(y)
:
:"memory");
color_printk(GREEN,BLACK,"Set LVT TPR:%#010x\t",x);
__asm__ __volatile__( "movq $0x80a, %%rcx \n\t"
"rdmsr \n\t"
:"=a"(x),"=d"(y)
:
:"memory");
color_printk(GREEN,BLACK,"Set LVT PPR:%#010x\n",x);
}
由于我们要使用cpu的Local APIC设备,所以需要查询设备信息,进行设备驱动。
上述代码首先采用0x01主功能号查询cpuid,从结果寄存器edx[9]获知cpu是否支持apic,从结果寄存器ecx[21]获知cpu是否支持x2 apic。
接着就是通过0x1b访问MSR寄存器组的IA32_APIC_BASE寄存器,来开启APIC,x2APIC。这样就完成了cpu层面对该cpu的Local APIC设备的启用。同时告知了采用x2APIC模式工作。
再接下来,我们通过0x80f访问MSR寄存器组的SVR寄存器,来从软件层面也保持Local APIC功能的开启,且开启禁止广播EOI消息功能。
再接下来,我们通过0x802访问MSR寄存器组的Local APIC ID寄存器,由于使用了x2 APIC模式。所以,EAX存储了32比特位的x2 APIC ID。将其打印出来。
再接下来,我们通过0x803访问MSR寄存器组的Local APIC Ver寄存器。解析该寄存器得到并打印Local APIC设备是否支持禁止广播EOI,Local APIC的LVT表项数,Local APIC的版本ID。
再然后,我们通过0x82f,0x832,0x833,0x834,0x835,0x836,0x837分别去设置MSR寄存器组的LVT CMCI,LVT 定时,LVT 温度,LVT性能,LVT LINT0,LVT LINT1,LVT Error寄存器。这里,统一将这些寄存器其余比特位设置为0,比特位16设置为1。这是将这些寄存器对应的Local APIC可接收的类型信号全部屏蔽的意思。
最后是,借助0x808访问MSR寄存器组的TPR寄存器,并打印。借助0x80A访问MSR寄存器组的PPR寄存器,并打印。
这样我们就完成了cpu层面,软件层面开启该cpu的Local APIC。同时设置了禁止广播EOI。同时也对可配置的LVT系列寄存器分别配置为屏蔽对应信号源信号。
下面我们继续进行I/O APIC的设备驱动。
IOAPIC_init();
void IOAPIC_init()
{
int i ;
*ioapic_map.virtual_index_address = 0x00;
io_mfence();
*ioapic_map.virtual_data_address = 0x0f000000;
io_mfence();
color_printk(GREEN,BLACK,"Get IOAPIC ID REG:%#010x,ID:%#010x\n",
*ioapic_map.virtual_data_address,
*ioapic_map.virtual_data_address >> 24 & 0xf);
io_mfence();
*ioapic_map.virtual_index_address = 0x01;
io_mfence();
color_printk(GREEN,BLACK,"Get IOAPIC Version REG:%#010x,MAX redirection enties:%#08d\n",
*ioapic_map.virtual_data_address ,((*ioapic_map.virtual_data_address >> 16) & 0xff) + 1);
for(i = 0x10;i < 0x40;i += 2)
ioapic_rte_write(i,0x10020 + ((i - 0x10) >> 1));
color_printk(GREEN,BLACK,"I/O APIC Redirection Table Entries Set Finished.\n");
}
上述是先访问I/O APIC索引为0x00的I/O APIC ID寄存器,向其写入0x0f00 0000。将I/O APIC ID设置为0xF。
然后再读取并打印I/O APIC ID的值。
接着,访问I/O APIC索引为0x01的I/O APIC Ver寄存器。解析寄存器得到并打印I/O APIC中可用RTE数量。
再接着,访问I/O APIC索引为0x10到0x40之间的R-T-X寄存器,设置寄存器的内容。ioapic_rte_write的参数2是64个比特位组成的数值。每次设置时只设置对应R-T-X的Mask,让其处于屏蔽,然后设置IDT-No区域。将I/O APIC的引脚0分配向量号0x20,引脚1分配向量号0x21,依次加1。
ioapic_ret_write构造R-T-X寄存器内容时,先写低32比特,再写高32比特。
ioapic_ret_ret读取R-T-X寄存器内容时,先读高32比特,再读低32比特。
到这里我们获取了I/O APIC设备的一些信息,并对I/O APIC的RTE进行了设置。但是,如果要正常使用I/O APIC设备,我们还必须进行显式的设备启用。
//get RCBA address
io_out32(0xcf8,0x8000f8f0);
x = io_in32(0xcfc);
color_printk(RED,BLACK,"Get RCBA Address:%#010x\n",x);
x = x & 0xffffc000;
color_printk(RED,BLACK,"Get RCBA Address:%#010x\n",x);
//get OIC address
if(x > 0xfec00000 && x < 0xfee00000)
{
p = (unsigned int *)Phy_To_Virt(x + 0x31feUL);
}
//enable IOAPIC
x = (*p & 0xffffff00) | 0x100;
io_mfence();
*p = x;
io_mfence();
memset(interrupt_desc,0,sizeof(irq_desc_T)*NR_IRQS);
我们已经知道为了启用I/O APIC硬件设备,我们需要将OIC[8]设置为1。访问OIC的方式是通过RCBA得到芯片组配置寄存器物理基地址。将此物理地址+0x31FE得到OIC物理地址。
上述io_out(0xcf8, 0x8000f8f0),x = io_in32(0xcfc)是通过间接寻址得到RCBA寄存器内容。
从RCBA值得到芯片组配置寄存器物理基地址。
从芯片组配置寄存器物理基地址+0x31fe得到OIC寄存器物理地址,转化为线性地址。
通过线性地址访问内存取得OIC内容。将低8位设置为0,将OIC[8]设置为1。这样我们就完成了I/O APIC设备的启用。
这里的关键是
// 获得RCBA寄存器值。
io_out32(0xcf8,0x8000f8f0);
x = io_in32(0xcfc);
上述获取RCBA寄存器值得访问方法是查询主板的编程手册后获知的。