事实上, 网上可以搜出很多讲代码运行的相关帖子。但对于一些没入门的人来说理解起来仍有挑战性。 当然, 这篇日志也没必要从二极管、门电路、地球是怎样形成的之类的本源开始。 但尽量写的详细些, 主要目的在于把目前在这一方面认知整理成文, 细加梳理。
尽量通俗易懂的用自己的话阐述,试试自己能不能串起来。
0 0与1
首先, 需要一些操作系统知识, 两个寄存器, SP(堆栈指针), PC(程序计数器)。 所谓万变不离其宗, 记住每一行代码的有序运行, 每一个变量的存取离不开这两个寄存器。 不管各种书上和网页上怎么解释启动过程, 实质上都是通过改变这两个寄存器以达到驱使程序执行的目的。 当然, 让代码运行起来肯定不止这些, ROM、RAM甚至NOR FLASH、 SDRAM等都很重要, 但是目前还是集中精力把启动过程理清。
1 代码的本质
从依稀有印象的数电知识里, 可以知道各种数字逻辑是怎么从自然电压通过施密特触发器转换为0、1二值信号开始一一实现的。 嗯, 上一句的逻辑很严密, 非常棒。 寄存器、锁存器、触发器、 编码器、 译码器, 还有啥来着, 想不起来了。编码器是后想起来的,不知道为什么对它的印象好浅,可能是因为大学里数电期末考试关于它的大题没有做对吧。嗯, 合理。 刚才说的SP和PC就是寄存器。 总之, 这些器件与与逻辑门电路组合起来构成了更复杂的各种器件。 好了, 实际上只需要记住两个关键字, 0 和 1。 没错, 从模拟电路里的各种电压开始, 所有的数字逻辑门和数字器件都是围绕这二值信号运行。 理解代码背后的运行机制, 理解到这一层面即可, 有个笼统的概念, 更深更细的回去查书吧。 现在也记不起来,不知道。 到目前为止,只要心中有数: 我们可以通过0和1这二值信号驱动一个硬件电路完成我们指定的工作,进一步的讲,这些信号是通过电压的变化得到的。 比如最简单的,点亮一个二极管; 比如一个复杂的,给你国外的亲戚打个视频电话; 比如一个高级的,发射火箭。嗯, 好棒棒。 呵呵。
知道所有数字电路里的芯片、器件本质都是遵循二值信号的变换规律工作后, 就不难理解, 我们在PC上, 各种所谓开发环境也好、IDE也好、 框架也好, 总之各种编辑器里写下的代码都会经过编译的过程, 把这些代码翻译成为一个个的二进制文件。 当然, 一个完整的可以工作的工程, 还有更多的工作, 比如链接、pack iamge之类的, 现在也暂时只到这里了, 毕竟我现在在以加班的名义写日志, 我工作很饱和, 不过今晚老大去开会了。 呸, 我真不要脸。 事实上, 现在不管是PC机还是芯片,跑在这些平台上的工程,代码链接、编译、打包直到最后生成镜像文件的过程都被开发环境各种封装好的工具链自动完成了。
基于前两段, 我们分别知道了, 芯片的本质是通过0和1驱动的器件组成的电路、 我们写的代码最终会变成一个个都是由0和1组成的二进制文件。 好了, 我们需要的是只要把这一大堆一大堆的0和1 弄到芯片(不管是X86还是ARM)上,理论上就能有我们想看到的结果了。最近在看动漫石纪元,男主从石头开始做出了各种现代化设备, 真的牛逼。MMP, 活来了, 明天继续写。
2 代码的运行
很多人都知道ROM和RAM甚至还知道FLASH,更牛比的还知道什么是NOR FLASH什么是NAND FLASH, 还有什么SDRAM之类的种种。 简单讲, ROM是一种非易失性的只读存储器, RAM则掉电不可存储但可读写的存储器。 什么意思呢, 电脑运行的一个个软件都是一个个程序,这些程序存在哪里呢? 硬盘上。 不对不形象。 做菜。 锅碗瓢盆, 平时是放在柜子里的。 当然,有人做完饭不收拾,请原地爆炸。 做饭的时候这些锅碗瓢盆会从柜子里拿出来放在灶台上。 对应下来, 柜子就是平时存程序的地方,叫ROM。 灶台是用户使用这些程序的地方,叫RAM。但随着技术的进步, ROM与RAM之间的这种隔阂越来越模糊,尤其是在ARM架构下, 对存储器的分段定义更能证明所言非虚。
记得以前各种折腾电脑的系统, 动不动就重装。 有时会进入系统的BIOS然后做一些硬件相关的配置。 其实, 这个BIOS系统就是PC机的boot loader引导程序, 用来引导目标系统启动执行。 在嵌入式系统的开发过程中, 一个完整的产品需要有boot loader, 需要有app。 有时候,我们还需要给app固件进行升级, 因此一个优秀的boot loader代码能够完成这些功能:加载app、升级app。 这部分是目前的重点内容, 接下来展开讲。
2.1 代码的执行
事实上app的加载就是一段代码搬运的过程。 (1)里面的内容已经说了代码的来历, 是经过了一个编译链接烧录的过程。好, 所谓烧录就是把打包好的镜像文件(image file)存储到各个平台上。然后呢, 代码是怎么运行的呢? 别急, 还记得刚开始说的SP和PC吗,, 对, 这是两个寄存器:
SP(堆栈指针)寄存器总是指示栈顶所在位置, 为何栈顶的位置那么重要---- 所谓入栈出栈, 程序在这些出入的过程中总会产生对应的数据, 因此, 有了栈顶指针我们便能遍历到R0 --- R12通用寄存器,PC寄存器等(ARM体系)。
PC(程序计数器)总是自动指向下一个指令(ARM32位平台PC指向当前指令地址+4) , 对PC进行写操作,会使程序跳转到目的地址。
因此,上面这两个寄存器是能够引导代码的执行的存在。 再回头看, 芯片上电后,代码是怎么跑起来的呢? 首先, 这些都离不开人们的配置。 简单讲, 我把编译链接打包好的image文件放到了芯片的存储器上, 但是放到了哪个位置呢? 32位的MCU寻址范围并不是任意分配的, 有些是芯片的寄存器, 有些被划为了ROM, 有些被划为了RAM, 有些指定放代码, 有些指定放数据, 事实上,再往里这就牵扯到芯片的设计了。
目前还是再继续看代码的执行。 只说了SP和PC,却还没说明白代码怎么从上电后开始执行的。 要知道各个芯片的厂家都会都会根据各自的芯片内核架构去做相应的配置。 比如有些可以做到上电后从存储器的前4个字节取出的值即默认为SP指向地址, 之后的4个字节即为PC指向地址, 有了SP和PC内存储的地址,程序就可以运行起来了。
接下来,还要再把boot loader具体做了哪些事以及分散加载整理一下。 看看还是再补一些图吧。
2.2 启动代码解析
可能还是有点云山雾绕, 那就先从启动代码开始看吧。注意, 这个启动代码说的不是bootloader 。 bootloader是什么?说白了是一个程序, 最主要的作用是跳转到应用程序的主程序(app)。 也就是,bootloader里会有一个main函数, 应用程序里会有一个main函数。 而启动代码是整个系统在上电或者复位后运行的第一段代码, 是进入C 语言的main 函数之前需要执行的那段汇编代码。 启动代码里会做这几件事: 初始化并正确放置异常/中断向量表、 完成分散加载、 初始化系统运行环境(包括设置堆栈、FPU浮点数运算单元)等。 概括就是为代码在具体芯片上的运行进行配置。
https://blog.csdn.net/weixin_39118482/article/details/79508747blog.csdn.net这位老师讲的很不错, 恰好最近也在用NXP的片子。 再跟着嚼一遍捋一下。 划重点, 1、大多数cortex-M控制器复位后会进入厂商bootrom,进行一些芯片级别的初始化配置; 2、bootrom后进入用户启动代码; 3、 启动代码里完成的工作有设置MSP(main SP)和PC,以及重映射向量表; 4、 代码会从复位中断ResetHandler开始执行。
以及, 最重要的这个启动流程图:
cortex-M启动流程
这位老师总结的很好。 再上几段<<cortex权威手册>>官方内容巩固一下认知。
内存空间的最开始保存的是一个向量表, 这个表里最开始的四个字节就是强调了很多次的堆栈指针,再然后是复位向量。 处理器在上电或者复位后会直接从内存的开始(事实上是看启动模式,但这里就先这样)读取栈顶值并赋给SP,然后设置PC值为复位向量,这样子一来前面的所谓程序就可以执行了。 怎么执行, _main() (注意_main()与main的不同, 一个是系统函数,一个是用户函数)。
上图里的地址是从0x0000 0000开始,其实这个启动地址也是可以根据代码加载情况做配置的。 写完启动后会写加载。
上图意在强调SP为要设为栈顶。
好了,这样子就知道上电后代码是怎么走的了。 进入ResetHandler后都做了哪些事呢,
0XE000 ED08是VTOR寄存器地址, 用到这个是因为上图是APP的启动代码。暂先忽略。
SystemInit,这个函数里做了各个内部总线的时钟配置,做分频之类的,相当于给CPU内部各种器件(寄存器、编码器之类但又分不同总线,这里不写总线不深究)。 其实还是初始化。 再然后跳到_main,这个系统函数上面的链接里有跟,可以看到,__main里面会做分散加载,运行环境配置并且最后拉起用户代码。
今天就这样吧,腰疼。
[参考]
- <<cortex权威手册>>
- <<嵌入式系统设计与实践>> 机械工业出版
- <<现代嵌入式系统开发专案实务>> 电子工业出版
4. 高手带你分析STM32 的启动过程(写的不错)
5. https://blog.csdn.net/weixin_39118482/article/details/79508747