当前位置:首页 > 公众号精选 > 裸机思维

一.工具准备

1.获取arm交叉编译器

注意:如果你的机器是ubuntu,可以通过apt-get包管理器直接下载:

sudo apt-get install gcc-arm-none-eabi

个人建议通过下载软件包的形式安装

2.安装交叉编译器

- 如果你的机器是linux,将下载的arm交叉编译器解压,并将目录拷贝至你机器安装软件的目录(我假设是/opt/bin/arm-none-eabi-q3/),将此目录下的bin及arm-none-eabi/bin目录添加至环境变量(也就是将/opt/bin/arm-none-eabi-q3/bin及/opt/bin/arm-none-eabi-q3/arm-none-eabi/bin加入到环境变量)。

- 如果你的机器是windows,一样也是把交叉编译器解压,并将目录拷贝至你机器安装软件的目录(我假设是D/portable/arm-none-eabi-q3/),将此目录下的bin及arm-none-eabi/bin目录添加至环境变量(也就是将D/portable/arm-none-eabi-q3/bin及D/portable/arm-none-eabi-q3/arm-none-eabi/bin加入到环境变量)。

3.安装外围工具

- 对于linux来说,还需要安装make工具:

sudo apt-get install make

- 对于windows来说:

  • 需要安装mingw(此软件安装不展开讲解,把能勾的都勾上)

  • 安装gnu make for windows(下载压缩包解压并添加环境变量

Binaries里下载下来的是make工具本身(make-3.81-bin.zip),Dependencies里下载下来的是make工具依赖的dll(make-3.81-dep.zip)。我假设你已经将make工具本身解压并放至D/portable/make-3.81-bin目录,并将D:\portable\make-3.81-bin\bin路径添加至环境变量。解压make-3.81-dep.zip并将此压缩包bin目录下的libiconv2.dll以及libintl3.dll拷贝至D:\portable\make-3.81-bin\bin目录下。最终看起来的效果应该是这样:

4.测试你的环境是否已完备

注意:后面教程的写作都是基于windows下mingw编译平台讲解

打开你的命令控制符(或者powershell或者git bash或者mingw sh或者xshell任意你喜欢的终端工具),在命令行里依次敲下arm-none-eabi-gcc以及make,命令行回馈的内容如图所示证明工具已安装好,如果如第3个场景所示,证明工具没有安装好(请检查对应工具是否下对版本并添加至环境变量):

注意:我本人的pc是英文版的系统,中文版请自行甄别。

二.Makefile(已掌握的同学可以跳过)

1.初体验

在当前目录下创建一个makefile文件,makefile文件中的内容如上。

在终端里敲入make pepper_fired_potato,依次打印出箭头所指的内容(echo是make里面的一个函数,作用是向终端打印信息,至于echo前面的那个@是什么意思,大家可以自行尝试加上和不加上的效果对比)。

那么makefile的语法到底长啥样呢:

7: pepper_fired_potato: pepper potato8:     @echo "cook pepper_fired_potato with pepper and potato"

第7行,pepper_fired_potato: 这是你要制作(make)的东西(青椒炒土豆)

青椒炒土豆后面有个冒号(:),冒号后面跟了pepper(青椒)、potato(土豆),冒号的意思是依赖,也就是说我制作“青椒炒土豆”这个目标,需要依赖“青椒”和“土豆”两种材料。

第8行,是具体制造“青椒炒土豆”的过程。

这两行展现了makefile语法最基本的使用范式,冒号前面是你要制作的目标,冒号后面是制作这个目标所要依赖的材料,紧跟着一行(也可以是多行)带缩进的语句是具体制作这个目标的步骤。

当然冒号不是必须的,当有些制作目标不需要依赖其他材料的时候,可以不需要带依赖项,比如第1行和第4行的“青椒”与“土豆”的制作。

2.还是不会用?

我假设你已在当前目录下创建了三个.c文件,分别为module_a.c、module_b.c、main.c,其中module_a.c、module_b.c的实现分别是vim上面两个窗口里的内容,main.c的实现是vim下面窗口里的内容。module_a.c和module_b.c分别提供了module_[ab]_api接口,而这两个接口皆被main.c依赖调用(注意这里产生了依赖关系),也就是说,你要把main.c编译成可执行文件,必须要先把module_[ab].c编译成.o(依赖的原材料)。

Makefile这样写:

当我在终端里make app时,make得到指令要制造app,但是app依赖module_a_obj(module_a_obj在第1行被定义为module_a.o)以及module_b_obj,因此他按照依赖关系,依次先去制造app所依赖的module_a_obj和module_b_obj,module_a_obj的制造规则里没有依赖项,制造方法只是把module_a.c编译成为object文件,module_b_obj类似,于是他很happy的依次制造出了module_a_obj和module_b_obj。

依赖的原材料都齐备了,make开始制造app本身,如你所知,app的制造流程就是把main.c和依赖的module_a.o以及module_b.o加一起,编译链接成app,并且在最后调用了app这个程序。

注意:对照上面的例子和讲解好好琢磨一下makefile的道道,其他的细节不再展开讲解,可以上网对症学习。到这里我假设你已经学会了makefile的基本用法(其他更高阶的暂时不管,主要是我也不太会)

三.徒手编写裸机程序

1.你的程序(操作系统)是怎么从按下上电按钮到运行起来的?

这个网上有大量的资料,大家可以对比pc启动流程来理解。当你的pc上电时,最先执行的程序是一段固化在BIOS中的引导程序,此程序的主要责任是进行硬件自检,以及将系统引导盘中第一个扇区(引导扇区)中的512个字节读到特定的内存区域(x86下这个区域的起始地址是0x7c00),并随后跳转到这个0x7c00这个地址处,这个引导扇区是程序员编写的,也就是说,经过BIOS的引导加载后,程序员才真正接管了cpu。

arm是类似的,一般来说arm的mcu芯片(如stm32)出厂时都有一段固化在芯片里的引导程序(一级BootLoader),此程序负责把你烧写在flash上的程序读到ram里,并将ram里的中断向量表映射到0地址处,再跳转执行中断向量表中的复位中断向量。此后,cpu的控制权交给程序员(因为复位向量是你写的)。(arm a核因为有mmu的存在,如果你想要写带虚拟地址空间的内核态/用户态地址空间隔离的内核,启动流程会复杂的多,可能需要自己写BootLoader)

不管怎样,我们得出一个结论,ram的起始地址处放置的是中断向量表:

- 对于arm v7-m来说,中断向量表的第一个成员是cpu上电后的主栈(MSP)栈顶,后面依次是复位中断向量、NMI中断向量、硬件错误中断向量等等中断向量(中断服务程序)的起始地址。

- 对于arm v7-a来说,中断向量表的构造有所不同,第一个成员不再是主栈栈顶,而直接是复位中断向量,但arm v7-a的中断向量又与v7-m的有所不同,他放的不是中断向量的起始地址,而是一条跳转指令,比如 b Reset_Handler,或者ldr pc, Rest_Handler语句,这里需要自己体会一下。a核架构因为上来没有分配栈,所以你应该尽早(执行你的第一次函数调用之前,保险起见最好是复位向量的头几句话)完成栈的分配(此处所谓的分配不是说分配内存,而是说给你管理模式[以及其他你会用到的模式]的sp赋值)。

2.管窥启动文件

上面是我从stm32L4官方库里拷贝的一段启动文件。

注意观察,上面的__Vectors就是中断向量表起始地址,里面的内容依次存放的就是我上文所述的东西。

我们可以打开TencentOS tiny自带的TencentOS_tiny_EVB_MX\KEIL\hello_world工程,编译并生成镜像(镜像为TencentOS_tiny_EVB_MX\KEIL\hello_world\obj目录下的TencentOS_tiny.axf)。

结合readelf文件,查看__Vectors符号所在的地址:

注意:keil编译生成的axf文件本质上也是elf格式的,因此完全可以使用readelf文件对其进行解析。

这里可能有对readelf工具不熟悉的同学,此工具的使用不是重点因此只简单讲解一下,够用就行:

readelf工具带-s选项,后面跟一个elf文件,可以查看此elf格式文件中的符号表,其中:

- 第1列是符号的index(此index会在装载链接的时候用到,不需要了解)

- 第2列是符号在运行时的地址(这一列的信息很重要!!!)

- 第3列是符号所占大小(可以简单理解为数据类型的大小,比如一个int型的符号在32位机器上占4个字节,这里的大小就是4)

- 第4列说的是这个符号的类型,OBJECT表示是一个变量(如果是一个函数则是FUNC)

- 第5列说的是此符号的可见属性(这也是你写装载链接器的时候才会关心的属性,请忽略)

- 第6列含义具体有点遗忘了,不过也是装载链接的时候才会用到的,请忽略

- 第7列,此符号的名称

注意看上面readelf的结果,__Vector_End是中断向量的结束地址(0x0800 018C),与中断向量的起始地址(0x0800 0000)相隔0x18C的距离,一个中断向量是一个4字节的地址,因此可以计算出一共包含了0x18C / 4 = 99个中断向量(感兴趣的同学可以自己去数一下,我上图只截取了其中一小部分)。

那么中断向量表里到底存的是啥呢,我们把代码运行起来并在程序中把0x0000 0000以及0x0800 0000地址中的内容打印出来看一下(只打印5个):

我们再分别看一下这几个地址是什么:

结果是很显然的,不多解释。所以,你知道为什么不能对空指针进行操作了吗?(其实操作一下也没事)

最终我们得出一个结论:如果你要自己徒手写arm mcu裸机程序,你编译出来的镜像,需要把中断向量表放到ram的最起始位置处(此处讯息十分重要,请做笔记。至于如何做到,有两种手法,后面会介绍)。

思维拓展:如何不通过运行程序,查看中断向量表里存的到底是什么?

让我们用readelf加-S(大写S)命令来查看一下TencentOS_tiny.axf:

这里简单解释一下结果是什么意思。如你所见,一共有11列:

- 第1列:这里是工具生成的让你更清晰查看的index(没有意义)。

- 第2列:节区表的名称。节区表这个概念很难解释,我尝试用与实际挂钩的话语来解释:你的代码最终编译生成程序时,代码中的code(比如你写的if else逻辑)会被放入代码段,代码中的data(数据,比如你定义的全局变量等)会被放入数据段,定义的常量(包括字符串常量)会被放入只读数据段,代码中的全局或静态未初始化变量(或被初始化为0)的变量,会被放入bss段。上面提到的各种段可以理解为一个一个节区(当然用户也可以用链接脚本生成自己的节区,但这个节区的属性必然是属于代码、数据、只读、bss其中之一)。这么一解释你可能更懵了,没关系,先放下不管。

- 第3列:节区的运行时地址。表示的是这个节区在程序运行的时候,在内存中的地址(准确的来说应该是虚拟地址,但因为我们正在学习的arm v7-m不带mmu,没有地址映射功能,所以其实虚拟地址与物理地址在值上相等)。

- 第4列:节区在elf文件中的偏移。从图中可以得知,运行态位于0x0800 0000地址处的符号,在文件中的偏移是0x34。

- 第5列:节区的大小。

- 第6列 - 第11列:如果你不写动态链接器的话可以不用管(后面我们也可以从零写一个动态链接器)。

从上面的信息中我们可以得出,0x0800 0000地址处的符号在文件中存放的位置偏移是0x34(根据上面的分析,你应该也要知道怎么算运行地址在0x0800 01c2c或者0x2000 1c2c的符号在文件中的位置):

用二进制文本工具打开TencentOS_tiny.axf文件,如上图,依次查看文件0x34位置偏移处文件的内容,arm是小端存储,我相信你应该知道里面是什么了。所以,现在的你能彻底弄明白进程和程序之间的区别了吗?(反复琢磨一下,一个符号的运行态地址和其在文件中的地址)

可能有人会问,既然0x0800 0000是程序第一条指令的运行态地址,那为啥不把这条指令直接放在文件的0偏移处?答案是,因为我们现在分析的是elf文件,elf是executable linkable format的缩写(此概念好像应该放在一开始讲),elf格式可以理解是围绕真正可执行的代码和数据部分,添加了一系列附加信息,这些信息在elf文件被装载、链接的时候(或者是程序员拿来分析的时候,比如我们用readelf工具查看)被用到。实际上,对于elf文件来说,位于其0偏移处的是它的elf文件头,可以理解为是检索elf文件中所包含信息的大纲目录。

对于我们写裸机程序来说,没有一个装载、链接器帮我们解析这些信息(实际上也不需要),我们只需要一个纯粹的可执行文件就可以了(怎么得到纯粹的可执行文件,后面会讲)。

关于elf文件格式,可以讲的非常多,但目前的知识暂时够用,如果后面会写《从零写动态链接器》的话,会详细讲。但本人很懒很水,可能不会写,也可能写不出来,甚至本系列也有可能太监。

另外本人水平很有限,读书不够精细,文中若出现纰漏,恳请勘误斧正。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭