找回密码
 立即注册
Qt开源社区 门户 查看内容

嵌入式CPU第一条指令是如何炼成的

2019-3-23 05:12| 发布者: admin| 查看: 1326| 评论: 0

摘要: 嵌入式CPU完成上电、复位,片内bootloader运行,转入user bootloader执行,从user bootloader“第一条指令”开始执行。从“第一条指令”开始执行,要问:【一】CPU如何从usr bootloader芸芸指令中甄别出“第一条指令 ...
嵌入式CPU完成上电、复位,片内bootloader运行,转入user bootloader执行,从user bootloader“第一条指令”开始执行。

从“第一条指令”开始执行,要问:

【一】CPU如何从usr bootloader芸芸指令中甄别出“第一条指令”?是事先“确认过眼神,遇上对的人”,还是随心所欲,“一见钟情”。

【二】“第一条指令”为什么偏偏出现在CPU第一次取指的期望位置?是巧合,还是“艳遇”。

【三】CPU拿到的“第一条指令”为什么就是它的可执行码,而不是其它CPU架构的可执行码?

备注:由于工作时间关系,本文先针对第一点展开说明,第二第三点只能后期再续写了。

CPU加载user bootloader的途径很多,本文以powerPC T4081从norFlash启动u-boot(2015.10)展开本文话题(其它启动模式大同小异,各种启动方式都说一遍,会搞坏人的)。所以,后文不再单独提CPU与user bootloader,而直接提T4081与u-boot。

        norflash启动,启动初期代码不需要拷贝,一方面,与norFlash的内部结构有关,使其读取支持linear address访问;另一方面,与norFlash对接的IFC/LBC接口也支持CPU linear address寻址。所以,CPU-IFC/LBC-norFlash这种方式的启动,代码可以在norFlash里面“执行”。同理,CPU+支持linear模式的QSPI+norFlash也能实现norFLash“执行”代码。支持linear address Q-SPI控制器的代表是XILINX zynq-7000这货,这货就能实现从Q-SPI挂接的norFlash启动,佯装直接透过CPU地址总线从norFlash启动。当然,非得要在SD card里面执行代码,不是不可能。只要CPU设计者愿意,顶着被炒鱿鱼的风险,在CPU内部地址总线与SD控制器之间加一个linear address硬转换层,也是可以的。然后,CPU支持在SD里面执行代码。当然,这个人很可能就滚出公司啦,没有得到任何赔偿金,因为他做了一件看起来高大上而没有商业价值的东西。

备注:“norflash启动,启动初期代码不需要拷贝”这句话严格来说是不对的。事实上也是需要拷贝之后再执行的。这里说的“不拷贝”,指的是不需要拷贝到CPU内部SRAM或者外挂DDR。事实上又需要“拷贝”,指的是指令代码需要以cache line为单位被“拷贝”到CPU指令cache的某些cache line。指令是不可能在norflash里面执行的,只能贮存在norFlash里面。

从norFlash启动,T4081拿到u-boot第一条指令,就执行。

调试串口有输出,调试网口ping通;

加载linux kernel与rootFs,主板各种接口功能、性能都满足要求;

加载测试程序,测试满足要求;

代码入库归档;

总共耗时不到30秒,一气呵成,项目开发完成,开发者轻松拿到项目奖。

哪有这么轻松!

事实上,仅搞定CPU的“第一条指令”牵涉的各个面,就够程序汪(比如本人)玩一辈子。

【一】

        T4081 E6500内核从预取到解码第一条机器码,在T4081内部要经过很多流程,实际上软件开发者需要了解的是:复位向量指向,内核指令内容来自I-Cache,Cahe指令内容来自IFC控制器,IFC控制器获取指令来自norFlash,norFlash里面存放的就是uboot二进制码,uboot二进制码是通过源码、MAKE、编译、链接精心炮制出来的。

T4081取第一条指令大致流程:CPU复位,内核复位向量指向第一条指令的“绝对地址”;几乎同时,CPU内部Instructtion Unit从I-Cache取指,几乎同时I-Cahe通过Coherency Fabric向IFC控制器提交一个cache line的burst读;紧接着,IFC控制器依次完成对norFlash的“相对地址”(地址偏移)读访问;norFlash将“相对地址”里面驻存的二进制码返回;最后,IFC控制器将获取到的二进制码返回给T4081 I-Cache cache line,I-Cache将复位向量指向的那条指令返回给Core(e6500)的Instructtion Unit, Instructtion Unit对这条二进制码解码,完成第一条指令执行。与此同时,要顺利完成取指,少不了CPU 内部存储架构 MMU/LAW窗口指向IFC 控制器所在memory space。



复位向量

        E6500内核复位向量为0x0_FFFF_FFFC,当T4081复位完成、Pre-boot loader执行完成,E6500就转入这个向量指向的地址去获取指令,这个取指动作不受软件干预,CPU内部完成取指。引起E6500掉入这个向量的必要条件,可以是上电复位,或是硬复位等。当然,软件或者仿真器强制PC指针指向这个地址,也是可以的,如果程序猿想玩儿CPU复位,当然前提是此时的norFlash状态in read mode,否则玩不起复位启动游戏。

从norFlash启动,T4081的硬件配置决定了其MMU窗口、LAW窗口均指向IFC CS0上的地址总线空间。所以,0x0_FFFF_FFFC这个复位向量,最终也就指向了IFC CS0上挂接的norFlash的某个地址。具体指向的哪一个地址,相对于norFlash内部来说,这个取决于IFC地址总线与norFlash地址线如何对接的。如果选用16Bits位宽的norFlash(S29GL01GPx),通常的做法是将IFC_AD[30:5]与norFlash的地址线A[0:25]一一对接。那么,0x0_FFFF_FFFC这个“绝对地址”,穿过MMU、LAW,到达IFC传递给norFlash内部,就是对应的相对于norFash顶端地址减去4,这是一个相对于norFlash mem top的一个“相对地址”。复位向量“绝对地址”到了IFC控制器对norFlash发起读的时候,变成了对“相对地址”(地址偏移)的访问。这个复位向量到底指向了norFlash的什么位置,已经不重要,只要我们把第一条指令存放到这个地址对应的norFlash相对偏移就行了。这个偏移到底在哪里,程序汪高兴就好。当然,也会受限于实际IFC与norflash的链接top,以及RCW配置(RCW怎么个限制法,参看T4081 RCW field关于IFC地址模式的介绍)。

ICache-L1/Cache-L2 load一个cache line

E6500内核获得复位向量0x0_FFFF_FFFC地址的指令前,T4081外设IFC上对norFlash并非仅发起了对0x0_FFFF_FFFC-0x0_FFFF_FFFF之间的读访问。而是一次性读取了64Bytes(T4081 RM手册描述:Each core begins execution with the instruction at effective address 0x0_FFFF_FFFC. To get this instruction, the core's first instruction fetch is a burst read of boot code from effective address 0x0_FFFF_FFC0)。

T4081上电时刻,复位向量在0x0_FFFF_FFFC,为什么一次性从0x0_FFFF_FFC0读取了64Bytes指令?因为T4081 RM手册就是这么描述的,所以……这完全是在扯淡。E6500取指时刻,如果指令Cache里面没有命中所指向地址的指令数据(数据:这种数据是E6500的机器码),几乎同时,指令Cache将从IFC控制器load至少一个cache line的数据。load指向的地址窗口是:0x0_FFFF_FFFC向下按一个cache line对齐的地址,到这个地址加cache line size。E6500 从IFC获取一条指令,需要通过内核ICache-L1,Cache-L2,这两者的cache line都是64Byts。原来,决定上电复位时刻一次性从norFlash获取了64Byts的指令,是Cache在捣。下图是T4081的L1/L2 cache的top:





备注:透过E6500的cache TOP,也说明这货的cache仅支持组相连(way-set-associative),不支持全相连(fully-associative),cache性能上有点小遗憾。

复位存储总线 MAP

启动时刻,E6500的PC需要访问0x0_FFFF_FFFC地址里面存放的指令,它说访问就访问?不给点过路费?难道你开车去几千公里以外,不经过几个收费站,给点买路钱什么的?T4081的内部存储架构的每一级就类似高速路出入口的收费站。要从此路行,留下买路钱。T4081存储架构由Core到IFC设备,要经过:MMU、LAW windows、IFC address space。E6500内核需要访问IFC外设的某个地址,这三个路口均需要打开窗口,寻址指令才能访问成功,否则等待你的就是异常。刚一上电就地址访问异常,异常处理入口还没有挂接,不死给你看,往哪里飞呢!而且死得不知道怎么死的。而这一阶段的异常,有可能导致仿真器通过JTAG接口没有办法链接CPU。这时候的CPU真的是失联了。偶尔,IFC接口的地址线或者数据随机虚焊或者不小心在norFlash里面瞎搞了一些“非法指令”,CPU真就会玩失联游戏(这里的“非法”不好界定,只有CPU厂商那帮做指令解码器的大佬们知道)。

T4081复位之后,针对每一个Core,MMU有一个页默认已经map,页(page)大小4KB,其地址范围为:[0x0_FFFF_Fnnn,0x0_FFFF_FFFF],map属性为实地址;其boot windows默认有8MB窗口可访问,其范围为:[0x0_FF80_0000,0x0_FFFF_FFFF];其IFC_CS0上IFC_CSPR0的默认值为0x0000_0000,IFC_AMSK0默认值为0xFFFF_FFFF,说明复位之后IFC_CS0上地址空间MAP到了[0x0000_0000,0xFFFF_FFFF]。再来看E6500复位之后首次一共访问了[0x0_FFFF_FFC0,0x0_FFFF_FFFF]之间的指令数据,落在了T4081复位之后的所有存储窗口的交集内。所以,内核对“第一条指令”的寻址会成功,不会死给你看。

        以上,大致理清楚了T4081复位对“第一条指令”访问的粗粒度过程。E6500内核复位向量指向0x0_FFFF_FFFC地址,ICache-L1/Cache-L2完成从IFC控制器load至少一个cache line地址里面的数据,其load范围为[0x0_FFFF_FFC0],0xFFFF_FFFF]。Load过程中,T4081内部存储框架的每一级的MEM窗口(MMU/LAW/IFC)均超过指令访问的窗口。所以,“第一条指令”地址里面的二进制码最终返回给了E6500的Instructtion Unit。其完成二进制码解码,E6500内核Instructtion Unit完成机器码执行。

这是为了“遇上对的人”,事先“确认过眼神”。不是“一见钟情”。

        人类精心炮制的第一条指令

        那我们就来一睹T4081第一条指令的芳容(来自uboot):

=> md 0xeffffff0 0x4                  

effffff0: ffffffff ffffffff ffffffff 4bfff004     

没错,就是“4bfff004”这货;准确地说,应该是二进制码(CPU能看懂的机器语言)“01001011111111111111000000000100”这货。

为什么在uboot里面读取第一条指令的存放地址为0xEFFF_FFFC,而非T4081复位向量0x0_FFFF_FFFC?前面都已经说过了,这个地址相对于norFlash来说,是一个相对偏移地址。说的直白一点,uboot反汇编出来的这个0xEFFF_FFFC norFlash地址,提示T4081第一条指令的存放位置,是给人看的。实际物理上,只要CPU域地址0x0_FFFF_FFFC通过IFC地址总线,在norFlash内部能访问到这条指令所在的偏移地址,就行了。至于在norFlash里面,这个地址在什么偏移位置,对于CPU来说,已经不重要。0x0_FFFF_FFFC通过IFC地址总线,硬件上想把他对接到norFlash的什么偏移地址上,硬件人员高兴就好。

那么,这里的0xEFFF_FFFC是怎么来的呢?这个地址与uboot存放的基地址定义(0xEFF4_0000)有关,他们仅仅是为了uboot链接统一编址使用。如果uboot统一链接编址的基地址为0xFFF4_0000,那么这里的0xEFFF_FFFC就变成了0xFFFF_FFFC。或者把它们按照某偏移换成别的地址试试,都可以!这个0xEFFF_FFFC在uboot的链接脚本里面也会用到,指向第一条指令的链接地址。在vxworks的bootrom里面,这个地址被换成了0xFFFF_FFFC。说白了,这个地址是给人看的,不至于人在写链接脚本与MAKE的时候,编址没有个统一的基址。

  先看看T4081第一条指令在uboot源码里面是什么。位于$(UBOOT_BASE)/arch/powerpc/cpu/mpc85xx/resetvec.s文件,如下:

.section .resetvec,"ax"

b _start_e500

其中,“b _start_e500”才是第一条指令,其它为链接器信息。这是一条跳转指令,跳转到了 _start_e500 lable所在位置。_start_e500位于:$(UBOOT_BASE)/arch/powerpc/cpu/mpc85xx/start.s,如下:

.section .bootpg,"ax"

.globl _start_e500

_start_e500:

/* Enable debug exception */

lir1,MSR_DE

mtmsr r1

……

其中,_start_e500:以下的为跳转之后的代码。仅仅从汇编代码看,b _start_e500确实跳转到了_start_e500位置,那就问,怎么证明的?不要说代码就是这么写的!这是在扯淡,拿结果当原因。就好比回答为什么存在万有引力:“是因为月球围绕地球转,自古以来就是这样的”,这么荒谬。之所以需要证明,是因为CPU跳转指令关心的是跳转到什么地址或者跳转的偏移,不关心汇编代码,也看不懂汇编代码;代码是人看的,不是CPU能读懂的;这里的代码是汇编语言,CPU能看懂的是机器语言。

既然第一条指令是一条跳转指令,那么,到底跳转到了什么地址?通过objdump指令,反汇编uboot文件,知道了答案。

在(UBOOT_BASE)执行如下命令:

$OBJDUMP u-boot -D > u-boot.s

查看u-boot.s,找到了“第一条指令”如下:



看到这个是不是觉得似曾相识。其中的4bfff004就是上文我们在norFlash里面看到的T4081第一条指令的机械码,以16进制形式出现在这里。这条指令跳转到了0xEFFF_F000地址,这个地址就是start.s _start_e500 里面的实现,如下:



这里面到底干了些什么,这里我们先不讨论。我们要讨论的是首条指令“4b ff f0 04”怎么就成了“b effff000 <_start_e500>”。因为看到目前的反汇编代码,依然仅仅是拿个结果去证明原因的存在。

“4b ff f0 04”,二进制是0b01001011111111111111000000000100,这已经是E6500内核的二进制机械码,也就是T4081能够读懂的机器语言。这条机器语言的汇编语言是“b _start_e500”,这条指令只有一个操作数据。PowerPC gcc编译本汇编指令,采用I-form指令格式去编译这条指令。下图是powerPC支持的指令格式:

格式

0

6

11

16

21

26

30

31

D-form

opcd

tgt/src

src/tgt

immediate

X-form

opcd

tgt/src

src/tgt

src

extended opcd

A-form

opcd

tgt/src

src/tgt

src

src

extended opcd

Rc

BD-form

opcd

BO

BI

BD

AA

LK

I-form

opcd

LI

AA

LK



对4b ff f0 04采用I-form指令格式拆分,变成:

opcd=010010,LI=111111111111110000000001,AA=0,LK=0。

查询powerPC 汇编指令集,翻译这段机器语言到底什么意思:



我们按照上图所示来一层层解析本指令。

Opdc=18,一条无条件跳转指令。

AA=0,LK=0。

LI||0b00:111111111111110000000001||0b00变成11111111111111000000000100

EXTS(LI||0b00):这句话的意思是将(LI||0b00)高位全部补符号位,就变成:

11111111111111111111000000000100,转换成16进制就是:0xFFFF_F004。

CIA+ EXTS(LI||0b00):这句话的意思是将EXTS(LI||0b00)与本指令的地址相加,即:0xEFFF_FFFC+ 0xFFFF_F004。再来看0xFFFF_F004是个什么鬼,在计算机世界这货就是负数0x0000_0FFC的计算机存储形式。CPU内部没有减法,减去一个数,被转换成与被减数的补码之间的加法。恰好负数的补码就是其源码的反码+1。所以这货0xFFFF_F004就是0x0000_0FFC的补码+1得来的。所以,0xEFFF_FFFC+ 0xFFFF_F004等价于0xEFFF_FFFC-0x0000_0FFC,结果就是0xEFFF_F000。0xEFFF_F000就是_start_e500的代码所在首地址。

        真相大白了,4b ff f0 04实际上就是机器语言的无条件相对跳转指令,跳转到当前指令地址偏移0xFFC的位置。也就是跳转到0xEFFF_F000。而0xEFFF_F000这里就放了如下代码:



_start_e500位于:$(UBOOT_BASE)/arch/powerpc/cpu/mpc85xx/start.s,如下:

.section .bootpg,"ax"

.globl _start_e500

_start_e500:

/* Enable debug exception */

lir1,MSR_DE

mtmsr r1

……

        翻译这条机器语言,脑袋整晕了。现在终于明白了汇编语言诞生的重要意义。如果整个uboot源码人工编译机器码,我估计没有人能坚持到最后。除非给一个小目标,比如一个亿,使用纸带打孔的方式搞一个uboot机器码出来。同时,也明白了编译器对节省人力的重要作用。因为uboot源码绝大部分是C语言,需要先把uboot C语言一条一条转换成汇编语言,然后再转换成机器语言,除了转换语言,还需要对跳转地址进行无冲突的链接。全部人工搞,给一个亿应该都不够,起码10个亿。

以上,回答了:“【一】CPU如何从usr bootloader芸芸指令中甄别出[第一条指令]?是事先[确认过眼神,遇上对的人],还是随心所欲,[一见钟情]。”

以上,都是基于单核去梳理的。T4081有四个E6500内核,一上电如果每一个内核都开启去启动,岂不是就紊乱了?这就超出了本节的讨论。后期写多核启动的时候再来细说。

【二】

        下一篇续…

【三】

 下一遍续…


----------------------------------------------------------------------------------------------------------------------
我们尊重原创,也注重分享,文章来源于微信公众号:风云幻 壹比特,建议关注公众号查看原文。如若侵权请联系qter@qter.org。
----------------------------------------------------------------------------------------------------------------------

鲜花

握手

雷人

路过

鸡蛋

公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了