前言
通过上一个博客,我们已经非常舒适的读完cortex-m0的启动代码,本节继续解读keil自带的cortex-m3启动代码,找找你的这个文件吧: G:\Keil_v5\ARM\PACK\ARM\CMSIS\5.3.0\Device\ARM\ARMCM3 G:\Keil_v5\ARM\PACK\ARM\CMSIS\5.3.0\CMSIS\Include
ARMCM3/
├── Include
│ ├── ARMCM3.h
│ └── system_ARMCM3.h
└── Source
├── ARM
│ └── startup_ARMCM3.s
├── GCC
│ ├── gcc_arm.ld
│ ├── startup_ARMCM3.c
│ └── startup_ARMCM3.S
├── IAR
│ └── startup_ARMCM3.s
└── system_ARMCM3.c
5 directories, 8 files
我们关注的是:
- startup_ARMCM3.S 启动脚本GCC汇编版本
- gcc_arm.ld链接脚本
- system_ARMCM3.c系统文件
- startup_ARMCM3.c启动脚本C语言部分
这次我们就不直接列出源文件了,而是一行不落的逐个分析,不然翻页困难。
1. Keil M3启动脚本
1.1 编译它
$ arm-none-eabi-gcc -O -c -g -Wall -mthumb -o startup_ARMCM3_as.o startup_ARMCM3.s
$ readelf.exe -h startup_ARMCM3_as.o
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: ARM
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 7392 (bytes into file)
Flags: 0x5000000, Version5 EABI
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 21
Section header string table index: 20
$ readelf.exe -S startup_ARMCM3_as.o
There are 21 section headers, starting at offset 0x1ce0:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 00008c 00 AX 0 0 4
[ 2] .rel.text REL 00000000 001a80 000058 08 I 18 1 4
[ 3] .data PROGBITS 00000000 0000c0 000000 00 WA 0 0 1
[ 4] .bss NOBITS 00000000 0000c0 000000 00 WA 0 0 1
[ 5] .stack PROGBITS 00000000 0000c0 000400 00 0 0 8
[ 6] .heap PROGBITS 00000000 0004c0 000c00 00 0 0 8
[ 7] .vectors PROGBITS 00000000 0010c0 0000c0 00 0 0 4
[ 8] .rel.vectors REL 00000000 001ad8 000110 08 I 18 7 4
[ 9] .debug_line PROGBITS 00000000 001180 000087 00 0 0 1
[10] .rel.debug_line REL 00000000 001be8 000008 08 I 18 9 4
[11] .debug_info PROGBITS 00000000 001207 000026 00 0 0 1
[12] .rel.debug_info REL 00000000 001bf0 000038 08 I 18 11 4
[13] .debug_abbrev PROGBITS 00000000 00122d 000014 00 0 0 1
[14] .debug_aranges PROGBITS 00000000 001248 000020 00 0 0 8
[15] .rel.debug_arange REL 00000000 001c28 000010 08 I 18 14 4
[16] .debug_str PROGBITS 00000000 001268 000032 01 MS 0 0 1
[17] .ARM.attributes ARM_ATTRIBUTES 00000000 00129a 00001b 00 0 0 1
[18] .symtab SYMTAB 00000000 0012b8 0004a0 10 19 22 4
[19] .strtab STRTAB 00000000 001758 000326 00 0 0 1
[20] .shstrtab STRTAB 00000000 001c38 0000a6 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
y (purecode), p (processor specific)
1.2 语法规则和架构
Cortex-M3是第一个Cortex处理器,其架构称为ARMv7-M。
.syntax unified
.arch armv7-m
1.3 栈、堆
定义了一个名为“.stack”的段,首地址8字节对齐,缺省占用空间0x400,链接器可以看到指定符号__StackTop 和__StackLimit 。
定义了一个名为“.heap”的段,首地址8字节对齐,缺省占用空间0xc00,链接器可以看到指定符号__HeapBase 和__HeapLimit 。
其中#ifdef-else-endif 是GNU的编码标准,跟C语言中的用法一样,不是汇编器独有的语法。
.if 、.endif 这种以点号. 打头的是汇编伪指令,意思也很直白。
.section .stack
.align 3
.equ Stack_Size, __STACK_SIZE
.equ Stack_Size, 0x00000400
.globl __StackTop
.globl __StackLimit
__StackLimit:
.space Stack_Size
.size __StackLimit, . - __StackLimit
__StackTop:
.size __StackTop, . - __StackTop
.section .heap
.align 3
.equ Heap_Size, __HEAP_SIZE
.equ Heap_Size, 0x00000C00
.globl __HeapBase
.globl __HeapLimit
__HeapBase:
.if Heap_Size
.space Heap_Size
.endif
.size __HeapBase, . - __HeapBase
__HeapLimit:
.size __HeapLimit, . - __HeapLimit
1.4 IVT
首先定义一个名为.vectors的段,对齐方式为4字节,定义了一个链接器可见的全局变量__Vectors。 .long 汇编伪指令同.int ,的语法为.long expressions,插入一个4字节的数据。
可见,链接脚本中将一系列数值塞入向量表,把__StackTop 塞入IVT的第一个位置,把Reset_Handler 塞入IVT的第二个位置,随后是14个异常向量、32个中断向量。
从readelf -S的结果可以看到,目标文件中的确有一个叫做“.vectors"的段,而且size是0x00c0=192字节,正好是塞进去的48个4字节数据。
.section .vectors
.align 2
.globl __Vectors
__Vectors:
.long __StackTop /* Top of Stack */
.long Reset_Handler /* Reset Handler */
.long NMI_Handler /* NMI Handler */
.long HardFault_Handler /* Hard Fault Handler */
.long MemManage_Handler /* MPU Fault Handler */
.long BusFault_Handler /* Bus Fault Handler */
.long UsageFault_Handler /* Usage Fault Handler */
.long 0 /* Reserved */
.long 0 /* Reserved */
.long 0 /* Reserved */
.long 0 /* Reserved */
.long SVC_Handler /* SVCall Handler */
.long DebugMon_Handler /* Debug Monitor Handler */
.long 0 /* Reserved */
.long PendSV_Handler /* PendSV Handler */
.long SysTick_Handler /* SysTick Handler */
/* External interrupts */
.long WDT_IRQHandler /* 0: Watchdog Timer */
.long RTC_IRQHandler /* 1: Real Time Clock */
.long TIM0_IRQHandler /* 2: Timer0 / Timer1 */
.long TIM2_IRQHandler /* 3: Timer2 / Timer3 */
.long MCIA_IRQHandler /* 4: MCIa */
.long MCIB_IRQHandler /* 5: MCIb */
.long UART0_IRQHandler /* 6: UART0 - DUT FPGA */
.long UART1_IRQHandler /* 7: UART1 - DUT FPGA */
.long UART2_IRQHandler /* 8: UART2 - DUT FPGA */
.long UART4_IRQHandler /* 9: UART4 - not connected */
.long AACI_IRQHandler /* 10: AACI / AC97 */
.long CLCD_IRQHandler /* 11: CLCD Combined Interrupt */
.long ENET_IRQHandler /* 12: Ethernet */
.long USBDC_IRQHandler /* 13: USB Device */
.long USBHC_IRQHandler /* 14: USB Host Controller */
.long CHLCD_IRQHandler /* 15: Character LCD */
.long FLEXRAY_IRQHandler /* 16: Flexray */
.long CAN_IRQHandler /* 17: CAN */
.long LIN_IRQHandler /* 18: LIN */
.long I2C_IRQHandler /* 19: I2C ADC/DAC */
.long 0 /* 20: Reserved */
.long 0 /* 21: Reserved */
.long 0 /* 22: Reserved */
.long 0 /* 23: Reserved */
.long 0 /* 24: Reserved */
.long 0 /* 25: Reserved */
.long 0 /* 26: Reserved */
.long 0 /* 27: Reserved */
.long CPU_CLCD_IRQHandler /* 28: Reserved - CPU FPGA CLCD */
.long 0 /* 29: Reserved - CPU FPGA */
.long UART3_IRQHandler /* 30: UART3 - CPU FPGA */
.long SPI_IRQHandler /* 31: SPI Touchscreen - CPU FPGA */
.size __Vectors, . - __Vectors
1.5 代码段
.text告诉汇编器as把下列内容放入代码段; .thumb选择Thumb指令集; .thumb_func用来指明随后的符号Reset_Handler是Thumb指令集编码的函数; .type汇编伪指令指定Reset_Handler符号的类型是函数。
.text
.thumb
.thumb_func
.align 2
.globl Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
1.6 程序入口Reset_Handler
Keil提供的链接脚本中有两种拷贝数据的方式,一种是一次可以拷贝多个段,但是要用到更多的指令;另一种是一次拷贝一个段,我们看这个简单的写法:
Reset_Handler:
...
/* copy from flash to ram */
ldr指令用于将执行单次存储器的读操作,读到寄存器中,读4字节。 ldr r1, =__etext的作用是将标号__etext代表的程序加载到寄存器r1中,其他两句类似。
怎么没看到__etext呢?因为它并不在这个文件中,而是在链接脚本gcc_arm.ld中,我把__etext摘抄到这里:
MEMORY
{
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 256K
}
ENTRY(Reset_Handler)
SECTIONS
{
.text :
{
KEEP(*(.vectors))
...
} > FLASH
...
__etext = .;
.data : AT (__etext)
{
__data_start__ = .;
...
__data_end__ = .;
} > RAM
...
.heap (COPY):
{
__HeapBase = .;
__end__ = .;
end = __end__;
KEEP(*(.heap*))
__HeapLimit = .;
} > RAM
.stack_dummy (COPY):
{
KEEP(*(.stack*))
} > RAM
...
__StackTop = ORIGIN(RAM) + LENGTH(RAM);
__StackLimit = __StackTop - SIZEOF(.stack_dummy);
PROVIDE(__stack = __StackTop);
/* Check if data + heap + stack exceeds RAM limit */
ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed with stack")
可见:
- 链接脚本把启动脚本中定义的
Reset_Handler 函数设置为程序入口 - 链接脚本把启动脚本中定义的向量表
.vectors 放在.text 输出段的0地址 - 启动脚本将链接脚本中定义的
__etext 加载到寄存器r1中,也就是.data的起始地址、也是.text段的结束地址 - 启动脚本将链接脚本中定义的
__data_start__ 、__data_end__ 分别加载到寄存器r2、r3中 - 启动脚本中定义了的
__StackTop 、__StackLimit 、__HeapLimit 等在链接脚本中又重新计算了
1.7 拷贝数据段
为什么要拷贝数据,因为flash是只读的,而每个进程运行时要对数据进行读和写,因此要拷贝到RAM中。而且RAM的访问速度大于flash。
ldr r1, =__etext
ldr r2, =__data_start__
ldr r3, =__data_end__
.L_loop1:
cmp r2, r3
ittt lt
ldrlt r0, [r1],
strlt r0, [r2],
blt .L_loop1
cmp 比较:计算r2-r3,APSR更新但结果不会保存 it 指令也就是条件执行指令(If-Then),后面跟条件,条件满足时接下来的最多4条指令可以根据条件执行,it操作码可以附加最多3个可选后缀t (Then)和e (Else)。 因此上面的语句伪代码可以理解为:
if (r2 < r3)
then
{
ldrlt r0, [r1],
strlt r0, [r2],
blt .L_loop1
}
else {
}
三个操作数的形式为Rm, [Rn], #offset ,这就是个后序存储器寻址模式,注意跟前序对比Rm, [Rn, #offset] 。
综上为:
- r1内容是flash中text段的截止位置、也就是数据的开始位置;r2、r3分别是RAM中数据区的起止地址;
- 比较r2、r3,也就是还没有到终点
__data_end__ ; - 满足r2<r3的条件的话,执行下列三条指令
- 将r1的内容指向的4字节数据从存储器中拷贝到寄存器r0中,并将r1的内容增加4;
- 将r0的数据从寄存器中拷贝到r2的内容指向的4字节存储器位置中,并将r2的内容增加4;
- 跳到2,继续。
1.8 bss置0
与上一小节类似,不过这段多了一个#elif ,也就是说想要在汇编语言中实现bss置0的话,要显式的定义宏__STARTUP_CLEAR_BSS ,否则这事汇编就不干了,交给C语言去干。
movs 指令在处理器内来回传送数据,例如寄存器之间、寄存器与特殊寄存器、将立即数送到寄存器。
ldr r1, =__bss_start__
ldr r2, =__bss_end__
movs r0, 0
.L_loop3:
cmp r1, r2
itt lt
strlt r0, [r1],
blt .L_loop3
1.9 调用SystemInit
要调用函数,可以使用链接跳转bl <label> 或带链接的间接跳转blx <Rm> 指令。
bl SystemInit
SystemInit()函数定义在system_ARMCM3.c系统文件中,如下:
#include "ARMCM3.h"
#define XTAL ( 5000000UL)
#define SYSTEM_CLOCK (5U * XTAL)
...
uint32_t SystemCoreClock = SYSTEM_CLOCK;
...
void SystemInit (void)
{
#if defined (__VTOR_PRESENT) && (__VTOR_PRESENT == 1U)
SCB->VTOR = (uint32_t) &__Vectors;
#endif
SystemCoreClock = SYSTEM_CLOCK;
}
1.10 调用_start
C程序的入口并不是main()函数,而是_start()函数,只不过gcc链接器默认会依赖一些库,然后_start()会调用main():
bl __START
例如在linux操作系统中: main()则是上次应用自己写的了,如果是裸机嵌入式系统开发,这里可以直接写成: (假设有个main()结束后的exit()函数)
bl main
bl exit
1.11 结束Reset_Handler
同cortex-m0。
.pool
.size Reset_Handler, . - Reset_Handler
1.12 定义宏与函数指针
同cortex-m0。
.macro def_default_handler handler_name
.align 1
.thumb_func
.weak \handler_name
.type \handler_name, %function
\handler_name :
b .
.size \handler_name, . - \handler_name
.endm
/* System Exception Handlers */
def_default_handler NMI_Handler
def_default_handler HardFault_Handler
def_default_handler MemManage_Handler
def_default_handler BusFault_Handler
def_default_handler UsageFault_Handler
def_default_handler SVC_Handler
def_default_handler DebugMon_Handler
def_default_handler PendSV_Handler
def_default_handler SysTick_Handler
/* IRQ Handlers */
def_default_handler UART0_Handler
def_default_handler GPIO0_Handler
def_default_handler GPIO1_Handler
def_default_handler QSPI0_Handler
def_default_handler DAP_QSPI0_Handler
def_default_handler DAP_SPI0_Handler
def_default_handler DAP_QSPI_XIP_Handler
def_default_handler DAPLinkFittedn
def_default_handler Unused_IRQ8
def_default_handler Unused_IRQ9
def_default_handler Unused_IRQ10
def_default_handler Unused_IRQ11
def_default_handler Unused_IRQ12
def_default_handler Unused_IRQ13
def_default_handler Unused_IRQ14
def_default_handler Unused_IRQ15
def_default_handler Unused_IRQ16
def_default_handler Unused_IRQ17
def_default_handler Unused_IRQ18
def_default_handler Unused_IRQ19
def_default_handler Unused_IRQ20
def_default_handler Unused_IRQ21
def_default_handler Unused_IRQ22
def_default_handler Unused_IRQ23
def_default_handler Unused_IRQ24
def_default_handler Unused_IRQ25
def_default_handler Unused_IRQ26
def_default_handler Unused_IRQ27
def_default_handler Unused_IRQ28
def_default_handler Unused_IRQ29
def_default_handler Unused_IRQ30
def_default_handler Unused_IRQ31
/*
def_default_handler Default_Handler
.weak DEF_IRQHandler
.set DEF_IRQHandler, Default_Handler
*/
.end
其中伪指令.end 标识这汇编文件全文结束,优雅。
2. Keil M3链接脚本
我们在这篇博客中已经系统的学习过了GCC链接器,正好借这个机会练练手。
2.1 内存配置
MEMORY
{
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 256K
}
2.2 增加输入库文件
GROUP(file, file, …),增加输入文件,只能是库文件libxx.a,效果等同于在ld命令后加选项-l ;
GROUP(libgcc.a libc.a libm.a libnosys.a)
这些依赖的库文件在哪里呢?不在Keil的安装包里,而是在GNU的交叉编译工具链里:
2.3 设置程序入口
ENTRY(Reset_Handler)
SECTIONS
{
...
}
2.4 代码段
flash的绝对地址本来就是0x0,这里又把flash的0偏移位置先存IVT,这个IVT就是启动脚本1.4小节中设置的.vectors 。随后放所有输入文件的.text段、再放所有输入文件的.rodata段。
.text :
{
KEEP(*(.vectors))
...
*(.text*)
...
*(.rodata*)
...
} > FLASH
中间省略了一些我们启动脚本中不涉及的部分; 其他放入flash的还有:.ARM.extab .ARM.exidx .copy.table .zero.table。
2.5 数据段
注意,这里和启动脚本相互配合,这些数据在芯片启动过程中要从FLASH拷贝到RAM。
__etext = .;
.data : AT (__etext)
{
__data_start__ = .;
...
*(.data*)
...
__data_end__ = .;
} > RAM
2.6 bss段
.bss段要和启动脚本相互配合,在启动过程中分配空间、并置0.
.bss :
{
. = ALIGN(4);
__bss_start__ = .;
*(.bss*)
*(COMMON)
. = ALIGN(4);
__bss_end__ = .;
} > RAM
2.7 堆、栈
以下都很直白,其中COPY 是输出段的type,功能是:指明该输出段不可分配(not allocatable),这样在程序运行时就不会为该部分分配内存。
.heap (COPY):
{
__HeapBase = .;
__end__ = .;
end = __end__;
KEEP(*(.heap*))
__HeapLimit = .;
} > RAM
.stack_dummy (COPY):
{
KEEP(*(.stack*))
} > RAM
__StackTop = ORIGIN(RAM) + LENGTH(RAM);
__StackLimit = __StackTop - SIZEOF(.stack_dummy);
PROVIDE(__stack = __StackTop);
ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed with stack")
至此,分析完毕。
总结
启动脚本还可以用C语言编写,启动脚本与uboot、外设初始化相关内容还没有研究。
|