STM32高階開發(8)-連結器與啟動檔案
最近休息了一下,中間斷斷續續在虛擬機器上靠著記憶恢復了原來崩潰的虛擬機器上80%的工作成果,還算過得去吧,完全丟失的也就是些不大重要的資料。今天新買的機械鍵盤也到貨了,不得不說順豐的工作人員好評,給過年假期裡仍在工作的商家和快遞員們點個贊。現在我的感覺炒雞棒,所以我們繼續下面的教程吧~
在上一篇中我們介紹了,樣例工程中的makefile的工作原理和功能。我想對大多數童鞋來說理解編譯器將.c檔案編譯為.o檔案並不大困難,但是卻難以明白最後連結的過程是什麼作用和為什麼要這樣做。還有就是我們在樣例工程中啟動的檔案為什麼是自己編寫的,它又怎樣做到將程式入口引導到main函式上,那麼在這篇中我們就來深入的討論下這兩個話題。
連結器
連結的過程
首先,想要明白連結器的工作原理我們還是要來深入的看看整個編譯過程中具體的方式和原理。
我想大家都知道高階語言出現之前我們所用的組合語言是除機器碼外最接近硬體的語言。使用匯編的程式碼甚至可以很容易的手動轉換為機器程式碼。那麼接下來的介紹就需要童鞋們多少了解一點彙編程式了(如8051的彙編)。在微控制器執行的過程中命令被執行的順序只有兩種:順序執行和根據指令跳轉執行位置。在彙編的程式碼中,良好的寫法是把各個函式分塊放在儲存的不同位置上,並在前面寫上程式的標號 (如:“START:”),最後由編譯器將START程式處的地址裝入寫有 START標號跳轉指令的地方。
由此,我們就可以理解C語言被編譯為二進位制執行檔案的過程了,首先每個C檔案都被編譯為了.o
.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_IRQHandler
所以我們的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
在上面這段擷取自樣例工程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) }
}
至於連結時其他的連結引數大部分和編譯引數相同,不同的也就是:
--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
對於這些指令我只是大致的清楚是什麼,但具體的一些引數我也不大瞭解,如果大家有興趣可以自己檢索一下,或者最好的辦法就是到工具鏈中的說明文件尋找說明。
在我們實際的工程建立及編寫中,我們使用的都是從別處找來的ld檔案,在樣例工程中的.ld檔案只要在記憶體大小堆疊等位置上根據stm32具體的型號稍作修改就可以使用了。或者在之後我們介紹libopencm3的驅動庫中,其作者就有寫好的所有晶片型號的ld檔案,我們也可以從那裡複製並修改以用於我們自己的工程。其中ld檔案中一些變數如堆疊大小等我們會在講解啟動檔案的過程中來解析,因為啟動檔案和ld檔案中的東西息息相關。
啟動檔案
很多剛接觸stm32不久的童鞋對stm32的啟動檔案的印象大多就是教程裡的一句話:啟動檔案就是stm32在執行main函式前將系統初始化並把PC(即程式計數器,也就是當前執行程式碼位置的指標)設定到main函式的檔案。確實在KEIL或IAR之類的整合開發環境中我們不必關心啟動檔案的存在,但是在我們的gcc的使用中,我們就需要去理解這個檔案了。
在樣例工程中,我放置的是一個從CooCox開源整合開發環境中拷貝修改的啟動檔案,在USER目錄下的CoIDE_startup.c,這裡我就不放檔案的內容了,我們只去其中一部分來講。
其中具體的啟動檔案的說明和標註大家可以在下面的連結下載,這是一篇我當初學習啟動檔案時做的學習筆記,裡面詳細的標註了啟動檔案各個部分的作用,大家可以下載下來自己學習,由於是在OneNote中做的筆記,而且篇幅超長,在匯出為pdf時候會被截斷成幾個部分影響閱讀,所有就做成了網頁格式,大家可以用自己的瀏覽器開啟閱讀。(為了保證資源的永久有效性設定了下載1點的積分,請見諒。)
想要理解啟動程式碼,首先我們需要看看GNU編譯器的與其他編譯器不同的新特性之一:_attribute((xxx)),在gcc中attribute關鍵詞用於為函式或變數等賦予特性,就像MDK中的weak 說明符類似,只不過attribute的使用更具多樣性且靈活,關於attribute的具體介紹大家可以看看這個文章來了解:
其次我們要知道,在我們使用的Cortex-M3核心中,程式執行的最開始會從ROM首地址的第一位取出MSP的數值(即棧頂地址指標暫存器),然後會在第二位取出復位中斷函式的地址,並跳轉過去。且在一般來說,微控制器系統的所有中斷向量表初始時會放在ROM的最前段,所以我們定義了一個函式指標陣列在堆疊初始值的後方,構成了這樣一個被裝入ROM首段地址的資料:
__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\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; /*!< 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))屬性,該屬性表面其後的變數或是函式為弱申明,即在沒有其他申明情況下呼叫改函式,而如果其他地方申明瞭,則會頂替該函式。所以在啟動檔案中,他們被用來修飾中斷處理函為中斷向量表提供一個預設的地址,而當用戶定義後,就將地址轉為使用者定義的位置。
總結
說了這麼多,這也是我們在這個系列中比較難以理解的部分,因為涉及到了GNU C的特性和計算機編譯連結的最基礎的部分,還有Cortex-M3核心工作的方式,但是請大家仔細的去理解學習,如果看了這篇文章還不懂那就多查查相關的資料,當你理解並貫通 這些知識時,你會發現原來在微控制器上c語言是這樣工作的,原來中斷系統是這麼的重要,你會發現微控制器在你的眼前是如此的透徹。
在最後,我們還要說說,其實很多同學目前掌握的都是一個很簡單的微控制器應用方式,這都是被keil、IAR之流慣壞的,實際上在微控制器背後,其實際的工作複雜而又充滿著精緻的設計,這點我們會在之後的nuttx系統使用中見到。那時你會發現原來我們使用的M3微控制器還有這麼多的我們之前沒用過的中斷,原來m3的核心如此強大。對此我推薦大家還是學一遍51微控制器的彙編教程,當你理解和使用過彙編後,你會更容易理解未來的講解內容,同時也更容易理解此篇的內容。當然如果大家有興趣可以先自己看看由宋巖前輩翻譯的Cortex-M3 權威指南,來提前感受一下Cortex-M3核心的魅力。