对于许多初学者来说,理解编译器如何将.C源代码文件转换成.O目标文件通常是直接了当的。然而,编译流程的最后一步——链接,往往使人们感到迷惑,不理解它到底有什么用途以及为什么必须进行链接。
此外,我们在项目示例中使用的初始启动文件究竟是如何创建的,它是怎样将程序的执行入口定向到main函数的呢?在本文章中,我们将深入探讨这两个问题。
链接器简析
链接过程解析
要理解链接过程本身,我们首先需要更深入地考察整个编译流程的具体机制和原则。
大家可能都晓得,在高级编程语言诞生之前,汇编语言是继机器语言之后与硬件关系最为密切的编程语言。汇编编写的代码可以较为容易地被直接转换成机器码。因此,为了进一步了解链接的过程,读者可能需要具备一定的汇编语言知识(比如说,8051汇编语言)。
在微控制器执行指令时,指令通常有两种执行顺序:按顺序执行或根据跳转指令进行位置转移。在汇编代码中,建议的做法是将不同的功能函数编写在不同的存储位置,并在它们之前附上标签(例如:“START:”),编译器随后会将跳转到“START”标签的指令替换为实际的内存地址。
这样,我们就能理解C语言是如何被编译成二进制执行文件的了。每个C源文件都被编译器转化为带有未解析地址引用的.O目标文件。然后,工具链中的链接器负责将所有的.O目标文件进行组合,让它们在存储空间中有序排列,并解析函数地址,确保来自不同位置的代码能够跳转到正确的函数入口地址。最后,一个组织良好且可以被微控制器执行的文件就被成功生成。
至于源文件(.C)中定义的函数在微控制器存储空间中的实际排列顺序和地址位置,可以通过链接器产生的.MAP文件来查看。下面是从一个项目示例的.MAP文件中提取的片段:
.isr_vector 0x08000000 0x134
0x08000000 . = ALIGN (0x4)
*(.isr_vector)
.isr_vector 0x08000000 0x134 ./USER/CoIDE_startup.o
0x08000000 g_pfnVectors
0x08000134 . = ALIGN (0x4)
.text 0x08000134 0x1464
0x08000134 . = ALIGN (0x4)
*(.text)
.text 0x08000134 0x5c /home/yangliu/Library/gcc-arm-none-eabi-5_4-2016q3/bin/../lib/gcc/arm-none-eabi/5.4.1/armv7-m/crtbegin.o
.text 0x08000190 0x80 ./USER/main.o
0x08000190 main
.text 0x08000210 0x68 ./USER/CoIDE_startup.o
0x08000210 Reset_Handler
0x08000210 Default_Reset_Handler
0x08000268 EXTI2_IRQHandler
0x08000268 TIM8_TRG_COM_IRQHandler
0x08000268 TIM8_CC_IRQHandler
0x08000268 TIM1_CC_IRQHandler
0x08000268 TIM6_IRQHandler
0x08000268 PVD_IRQHandler
0x08000268 SDIO_IRQHandler
0x08000268 EXTI3_IRQHandler
0x08000268 EXTI0_IRQHandler
0x08000268 I2C2_EV_IRQHandler
0x08000268 ADC1_2_IRQHandler123456789101112131415161718192021222324252627
所以我们的gcc链接器就是用来做这个工作的,当然不只是gcc的链接器,世上所有c程序的编译工具链应该都是以这种理念设计的。。当然不排除我见识少,没见过特殊的。
工具链中链接器的用法
在实际中,链接器的执行程序实际上是arm-none-eabi-ld这个文件,但是我再实际的编写过程中在遇到.c和.cpp文件混合的工程中,ld会在链接过程中报错。而对此官方的说明是推荐使用arm-none-eabi-gcc指令来链接工程,它会自动的调用ld程序且不会出现上面这种情况,所以接下来我们都是以arm-none-eabi-gcc指令来介绍链接器工作的。
$(CC) $(C_OBJ) -T stm32_f103ze_gcc.ld -o $(TARGET).elf -mthumb -mcpu=cortex-m3 -Wl,--start-group -lc -lm -Wl,--end-group -specs=nano.specs -specs=nosys.specs -static -Wl,-cref,-u,Reset_Handler -Wl,-Map=Project.map -Wl,--gc-sections -Wl,--defsym=malloc_getpagesize_P=0x80
1
在上面这段截取自样例工程makefile的代码片中,我们可以看到在最后生成.elf文件时的指令。变量CC为arm-none-eabi-gcc,变量OBJ为所有.o文件。**-o xx.elf**为链接.o文件生成.elf文件。
ld文件
在链接的过过程中与编译过程相比其中显著的与编译指令不同的便是 -T xx.ld。
在这里 -T xx.ld实际上是调用了一个.ld的文件,那么.ld文件是做什么的呢?
这里就比较高深了,在51单片机中我们知道最后在生成代码后51单片机内存中会有如 code、xdata、data的区段,来讲代码中执行部分、变量部分等分区块放置,而.ld就是一种链接器使用的规则性文件,他告诉链接器单片机系统的ROM、RAM的地址和他们的大小等信息,并指示链接器将什么代码保存在什么位置。
对于.ld文件它是有一套自己的语法及设置参数的规则的,大家可以不具体作了解,但求看懂其中一部分的信息。
/* Entry Point */
ENTRY(Reset_Handler)
/* Highest address of the user mode stack */
_estack = 0x20010000; /* end of 64K RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0; /* required amount of heap */
_Min_Stack_Size = 0x200; /* required amount of stack */
/* Specify the memory areas */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 64K
MEMORY_B1 (rx) : ORIGIN = 0x60000000, LENGTH = 0K
}
SECTIONS
{
/* The startup code goes first into FLASH */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH
/* The program code and other data goes into FLASH */
.text :
{
. = ALIGN(4);
*(.text) /* .text sections (code) */
*(.text*) /* .text* sections (code) */
*(.glue_7) /* glue arm to thumb code */
*(.glue_7t) /* glue thumb to arm code */
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .; /* define a global symbols at end of code */
} >FLASH
/* Constant data goes into FLASH */
.rodata :
{
. = ALIGN(4);
*(.rodata) /* .rodata sections (constants, strings, etc.) */
*(.rodata*) /* .rodata* sections (constants, strings, etc.) */
. = ALIGN(4);
} >FLASH
.ARM.extab : { *(.ARM.extab* .gnu.linkonce.armextab.*) } >FLASH
.ARM : {
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
} >FLASH
.preinit_array :
{
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array*))
PROVIDE_HIDDEN (__preinit_array_end = .);
} >FLASH
.init_array :
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array*))
PROVIDE_HIDDEN (__init_array_end = .);
} >FLASH
.fini_array :
{
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT(.fini_array.*)))
KEEP (*(.fini_array*))
PROVIDE_HIDDEN (__fini_array_end = .);
} >FLASH
/* used by the startup to initialize data */
_sidata = LOADADDR(.data);
/* Initialized data sections goes into RAM, load LMA copy after code */
.data :
{
. = ALIGN(4);
_sdata = .; /* create a global symbol at data start */
*(.data) /* .data sections */
*(.data*) /* .data* sections */
. = ALIGN(4);
_edata = .; /* define a global symbol at data end */
} >RAM AT> FLASH
/* Uninitialized data section */
. = ALIGN(4);
.bss :
{
/* This is used by the startup in order to initialize the .bss secion */
_sbss = .; /* define a global symbol at bss start */
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .; /* define a global symbol at bss end */
__bss_end__ = _ebss;
} >RAM
/* User_heap_stack section, used to check that there is enough RAM left */
._user_heap_stack :
{
. = ALIGN(4);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(4);
} >RAM
/* MEMORY_bank1 section, code must be located here explicitly */
/* Example: extern int foo(void) __attribute__ ((section (".mb1text"))); */
.memory_b1_text :
{
*(.mb1text) /* .mb1text sections (code) */
*(.mb1text*) /* .mb1text* sections (code) */
*(.mb1rodata) /* read-only data (constants) */
*(.mb1rodata*)
} >MEMORY_B1
/* Remove information from the standard libraries */
/DISCARD/ :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}
.ARM.attributes 0 : { *(.ARM.attributes) }
}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
至于链接时其他的链接参数大部分和编译参数相同,不同的也就是:
--start-group -lc -lm -Wl,--end-group -specs=nano.specs -specs=nosys.specs -static -Wl,-cref,-u,Reset_Handler -Wl,-Map=Project.map -Wl,--gc-sections -Wl,--defsym=malloc_getpagesize_P=0x80
1
对于这些指令我只是大致的清楚是什么,但具体的一些参数我也不大了解,如果大家有兴趣可以自己检索一下,或者最好的办法就是到工具链中的说明文档寻找说明。
在我们实际的工程建立及编写中,我们使用的都是从别处找来的ld文件,在样例工程中的.ld文件只要在内存大小堆栈等位置上根据stm32具体的型号稍作修改就可以使用了。
或者在之后我们介绍libopencm3的驱动库中,其作者就有写好的所有芯片型号的ld文件,我们也可以从那里复制并修改以用于我们自己的工程。其中ld文件中一些变量如堆栈大小等我们会在讲解启动文件的过程中来解析,因为启动文件和ld文件中的东西息息相关。
启动文件
很多刚接触stm32不久的童鞋对stm32的启动文件的印象大多就是教程里的一句话:启动文件就是stm32在执行main函数前将系统初始化并把PC(即程序计数器,也就是当前执行代码位置的指针)设置到main函数的文件。确实在KEIL或IAR之类的集成开发环境中我们不必关心启动文件的存在,但是在我们的gcc的使用中,我们就需要去理解这个文件了。
在样例工程中,我放置的是一个从CooCox开源集成开发环境中拷贝修改的启动文件,在USER目录下的CoIDE_startup.c,这里我就不放文件的内容了,我们只去其中一部分来讲。
想要理解启动代码,首先我们需要看看GNU编译器的与其他编译器不同的新特性之一:*_attribute*((xxx)),在gcc中attribute关键词用于为函数或变量等赋予特性,就像MDK中的weak 说明符类似,只不过attribute的使用更具多样性且灵活。
其次我们要知道,在我们使用的Cortex-M3内核中,程序执行的最开始会从ROM首地址的第一位取出MSP的数值(即栈顶地址指针寄存器),然后会在第二位取出复位中断函数的地址,并跳转过去。且在一般来说,单片机系统的所有中断向量表初始时会放在ROM的最前段,所以我们定义了一个函数指针数组在堆栈初始值的后方,构成了这样一个被装入ROM首段地址的数据:
__attribute__ ((used,section(".isr_vector")))
void (* const g_pfnVectors[])(void) =
{
/*----------Core Exceptions-------------------------------------------------*/
(void *)&pulStack[STACK_SIZE], /*!suspend */
TIM8_BRK_IRQHandler, /*!in RAM mode */
};
注意在数组的attribute的修饰中,它将函数的位置规定在了[section(“.isr_vector”)]的位置,而[.isr_vector]则在ld文件中定义在FLASH开始的地方:
/* The startup code goes first into FLASH */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH
所以显而易见的,在启动后第二个周期里内核读取了复位向量表的地址并跳转了过去,所以单片机的启动代码必然存放于rest vector中,我们在启动文件中找到复位函数:
#pragma weak Reset_Handler = Default_Reset_Handler
void Default_Reset_Handler(void)
{
/* Initialize data and bss */
unsigned long *pulSrc, *pulDest;
/* Copy the data segment initializers from flash to SRAM */
pulSrc = &_sidata;
for(pulDest = &_sdata; pulDest done with inline assembly since this
will clear the value of pulDest if it is not kept in a register. */
__asm(" ldr r0, =_sbss\n"
" ldr r1, =_ebss\n"
" mov r2, #0\n"
" .thumb_func\n"
"zero_loop:\n"
" cmp r0, r1\n"
" it lt\n"
" strlt r2, [r0], #4\n"
" blt zero_loop");
/* Setup the microcontroller system. */
SystemInit();
/* Call the application's entry point.*/
main();
}
在启动函数中我们可以清晰地看到,在最后一步中,单片机的程序被转入到了main函数的入口,那么在执行main函数之前,C语言,和内联汇编程序干了什么呢?首先头位置的C语言将终端向量表从ROM头位置,复制到了RAM头位置(即:0x20000000),这里在RAM中的终端向量表时间上没有没我们用到,当然这是因为在M3的内核中,它允许用户在NIVC的寄存器中重新定义终端向量表的位置,我们可以使用
NVIC_SetVectorTable(NVIC_VectTab_FLASH,0);
这个函数来将终端向量表设置到到0x20000000位置。该功能实际上是用于方便装有系统的环境中使用,可以加快终端响应的速度,同时可以快速的动态的更改终端处理的程序。
当然在我们的应用中并未使用到这一特性,所以此处的复制中断向量表的操作是可以删除的,它在此的作用只是为了防止用户在程序中使用了重定向向量表语句而使得程序跑飞所添加的。因为终端向量是系统最基础稳定性的保证,如果在硬件错误发生等中断发生的情况下单片机无法正确的跳转,会对代码调试和系统稳定运行带来严重的影响。
之后紧跟的这几条汇编代码实现的是:全局变量与静态变量的初始化并将其从flash中调入内存,即在C语言运行全局变量与静态变量的初始化操作。在此之后, SystemInit();函数被调用,配置好时钟等参数。最后我们的main函数就可以执行啦~。
这便是是我们在这个例程中使用的启动文件,而在keil工程中,这个文件是用汇编代码写成的,但这些文件功能都是一样的,设置终端向量表,初始化全局与静态变量,进入main函数,都是这样的流程。
在gcc的环境中我们也可以是用汇编编写这样的文件,我们面前的选择有很多,当然我们没必要自己编写这些链接文件和启动代码,在之后的实际的工程建立中我会告诉大家实际的方法。不过在此之前我们还是要先把基础的内容学好再说。
其他的说明
在文件中我们看到了**_sidata、_sdata**等变量,这些变量在文件的前面部分被定义为外部:
extern unsigned long _sidata; /*!for the initialization
values of the .data section. */
extern unsigned long _sdata; /*!for the .data section */
extern unsigned long _edata; /*!for the .data section */
extern unsigned long _sbss; /*!for the .bss section */
extern unsigned long _ebss; /*!for the .bss section */
而该文件却并未包含任何.h文件,那么他们从哪来的呢?细心的同学可能已经注意到了,我们之前提到过,这些变量的定义实际上都来自于ld文件中,他们在ld文件中被定义,最后链接器会将他们转换为实际的地址给我们的程序所使用的。
最后再说一下 attribute ((weak))属性,该属性表面其后的变量或是函数为弱申明,即在没有其他申明情况下调用改函数,而如果其他地方申明了,则会顶替该函数。所以在启动文件中,他们被用来修饰中断处理函为中断向量表提供一个默认的地址,而当用户定义后,就将地址转为用户定义的位置。
总结
说了这么多,这也是我们在这个系列中比较难以理解的部分,因为涉及到了GNU C的特性和计算机编译链接的最基础的部分,还有Cortex-M3内核工作的方式,但是请大家仔细的去理解学习,如果看了这篇文章还不懂那就多查查相关的资料,当你理解并贯通 这些知识时,你会发现原来在单片机上c语言是这样工作的,原来中断系统是这么的重要,你会发现单片机在你的眼前是如此的透彻。
在最后,我们还要说说,其实很多同学目前掌握的都是一个很简单的单片机应用方式,这都是被keil、IAR之流惯坏的,实际上在单片机背后,其实际的工作复杂而又充满着精致的设计,这点我们会在之后的nuttx系统使用中见到。
那时你会发现原来我们使用的M3单片机还有这么多的我们之前没用过的中断,原来m3的内核如此强大。对此我推荐大家还是学一遍51单片机的汇编教程,当你理解和使用过汇编后,你会更容易理解未来的讲解内容,同时也更容易理解此篇的内容。当然如果大家有兴趣可以先自己看看由宋岩前辈翻译的Cortex-M3 权威指南,来提前感受一下Cortex-M3内核的魅力。
以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !