CPU是靠执行指令“为生”的,从上电开始,到停电为止,都在执行指令,执行好一条,再执行下一条,执行好下一条,再执行下一条的下一条,执行好下一条的下一条,再执行下一条的下一条的下一条(^_^)。
既然如此,便有一个经典的问题,CPU是如何知道第一条指令在哪里的呢?
对于X86 CPU,答案比较简单,简单说是hard code的,也就是约定好的固定地址,即0xf000:0xfff0。
这个地址是所谓的实模式地址,冒号前面是段地址,后面是偏移,段地址左移4位加上偏移便得到20位的物理地址,即0xFFFF0。
上图中的汇编指令是通过DCI技术调试GDK7时得到的。可以看到第一条指令是一条跳转指令,此时还没有栈,所以不能做函数调用,只能跳转。
对于ARM CPU,这个问题要复杂一些,下面以ARM的M核CPU(以下简称M核)为例“格一下”。
ARMv7m的架构手册中定义了M核的复位行为,而且给出了伪代码。
伪代码的如下几行是关键:
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。
上面是文档上的说法,“纸上得来终觉浅”,下面再以基于M核的GDK3开发板为例,通过调试器来加深认识。
把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命令,观察栈回溯:
从栈回溯来看,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核的“复位即进入调试模式”开发的。
发出.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核上编程会给你带来独特的编程体验,摆脱操作系统的束缚,享受一且尽在掌握的自由,回归淳朴,重新思考计算机系统和软件的基本问题,会让你获益匪浅。
(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)
*************************************************
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物
也欢迎关注格友公众号