M核的第一条指令

CPU是靠执行指令“为生”的,从上电开始,到停电为止,都在执行指令,执行好一条,再执行下一条,执行好下一条,再执行下一条的下一条,执行好下一条的下一条,再执行下一条的下一条的下一条(^_^)。

既然如此,便有一个经典的问题,CPU是如何知道第一条指令在哪里的呢?

5c9e4f39cf3993efbf7bf4494acc2443.png

对于X86 CPU,答案比较简单,简单说是hard code的,也就是约定好的固定地址,即0xf000:0xfff0。

70c4610f27f3ccc6a452bb3694f7e38d.png

这个地址是所谓的实模式地址,冒号前面是段地址,后面是偏移,段地址左移4位加上偏移便得到20位的物理地址,即0xFFFF0。

a99afa26cbd27c38a05308d181727306.png

上图中的汇编指令是通过DCI技术调试GDK7时得到的。可以看到第一条指令是一条跳转指令,此时还没有栈,所以不能做函数调用,只能跳转。

对于ARM CPU,这个问题要复杂一些,下面以ARM的M核CPU(以下简称M核)为例“格一下”。

ARMv7m的架构手册中定义了M核的复位行为,而且给出了伪代码。

50b9eee3859cedbfac3179b2640eca05.png

伪代码的如下几行是关键:

bits(32) vectortable = VTOR<31:7>:'0000000';
SP_main = MemA_with_priv[vectortable, 4, AccType_VECTABLE] AND 0xFFFFFFFC<31:0>;
SP_process = ((bits(30) UNKNOWN):'00');
LR = 0xFFFFFFFF<31:0>; /* preset to an illegal exception return value */
tmp = MemA_with_priv[vectortable+4, 4, AccType_VECTABLE];
tbit = tmp<0>;
APSR = bits(32) UNKNOWN; /* flags UNPREDICTABLE from reset */
IPSR<8:0> = Zeros(9); /* Exception Number cleared */
EPSR.T = tbit; /* T bit set from vector */
EPSR.IT<7:0> = Zeros(8); /* IT/ICI bits cleared */
BranchTo(tmp AND 0xFFFFFFFE<31:0>); /* address of reset service routine */

根据上面的代码,M核是从向量表的起始4字节获得栈的位置,赋值给SP_main(即MSP)(第2行),从向量表偏移4开始的4字节中获取第一条指令的地址(第5行)。

那么向量表在何处呢?答案是VTOR寄存器。也就是VTOR寄存器的值代表向量表的位置。这个寄存器的值在复位时,会被设置为固定的值,即全0。f8971f40ea80cc8a7a3c9f975e0d7db6.png

上面是文档上的说法,“纸上得来终觉浅”,下面再以基于M核的GDK3开发板为例,通过调试器来加深认识。

9a41e6447595436ec89b95ecb1faea6a.jpeg

把GDK3通过挥码枪连到笔记本电脑后,唤出NanoCode(NDB调试器),发出break命令,将M核中断下来。

Loading symbols for 08000000         gem3.elf ->   gem3.elf
lk!Delay_Ms+39:
8000302 d004     beq    #0x800030e
r
 r0=000001f4  r1=00001770  r2=e000e010  r3=00000001  r4=00600030  r5=50000018
 r6=22000b40  r7=20004fb8  r8=02080044  r9=104c1200 r10=1100ca42 r11=0004b180
r12=08001993  sp=20041040  lr=080001db  pc=08000302 psr=21000000 --C-- ARM
lk!Delay_Ms+39:
8000302 d004     beq    #0x800030e

发出k命令,观察栈回溯:be7085b4bc247c37f7767100ca25c088.png

从栈回溯来看,main函数的父函数的名字叫Reset_Handler,是复位处理器的意思,看起来与前面的理论是一致的。

执行.frame 3切到Reset_Handler,可以看到它的代码,是用汇编语言写的。

/*******************************************************************************
 Reset handler
*******************************************************************************/
  .section  .text.Reset_Handler
  .weak  Reset_Handler
  .type  Reset_Handler, %function
Reset_Handler:  




/* Copy the data segment initializers from flash to SRAM */  
  movs  r1, #0
  b     LoopCopyDataInit

使用x命令观察函数Reset_Handler,可以看到它的内存地址,即08000a91  :

x lk!Reset_Handler
08000a91  lk!Reset_Handler

使用dds命令观察向量表:

dds 0
00000000  20005000
00000004  08000a91 lk!Reset_Handler [../../startup/startup_GDK3.s @ 38]
00000008  08000ad5 lk!EXTI2_IRQHandler [../../startup/startup_GDK3.s @ 77]
0000000c  08000ad5 lk!EXTI2_IRQHandler [../../startup/startup_GDK3.s @ 77]
00000010  08000ad5 lk!EXTI2_IRQHandler [../../startup/startup_GDK3.s @ 77]
00000014  08000ad5 lk!EXTI2_IRQHandler [../../startup/startup_GDK3.s @ 77]
00000018  08000ad5 lk!EXTI2_IRQHandler [../../startup/startup_GDK3.s @ 77]
0000001c  00000000
00000020  00000000
00000024  00000000
00000028  00000000
0000002c  08000ad5 lk!EXTI2_IRQHandler [../../startup/startup_GDK3.s @ 77]
00000030  08000ad5 lk!EXTI2_IRQHandler [../../startup/startup_GDK3.s @ 77]
00000034  00000000
00000038  08000ad5 lk!EXTI2_IRQHandler [../../startup/startup_GDK3.s @ 77]
0000003c  08000ad5 lk!EXTI2_IRQHandler [../../startup/startup_GDK3.s @ 77]
00000040  08000ad5 lk!EXTI2_IRQHandler [../../startup/startup_GDK3.s @ 77]
00000044  08000ad5 lk!EXTI2_IRQHandler [../../startup/startup_GDK3.s @ 77]

可以看到,偏移4的位置的确就是08000a91。

上面通过试验验证了文档中的描述。但是还不能确定是不是M核复位后就真的执行08000a91处的指令。

如果要做这个验证,可以使用NDB的重启命令,即.reboot,把目标系统复位,这个功能是基于M核的“复位即进入调试模式”开发的。

261af26070f8dec18a6e5594d330cd17.png

发出.reboot命令后,NDB的提示符短暂进入BUSY后又切换到命令状态。

meta poll returned 0
Loading symbols for 08000000         gem3.elf ->   gem3.elf
lk!$t:
8000a90 2100     movs   r1, #0
r
 r0=000001f4  r1=00001770  r2=e000e010  r3=00000001  r4=00600030  r5=50000018
 r6=22000b40  r7=20004fb8  r8=02080044  r9=104c1200 r10=1100ca42 r11=0004b180
r12=08001993  sp=20041040  lr=ffffffff  pc=08000a90 psr=01000000 ----- ARM
lk!$t:
8000a90 2100     movs   r1, #0

从寄存器上下文来看,M核真的是在8000a90 处的指令。

或许有细心的读者发现向量表里的地址和上面实际执行的地址略有差异,前者是08000a91,后者是08000a90。这是因为ARM的指令都是2字节或者4字节,所以当把一个地址加载到PC寄存器时,地址的最低位用来表示指令的类型,1代表2字节的Thumb指令。这个操作在arm手册中称为BXWritePC()或者LoadWritePC()。

BXWritePC(bits(32) address)
if CurrentMode == Mode_Handler && address<31:28> == ‘1111’ then
ExceptionReturn(address<27:0>);
else
EPSR.T = address<0>; // if EPSR.T == 0, a UsageFault(‘Invalid State’)
// is taken on the next instruction
BranchTo(address<31:1>:’0’);

看8000a90 处的指令,它的机器码只有两个字节,即0x2100,的确是Thumb指令。

8000a90 2100     movs   r1, #0

至此,我们不仅知道了M核的如何寻找第一条指令,也在GDK3上验证了这个过程,看到了它复位要执行的第一条指令是什么。

如果有读者意犹未尽,希望与格友们一起探索M核的更多奥秘,那么欢迎报名节后即将开始的在线课程《IoT实战之M之编程与调试》,无论你是否真的在做嵌入式开发,在小而美的M核上编程会给你带来独特的编程体验,摆脱操作系统的束缚,享受一且尽在掌握的自由,回归淳朴,重新思考计算机系统和软件的基本问题,会让你获益匪浅。

d15143d2b4ef842198672bc476df8d02.png

(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)

*************************************************

正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生

扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物

afb78b761faf9c1fa21b961f9749703d.png

也欢迎关注格友公众号

3bc2ab4d3feb4c1bd00e8a501675999e.jpeg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值