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 文件生成 hexbin 文件, 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 字节的空间。

  如果你真的把这个文件看明白了并能熟练应用,那么恭喜你,你就解锁了一个重要技能包。

 

posted on 2019-10-03 23:00  中蓝闻道  阅读(2067)  评论(0编辑  收藏  举报

导航