HelloOs总结之启动及中断


 

三、启动及中断

3.1 启动部分

3.1.1 boot简介

    对于大多数的嵌入式系统来说(包括计算机、平板、手机以及类似的电子设备)来说,其启动过程大都不是一个过程,尤其是带有操作系统的设备。一般来说,对于大部分高级的电子设备(带有操作系统),启动过程大都包含一个boot阶段,然后才是跳到操作系统内核。当然,不同的设备boot阶段的内容不尽相同,有的甚至把这个boot阶段又细化为两个或多个阶段,如以前的x86系统,其boot阶段就分为BIOS和MBR这两个单独的阶段(当然这是按照我的理解划分的,你也可以把x86的启动过程直接化为二个阶段)。我们这里采用的s3c2410设备,其启动过程就是普通一个boot阶段。如下图3.1所示。
在这里插入图片描述
图3.1 通用操作引导阶段示意图

    所谓的boot阶段,也就是引导阶段,其主要工作是初始化硬件设备,如内存、MMU、输入输出等等,为内核准备好环境之后,跳入内核进行执行(当然,大部分情况下需要把内核调入到内存,这个调入的过程有的是boot来完成,也有的是内核自己完成)。
    那么boot阶段的程序代码是怎么开始运行的呢?虽然没有经过完全的统计调查,但据我目前的了解,当机器上电之后,大多数CPU都会到一个固定的地方寻找第一条指令(这个固定的地方大都是由硬件厂商设计好的),如以前的32位x86架构的CPU,其上电之后CPU就会让其指令寄存器指向OxFFFF0这个入口地址,而这个地址上就是BIOS程序。相对应的,ARM架构的很多CPU其上电之后会首先从0x0地址开始执行第一条指令。我们的s3c2410就是这样。


 

3.1.2 boot代码分析

一般的boot流程大都如下图3.2所示。
在这里插入图片描述
图3.2 boot阶段流程图

    但是,对于skyeye来说,其可以直接仿真内存等地址空间,用不着设置这些硬件相关的配置,所以在这就省了。因此HelloOs的启动文件还算比较简单,如下所示:

.global _start

//引入外部符号
.extern __vector_undefined
.extern __vector_swi
.extern __vector_prefetch_abort
.extern __vector_data_abort
.extern __vector_reserved
.extern __vector_irq
.extern __vector_fiq

.extern main
.extern __bss_start__
.extern __bss_end__

//开始的位置,定义各种异常向量
_start:
	ldr pc,_vector_reset
	ldr pc,_vector_undefined
	ldr pc,_vector_swi
	ldr pc,_vector_prefetch_abort
	ldr pc,_vector_data_abort
	ldr pc,_vector_reserved
	ldr pc,_vector_irq
	ldr pc,_vector_fiq

	.align 4

//异常向量具体内容在外部定义,这里只引入其符号地址
_vector_reset:			.word  __vector_reset
_vector_undefined:		.word  __vector_undefined
_vector_swi:			.word  __vector_swi
_vector_prefetch_abort:	.word  __vector_prefetch_abort
_vector_data_abort:		.word  __vector_data_abort
_vector_reserved:		.word  __vector_reserved
_vector_irq:			.word  __vector_irq
_vector_fiq:			.word  __vector_fiq

//定义Reset异常向量
__vector_reset:
	msr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|SVC_MOD)
	ldr sp,=_SVC_STACK
	msr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|IRQ_MOD)
	ldr sp,=_IRQ_STACK
	msr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|FIQ_MOD)
	ldr sp,=_FIQ_STACK
	msr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|ABT_MOD)
	ldr sp,=_ABT_STACK
	msr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|UND_MOD)
	ldr sp,=_UND_STACK
	msr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|SYS_MOD)
	ldr sp,=_SYS_STACK

//将bss段置零
_clear_bss:
	ldr r1,_bss_start_
	ldr r3,_bss_end_
	mov r2,#0x0
1:
	cmp r1,r3
	beq _main
	str r2,[r1],#0x4
	b 1b		//重新跳到标号1

//进入主函数
_main:
	b main		//跳入主函数(在实际运行时,应该直接跳入到内核中KERNEL_ENTET_POINT)
_bss_start_:	.word  __bss_start__
_bss_end_:		.word  __bss_end__
.end

   以上代码中流程基本上就如下图3.3所示。
在这里插入图片描述
图3.3 HelloOS boot流程

   代码中有较为详细的注释,这里主要从整体上讲几点:
    (1)、Reset标号下面的即是系统上电后开始执行的指令,也就是Reset的异常处理。如上所述,对于真实的硬件环境,还需要做一系列的硬件初始化。在这里由于咱用的是仿真环境,不需要做这个(我也试过初始化硬件,但貌似没什么卵用)。所以,最终的HelloOs的Reset处理只包括设置各个模式的栈指针、将BSS段清零、跳入main函数,这几个重要的部分。
    (2)、“栈”这种后进先出的数据结构,用的最多的大概要数调用C函数时的保存上下文环境了(注意这里保存上下文环境并不是指进程切换时的上下文环境,而是调用新函数时,保存上一个函数的环境,主要是一些变量和返回地址)。 这里设置各个模式的栈指针也是为了系统进入某个模式时可以调用对应模式的C函数。这里在稍微多说两句,一般在汇编代码中,我们不太需要栈指针,或者说如果需要的话,也是我们可以控制的,但是在调用C函数之后,其保存环境和恢复环境的过程是由编译器控制的,所以说我们要提前设置好栈空间,给这些过程使用。

note:这里为图方便给几个模式都设置了栈空间,其实HelloOs只用到了系统模式(SYS)、中断模式(IRQ)、管理模式(SVC)和用户模式(和系统模式共用一个栈)。

    (3)、将bss段清零。bss段即未初始化的内存段,简单来说,bss段的内容就是程序中那些未初始化的全局变量和静态变量所占用的空间。这里简单介绍一点,详细的有关bss段的内容可以参照《真象还原》一书中bss简介一节。
    一般来说,我们在linux系统上用gcc编译后的C程序,其内存空间如下图3.4所示。

在这里插入图片描述
图3.4 C程序内存布局

    主要分为代码段(text段)、数据段(包括初始化的数据段data和未初始化的bss段)、堆(heap)、栈(stack)等等。而这个bss段,也就是那些未初始化的全局变量和静态变量的内存空间,其有一个特点,就是在程序未装载到内存空间之时,它的值或者内存空间的内容是不重要的(因为其还没有赋值),只有在程序正式运行时,才会给这些存在于bss段的内容赋值(通过用户代码中的赋值语句)。换句话说,我们不需要再C程序编译形成的可执行文件中具体保存未初始化的全局变量或者静态变量的值,当然,你也不知道它们的值是多少。
    举个栗子,假设有一个未初始化的全局变量A,我们在编译成的二进制文件代码中是不会存放其值的,只有在程序加载到内存中时,A变量的内存空间才会形成(存放在bss段),而且一般来说,加载器会把A内存空间赋值为0。
    说了这么多,那为什么我们这里要手动赋值呢? 你可能很快就明白了,我们没有操作系统啊,所以需要我们自己在程序中手动赋值了。从某种程度上来说,也算是加载器的一小小部分的功能。
(4)、跳入C main函数。汇编代码终究是比较难搞的,除非万不得已,否则我们应尽快进入C语言的世界。在main函数中完成后续一系列的初始化过程。同时这里也要稍微注意一下,就像我们前面说的,进入C语言之后,那些我们常见的i,j,k,c等变量就需要分配在栈空间中了,所以我们在前面指定了栈指针。


 

3.2 中断和异常

    所谓中断,就是CPU跑着跑着就“跑偏了”,也就是偏离了正常的执行流程,暂时去执行一些紧急的事情,执行完了之后,再返回来。而异常,非正式的定义,我觉得和中断差不多,只不过我们一般认为的中断就是时钟中断等外部中断,而异常包括能影响正常执行流程的所有事件。在S3c2410中包括了7种异常,也就是在程序开始处定义的那几种异常。如下图3.5所示:

在这里插入图片描述
图3.5 各种异常向量示意图

    注意,上图中还包括一个保留的异常地址。


 

3.2.1 异常向量和工作模式

    发生了中断或者异常之后,cpu会自动跳到对应的地方中去执行对应的异常处理程序(当然也包括中断处理)。这个处理程序的入口地址就是异常向量了。s3c2410强行规定了发生各种异常时就会去固定的地址去取对应的指令,如上图3.5所示,也就是0x0-0x20,(每个异常向量一个地址,占4个字节,总共7+1个,正好占据32个字节)。 (这里要注意,一般的处理程序肯定不止一条指令,所以这4个字节的一条指令,或者说地址,主要还是跳转到具体的异常处理程序地址上)
    上面说的这几种异常向量分别对应着几种工作模式,如下图3.6所示。

在这里插入图片描述
图3.6 异常向量与工作模式对应图

    所谓的工作模式也没什么特殊的,就是一些特殊的寄存器,和一些硬件规定。各个模式下使用的寄存器如下图3.7所示。而硬件规定说到底一句话:特权模式可以手动切换到其他模式,普通的用户模式则不可以。 详细的东西这儿就不多说了,请大家自行查阅资料吧。_

在这里插入图片描述
图3.7 各个模式下使用的寄存器

    对了,忘了说一点,HelloOS目前设计到的工作模式主要有中断模式(用于处理中断)、svc模式(主要用于swi指令、线程切换时保存上下文)、sys模式(内核空间,系统一上电就处在这个模式),用户模式(用户空间)。


 

3.2.2 中断切换上下文

    首先要介绍的就是中断了。简单的大家都懂,难的我也不一定懂。所以只能介绍一些说难不难,说简单不简单的了----中断上下文切换了。
    正常的执行流程被打断之后,需要保存其运行环境以便切换回来。(这个逻辑不仅适用于中断,也适用于进程的切换。)这个著名的“运行环境”就是上下文了,具体来说就是当时运行一些寄存器了。
    好了,废话我也不想多说了,直接上个流程图,体验一把
    整个中断执行的流程如下图3.8所示:
在这里插入图片描述
图3.8 中断流程示意图

    本来中断上下文切换也是比较简单的,保存上文,执行中断处理程序,恢复上文,可以不需要牵扯到模式切换。但是这种模式有个问题,就是由于在执行中断处理程序的时候需要关闭中断(防止嵌套中断导致中断的环境交叉出错)。这样的话,如果在处理中断处理程序的时候又发生了中断,那么这个后来的中断就会丢失了。
    如果我们把中断具体的、比较耗时的处理程序放在系统模式下执行,那么如果发生了嵌套中断,就又可以从中断模式下开始执行了。当然如果这样做的话,需要把上文的环境保存在svc模式,然后恢复上文的时候,直接就可以从svc模式下恢复了。
    这里有几个比较难理解的点需要着重说明一下。

(1)、为何普通模式下,中断模式不能接受中断呢?
    下图3.9中的文字,可以很好的解释这个问题(摘自《一步一步写嵌入式操作系统》)。
在这里插入图片描述
图3.9 解释文字

(2)、那为什么多引入一个svc模式可以很好的解决这个问题呢?
    原因就在于引入svc之后,具体的中断处理程序是在svc模式下运行的,如果发生中断,也是在这个模式下才可以(在svc模式下手动开中断),所以中断处理程序中调用的C函数,其返回值是保存在svc_r14中的,和irq_r14没有关系,如果发生了中断,irq_r14可以保存svc模式下的下一条指令的地址。

(3)CPU在不同的模式下运行本质上有什么区别?
    总的来说,CPU在不同的模式下运行其实是没有太大的区别的,有区别的就像我上面说的只是不同模式下允许使用的寄存器可能不一样,没有强制的要求中断处理程序一定要在中断模式下运行(当然一开始CPU是会被强制转化为中断模式,然后跳到中断向量地址处执行的)。我们完全可以在中断模式下跳到SVC模式下,然后在SVC模式下完成主要的处理程序。这里有点像linux中断中的顶半部和底半部机制了。

   好了,如果上面真的比较难理解也没关系,因为HelloOs程序中并没有利用这种机制来响应嵌套中断。相反的,为了简单起见,HelloOs不允许嵌套中断(没有在svc模式中打开中断)。引入这种机制的原因是为了和后面保存进程上下文环境进行统一。

   理论说的比较多了,下面来波代码吧。

__vector_irq:

	str		lr,[sp,#-0x4]	//lr:保存返回地址
	str		r4,[sp,#-0x8]	//保存r4,因为马上会用到

	mrs		r4,spsr			//保存上文中的状态寄存器
	str		r4,[sp,#-0xc]	

	str		r5,[sp,#-0x10]		//保存r5,因为马上会用到
	
	mov		r4,sp				// 保存中断模式下的sp

	CHANGE_TO_SVC				//切换到SVC模式
	ldr		lr,[r4,#-0x4]	//获得返回地址
	ldr		r5,[r4,#-0xc]	//获得上文的状态寄存器
	
	sub		lr,lr,#4		//lr=lr-4(这时候其实才是上文的返回地址)

	stmfd	sp!,{r8-r9,lr}		//在svc模式的栈中保存r8,r9和上文的返回地址
	stmfd	sp!,{r5}		//在svc模式的栈中保存上文的状态寄存器
	ldr		r5,[r4,#-0x10]		//恢复原本的r5
	ldr		r4,[r4,#-0x8]		//恢复原本的r4
	stmfd sp!,{r0-r12}			//存储所有的r0-r12,因为马上要进入中断处理程序,可能会用到这些寄存器
	ldr		r9,=INTOFFSET		//INTOFFSET定义在其他文件中,这是个寄存器的地址,其内容保存了哪个中断产生
	ldr		r9,[r9]				//获得发生的中断号
	ldr		r8,=HandleEINT0		//中断向量表的其实地址
	add		r8,r8,r9,lsl #2		//r8=r8+r9<<2
	ldr		r8,[r8]				//获得中断的向量地址
	mov		lr,pc				//保存返回地址
	mov		pc,r8				//进入到真正的中断处理程序
	ldmfd sp!,{r0-r12}			//恢复上文环境(这里是从svc模式下直接恢复了)
	ldmfd	sp!,{r8}			//恢复上文的状态寄存器
	msr		spsr_cxsf,r8
	
	ldmfd	sp!,{r8-r9,pc}^		//回到上文中断的地方,继续执行

    以上代码注释的还算比较详细,但如果没有arm汇编的知识,我估计可能还是搞不定。这时候要么把它当成黑箱子,知道大概怎么回事就行。要么就稍微补一下arm汇编相关的内容,可以看看《ARM体系结构与编程》。
    为了帮助理解,简单画了一下栈空间图。如下图3.10 所示。

在这里插入图片描述
图3.10 中断模式栈空间示意图

    注意,上图中保存的都是上文的寄存器(也就是从正常流程的环境,sys模式或者ueser模式)。
    为了帮助理解,再简单说一点点:由于现代CPU大部分都有指令预取技术,S3C2410也是一样,所以pc指的不一定是当前的指令,中断时lr保存的也不一定是正好返回的那条指令,不过相差也不是很多,多一条,少一条的问题,具体的可以参照下图3.11。

在这里插入图片描述
图 3.11 PC地址示意图


 

3.2.3 中断处理程序部分

    这一部分首先要说的就是HelloOs的中断跳转流程了。
    先上一个流程图。如下图3.12所示。
在这里插入图片描述
图 3.12 HelloOs跳转流程图

    是不是看的有点晕,什么一级处理程序,二级处理程序的?
    其实是这样的,原生的CPU只保证了发生中断时,跳转到0x1c地址去执行程序。但是我们要知道产生中断irq的原因不止一个,有可能是外部引脚中断、也有可能是时钟中断、Uart中断等等。所以必须还有一个判断的标志,用于判断到底是哪个中断产生了,这也就是INTOFFSET寄存器的作用,它指示了具体发生的中断。
   判断出具体的中断之后,还必须执行具体的中断处理程序。为了和上一级中断处理程序相区别,我把它叫做二级中断处理程序。而这个二级中断处理程序才是真正起作用的地方(一级中断只是起到保存上下文、调用二级处理程序的作用)。
   既然要调用二级中断处理程序,那么必须要有个地址吧,否则CPU知道到哪儿去执行呢? 就像一级中断处理程序其向量地址是原生规定的一样,HelloOs也规定了二级中断处理程序的地址(在64M内存的最顶端)(当然你也可以不硬性规定,声明一个二级中断处理程序的全局数组 让编译器帮你选择一个地址。像《真象还原》书中的那样。)
   一个简要的内存示意图如下3.13所示。

在这里插入图片描述
图3.13 调用中断处理程序示意图

   注意,以上的二级中断处理程序地址和具体的二级中断处理程序是通过注册完成的,如下代码。

//注册向量函数
void register_handler(uint32_t vector_no, intr_handler function) {
	 (*(unsigned *)(_ISR_STARTADDRESS+vector_no*4)) =(unsigned) function;
}

   其实也很简单,本质上,就是在二级中断处理程序的入口地址处填上具体的二级中断要调用的函数地址。

   这一部分下一个要说的就是定时器中断了。从某种程度上来说,定时器中断时是驱动操作系统进程切换的一个非常重要的基础。不仅对HelloOs,我觉得目前的大多数操作系统,尤其是通用操作系统在进程切换时都有或多或少定时器的影子。
   基本的定时器操作这儿就不说了了,无非就是初始化定时器,编写具体的中断处理程序,balabala … 。

   下面这段话有些爆粗口,请读者见谅。
   这儿主要是想吐槽一下skyeye这个仿真器,关于定时器的模拟,或者说时钟的仿真,真tm不好用,或者说完全无反应。我在定时器中设置了0.5s中断一次,靠,完全没反应。我还以为是程序有问题(时钟没设置,内存没调好) 又害得我找bug,调试了将近一个多星期。
   好了,吐槽完了,心平气和的继续往下来。
   en…
   关于中断这一节也没什么好说的了。简单再说两小点。
   (1)、中断中经常要涉及到关中断、开中断、获取CPU的模式等,这主要需要读取状态寄存器,HelloOs中把它封装成了C函数,不过这需要内联汇编的一些知识。
   (2)、虽然,程序预留了很多二级中断处理程序的接口,但整个HelloOs确实只用到了一个定时器中断。

   好了,这一章终于弄完了。不得不说,写总结真的是累。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值