转自:http://www.techbulo.com/1841.html

2015年11月30日 ⁄ 基础知识 ⁄ 共 6653字 ⁄ 字号    ⁄ Linux内核异常处理体系结构详解(一)已关闭评论

【首先来区分一下两个概念:中断(Interrupt)和异常(Exception)。中断属于异常的一种,就拿2440开发板来说,他有60多种中断源,例如来自DMA控制器、UART、IIC和外部中断等。2440有一个专门的中断控制器来处理这些中断,中断控制器在接收到这些中断信号之后就需要ARM920T进入IRQ或FIQ模式进行处理,这两种模式也是中断异常的仅有模式。而异常的概念要广的多,它包括复位、未定义指令、软中断、IRQ等等。还有一点知识就是,中断这种异常在响应之前到来之前是需要程序员进行什么优先级、是否要屏蔽信号之类的初始化的,而其他比如未定义指令是不用的,只要发生了就跳到异常向量入口取址执行。因此下面初始化内容中的第(2)点是针对中断这种异常的设置的】

一、初始化设置:

(1)异常向量相关的设置:start_kernel()-->setup_arch()-->early_trap_init()函数来担任这个任务。在arch/arm/kernel/traps.c文件件中定义:这个函数很有分量,值得细细分析!!!

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);
}

详细函数分析:

将异常向量表复制到vectors地址处,vectors在函数的第一句就被赋值为“CONFIG_VECTORS_BASE”,经验告诉我们它是个内核编译配置项,去内核的顶层目录里边的“.config”文件搜索就出来,果然就有“CONFIG_VECTORS_BASE=0xffff0000”这么一句话。
好,同样问题就来了,我们之前了解过的中断向量是放到0x00000000地址开始处,把中断向量放到0xffff0000 异常触发时cpu还能自动找到?答案是能!
在ARM920T的使用手册里边有涉及相关的内容:协处理控制寄存器CP15的C1寄存器的第[13]位就是用来设置异常向量的存放位置的,该位为0存放到0x0000000开始处,为1存放到0xffff0000开始处。

到这里Linux内核异常向量设置的工作就算是完成了。可是想想:设置完这些异常向量之后,异常发生了,CPU是怎么一个处理过程???接着往下分析

Linux内核处理异常主要流程

继续分析就得从异常向量表来开始入手,__vectors_start和__vectors_end在arch/arm/kernel/entry-armv.S文件中有定义。他们就是内核异常向量表的起始和结束地址。

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内核异常处理体系结构详解(二)>

Linux内核异常处理体系结构详解(一)【转】的更多相关文章

  1. linux 内核 RCU机制详解

    RCU(Read-Copy Update)是数据同步的一种方式,在当前的Linux内核中发挥着重要的作用.RCU主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用RCU机制读取数 ...

  2. linux内核调优详解

    cat > /etc/sysctl.conf << EOF net.ipv4.ip_forward = net.ipv4.conf.all.rp_filter = net.ipv4. ...

  3. Linux内核源码详解——命令篇之iostat[zz]

    本文主要分析了Linux的iostat命令的源码,iostat的主要功能见博客:性能测试进阶指南——基础篇之磁盘IO iostat源码共563行,应该算是Linux系统命令代码比较少的了.源代码中主要 ...

  4. 嵌入式Linux内核I2C子系统详解

    1.1 I2C总线知识 1.1.1  I2C总线物理拓扑结构     I2C总线在物理连接上非常简单,分别由SDA(串行数据线)和SCL(串行时钟线)及上拉电阻组成.通信原理是通过对SCL和SDA线高 ...

  5. linux 开机启动过程详解

    Linux开机执行内核后会启动init进程,该进程根据runlevel(如x)执行/etc/rcx.d/下的程序,其下的程序是符号链接,真正的程序放在/etc/init.d/下.开机启动的程序(服务等 ...

  6. Linux命令工具 top详解

    Linux命令工具 top详解 top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器.top是一个动态显示过程,即可以通过用户按键来不 ...

  7. Linux开机启动程序详解

    Linux开机启动程序详解我们假设大家已经熟悉其它操作系统的引导过程,了解硬件的自检引导步骤,就只从Linux操作系统的引导加载程序(对个人电脑而言通常是LILO)开始,介绍Linux开机引导的步骤. ...

  8. Linux开机启动程序详解[转]

    Linux开机启动程序详解 我们假设大家已经熟悉其它操作系统的引导过程,了解硬件的自检引导步骤,就只从Linux操作系统的引导加载程序(对个人电脑而言通常是LILO)开始,介绍Linux开机引导的步骤 ...

  9. linux系统设置服务开机启动3种方法,Linux开机启动程序详解

    linux系统设置服务开机启动 方法1:.利用ntsysv伪图形进行设置,利用root登陆 终端命令下输入ntsysv 回车:如下图     方法2:利用命令行chkconfig命令进行设置 简要说明 ...

随机推荐

  1. poj1002-487-3279(字符串处理)

    一,题意: 中文题,不解释!二,思路: 1,处理输入的电话号码 2,排序num[]数组 3,输出三,步骤: 1,消除 -.Q.Z 三种字符,将一个电话号码转化为一个整数存如num[]数组 如:num[ ...

  2. js阻止form表单重复提交

    防止表单重复提交的方法总体来说有两种,一种是在js中阻止重复提交:另一种是在后台利用token令牌实现,大致思路是生成一个随机码放到session和form表单的隐藏输入框中,提交表单时两者对比,表单 ...

  3. 虚拟机下linux安装mysql,apache和php

    由于腿伤了,卧床在家折腾下linux,尝试用虚拟机装mysql,apche和php.中间各种波折,装了好几天,觉得有些经验还是要记录下来,让自己别忘了:) 按照下面这篇文章的方法,基本可以顺利安装成功 ...

  4. python集合(set)操作

    python的set和其他语言类似, 是一个无序不重复元素集, 基本功能包括关系测试和消除重复元素. 集合对象还支持union(联合), intersection(交), difference(差)和 ...

  5. 现在就使用HTML5的十大原因

    你难道还没有考虑使用HTML5? 当然我猜想你可能有自己的原因: 它现在还没有被广泛的支持,在IE中不好使,或者你就是喜欢写比较严格的XHTML代码. HTML5是Web开发世界的一次重大的改变,事实 ...

  6. IOS - 消息推送原理和实现

    一.消息推送原理: 在实现消息推送之前先提及几个于推送相关概念,如下图1-1: 1.Provider:就是为指定IOS设备应用程序提供Push的服务器,(如果IOS设备的应用程序是客户端的话,那么Pr ...

  7. 洛谷 P1803 凌乱的yyy Label:Water 贪心

    题目背景 快noip了,yyy很紧张! 题目描述 现在各大oj上有n个比赛,每个比赛的开始.结束的时间点是知道的. yyy认为,参加越多的比赛,noip就能考的越好(假的) 所以,他想知道他最多能参加 ...

  8. css标签选择器的优先级

    1.行内样式>嵌入式>外部样式(就近原者) 当外部样式在嵌入式的前面时就是外部样式为主 2.重复样式p{color:red}p{color:green}<p>是green< ...

  9. C# 通过SerialPort简单调用串口

    问题 最近比较经常使用串口进行发送以及传输数据,但是笔者在刚开始接触SerialPort类时,对于Write之后去Read数据的时候,由于设备上面还没有返回数据,读取到的只能是空值.然而,再进行下一次 ...

  10. 【Thinking in Java-CHAPTER 1&amp;&amp;2】对象导论&amp;&amp;一切都是对象

    JAVA起源 从JDK诞生到现在已经有11年的时间了.沧海桑田一瞬间.转眼11年过去了,JDK已经发布了6个版本.在这11年里诞生了无数和Java相关的技术和标准.现在让我们进入时间隧道,重新回到19 ...