学了有半年的嵌入式,感觉无所成,总觉得学的不深,理解不透,而且很容易就忘了,于是也学人写写笔记,看看能否加深理解。 为了了解STM32从开机开始到C的main函数之前做了什么初始化的工作,分析编译出的可执行文件的运行流程,同时也可以学习bootloader的一些知识,所以有了这篇笔记。 下面的例子是基于Keil和STM32F103RBT6 1.首先,看STM32F10x.s(stm32的启动文件),里面的复位向量Reset_Handler执行了LDR R0, =__main BX R0也就是跳转到__main函数执行。一开始误以为__main就是C语言中main函数编译后的名称,后来查阅了Keil的帮助文件才知道:__main是C Library的一个函数,执行以下功能1)Copies non root (RO and RW) execution regions from their load addresses to their execution addresses. Also, if any data sections are compressed, they are decompressed from the load address to the execution address.2)Zeroes ZI regions.3)Branches to __rt_entry
也就是说,把加载域和执行域不同的RO和RW段从加载域复制到执行域(似乎还有把压缩的段解压缩,不清楚是什么情况),然后清零ZI(ZeroInit)段,最后跳转到__rt_entry(rt不知是不是runtime得缩写,求证)。
2.为了更清楚地知道__main做了什么事,可以查一下Keil生成的反汇编: 0x08000238 4808 LDR r0,[pc,#32] ; @0x0800025C@ LDR R0, =__main用PC的偏移量来存储__main函数的地址,主要是为了生成位置无关的代码。PC = 0x08000238,所以__main的地址为0x08000238+0x04(4)+0x20(32)=0x0800025C,查看0x0800025C处的代码:0x0800025C 00ED LSLS r5,r5,#30x0800025E 0800 LSRS r0,r0,#0看到__main的地址为0x080000EC(上面也说明了采用的是小端的存储方式,又因为Thumb模式PC的末尾必须是1,所以实际存储的地址应为0x080000ED),然后就跳转到__main函数执行。翻到__main所在的地址,看到__main函数就放在向量表的后面,向量表的大小为4*59(表里有59项)= 0xEC,所以__main的地址为0x080000EC。 3.进入__main后,先后调用的两个函数是__scatterload_rt2_thumb_only 和__rt_entry_sh。先看__scatterload_rt2_thumb_only,该函数把r10和r11分别赋值为0x08004724和0x08004744( 在这里有个小问题,Keil生成的汇编有这样一句:0x080000F4 A00A ADR r0,{pc}+4 ; @0x08000120查了一下文档,ADR没有这样的语法,而且若是把PC+4所得到的地址也不是0x08000120,后来查了《ARM v7-M Architecture Application Level Reference Manual》A00A这句机器代码应该是在PC的值上加上101000,8位立即数拓展成32位是要左移两位,然后存放到r0中,这样得到的地址就为PC+0x28+0x4=0x08000120,与程序执行结果一致。所以不清楚Keil的那句汇编是什么意思,求高手解释。) 那么0x08004724和0x08004744是什么地址呢,直接在生成的.map文件里面搜索这两个地址,Region$$Table$$Base 0x08004724 Region$$Table$$Limit 0x08004744从文件名猜测,这两个地址之间存储的是RO域和RW域的地址和其它相关信息,看一下执行流程:r0—r3分别被赋值为0x08004870 0x20000000 0x00000014 0x080001280x08004870是加载域RW段的起始地址,0x20000000是执行域RW段的起始地址,0x00000014是RW段的大小,0x08000128是__scatterload_copy函数的地址,之后就跳转到这个函数去执行RW段的复制。 执行完RW段的复制后,接着执行ZI段的清零,过程与上述相似。 4.接着该执行__rt_entry_sh(0x08000218)了: 1)跳转到__user_setup_stackheap(0x08003BC2) :MOV r5,lr @保存lr,下面要跳转,不用PUSH是因为SP还没设置好BL.W 0x08003D84 @跳到该地址,保存用户变量所占据的内存地址到r0MOV lr,r5 @还原lrMOVS r5,r0 @0x20000020MOV r1,spMOV r3,r10BIC r0,r0,#0x07 @将r0低三位清零,确保堆栈是八位对齐MOV sp,r0 @存入spADD sp,sp,#0x60 @SP加0x60,跳过了0x20000020到0x20000080的空间PUSH {r5,lr} @保存r5和lr,因堆栈已经设置好,所以可以PUSHBL.W __user_initial_stackheap (0x08000250)POP {r5,lr} @还原r5和lrMOV r6,#0x00MOV r7,#0x00MOV r8,#0x00MOV r11,#0x00 @清零四个寄存器BIC r1,r1,#0x07 @清零栈顶指针第三位MOV r12,r5 @0x20000020STM r12!,{r6-r8,r11}STM r12!,{r6-r8,r11}STM r12!,{r6-r8,r11}STM r12!,{r6-r8,r11} @清零地址0x20000020到0x20000080MOV sp,r1 @还原栈顶指针BX lr @返回 跳到__user_initial_stackheap是为了获取有关堆栈的地址信息:LDR R0, = Heap_Mem @堆的起始地址(0x20000080)LDR R1, =(Stack_Mem + Stack_Size) @栈顶指针(0x20000280)LDR R2, = (Heap_Mem + Heap_Size) @堆顶指针(0x20000080)LDR R3, = Stack_Mem @栈的起始地址(0x20000080) 在这里计算一下有关堆栈的地址:Execution Region :ER_RW (Base: 0x20000000, Size: 0x00000014, Max: 0xffffffff, ABSOLUTE)即RW的起始地址为0x20000000,大小为0x00000014Execution Region :ER_ZI (Base: 0x20000014, Size: 0x0000026c, Max: 0xffffffff, ABSOLUTE)即ZI段的起始地址为0x20000014,大小为0x0000026c。不过里面用户所用的大小仅为0x00000009,还有另外的0x00000003是用于补充对齐的。也就是说用户其实只占用到了0x20000020的内存。而上面又提及清零0x20000020到0x20000080。这个里面是什么东西呢?还是查一下.map文件:Base Addr Size Type Attr Idx E Section Name Object0x20000020 0x00000060 Zero RW 511 .bss libspace.o(c_w.l)所以这里面应该是为C库预留下来的一些空间。(可是在清零ZI段的时候,这些地址已经是清零过的了,至于为什么还要再细清零,可能是为了确保安全和可移植吧,不知这样理解对不对。) 也就是说,堆栈是从0x20000080开始的,堆大小为0x200,栈大小为0,这个是用户设定的,所以,栈顶指针就是0x20000280 2)执行__rt_entry_postsh_1(0x0800021E):什么也没做,跳到__rt_lib_init(0x080001F0)PUSH {r0-r4,lr} @下面的子程序要用到r0-r4,lr,保存BL.W _fp_init(0x08004624)BL.W main BL.W exit 无语,跳,_fp_initPUSH {r4,lr} @入栈保存BL.W __rt_fp_status_addr(0x08003E0C)MOV r1,#0x00STR r1,[r0,#0x00]POP {r4,pc} 。。。。。接着跳,__rt_fp_status_addrLDR r0,[pc,#4] ; @0x08003E14BX lr得到地址0x20000024后赋给r0,跳回_fp_init,之后将该地址清零。然后就跳回__rt_entry_postsh_1。 按Keil帮助文件的说法,__rt_fp_status_addr返回浮点数状态字的地址,而且浮点数状态字默认是0,至于这个是干嘛用的,还是不解,求高手指导。 3)紧接着终于轮到我们的main函数登场了(内牛满面啊!),单片机上的main函数一般是不会返回的,如果返回了就执行exit函数BL.W main (0x08000276)BL.W exit(0x08003D78) 5.总的来说,从上电到main函数之前,编译器为我们把RW段从加载域搬运到运行域,清零ZI段;设置好堆栈指针,初始化C库要用的的一些内存空间,设置浮点数状态字,做好一切初始化工作之后就跳到main函数执行,看来main函数前还是有很多活要干的,不是原来所想像的main就是C语言的入口这么简单。