linux异常处理体系结构

1、异常的作用

异常,就是可以打断CPU正在运行流程的一些事情,比如外部中断、未定义指令、试图修改只读数据、执行swi指令(中断指令)等。当这些事情发生时,CPU暂停当前的程序,先处理异常事件,然后再继续执行被中断的程序。

  • 未定义指令异常: CPU在执行一些未定义的机器指令时,触发“未定义指令异常”,操作系统可以利用这个特点使用一些自定义指令。
  • 数据访问终止异常: 将一块数据设为只读的,提供给多个进程共用, 这样可以节省内存。当某个进程试图修改其中的数据时,将触发"数据访问终止异常", 在异常处理函数中将这块数据复制出一份可写的副本,提供给这个进程使用。
  • 当用户程序试图读写的数据或执行的指令不在内存中时,也会出发一个“数据访问中止异常”或“指令预取中止异常”,在异常处理函数中将这些数据或指令读入内存(内存不足时还可以将不用的数据、指令换出内存),然后重新执行被中断的程序。这样可以节省内存, 还使得操作系统可以运行这类程序:它们使用的内存远大于实际的物理内存。
  • 当程序使用不对齐的地址访问时,也会触发"数据访问终止异常", 在异常处理程序中先使用多个对齐的地址读出数据。对于读操作,从中选取数据组合好后返回给被中断的程序;对于写操作,修改其中的部分数据后再写入内存。这使得程序(特别是应用程序)不用考虑地址的对齐的问题。
  • 用户程序可以 "swi" 指令触发 "swi异常" ,操作系统在swi异常处理函数中实现各种系统调用。

2、arm9的异常向量表

异常类型 处理器模式 异常向量 高地址向量
复位异常 (reset) 特权模式 0x00000000 0xFFFF0000
未定义指令异常(undefined interrupt) 未定义指令终止模式 0x00000004 0xFFFF0004
软件中断异常(software abort) 特权模式 0x00000008 0xFFFF0008
预取中止异常(prefetch) 数据访问终止模式 0x0000000C 0xFFFF000C
数据中止异常(data abort) 数据访问终止模式 0x00000010 0xFFFF0010
外部中断请求(IRQ) 外部中断模数 0x00000018 0xFFFF0018
快速中断请求(FIQ) 快速中断模式 0x0000001C 0xFFFF001C

3、linux内核对异常的设置

内核在start_kernel()函数(源码在init/main.c中)调用了setup_arch(&command_line)->early_trap_init()函数和init_IRQ()函数设置了异常(说明:在之前的版本是在trap_init()init_IRQ()设置)。

1)early_trap_init()函数分析(\arch\arm\kernel\traps.c):

early_trap_init 函数被用来设置各种异常向量,包括中断向量。ARM架构的CPU的异常向量基地址可以是0x00000000,也可以是0xffff0000,Linux 内核使用0xffff0000,early_trap_init 函数将异常向量复制0xffff0000处; 部分代码如下:

void __init early_trap_init(void)
{
    unsigned long vectors = CONFIG_VECTORS_BASE;    // CONFIG_VECTORS_BASE 这个宏是一个内核配置项.在 .config 里面。  CONFIG_VECTORS_BASE  = 0xffff0000
    extern char __stubs_start[], __stubs_end[];
    extern char __vectors_start[], __vectors_end[];
    ...
    /*vectors  等于 0xffff0000;  __vectors_start  和  __vectors_end 之间的代码就是异常向量*/
    memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
    /* __stubs_start和__stubs_end 之间的代码从异常向量跳转去执行跟复杂的代码 */
    memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
    ...
}

__vectors_start和__vectors_end之间的代码就是异常向量! 异常向量的代码只是一些跳转指令。发生异常时,CPU自动执行这些指令,跳转去执行跟复杂的代码;比如保存被中断程序的执行环境,调用异常处理函数,恢复被中断程序的执行环境并重新运行。

        .....
        .equ	stubs_offset, __vectors_start + 0x200 - __stubs_start        /*  */
        .....
	.globl	__vectors_start
__vectors_start:
	swi	SYS_ERROR0                       /* 复位时,CPU将执行这条指令 */
	b	vector_und + stubs_offset      /* 未定义指令 */
	ldr	pc, .LCvswi + stubs_offset      /* swi异常 */
	b	vector_pabt + stubs_offset     /* 指令预取中止 */
	b	vector_dabt + stubs_offset     /* 数据访问中止 */
	b	vector_addrexcptn + stubs_offset    /* 没有用到 */
	b	vector_irq + stubs_offset        /* irq异常 */
	b	vector_fiq + stubs_offset       /* fiq异常 */
       /* 上面这些表示发生异常时,要跳转去执行的代码。 */

	.globl	__vectors_end
__vectors_end:
    
    .....

vector_und 为例。当发生异常时,跳转到异常向量,执行 b vector_und + stubs_offset,然后跳转到下面的代码:

        .....
vector_stub	und, UND_MODE         /* "vector_stub" 是一个宏,根据 "und, UND_MODE"  定义一段代码*/
         /* 下面这些代码是跳转去执行更复杂的代码 */
	.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
	.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
        .....

vector_stub 宏的功能, 计算处理完异常后的返回地址,保存一些寄存器,然后进入管理模式,最后根据异常的工作模式跳转到 上面 代码 的某个分支 。 这个宏的代码如下:

/*
 * Vector stubs.
 *
 * This code is copied to 0xffff0200 so we can use branches in the
 * vectors, rather than ldr's.  Note that this code must not
 * exceed 0x300 bytes.
 *
 * Common stub entry macro:
 *   Enter in IRQ mode, spsr = SVC/USR CPSR, lr = SVC/USR PC
 *
 * SP points to a minimal amount of processor-private memory, the address
 * of which is copied into r0 for the mode specific abort handler.
 */
	.macro	vector_stub, name, mode, correction=0       /* “.macro” 这个伪汇编 是定义一个宏 ,使用方法可以参考这个 https://www.cnblogs.com/Widesky/p/9006954.html    */
	.align	5      /* 4字节对齐 linux下交叉 编译器的对齐方式 2^5bit对齐  */
vector_\name:
	.if \correction
	sub	lr, lr, #\correction    /* 根据不同的异常,计算返回地址 */
	.endif

	@
	@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
	@ (parent CPSR)
	@
	stmia	sp, {r0, lr}		@ save r0, lr     /* 将r0,lr 压入到各自异常的堆栈中 */
	mrs	lr, spsr                                              /* 将spsr赋给lr */
	str	lr, [sp, #8]		@ save spsr    /* 将lr入栈,即spsr入栈 */

	@
	@ Prepare for SVC32 mode.  IRQs remain disabled.
	@
	mrs	r0, cpsr                         
	eor	r0, r0, #(\mode ^ SVC_MODE)
	msr	spsr_cxsf, r0                           /* 将r0的值赋给spsr_cxsf,此时的状态还是处于und模式 */

	@
	@ the branch table must immediately follow this code
	@
	and	lr, lr, #0x0f                            /* lr=lr&0x0f,lr起始就是spsr的值,它保存了进入IRQ模式前的CPU模式,其实是5位控制的,这里只用到4位,用来跳转到不同的处理函数 */
	mov	r0, sp                                   /* 将管理模式的sp的值给r0 */
	ldr	lr, [pc, lr, lsl #2]                    /* lr = *(pc+lr<<2)。如果在进入IRQ之前是用户模式即是从应用层进入的,那么lr = pc = __und_usr.否则是管理模式也就是处于内核层时发生了IRQ异常 lr = pc+12=__und_svc */
	movs	pc, lr			@ branch to handler in SVC mode          /* 将lr的值给pc,同时将spsr的值赋给cpsr,此时才是进入了管理模式 */
	.endm

__und_usr__und_svc 两个不同的分支,只是在它们的入口处(比如保存被中断程序的寄存器)稍有差别,后续的处理大体,相同,都是调用相应的C函数。未定义指令异常最会调用do_undefinstr函数来处理,跳转的代码如下。

用户模式下发生未定义指令异常

/* __und_usr  用户模式下发生未定义指令异常的处理代码 */
__und_usr:
	usr_entry

	tst	r3, #PSR_T_BIT			@ Thumb mode?
	bne	__und_usr_unknown		@ ignore FP    //跳转到__und_usr_unknown
	sub	r4, r2, #4
        ........     //下面还有一些汇编指令
__und_usr_unknown:
	mov	r0, sp               /* 将栈顶地址,作为参数传入 */
	adr	lr, ret_from_exception    /* 将返回地址写入到 r0, 处理完C函数后将返回到这里 */
	b	do_undefinstr    /*C函数入口,处理未定义指令异常*/

管理模式下发生未定义指令异常

__und_svc:
	svc_entry

	@
	@ call 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
	@
	ldr	r0, [r2, #-4]
	adr	r9, 1f
	bl	call_fpe

	mov	r0, sp				@ struct pt_regs *regs
	bl	do_undefinstr             /*C函数入口,处理未定义指令异常*/

	@
	@ IRQs off again before pulling preserved data off the stack
	@
1:	disable_irq

	@
	@ restore SPSR and restart the instruction
	@
	ldr	lr, [sp, #S_PSR]		@ Get SVC cpsr
	msr	spsr_cxsf, lr
	ldmia	sp, {r0 - pc}^			@ Restore SVC registers     /* 将之前保存到堆栈的寄存器,出栈,并且恢复相应的cpsr寄存器,然后返回断点处继续执行 */

4、小结

未定义指令异常简单的分析了一下linux 下的异常处理体系。
linux 下地异常处理 和 裸机地差不多,只不过在linux下使用了高地址
异常向量和处理异常的代码,搬移前和搬以后的地址如下图:

中断也是异常的一种,因为中断的处理一些中断的处理函数,必须由驱动开发者提供,下一章会单独分析中断管理的框架。

posted @ 2019-10-10 21:42  古澜  阅读(2352)  评论(0编辑  收藏  举报