Detailed explanation of Linux kernel exception handling architecture (1) [transfer]

Reprinted from: http://www.techbulo.com/1841.html

November 30, 2015  ⁄  Basic knowledge  ⁄ Total 6653 words ⁄  Small  , medium and  large  ⁄  Comments Off on Linux Kernel Exception Handling Architecture (1)

[First of all, let's distinguish two concepts: Interrupt and Exception . Interrupt is a kind of exception. Take the 2440 development board as an example, it has more than 60 interrupt sources, such as from DMA controller, UART, IIC and external interrupts. 2440 has a special interrupt controller to handle these interrupts. After the interrupt controller receives these interrupt signals, it needs ARM920T to enter IRQ or FIQ mode for processing. These two modes are also the only modes of interrupt exception. The concept of exception is much broader, it includes reset, undefined instruction, soft interrupt, IRQ and so on. Another bit of knowledge is that before the exception such as interrupt arrives before the response, the programmer needs to perform initialization such as what priority and whether to shield the signal, while other such as undefined instructions are not used, as long as it occurs, skip to Exception vector entry is executed by address fetching. Therefore, point (2) in the following initialization content is for the setting of interrupting this exception]

1. Initialization settings:

(1) Settings related to the exception vector : start_kernel()-->setup_arch()-->early_trap_init() function to undertake this task. Defined in the arch/arm/kernel/traps.c file: This function is very important and deserves a detailed analysis! ! !

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void __init early_trap_init( void )
{
  unsigned long vectors = CONFIG_VECTORS_BASE;
  extern char __stubs_start[], __stubs_end[];
  extern char __vectors_start[], __vectors_end[];
  extern char __kuser_helper_start[], __kuser_helper_end[];
  int kuser_sz = __kuser_helper_end - __kuser_helper_start;
  
  /*
  * 看下面这段英文注释,代码就一目了然了,就是把异常向量表、
 
*和异常处理那部分代码复制到指定的地址处
  * Copy the vectors, stubs and kuser helpers (in entry-armv.S)
  * into the vector page, mapped at 0xffff0000, and ensure these
  * are visible to the instruction stream.
  */
  memcpy (( void *)vectors, __vectors_start, __vectors_end - __vectors_start);
  memcpy (( void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
  memcpy (( void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);
  
  /*
  * Copy signal return handlers into the vector page, and
  * set sigreturn to be a pointer to these.
  */
  memcpy (( void *)KERN_SIGRETURN_CODE, sigreturn_codes,
  sizeof (sigreturn_codes));
  
  flush_icache_range(vectors, vectors + PAGE_SIZE);
  modify_domain(DOMAIN_USER, DOMAIN_CLIENT);
}

Detailed function analysis:

Copy the exception vector table to the address of vectors. The vectors are assigned as "CONFIG_VECTORS_BASE" in the first sentence of the function. Experience tells us that it is a kernel compilation configuration item. Go to the ".config" file in the top-level directory of the kernel and search for it. , and sure enough there is a sentence like "CONFIG_VECTORS_BASE=0xffff0000".
Well, the same problem comes. The interrupt vector we learned before is placed at the beginning of the address 0x00000000, and the interrupt vector is placed at 0xffff0000. Can the CPU automatically find it when an exception is triggered? The answer is yes!
There are related contents in the ARM920T manual: the [13] bit of the C1 register of the co-processing control register CP15 is used to set the storage location of the exception vector. This bit is stored as 0 to the beginning of 0x0000000, and stored as 1 to the beginning of 0xffff0000.

At this point, the work of Linux kernel exception vector setting is completed. But think about it: After setting these exception vectors, an exception occurs, how does the CPU process it? ? ? Then analyze below

The main process of the Linux kernel handling exceptions

To continue the analysis, we have to start with the exception vector table. __vectors_start and __vectors_end are defined in the arch/arm/kernel/entry-armv.S file. They are the start and end addresses of the kernel exception vector table.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
........
  .globl __vectors_start
__vectors_start:
  swi SYS_ERROR0 ;arm在复位异常发生时来这里执行
  b vector_und + stubs_offset
  ldr pc, .LCvswi + stubs_offset
  b vector_pabt + stubs_offset
  b vector_dabt + stubs_offset
  b vector_addrexcptn + stubs_offset
  b vector_irq + stubs_offset
  b vector_fiq + stubs_offset
  
  .globl __vectors_end
  ........

下面以第一个调转指令“b vector_und + stubs_offset”的分析为例,发现怎么在源码里面都找不到vector_und这个东东,各种查资料之后发现特么是个汇编宏定义,先熟悉一下汇编宏定义规则。

.macro MACRO_NAME PARA1 PARA2 ......

......内容......

.endm

同样在这个文件中找到了vector_stub这个宏:

1
2
3
4
5
6
7
8
9
10
11
.macro vector_stub, name, mode, correction=0
.align 5 @将异常入口强制进行2^5字节对齐,即一个cache line大小对齐,出于性能考虑
vector_\name:
. if \correction @correction=0 所以分支无效
sub lr, lr, #\correction
.endif
.endif
...........
movs pc, lr @ branch to handler in SVC mode
ENDPROC(vector_\name)
.endm

以宏“vector_stub und, UND_MODE”为例将其展开为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
vector_und:
@
@ 此时已进入UND_MOD,lr=上一个模式被打断时的PC值,下面三条指令是保护上个模式的现场
@
stmia sp, {r0, lr} @ save r0, lr
mrs lr, spsr @ 准备保存上个模式的cpsr值,因为他被放到了UND_MODE的spsr中
str lr, [sp, #8] @ save spsr to stack
@
@ Prepare for SVC32 mode. IRQs remain disabled. 注意前面的“Prepare”,这里还不是真正切换到SVC,只是准备!!不要紧张
@
mrs r0, cpsr @ r0=0x1b (UND_MODE)
eor r0, r0, #(\mode ^ SVC_MODE) @ 逻辑异或指令
msr spsr_cxsf, r0 @ cxsf是spsr寄存器的控制域(C)、扩展域(X)、状态域(S)、标志域(F),注意这里的spsr是UND管理模式的
@
@ the branch table must immediately follow this code 下一级跳转表必须要紧跟在这一段代码之后(这一点很重要)
@
and lr, lr, #0x0f @ 执行这条指令之前:lr = 上个模式的cpsr值,现在取出其低四位--模式控制位的[4:0],关键点又来了:查看2440芯片手册可以知道,这低4位二进制值为十进制数值的 0-->User_Mode; 1-->Fiq_Mode; 2-->Irq_Mode; 3-->SVC_Mode; 7-->Abort_Mode; 11-->UND_Mode,明白了这些下面的处理就会恍然大悟,原来找到那些异常处理分支是依赖这4位的值来实现的
mov r0, sp @ 将SP值保存到R0是为了之后切换到SVC模式时将这个模式下堆栈中的信息转而保存到SVC模式下的堆栈中
ldr lr, [pc, lr, lsl #2] @ 我第一次遇到LDR的这种用法,找了一下LDR的资料发现是这个意思:将pc+lr*4的计算结果重新保存到lr中,我们知道pc是指向当前指令的下两条指令处的地址的,也就是指向了“. long __und_usr”
movs pc, lr @ branch to handler in SVC mode 前方高能!关键的地方来了!在跳转到第二级分支的同时CPU的工作模式从UND_MODE强制切换到SVC_MODE,这是由于MOVS指令在赋值的同时会将spsr的值赋给cpsr
ENDPROC(vector_und)
. long __und_usr @ 0 (USR_26 / USR_32)运行用户模式下触发未定义指令异常
. long __und_invalid @ 1 (FIQ_26 / FIQ_32)
. long __und_invalid @ 2 (IRQ_26 / IRQ_32)
. long __und_svc @ 3 (SVC_26 / SVC_32)运行用户模式下触发未定义指令异常
. long __und_invalid @ 4 其他模式下面不能发生未定义指令异常,否则都使用__und_invalid分支处理这种异常
. long __und_invalid @ 5
. long __und_invalid @ 6
. long __und_invalid @ 7
. long __und_invalid @ 8
. long __und_invalid @ 9
. long __und_invalid @ a
. long __und_invalid @ b
. long __und_invalid @ c
. long __und_invalid @ d
. long __und_invalid @ e
. long __und_invalid @ f

【附加注释:在arch\arm\include\asm\ptrace.h中有:

#define SVC_MODE 0x00000013

#define UND_MODE 0x0000001b

Linux的中断管理的设计思路都是这样的:异常事件触发,cpu自动跳到异常向量表处执行,同时也切换到对应的模式,但是随后立即有段代码强制让cpu切换到SVC管理模式进行异常处理,当然有一点值得一说,reset异常是进入用户模式的,此时的异常向量存放的是swi指令,swi指令是进入svc管理模式的(也叫内核模式)结果可想而知,也是进入管理模式。如此一来,内核管理异常就方便多了,从宏观的角度来看,cpu绝大部分时间是停留在user和svc模式的,要不就是user模式下正常工作,要不就是svc模式下异常处理,那段切换的时间完全被忽略。也就是说可以看做内核要不就是在user模式下要不就是在svc模式下被其他各种异常中断打断。

执行到“movs pc, lr”这一句,找到了branch table中的一项,现在我们继续往下分析,假设进入UND_MODE之前是User模式,那么接下来会到__und_usr分支去继续执行
__und_usr标号也是在该文件中定义,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
__und_usr:
usr_entry @搜一下发现这是一个宏定义,先猜测一下功能是:将usr模式下的寄存器、中断返回地址保存到堆栈中。可以说是接管UND_MODE下保存的信息和未保存信息
@
@ fall through to the emulation code, which returns using r9 if
@ it has emulated the instruction, or the more conventional lr
@ if we are to treat this as a real undefined instruction
@
@ r0 - instruction
@
adr r9, ret_from_exception
adr lr, __und_usr_unknown
tst r3, #PSR_T_BIT @ Thumb mode?
subeq r4, r2, #4 @ ARM instr at LR - 4
subne r4, r2, #2 @ Thumb instr at LR - 2
1: ldreqt r0, [r4]
beq call_fpe
@ Thumb instruction
#if __LINUX_ARM_ARCH__ >= 7
2: ldrht r5, [r4], #2
and r0, r5, #0xf800 @ mask bits 111x x... .... ....
cmp r0, #0xe800 @ 32bit instruction if xx != 0
blo __und_usr_unknown @blo小于跳转指令。找到真正异常处理函数入口
3: ldrht r0, [r4]
add r2, r2, #2 @ r2 is PC + 2, make it PC + 4
orr r0, r0, r5, lsl #16
#else
b __und_usr_unknown
#endif
UNWIND(.fnend)
ENDPROC(__und_usr)

usr_entry宏内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
.macro usr_entry
UNWIND(.fnstart )
UNWIND(.cantunwind )
sub sp, sp, #S_FRAME_SIZE @ 通过查找和计算S_FRAME_SIZE=4*18=72
stmib sp, {r1 - r12} @ 从开始的Usr_MODE到UND_MODE,再到现在的SVC_MODE,程序中都没有去操作通用寄存器中的R1-R12,因此可以直接将他们入栈。接下来就可以随便使用这些寄存器了。
 
ldmia r0, {r1 - r3} @ 之前已将UND_MODE下栈顶指针保存到R0,出栈后r1=Usr_r0,r2=Usr_lr,r3=Usr_cpsr
add r0, sp, #S_PC @ here for interlock avoidance 从这往下一小部分代码尚未消化
mov r4, #-1
str r1, [sp] @ save the "real" r0 copied
@ from the exception stack
 
@
@ We are now ready to fill in the remaining blanks on the stack:
@
@ r2 - lr_<exception>, already fixed up for correct return /restart
@ r3 - spsr_<exception>
@ r4 - orig_r0 (see pt_regs definition in ptrace.h)
@
@ Also, separately save sp_usr and lr_usr
@
stmia r0, {r2 - r4}
stmdb r0, {sp, lr}^
 
@
@ Enable the alignment trap while in kernel mode
@
alignment_trap r0
 
@
@ Clear FP to mark the first stack frame
@
zero_fp
.endm

__und_usr_unknown也是在这个文件中定义:

1
2
3
4
5
6
__und_usr_unknown:
enable_irq
mov r0, sp
adr lr, ret_from_exception @ 这里就是异常中断的返回,先将返回前处理的处理函数的地址给lr寄存器,下面调用完C函数之后直接就可以返回
b do_undefinstr @ 最终调用C函数进行复杂的处理 在arch/arm/kernel/traps.c中
ENDPROC(__und_usr_unknown)

小结一下Linux异常处理流程:

异常发生前工作状态,到异常发生,去异常向量表找到入口地址,(这算异常发生之后跳转到第一个处理分支),进入异常模式,保护部分现场,强制进入SVC管理模式,根据异常发生前的工作模式找到异常处理的第二级分支,在该模式下面接过异常模式堆栈中的信息,接着保存异常发生时异常模式还未保存的信息,准备好处理完毕返回处理程序的地址,调用异常处理函数。

(2)中断相关初始化:init_IRQ()函数来完成,他直接由srart_kernel()函数来调用。定义于arch/arm/kernel/irq.c,

这一部分的分析见下一篇文章。<linux内核异常处理体系结构详解(二)>

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324612999&siteId=291194637