启动及中断

  • 三、启动及中断
    • 3.1 启动部分
      • 3.1.1 boot简介
      • 3.1.2 boot代码分析
    • 3.2 中断和异常
      • 3.2.1 异常向量和工作模式
      • 3.2.2 中断切换上下文
      • 3.2.3 中断处理程序部分

三、启动及中断

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_resetldr pc,_vector_undefinedldr pc,_vector_swildr pc,_vector_prefetch_abortldr pc,_vector_data_abortldr pc,_vector_reservedldr pc,_vector_irqldr 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_STACKmsr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|IRQ_MOD)ldr sp,=_IRQ_STACKmsr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|FIQ_MOD)ldr sp,=_FIQ_STACKmsr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|ABT_MOD)ldr sp,=_ABT_STACKmsr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|UND_MOD)ldr sp,=_UND_STACKmsr 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,r3beq _mainstr r2,[r1],#0x4b 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               // 保存中断模式下的spCHANGE_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]      //恢复原本的r5ldr        r4,[r4,#-0x8]       //恢复原本的r4stmfd sp!,{r0-r12}         //存储所有的r0-r12,因为马上要进入中断处理程序,可能会用到这些寄存器ldr        r9,=INTOFFSET      //INTOFFSET定义在其他文件中,这是个寄存器的地址,其内容保存了哪个中断产生ldr        r9,[r9]             //获得发生的中断号ldr       r8,=HandleEINT0        //中断向量表的其实地址add     r8,r8,r9,lsl #2     //r8=r8+r9<<2ldr        r8,[r8]             //获得中断的向量地址mov      lr,pc               //保存返回地址mov     pc,r8               //进入到真正的中断处理程序ldmfd sp!,{r0-r12}            //恢复上文环境(这里是从svc模式下直接恢复了)ldmfd    sp!,{r8}            //恢复上文的状态寄存器msr     spsr_cxsf,r8ldmfd   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确实只用到了一个定时器中断。

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

HelloOs总结之启动及中断相关推荐

  1. 《Tsinghua os mooc》第1~4讲 启动、中断、异常和系统调用

    资源 OS2018Spring课程资料首页 uCore OS在线实验指导书 ucore实验基准源代码 MOOC OS习题集 OS课堂练习 Piazza问答平台 暂时无法注册 疑问 为什么用户态和内核态 ...

  2. 操作系统(thuOS)笔记(一) 第三讲 启动、中断、异常和系统调用

    thuOS笔记(一) 第三讲 启动.中断.异常和系统调用 3.1 BIOS BIOS 加载程序 BIOS系统调用 3.2 系统启动流程 CPU初始化:**加点稳定后**,从0xFFFF0读第一条跳转指 ...

  3. 操作系统概述以及启动、中断和系统调用

    操作系统概述 什么是操作系统? 没有公认定义,可以理解为起协助作用的控制程序,或者是介于软硬件之间的资源管理器. 操作系统软件组成 Shell 命令行接口 通过键盘操纵 方便用户进行命令输入 GUI ...

  4. 《操作系统》OS学习(二):启动、中断、异常

    Bootloader:加载OS.操作系统一开始是放在DISK(硬盘)中,并不是放在内存中. BIOS:基本I/O处理系统.存放在ROMRead-Only Memory)只读存储中 BIOS(Basic ...

  5. 【线程】——线程的启动和中断

    启动一个线程 之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行 了. 覆写 run 方法是提供给线程要做的事情的指令清单 线程对象可以认为是把 ...

  6. 【操作系统/OS笔记03】启动、中断、异常和系统调用

    本次笔记内容: 2.1 启动 2.2 中断.异常和系统调用 文章目录 启动 BIOS加载Bootloader 操作系统与设备和程序交互 定义 从源头区分 从处理时间区分 从响应区分 中断.异常和系统调 ...

  7. 操作系统from清华大学向勇,陈渝 笔记(二)操作系统的启动、中断、异常、系统调用

    下一篇在这里(三)(四)内存层次和连续.非连续分配 上一篇绪论在这里(一)操作系统绪论 INDEX 2-1 操作系统的启动 2-2 操作系统的中断.异常.系统调用 2-1 操作系统的启动 启动:机器三 ...

  8. 操作系统-Operating-System第二章:启动、中断、异常和系统调用

    B站资源:操作系统_清华大学(向勇.陈渝) Github资源:chyyuu/os_course_info 参考书籍:Operating systems: internals and design pr ...

  9. 56/14 shell脚本 后台启动 程序1 + “tail -f log“, ctrl +c 导致程序1中断

    前言 接上一篇文章, node 程序后台执行加上 tail 命令, 中断 tail 命令, 同时也中断了 node 程序 我们来详细 参照对比一下 这个问题的各种情况 主要的脚本如下类似, 第一条命令 ...

最新文章

  1. 如何使用jdbc连接数据库
  2. weblogic 的一些说明
  3. jqgrid的实用方法集合
  4. MongoDB的高级语法
  5. Angular 应用级别的依赖 Fake
  6. Java中的延迟分配
  7. 怎样教一台计算机区分猫和狗?一文零基础入坑机器学习
  8. PHP程序员如何突破成长瓶颈(php开发三到四年)
  9. 对PV操作问题的理解综合
  10. 基于TortoiseGit完成本地代码上传Git远程仓库中
  11. Servlet容器和IOC容器
  12. 4. 查询表orders——检索所有订单订购物品的总数
  13. 计算机网络工程师干嘛的,什么是网络工程师?网络工程师是做什么的?
  14. 【CEP 扩展开发二】Hello World
  15. 55欧式空间02——正交矩阵、欧氏空间的同构
  16. ECharts :lable显示所有数据、修改字体样式
  17. 二级分销商城简单的设计方式
  18. 更换kindle书籍的字体
  19. ArcGis将2000国家大地坐标系转WGS84
  20. 人脸识别技术原理及解决方案

热门文章

  1. ULR Web 三种路径
  2. Origin学生版的获取方法和安装方法
  3. 傅盛推荐的十六本书:关于成长 认知 思维模式和进化
  4. [创业故事]清风明月我 与谁同坐--我的创业故事-2
  5. 操作系统基础知识点(从题目中总结)期末复习总结 终极版 ctrl+f 寻找你想要的答案
  6. 通过 Z-Order 技术加速 Hudi 大规模数据集分析
  7. 简单聊天室的设计 C++ MFC
  8. 突破市场壁垒:如何利用关键词采集和市场调查找到你的细分市场?
  9. 【毕业设计】基于SSM的酒店客房信息管理系统 - java web
  10. 计算机java项目(毕设课设) 之 含文档+PPT+任务书+中期检查表+源码等]基于ssm的NBA球队管理系统