对大多数童鞋来说理解编译器将.c文件编译为.o文件并不大困难,但是却难以明白最后链接的过程是什么作用和为什么要这样做? 还有就是我们在样例工程中启动的文件为什么是自己编写的,它又怎样做到将程序入口引导到main函数上,那么在这篇中我们就来深入的讨论下这两个话题。
.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程序的编译工具链应该都是以这种理念设计的。。当然不排除我见识少,没见过特殊的。
$(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文件。
/* 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文件中的东西息息相关。
__attribute__ ((used,section(".isr_vector"))) void (* const g_pfnVectors[])(void) = { /*----------Core Exceptions-------------------------------------------------*/ (void *)&pulStack[STACK_SIZE], /*!< The initial stack pointer */ Reset_Handler, /*!< Reset Handler */ NMI_Handler, /*!< NMI Handler */ HardFault_Handler, /*!< Hard Fault Handler */ MemManage_Handler, /*!< MPU Fault Handler */ BusFault_Handler, /*!< Bus Fault Handler */ UsageFault_Handler, /*!< Usage Fault Handler */ 0,0,0,0, /*!< Reserved */ SVC_Handler, /*!< SVCall Handler */ DebugMon_Handler, /*!< Debug Monitor Handler */ 0, /*!< Reserved */ PendSV_Handler, /*!< PendSV Handler */ SysTick_Handler, /*!< SysTick Handler */ /*----------External Exceptions---------------------------------------------*/ WWDG_IRQHandler, /*!< 0: Window Watchdog */ PVD_IRQHandler, /*!< 1: PVD through EXTI Line detect */ TAMPER_IRQHandler, /*!< 2: Tamper */ RTC_IRQHandler, /*!< 3: RTC */ FLASH_IRQHandler, /*!< 4: Flash */ RCC_IRQHandler, /*!< 5: RCC */ EXTI0_IRQHandler, /*!< 6: EXTI Line 0 */ EXTI1_IRQHandler, /*!< 7: EXTI Line 1 */ EXTI2_IRQHandler, /*!< 8: EXTI Line 2 */ EXTI3_IRQHandler, /*!< 9: EXTI Line 3 */ EXTI4_IRQHandler, /*!< 10: EXTI Line 4 */ DMA1_Channel1_IRQHandler, /*!< 11: DMA1 Channel 1 */ DMA1_Channel2_IRQHandler, /*!< 12: DMA1 Channel 2 */ DMA1_Channel3_IRQHandler, /*!< 13: DMA1 Channel 3 */ DMA1_Channel4_IRQHandler, /*!< 14: DMA1 Channel 4 */ DMA1_Channel5_IRQHandler, /*!< 15: DMA1 Channel 5 */ DMA1_Channel6_IRQHandler, /*!< 16: DMA1 Channel 6 */ DMA1_Channel7_IRQHandler, /*!< 17: DMA1 Channel 7 */ ADC1_2_IRQHandler, /*!< 18: ADC1 & ADC2 */ USB_HP_CAN1_TX_IRQHandler, /*!< 19: USB High Priority or CAN1 TX */ USB_LP_CAN1_RX0_IRQHandler, /*!< 20: USB Low Priority or CAN1 RX0 */ CAN1_RX1_IRQHandler, /*!< 21: CAN1 RX1 */ CAN1_SCE_IRQHandler, /*!< 22: CAN1 SCE */ EXTI9_5_IRQHandler, /*!< 23: EXTI Line 9..5 */ TIM1_BRK_IRQHandler, /*!< 24: TIM1 Break */ TIM1_UP_IRQHandler, /*!< 25: TIM1 Update */ TIM1_TRG_COM_IRQHandler, /*!< 26: TIM1 Trigger and Commutation */ TIM1_CC_IRQHandler, /*!< 27: TIM1 Capture Compare */ TIM2_IRQHandler, /*!< 28: TIM2 */ TIM3_IRQHandler, /*!< 29: TIM3 */ TIM4_IRQHandler, /*!< 30: TIM4 */ I2C1_EV_IRQHandler, /*!< 31: I2C1 Event */ I2C1_ER_IRQHandler, /*!< 32: I2C1 Error */ I2C2_EV_IRQHandler, /*!< 33: I2C2 Event */ I2C2_ER_IRQHandler, /*!< 34: I2C2 Error */ SPI1_IRQHandler, /*!< 35: SPI1 */ SPI2_IRQHandler, /*!< 36: SPI2 */ USART1_IRQHandler, /*!< 37: USART1 */ USART2_IRQHandler, /*!< 38: USART2 */ USART3_IRQHandler, /*!< 39: USART3 */ EXTI15_10_IRQHandler, /*!< 40: EXTI Line 15..10 */ RTCAlarm_IRQHandler, /*!< 41: RTC Alarm through EXTI Line */ USBWakeUp_IRQHandler, /*!< 42: USB Wakeup from suspend */ TIM8_BRK_IRQHandler, /*!< 43: TIM8 Break */ TIM8_UP_IRQHandler, /*!< 44: TIM8 Update */ TIM8_TRG_COM_IRQHandler, /*!< 45: TIM8 Trigger and Commutation */ TIM8_CC_IRQHandler, /*!< 46: TIM8 Capture Compare */ ADC3_IRQHandler, /*!< 47: ADC3 */ FSMC_IRQHandler, /*!< 48: FSMC */ SDIO_IRQHandler, /*!< 49: SDIO */ TIM5_IRQHandler, /*!< 50: TIM5 */ SPI3_IRQHandler, /*!< 51: SPI3 */ UART4_IRQHandler, /*!< 52: UART4 */ UART5_IRQHandler, /*!< 52: UART5 */ TIM6_IRQHandler, /*!< 53: TIM6 */ TIM7_IRQHandler, /*!< 54: TIM7 */ DMA2_Channel1_IRQHandler, /*!< 55: DMA2 Channel1 */ DMA2_Channel2_IRQHandler, /*!< 56: DMA2 Channel2 */ DMA2_Channel3_IRQHandler, /*!< 57: DMA2 Channel3 */ DMA2_Channel4_5_IRQHandler, /*!< 58: DMA2 Channel4 & Channel5 */ (void *)0xF108F85F /*!< Boot 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 < &_edata; ) { *(pulDest++) = *(pulSrc++); } /* Zero fill the bss segment. This is done with inline assembly since this will clear the value of pulDest if it is not kept in a register. */ __asm(" ldr r0, =_sbss " " ldr r1, =_ebss " " mov r2, #0 " " .thumb_func " "zero_loop: " " cmp r0, r1 " " it lt " " strlt r2, [r0], #4 " " 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的环境中我们也可以是用汇编编写这样的文件,我们面前的选择有很多,当然我们没必要自己编写这些链接文件和启动代码,在之后的实际的工程建立中我会告诉大家实际的方法。不过在此之前我们还是要先把基础的内容学好再说。
extern unsigned long _sidata; /*!< Start address for the initialization values of the .data section. */ extern unsigned long _sdata; /*!< Start address for the .data section */ extern unsigned long _edata; /*!< End address for the .data section */ extern unsigned long _sbss; /*!< Start address for the .bss section */ extern unsigned long _ebss; /*!< End address for the .bss section */ 而该文件却并未包含任何.h文件,那么他们从哪来的呢?细心的同学可能已经注意到了,我们之前提到过,这些变量的定义实际上都来自于ld文件中,他们在ld文件中被定义,最后链接器会将他们转换为实际的地址给我们的程序所使用的。 最后再说一下 attribute ((weak))属性,该属性表面其后的变量或是函数为弱申明,即在没有其他申明情况下调用改函数,而如果其他地方申明了,则会顶替该函数。所以在启动文件中,他们被用来修饰中断处理函为中断向量表提供一个默认的地址,而当用户定义后,就将地址转为用户定义的位置。
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !