ARM架构凭借其低功耗、高性能等优势,在嵌入式系统领域占据重要地位。在基于ARM架构的嵌入式系统开发中,程序的编译、链接与运行是关键环节,其中蕴含着诸多奥秘,下面将详细展开。
编译流程与工具:基于ARM架构的嵌入式系统程序编译是一个复杂且有序的过程。以C程序为例,通常一个项目由多个C源文件组成,如main.c和sub.c 。编译时,首先是预处理阶段,预处理器会处理源文件中的预处理命令,像#include头文件包含指令会被展开,#define定义的宏会被替换,条件编译指令会根据设定的宏标记选择相应代码进行编译,同时注释会被删除,#pragma命令会被保留用于后续编译阶段。接着进入编译阶段,编译器调用一系列解析工具,将C源文件转换为汇编文件。这个过程包括词法分析,把源程序分解为一个个token;语法分析,检查token序列能否构建成正确的语法短语;语义分析,检查表达式和语句是否存在错误;生成中间代码,再将中间代码根据ARM指令集转换为汇编代码。最后由汇编器将汇编文件汇编成可重定位的目标文件 。
符号表与重定位表:在编译过程中,符号表和重定位表起着至关重要的作用。符号表用于保存源程序中各种符号的信息,包括符号的地址、类型、占用空间大小等。编译器利用符号表进行语义检查,确保程序中符号的使用正确,同时辅助代码生成过程中的地址和空间分配。重定位表则记录了需要重定位的符号信息。由于编译器是以C源文件为单位进行编译,生成的目标文件中函数和变量的地址是相对于零地址的偏移,在链接阶段,这些地址需要根据最终的可执行文件布局进行调整,重定位表就是用来记录这些需要调整的符号及其相关信息的,链接器根据重定位表来修正指令中的符号地址 。
分段组装与链接脚本:链接过程的第一步是分段组装,链接器将各个可重定位目标文件重新分解组装。把代码段、数据段等分别合并,形成最终可执行文件的相应段。在这个过程中,链接脚本发挥着关键作用。链接脚本规定了各个段的组装顺序、起始地址、位置对齐等重要信息,同时也描述了输出可执行文件的格式、运行平台、入口地址等。在嵌入式裸机环境下,开发者常常需要根据开发板的硬件配置,如内存大小和地址,灵活指定链接地址或编写链接脚本。像U-boot源码编译的链接脚本U-boot.lds和Linux内核编译的链接脚本vmlinux.lds,它们对于系统的正确链接和运行起着决定性作用 。
符号决议与重定位:符号决议是链接过程中解决符号冲突的关键步骤。在一个多文件的工程中,可能会出现全局变量、函数重名的情况,链接器通过一套符号决议规则来处理这些冲突。函数名和初始化的全局变量是强符号,未初始化的全局变量是弱符号。规则规定强符号不允许多次定义;强符号和弱符号共存时,选择强符号;多个弱符号共存时,选择占用空间最大的那个。链接过程的最后一步是重定位,由于各个目标文件在链接时的地址发生了变化,需要对符号表中的符号地址进行修正。链接器根据重定位表中的信息,将指令中的符号地址更新为在可执行文件中的真实地址,确保程序在运行时能够正确访问函数和变量 。
操作系统环境下的运行:在有操作系统的环境下,基于ARM架构的嵌入式系统程序运行依赖于加载器。加载器会根据软件的安装路径信息,将可执行文件从ROM加载到内存,然后进行一系列初始化和动态库重定位操作,最后跳转到程序的入口运行。在Linux环境下,当在Shell终端运行一个程序时,类似sh、bash这样的Shell终端程序就充当加载器的角色。它会为程序创建一个子进程,将程序加载到进程空间,并建立进程虚拟地址空间与可执行文件的映射关系,将PC指针设置为程序的入口地址,从而启动程序运行。每个进程都有自己独立的虚拟地址空间,通过MMU(内存管理单元)将虚拟地址转换为物理地址,保证各个进程之间的内存隔离和安全 。
裸机环境下的运行:在裸机环境中,系统上电后没有像操作系统那样的程序运行环境,需要借助第三方工具将程序加载到内存才能运行。许多集成开发环境,如ADS1.2、Keil等,具备通过JTAG接口与开发板通信的功能,可以将编译好的ARM可执行文件下载到开发板的内存中。在下载时,需要根据开发板的实际RAM物理地址,在编译程序时通过开发环境提供的设置选项进行相应设置。以Linux内核镜像为例,它在启动过程中通常借助U-boot这个加载工具将其从Flash存储分区加载到内存中运行。U-boot不仅负责加载内核镜像,还会完成自身代码的“自复制”和重定位操作,以确保系统能够正确启动 。