01
单片机存储分配
在玩单片机(以stm32为例)的时候会有RAM空间和ROM空间,RAM空间主要是用于数据的访问,而ROM空间用于存放烧录的固件,当然固件也可以直接加载到RAM中运行,只是说每次上电都需要重新加载。
如上图所示ROM为FLASH地址,而RAM为SRAM地址,毋庸置疑生成的单片机固件会烧录到Flash上,这样才能保证每次上电都有可以正常运行。
对于很多初学者该有疑问了,明明全局变量等等都是分配到RAM上的呀,怎么说固件放到Flash上的呢?
其实并不矛盾,程序指令中访问变量都是访问变量的地址也就是内存的地址,所谓的分配到RAM上,仅仅只是说相应的变量占据了对应的RAM地址,并不能理解为这个变量存在于RAM里面。
可能你还会继续问 : 暂且认同上面的说法,那对这些变量的初值该如何解释呢?
可以肯定的是,这些全局变量的初值并不是来源于RAM,因为RAM掉完电以后数据就丢失了,而在程序正常运行过程中,不管怎么上下电其初值都是我们程序中规定的,也就是在编译中确定的。
所以这些初值要保存只可能存在ROM中,这中间肯定有这样一种机制 : 在上电以后把ROM中存储的这些变量初值来重新初始化到对应的RAM地址,以便后续程序指令访问,这种机制通常叫分散加载。
02
简述分散加载
上图是一种简单的分散加载机制,映像文件由不同的段组成,通常都有代码段(.text)、已初始化数据段(.data)、未初始化及初始化为0的数据段(.bss)等等,而且他们具有不同的属性RO,RW,ZI等等。
为了便于大家理解,整个系统的存储区分为ROM和SRAM,左边Load View表示的是程序存储地址空间分布情况,也就是程序烧录到ROM以后的空间分配情况。
固件烧录到ROM区域并且分为RW区和RO区,RW区域为可读可写区域而RO区域为只读区,分这两个区域并不是说RW区域存储地址区域以后就用来数据的读写,而是为了上电过程中的copy/decompress(复制或者解压)过程做好标记,这个过程会把一些非零全局变量(或者静态变量等)的SRAM地址(实际的运行地址)处赋予初始值。
ZI区域是零填充区域,主要是.bss段的一些初始化为0或者未初始化的全局或者静态变量分布区域,这些数据没有必要保存到固件中,所以由加载机制自行清零即可。
一切准备就绪就形成了右侧的execution View的运行空间视野,由于ROM中程序运行所涉及到的全局变量等的访问都是SRAM地址的访问,而这些地址恰好在程序编译链接过程中已经分配到SRAM里面,经过前面的该部分地址的重新定位,运行空间的程序就可以正确访问到这些变量的初值等等。
03
stm32启动流程
很多刚玩MCU的朋友,都会以main函数作为程序的开始运行处,不过几乎所有的C程序在执行前都会使用汇编指令,通过汇编指令构建C语言运行环境,并运行C程序,所以在C程序执行前做了非常多的工作,其中非常重要的就是堆栈指针的设置,这也是从汇编到C运行环境一定要做的一件事了。
那么stm32的启动大致流程是怎样的?这里小哥就简述一下:
当然还有一些小细节,这里就不展开了,stm32的Flash可以直接运行程序,采用分散加载,只需要把相应的数据区域加载到运行地址处便可以正常的访问,这个与前面的所说是类似的。
04
uboot部署Linux
在进行Linux系统开发过程中,一切从Bootloader开始,而bootloader本质上就是一个单任务的裸机程序,和单片机程序是一样的,而在众多bootloader中最为常用和广泛的就是uboot了,他就是为了部署Linux环境而生的,下载、烧录、运行Linux映像、文件系统等等。
uboot都可以搞定,所以它对地址是非常敏感的,程序、参数等等应该存储在什么地址,在什么地方运行都是需要确定好的,而这些地址在编译链接的过程中,链接脚本已经确定好了这一切,uboot的工作就是把这些固件放在编译链接所规定的运行地址处进行运行即可。
比如全局变量在什么地址,函数在什么地址,当程序运行的过程中就会从这些确切的地址处取数据,如果你把全局函数指针变量的地址分配到了NANDFlash上,那么程序在访问的过程中就有可能跑飞。
程序运行最重要的两个地址加载地址和 运行地址 。
加载地址也常被大家成为存储地址 ,即实际固件存储的位置,其实该地址也只是一个相对的概念,就相当于单片机中bin文件烧录在什么位置一样的道理。
运行地址也叫链接地址,即程序的绝对地址 。全局变量等等都是以该地址为基础,来确定程序的运行状态的各部分的地址布局。
当然Linux以上各部分直接烧写到RAM也是也可以直接运行的,不过还是那个问题,一旦掉电则全部丢失,所以最终每个部分都会写入到Flash上(当然在前期调试的时候可以直接下载到RAM中,减少对Flash的反复擦写),但对于大部分Flash都是无法直接运行程序的,即使能够运行,比如Norflash也是非常的慢,且不能够直接写入,所以Linux内核等都会加载到RAM来运行,以获得更快的执行速度,那么前面介绍的那种单片机方式只重定位数据段的方式不太适用了。
在嵌入式Linux平台上,首先执行的就是bootloader,而它只是一个顺序执行的程序,它有一个重要的工作就是把Linux内核搬运到RAM中运行,由于我们的内核兼容不同的单板,uboot也会传递给内核一些配置参数以配置内核。
往往RAM分配的地址比较高,而整个程序往往都是0地址开始执行了的,如果让存储地址与运行地址相同来进行编译,会导致最终烧录文件非常之大,并且中间有一大片地址区域是无效的。
那么有什么办法来解决这个无效区域以缩小我们的固件大小呢?先了解下位置无关指令。
05
位置无关指令
既然有位置无关指令就有位置有关指令,简单的说所执行的指令是不是与位置相关才能达到目的。
可以类比与 绝对路径与相对路径 ,相对路径你可以把程序放在任何文件夹下面,编辑器均可以根据工程文件路径找到其他每一个文件,而绝对路径却不行,一旦文件夹换了,基本上就是定位不到具体的每个文件了。
所以位置无关就相当于相对路径,数据的访问、函数的调用几乎都是相对的,为什么说是几乎呢?因为有些情况下访问绝对地址也是与位置关系不大的,可以把这段程序放在可以执行的任何位置,所以位置无关码的运行与链接地址也没有直接的联系。
比如跳转指令B BL等这些跳转指令采用PC+偏移量,所以为位置无关指令;而如果我们采用 ldr r0, =标记 ,而这些标记都是实际在链接过程中确定的运行地址,所以该指令为位置有关指令;并且全局变量基本上都是位置有关,而局部变量为位置无关;所以对于位置无关代码区域,跳转一般都使用B指令,而从位置无关代码区域跳转到位置有关指令代码区域去执行就需要借助位置有关跳转指令。
06
加载与运行地址不同
当存储地址与链接地址不同时,多数情况下由于采用位置有关指令会出问题,最常见的就是PC指针取的绝对地址,而此时该绝对地址处无存储,导致程序飞掉。
既然有了位置无关的程序,那么我们就可以把其当作一个搬运工放在位置有关部分的后面,一旦需要运行位置有关码,那么就会通过位置无关码把有关部分拷贝到运行地址处,然后跳转执行即可,这样整个的程序就可以做得非常的连续且中间几乎没有无效区域,该搬运的过程就是常说的 重定位 。
07
地址的设置
大部分ARM处理器其PC都是从0地址开始执行,所以在0地址处要么是运行程序,要么就是引导程序,如果没有这两样,你的程序烧录到其他位置均无法得到运行。
对于S3C2440芯片能够支持NorFlash和NandFlash启动,其中NorFlash上可以直接运行,而NandFlash启动由于其程序无法直接在上面运行,芯片会把内部SRAM作为0地址处,并且把NandFlash前4K代码拷贝到SRAM上运行。
因为这里最终想让所有的程序都在SDRAM里面运行,考虑使用全部重定位的办法,在链接脚本中确定好程序的存储地址和运行地址。
上图是GUN linker中截取的段描述格式
具体详细解读大家可以参考上面的链接,下面看看几个常用的。
可执行文件由各个段组成,:
1、secname段名,一般使用数据段.data段,代码段.text段等等。
2、AT(ldadr)表示该段存储地址,也就是加载地址。
3、contents表示目标文件(比如.o目标文件)中的哪些段放在本段,也可以是整个目标文件全部放在这个段内。
4、start表示本段链接(或者称为运行)的地址,如果没有使用AT(ldadr),本段存储的地址也是start,也就是说存储地址与运行地址相等。
通过上面的段描述格式就可以在链接过程中确定好程序的运行地址和载入地址,以方便后续的重定位地址的使用。
下面以一个简单的实例说明一下:
1//....格式
2SECTIONS {
3...
4secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
5 { contents } >region :phdr =fill
6...
7}
8//.....示例
9SECTIONS {
10...
11.text 0x30000 : AT ( 0x0000 )
12 { *(.text) }
13
14.data 0x3FFFF : AT ( 0xFFFF )
15 { *(.data) }
16...
17}
这样固件的代码段的存储地址为0,数据段存储地址为0xFFFF,而运行地址分别为0x30000和0x3FFFF,最终重定位部分就根据这链接脚本中的符号获得相应地址,然后把相应的部分"搬运"到运行地址处运行处,比如如果载入地址在NandFlash上,那么重定位的过程中就需要初始化NandFlash控制器,然后读取NandFlash上的数据并"搬运"到运行地址处。
在嵌入式linux中很多时候这些地址都需要我们自己确认和设置的,不然Linux内核无法启动或者加载相应程序,而在单片机开发中用惯了IDE工具,所以大部分人涉及得不多~