ARM Cortex-M底层技术(3)—编译内核的原理及其应用
概述:
当前开发中,我使用的Keil开发工具较多(keil526),故以keil为例进行介绍,其他开发环境大同小异。
1. 编译链接的定义
不管我们编写的代码有多么简单,都必须经过「编译 --> 链接」的过程才能生成可执行文件:
- 编译就是将我们编写的源代码“翻译”成计算机可以识别的二进制格式,它们以目标文件的形式存在;
- 链接就是一个“打包”的过程,它将所有的目标文件以及系统组件组合成一个可执行文件。
抛开嵌入式而言,C语言的编译器有很多种,不同的平台下有不同的编译器,例如:
- Windows 下常用的是微软开发的 Visual C++,它被集成在 Visual Studio 中,一般不单独使用;
- Linux 下常用的是 GUN 组织开发的GCC,很多 Linux 发行版都自带 GCC;
- Mac 下常用的是 LLVM/Clang,它被集成在 Xcode 中(Xcode 以前集成的是 GCC,后来由于 GCC 的不配合才改为 LLVM/Clang,LLVM/Clang 的性能比 GCC 更加强大)。
意思就是说,在windows平台下进行开发时,我们选用的开发工具大部分是帮我们集成了一些编译器,只需要进行界面配置就可以了。这里我要最好是知道Keil开发环境中,这些编译链接工具是怎么使用的。
keil中编译链接如下,下面会对编译连接器进行解释:
2. Keil的编译链接
在第一讲中提过,当我们利用Keil进行编译一个工程时,下面的输出框中的内容是这样的:
Compiler编译器,可以看到该编译器在我们电脑中的D盘D:\Keil_v526\ARM\ARMCC\bin,如下图所示:
armar 是用于把.o 文件打包成 lib 文件,armasm 编译汇编文件 ,armcc 编译 c/c++文件 ,armlink 链接对象文件 ,fromelf 生成下载格式文件,它根据 axf 映像文件转化成 hex 文件,并列出编译过程出现的错误(Error)和警告(Warning)数量 。调用这些编译工具,需要用到Windows的“命令行提示符工具”,为了让命令行方便地找到这些工具,我们先把工具链的目录添加到系统的环境变量中。
2.1 添加环境变量
Win7 系统为例添加工具链的路径到 PATH 环境变量 。
(1)右键电脑系统的“计算机图标”,在弹出的菜单中选择“属性” ,如下图
(2)在弹出的属性页面依次点击“高级系统设置” ->“环境变量”,在用户变量一栏中找到名为“PATH”的变量,若没有该变量,则新建一个。编辑“PATH”变量,在它的变量值中输入工具链的路径,如本机的是“;D:\Keil_v526\ARM\ARMCC\bin”,注意要使用“分号;”让它与其它路径分隔开(英文分号),输入完毕后依次点确定,如下图
(3) 打开 Windows 的命令行,点击系统的“开始菜单”,在搜索框输入“cmd”,在搜索结果中点击“cmd.exe”即可打开命令行,见图
(4)在弹出的命令行窗口中输入“fromelf”回车,若窗口打印出 formelf 的帮助说明,那么路径正常,就可以开始后面的工作了;若提示“不是内部名外部命令,也不是可运行的程序…”信息, 说明路径不对,请重新配置环境变量,并确认该工作目录下有编译工具链。
这个过程本质就是让命令行通过“PATH”路径找到“fromelf.exe”程序运行,默认运行“fromelf.exe”时它会输出自己的帮助信息,这就是工具链的调用过程, Keil本质上也是如此调用工具链的,只是它集成为 GUI(界面),相对于命令行对用户更友好,毕竟上述配置环境变量的过程已经让新手烦躁了。解释一下,这个cmd框中的内容怎么和Keil对应起来。fromelf 可根据 axf 文件生成 hex、 bin 文件, hex和 bin 文件是大多数下载器支持的下载文件格式 。例如如果我们想利用 fromelf 生成 bin 文件,可以在 MDK的“Option for Target->User”页中添加调用 fromelf 的指令,如下图
还有链接器的配置界面如下:
那么这些东西有什么用呢?来看一下这段代码:
1 Reset_Handler PROC 2 EXPORT Reset_Handler [WEAK] 3 IMPORT SystemInit 4 IMPORT __main 5 6 LDR r0, =errorfunc 7 BLX r0 8 LDR r0, =__main 9 BX r0 10 ENDP
启动代码中的Reset_Handler代码,大家可以一眼看出来这段代码是错误的,其中红色字体的errorfunc是我故意填在这里的一个不存在的函数,编译必然报错,那么我们看看编译器是怎么报错的?
重点在途中蓝色底纹的部分,注意第一个单词:assembling……,然后后面跟着出错信息,没错,这是一个汇编器错误;再看下面,我再代码里面给出一个错误(我在main.c里面删掉了一个头文件):
看蓝色底纹部分,注意第一个单词:compiling……,然后后面跟着出错信息,没错,这是一个编译器错误,跟上面的不同,这是编译器报错,我们再看下面一个错误,代码如下
1 int main(void) 2 { 3 char ch; 4 5 /* Init board hardware. */ 6 /* attach 12 MHz clock to FLEXCOMM0 (debug console) */ 7 CLOCK_AttachClk(BOARD_DEBUG_UART_CLK_ATTACH); 8 9 BOARD_InitPins(); 10 BOARD_BootClockFROHF48M(); 11 BOARD_InitDebugConsole(); 12 13 PRINTF("hello world.\r\n"); 14 15 while (1) 16 { 17 ch = errorfunc(); 18 PUTCHAR(ch); 19 } 20 }
看蓝色底纹部分,注意第一个单词:linking……,然后后面跟着出错信息,这次是一个链接器错误!
大家经常说的编译器报错,其实是几个不同的东西再报错,编译器、汇编器、链接器都会报错,那你可能会问,知道这个有啥用呢?当然是有用的,比如汇编器报错,基本跟C语言没关系,基本可以断定是汇编语言语法错误或者是嵌入C语言的汇编语言出错(在C中嵌入汇编是一种非常有效的编程手段);如果是链接器报错,那就基本跟C语言语法无关,不是你的C语法上出错,很可能是你调用了不存在的函数或者链接器脚本写错了,或者使用了不存在的标号Symbol,或者没有包含头文件.h;而只有编译器报错,才总是你的C写的有问题.
3. 工作报表.map文件
真正有用的链接器描述文件“*.map”非常有用(编译链接后,双击工程列表下的第二个文件夹可以打开,变成灰色的那个)
“*.map”绝对是你的“核心员工”的工作报表,也是最复杂的一个。它主要包含交叉链接信息,查看该文件可以了解工程中各种符号之间的引用以及整个工程的 Code、 RO-data、 RW-data 以及 ZI-data 的详细及汇总信息。它的内容中主要包含了“节区的跨文件引用”、“删除无用节区”、“符号映像表”、“存储器映像索引”以及“映像组件大小”。
3.1 节区的跨文件引用
1 Section Cross References 2 3 startup_stm32f10x_hd.o(RESET) refers to startup_stm32f10x_hd.o(STACK) for __initial_sp 4 startup_stm32f10x_hd.o(RESET) refers to startup_stm32f10x_hd.o(.text) for Reset_Handler 5 startup_stm32f10x_hd.o(RESET) refers to stm32f10x_it.o(i.NMI_Handler) for NMI_Handler 6 startup_stm32f10x_hd.o(RESET) refers to stm32f10x_it.o(i.HardFault_Handler) for HardFault_Handler 7 startup_stm32f10x_hd.o(RESET) refers to stm32f10x_it.o(i.MemManage_Handler) for MemManage_Handler 8 startup_stm32f10x_hd.o(RESET) refers to stm32f10x_it.o(i.BusFault_Handler) for BusFault_Handler 9 startup_stm32f10x_hd.o(RESET) refers to stm32f10x_it.o(i.UsageFault_Handler) for UsageFault_Handler 10 startup_stm32f10x_hd.o(RESET) refers to stm32f10x_it.o(i.SVC_Handler) for SVC_Handler 11 startup_stm32f10x_hd.o(RESET) refers to stm32f10x_it.o(i.DebugMon_Handler) for DebugMon_Handler 12 startup_stm32f10x_hd.o(RESET) refers to stm32f10x_it.o(i.PendSV_Handler) for PendSV_Handler
......(省略)
在这部分中,详细列出了各个*.o 文件之间的符号引用。由于*.o 文件是由 asm 或 c/c++源文件编译后生成的,各个文件及文件内的节区间互相独立,链接器根据它们之间的互相引用链接起来,链接的详细信息在这个“Section Cross References”一一列出。
解释一下第3行,其他行的解释也差不多,第3行说明的是 startup_stm32f10x.o 文件中的“RESET”节区中的“__initial_sp” 符号(/函数)引用了同文件“STACK”节区(/函数)。这些跨文件引用的符号其实就是源文件中的函数名、变量名 。
3.2 删除无用节区
1 Removing Unused input sections from the image. 2 3 Removing startup_stm32f10x_hd.o(HEAP), (512 bytes). 4 Removing core_cm3.o(.emb_text), (32 bytes). 5 Removing system_stm32f10x.o(i.SystemCoreClockUpdate), (164 bytes). 6 Removing system_stm32f10x.o(.data), (20 bytes). 7 Removing misc.o(i.NVIC_Init), (112 bytes). 8 Removing misc.o(i.NVIC_PriorityGroupConfig), (20 bytes). 9 Removing misc.o(i.NVIC_SetVectorTable), (20 bytes). 10 Removing misc.o(i.NVIC_SystemLPConfig), (32 bytes). 11 Removing misc.o(i.SysTick_CLKSourceConfig), (40 bytes). 12 Removing stm32f10x_adc.o(i.ADC_AnalogWatchdogCmd), (20 bytes). 13 Removing stm32f10x_adc.o(i.ADC_AnalogWatchdogSingleChannelConfig), (16 bytes). 14 Removing stm32f10x_adc.o(i.ADC_AnalogWatchdogThresholdsConfig), (6 bytes). 15 Removing stm32f10x_adc.o(i.ADC_AutoInjectedConvCmd), (22 bytes).
......(省略)
这部分列出了在链接过程它发现工程中未被引用的节区,这些未被引用的节区将会被删除(指不加入到*.axf 文件,不是指在*.o 文件删除),这样可以防止这些无用数据占用程序空间。这部分是编译器自动做的,不需要人工参与。
3.3 符号映像表
这个表列出了被引用的各个符号在存储器中的具体地址、占据的空间大小等信息。如我们可以查到 LED_GPIO_Config 符号(0x080002c5)存储在 0x080002c4 地址,它属于 Thumb Code 类型,大小为 90 字节,它所在的节区为 bsp_led.o 文件的 i.LED_GPIO_Config 节区。
3.4 存储器映像索引
1 Memory Map of the image 2 3 Image Entry point : 0x08000131 4 5 Load Region LR_IROM1 (Base: 0x08000000, Size: 0x000005d4, Max: 0x00080000, ABSOLUTE) 6 7 Execution Region ER_IROM1 (Exec base: 0x08000000, Load base: 0x08000000, Size: 0x000005d4, Max: 0x00080000, ABSOLUTE) 8 9 Exec Addr Load Addr Size Type Attr Idx E Section Name Object 10 11 0x08000000 0x08000000 0x00000130 Data RO 3 RESET startup_stm32f10x_hd.o 12 0x08000130 0x08000130 0x00000000 Code RO 3207 * .ARM.Collect$$$$00000000 mc_w.l(entry.o) 13 0x08000130 0x08000130 0x00000004 Code RO 3210 .ARM.Collect$$$$00000001 mc_w.l(entry2.o) 14 0x08000134 0x08000134 0x00000004 Code RO 3213 .ARM.Collect$$$$00000004 mc_w.l(entry5.o) 15 16 ...... 17 18 0x080002c4 0x080002c4 0x00000060 Code RO 3192 i.LED_GPIO_Config bsp_led.o 19 0x08000324 0x08000324 0x00000004 Code RO 3132 i.MemManage_Handler stm32f10x_it.o 20 0x08000328 0x08000328 0x00000002 Code RO 3133 i.NMI_Handler stm32f10x_it.o 21 0x0800032a 0x0800032a 0x00000002 Code RO 3134 i.PendSV_Handler stm32f10x_it.o 22 23 ...... 24 25 26 Execution Region RW_IRAM1 (Exec base: 0x20000000, Load base: 0x080005d4, Size: 0x00000400, Max: 0x00010000, ABSOLUTE) 27 28 Exec Addr Load Addr Size Type Attr Idx E Section Name Object 29 30 0x20000000 - 0x00000400 Zero RW 1 STACK startup_stm32f10x_hd.o
本工程的存储器映像索引分为 ER_IROM1 及 RW_IRAM1 部分,它们分别对应 STM32内部 FLASH 及 SRAM 的空间。相对于符号映像表,这个索引表描述的单位是节区,而且它描述的主要信息中包含了节区的类型及属性,由此可以区分 Code、 RO-data、 RW-data 及ZI-data。
从上面的表中我们可以看到 i.LED_GPIO_Config 节区存储在内部 FLASH 的0x080002c4 地址,大小为 0x00000060,类型为 Code,属性为 RO。而程序的 STACK 节区(栈空间)存储在 SRAM 的 0x20000000 地址,大小为 0x00000400,类型为 Zero,属性为 RW(即 RW-data)。
3.5 映像组件大小
map 文件的最后一部分是包含映像组件大小的信息(Image component sizes),这也是最常查询的内容。
最后一部分列出了只读数据(RO)、可读写数据(RW)及占据的 ROM 大小。其中只读数据大小为 1492 字节,它包含 Code 段及 RO-data 段; 可读写数据大小为 1024 字节,它包含RW-data 及 ZI-data 段;占据的 ROM 大小为 1492 字节,它除了 Code 段和 RO-data 段,还包含了运行时需要从 ROM加载到 RAM的 RW-data数据(本工程中 RW-data数据为 0字节)。其实,在map文件中,连每一个函数的存储空间占用情况都会列出来(符号映像表 )。
总结:综合整个 map 文件的信息,可以分析出,当程序下载到 STM32 的内部 FLASH 时,需要使用的内部 FLASH 是从 0x0800 0000 地址开始的大小为 1492 字节的空间;当程序运行时,需要使用的内部 SRAM 是从 0x20000000 地址开始的大小为 1024 字节的空间。
如果你真的把这个文件看明白了并能熟练应用,那么恭喜你,你就解锁了一个重要技能包。