我是靠谱客的博主 义气水池,最近开发中收集的这篇文章主要介绍龙芯PMON(2K1000)启动流程(二、汇编部分)1、pmon 文件相关的地址问题2、start.S 在pmon 中的作用?3、为什么start.S 是在cpu 上电之后立即执行的代码?4、为什么gzrom.bin 的开头就是start.S?5、start.S 代码进行逐一分析6、initmips 代码分析,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

1、pmon 文件相关的地址问题

  cpu眼中的地址是虚拟地址,cpu 取指和取数据的地址是物理地址,经过北桥解释后的地址是总线地址,编译器产生的地址(包括解析了所有引用和重定位的符号后)为程序地址,也就是程序它自己理解的地址。不同的地址概念间需要有映射函数来关联。
  mips 的虚拟地址到物理地址的映射,既有可供页表之类动态建立映射函数的机制,也有unmapped cacheed/uncached 这种固定映射关系的转换方式。bios 代码由于其执行时机的限制,开始阶段只能使用unmapped uncached 这个段。地址范围是0xa0000000~0xc0000000,也就是2.5G 到3G 的512MB 空间。这个范围的映射函数为物理地址=程序地址-2.5G(高三位清0)。

kuseg:0x0000 0000-0x7FFF FFFF(低端2G) :用户太可用地址,必须通过MMU进行地址映射后才能正常访问。
kseg0:0x8000 0000 - 0x9FFF FFFF(512M):最高位清零就是对应的物理地址,映射到连续的低端512M物理地址。该地址空间通过高速缓存存取,主要用来存放操作系统核心。
kseg1:0xA000 0000 - 0xBFFF FFFF(512M):高3位清零就是对应的物理地址,映射到连续的低端512M物理地址。该地址空间不通过高速缓存存取,该区域主要用做I/O寄存器,系统ROM和启动时入口向量(0xBFC00000)就存于这个地址段内,因为系统启动时cache还未进行初始化。
kseg1:0xC000 0000 - 0xFFFF FFFF (1G):该地址段只能在核心态使用,并且需要经过MMU转换。

2、start.S 在pmon 中的作用?

  核心是把pmon 的二进制文件复制到内存。并初始化cache,内存控制器,内存和南桥的部分信号。这个代码执行之后会执行c 代码,解压在二进制中压缩的bin 文件,跳到解压后的代码继续执行。

  由于在这类介质中的执行速度比较慢,所以尽可能的,要把其中的bios 代码载入到内存执行,这需要先初始化内存控制器。要初始化内存控制器必须获得内存的spd 信息,而内存的spd 信息需要通过i2c 总线读取,而i2c 模块在南桥上,访问要在初始化smbus 之后,要访问南桥又必须先初始化北桥,要访问北桥又需要先初始化cpu本身。这是个一环扣一环的过程。start.S基本就是围绕这个流程展开的。当然在初始化南桥的SMB 总线以外,顺便把南桥相关的其他一些信号也初始了,这些具体的可能要看电路图了。

3、为什么start.S 是在cpu 上电之后立即执行的代码?

  MIPS cpu 约定cpu 执行的第一条指令位于虚拟地址0xbfc00000,而pmon 的二进制代码是以load -r -f bfc00000 gzrom.bin这个命令烧入bfc00000这个地址开始的flash,硬件布线会使得虚拟地址0xbfc00000映射到这块flash(物理地址0x1fc00000,如下图boot区)上。而start.S就占据了gzrom.bin 的开头部分。
在这里插入图片描述

4、为什么gzrom.bin 的开头就是start.S?

是由./zloader.ls2k/Makefine.inc 中的L98:${LD} -T ld.script -e start -o gzrom ${START} zloader.o 决定的。

5、start.S 代码进行逐一分析

-------------210行

mtc0 zero,COP_0_STATUS_REG
mtc0 zero,COP_0_CAUSE_REG

  zero 就是寄存器0($0),和下面的t0($8)之类相似,都由asm.h 中incldue 的refdef.h 定义的,由预处理去解析。
  一开始将状态寄存器和原因寄存器清零主要是禁用所有的中断和异常检测,并使当前处于内核模式。之后执行:

-------------212行

li t0,SR_BOOT_EXC_VEC
/* Exception to Boostrap Location */
mtc0 t0,COP_0_STATUS_REG

  以上两句只是设定当前的异常处理模式处于启动模式,启动模式(BEV=1)和正常模式(BEV=0)的区别是异常处理的入口地址不同。

-------------532行

la sp, stack
la gp, _gp

  sp 的赋值就是初始化栈,栈是函数(或过程)调用的基础设施,也是使用c 语言的前提。
stack= start -0x4000,可见栈的大小为16KB,不算小了。
_gp 在ld.script 中定义,gp 的作用主要是加快数据的访问,这里不予关注。

-------------236行

bal uncached /*Switch to uncached address space */
nop
uncached:
or ra,UNCACHED_MEMORY_ADDR
j ra
nop

  bal uncached这条指令是一个pc 相对跳转指令。or ra,UNCACHED_MEMORY_ADDR是把ra 寄存器和0xa0000000 相或,将cpu 的执行地址映射到unmapped uncached 段(对于一般的情形,这是多此一举,想想当前pc 寄存器的值)。

  由于地址问题是一个核心问题。下面解释一下龙芯2K1000 地址转化过程。

  CPU 获得的地址是虚拟地址,在获得这个地址之后,CPU 的内部逻辑首先判断这个地址落在哪个段,有的段虚拟地址到物理地址的转换公式是固定的,比如在32 位模式下,kseg0,kseg1 就是直接映射的。无论如何,在获得转换后的地址也就是物理地址后,cpu 并不知道这个地址是发往哪里,是内存,pci,还是内部的cpu 的io地址。这个发送方向也就是地址路由是由地址窗口寄存器决定的。

  2K1000的地址窗口是供 SCACHE0、SCACHE1 和 IO DMA 三个具有主功能的 IP 进行路由选择和地址转换而设置的。CPU 和 IO DMA 的访问都拥有 8 个地址窗口,可以完成目标地址空间的选择以及从源地址空间到目标地址空间的转换。每个地址窗口由 BASE 、MASK 和MMAP 三个 64 位寄存器组成,BASE 以 K 字节对齐,MASK 采用类似网络掩码高位为 1的格式,MMAP 的低三位是路由选择。

  二级交叉开关的地址配置具有地址转换的功能。不需要维护 CACHE 一致性。
窗口命中公式: (IN_ADDR & MASK) == BASE
新地址换算公式: OUT_ADDR = (IN_ADDR & ~MASK) | {MMAP[63:10],10’h0}

  在2K1000中,这些地址寄存器的地址是从0x1fe1_0000 开始的48 个寄存器。注意,这个地址是物理地址。

  比如0xbfc00000 这个虚拟地址,cpu 首先判断这个地址落在kseg1(0xa0000000~0xc0000000) 段,直接转换且不经过cache,转换成的物理地址0x1fc00000。接着地址窗口寄存器就派上用场了。一个一个窗口去判断这个地址是否落在自己管辖的范围内(当然更有可能这个动作是并行的)。假设首先cpu 窗口0 去判断,它拿自己的mask 寄存器值为(0xFFFF_FFFF_FFF0_0000)去mask 这个传入的地址。mask 后,0x1fc00000 就成了0x1fc0_0000,这个地址是cpu 地址窗口0 的基地址,所以它就负责由cpu 窗口0来路由。在传往下一个部件之前这个地址会被cpu 窗口0 包装一下,下面的就认识了。

  这个新地址转换公式我没怎么看懂,当然也可能是作者文档没写清楚。有兴趣的可以看看。以上的地址转换在cpu 核内,还没到桥片的层次。

根据缺省的寄存器配置,芯片启动后,

CPU0x1FC0_0000-0x1FCF_FFFF 的地址区间(1M)映射到 BOOT 设备(SPILIO)存储空间的 0x00000000-0x000F_FFFF 区间,
CPU0x10000000 - 0x1FFFFFFF 区间(256M)映射到 BOOT 设备的 0x10000000 - 0x1FFFFFFF区间,
CPU0x00000000 - 0x0FFFFFFF 的地址区间(256M)映射到 DDR30x00000000- 0x0FFFFFFF 的地址区间,
CPU0x100000000 - 0x1FFFFFFFF 的地址区间(4G)映射到DDR30x00000000 - 0xFFFFFFFF 的地址区间。

  软件可以通过修改相应的配置寄存器实现新的地址空间路由和转换。建议不要自己随意改动。
  此外,当出现 8 个地址窗口都不命中时,由内存控制器模块返回数据。
  下面结合代码解释pmon 二进制的地址问题。
  在链接阶段,使用了Targets/LS2K/conf/ld.script作为链接的规则。生成的gzrom 程序地址是从0xffffffff8f010000开始。ld.script 的片段如下:

ENTRY(_start)
SECTIONS
{
. =0xffffffff8f010000;
.text :
{
_ftext = . ;
*(.text)

  因此gzrom(包括用objcopy 生成的gzrom.bin)的程序地址是0xffffffff8f010000到其后的有限范围之内的。而在cpu 执行这段代码之初,pc 寄存器的值是0xffffffffbfc00000,而程序自己认为这条指令的地址是0xffffffff8f010000,在寻址方式为pc 相对寻址或立即寻址时没有任何问题,在其他寻址方式时就会出现问题。比如在pmon 拷贝到内存之后,现在要引用地址为0xffffffff82000000 这个程序地址的内容,指令实质为ld t0,0(0xffffffff82000000),问题就出来了,0xffffffff82000000这个程序地址对应的段会使用cache,而当前cache还没有初始化,cache 不可用,唯一的解决方法就是将这个地址转换到0xffffffffa0000000~0xffffffffc0000000这段空间内,且能映射到同一物理位置的地址,其实只要在原地址的基础上再加一个偏移值即可。

-------------223行

  lacate 的目的主要就是算出这个偏移值,具体计算如下:

locate:
la s0,start
subu s0,ra,s0
and s0,0xffff0000

  这里有个很合理的疑问,为什么链接规则文件ld.script 中程序地址(start)要从0xffffffff8f010000 开始呢?直接搞成0xffffffffbfc00000 不更简单吗?
下面分析分析这个选择的原因.从传统的32 位地址空间的分布看,共有4 个段:

0 ~0x80000000
0x80000000~0xa00000000
0xa0000000~0xc0000000
0xc0000000~0xffffffff

  首先第一个段和第四个段首先被排除,因为这两个段内的程序地址转化需要页表的辅助,现在连内存都不可用,不可能用页表。

  再要排除的是第三个段,用这个段内的地址,虽然可以免去上面这个偏移值的麻烦,但是在rom 执行的这段代码在整个bios 的执行过程中只占很少的一部分,99%以上的代码是要被载入内存执行的,就没有这个偏移值了,且在内存执行时,

  当然要尽可能使用cache 加快执行速度,第三个段的地址引用不使用cache。当然要提一句,如果不用cache,使用第三个段作为程序地址也是有可能的。

  locate 除了计算以上说的那个偏移值到s0 外,接着重新初始化状态寄存器和原因寄存器,这个代码应该是不必要的。如果使用cpu自带的串口模块,则初始化这个模块。在显卡没有初始化的情形下,调试最好的方式就是使用串口。

-------------1059行

bal
initserial
nop

  initserial代码和普通的串口除了地址,没有区别,具体可参看任何一本接口技术书籍,都有讲解这个NS16550 兼容芯片的。龙芯cpu的串口地址COMMON_COM_BASE_ADDR 为0xbfe00000,这里要说一句,其实这个0xbfe00000 就是0xffffffffbfe00000,因为龙芯2 号系列cpu 都是64位的,而且mips 结构的64 位cpu 都是32 位兼容的,32 位的地址都默认被符号扩展成64 位的,前头的0xbfc00000 其实和0xffffffffbfc00000 也是一样的。

-------------1171行


mfc0
a0, COP_0_CONFIG
/* enable kseg0 cachability */
ori
a0, a0, 0x3
// ENABLE
mtc0
a0, COP_0_CONFIG
#include "pcitlb.S" /* map 0x4000000-0x7fffffff to 0xc0000000 */

  使能kseg0的cache能力。
  将虚拟地0xc0000000~0xffffffff 映射到物理地址0x40000000~ 0x7fffffff 作为pci mem空间

-------------1439行

从开机启动log可以看到:

start = 0x8f900000
s0 = 0x30300000
_edata = 0x8f99f010
_end = 0x8f9a0028
用于后面清处bss段空间
8f990000
8f900000
copy text section done.

  L1439~L1468 是复制的具体代码,是指就是一个循环,每次复制一个字(4 bytes)。 之后清零bss 段所占的空间。

la
a0, start
li
a1, 0xbfc00000
la
a2, _edata
move
t0, a0
t0 表示复制的目的起始地址
start
0x8f900000
move
t1, a1
t1 是复制的源地址 ​​​​​ 0xbfc00000
0xbfc00000
move
t2, a2
t2 是复制的目的结束地址 _edata
0x8f999010
/* copy text section */
1:
and
t3, t0, 0x0000ffff
将start地址0x8f9000000x0000ffff
bnez
t3, 2f
判断结果是否为0,不为0跳转至2分支
nop
move
a0, t0
bal
hexserial
打印t0,log值显示为0x8f990000
nop
li
a0, 'r'
bal
tgt_putchar
打印t0,log值显示为0x8f900000
nop
2:
lw
t3, 0(t1)
从t1取数据写进寄存器
sw
t3, 0(t0)
将寄存器中的值写入t0
addu
t0, 4
一次写四个字节,所以地址需要增加4
addu
t1, 4
blt
t0, t2, 1b
判断当前地址是否大于_enddata,否则跳转到1分支
nop
PRINTSTR("ncopy text section done.rn")

-------------1568行

move
a0, msize
调试将msize打印出来显示:0x00000004
为什么是4呢?因为这个内存单位是512MB,板子上的内存总大小是2GB
la
v0, initmips
jalr
v0
nop

  以上三句,是cpu 从rom 取指令到从内存取指令的转变,从此内存执行的时代 开始了。

  这个initmips 在哪里呢?

  看过一些文档都说是在tgt_machdep.c 中,我一开始也 是这么认为的,但看看前面的a0,a1,a2 显然都是作为参数列表使用的(去看看 o32 的参数传递规则mips的函数调用有点意思, 它会将函数的部分参数直接保存在寄存器中,一般是[a0,a3],来提高性能。)。那个文件中的initmips 只有一个参数呀,实际上这个
函数的定义在initmips.c 中,这个文件是在编译过程中产生的。看看zloader.ls2k/Makefile.inc文件中的line119,看到了吧

initmips.c:
../Targets/${TARGET}/compile/${TARGETEL}/pmon ${gencode} $< > initmips.c

  initmips 函数定义在zloader.ls2k/initmips.c 中L74,当初我是怎么发现这个函数的呢?我在通过串口(显卡初 始化前只能用串口输出)做pmon 的实验的时候发现中间的“Uncompressing Bios”这一句在不知道什么时候打印出来的,

  jalr v0的操作是PC<–v0,不是bal 那样的PC+offset。

  initmips的地址是0x8开头的,并不使用TLB,是否使用cache要看config寄存器的低3位,前面说到了,程序只设置了低两位为。

6、initmips 代码分析

initmips(zloader.ls2k/initmips.c)

-----------------80行

if(!debug||dctrl&1)enable_cache();

  这个debug 的值正常情况下为0(因为msize不为0),也就是enable_cache函数要执行。但是有个问题,这个dctrl 是什么东西?这个只是通过形式参数传入的,按照o32 的标准也就是a0 对应函数的第一个形参(从左往右数,)依此类推。a2 对应的就是第三个参数。这个initmips 的函数定义是initmips(unsignedint msize,intdmsize,int dctrl),也就是a2 对应的就是dctrl,我们肯定还记得,调用initmips 前给a0,a1,a2 这三个寄存器赋了值,a0 是内存大小(单位MB),a1 是0xbfc00000,a2 是_edata地址,对照这个函数的定义,第一个参数肯定是对的,当后面的两个就对不上了。从形参命名上看,应该是cache的是否开启的判断标识。

---------------97行

while(1)
{
if(run_unzip(biosdata,0x8f010000)>=0)break;
}

  从名字就可以看出来是在解压。biosdate 是一个数组, 内容在zloader/pmon.bin.c 中。我第一次打开pmon.bin.c 文件是差点昏过去。pmon.bin.c 文件的生成见zloader/Makefile.inc文件126行:

pmon.bin.c:
../Targets/${TARGET}/compile/${TARGETEL}/pmon.bin
gzip $< -c > pmon.bin.gz
./bin2c pmon.bin.gz pmon.bin.c biosdata

  bin2c 也是个perl 脚本,就是把pmon.b in.gz 的内容转化成biosdata 这个数组,否则不方便集成到代码中。

  这个函数的作用就是把biosdata 的数组加压到0x8f010000 开始的地址上去。当然这个biosdata 的内容究竟是什么呢? 我们是一定要探究的。biosdata 来自于pmon.bin.gz,pmon.bin.gz 来自于pmon.bin ,那么pmon.bin 是哪儿来的呢?
由于gzrom.bin 是来自于objcopy 后的gzrom,这个pmon.bin 很可能是来自于名为pmon 的elf 文件。而且Targets/LS2K/compile/ls2k/下正好有个叫pmon的elf 文件,无疑了。

  问题来了,这个pmon 文件怎么来的!
  看Targets/LS2K/compile/ls2k/Makefile ,其实这个pmon 几乎把所有代码都编进去了,包括start.o 。 是代码段地址从0xffffffff80100000 开始分配。个人推断,这个pmon 用objcopy 后的pmon.bin是可以直接使用的,只是没有压缩。有兴趣时试试。

  实际上Targets/LS2K/compile/目录只是一个中转站。回到initmips 这个函数。
  接下是bss 段的初始化,就是清零。
  接着调用flush_cache2(),就是把二级缓存都置为无效。如果记性好的话,前面已经做了同样的工作了。大小是512*1024 。
  initmips 函数最后调用realinitmips 这个函数,从名字看出,这个realinitmips 才是大家都认为的initmips。genrom 脚本中获取了真实的initmips 的地址,这个函数还初始化sp 寄存器,栈大小为16KB(0x4000),并把msize 作为参数传递给那个initmips 函数,然后跳转到那个函数。
  从现在开始执行的就是那个叫pmon.bin 的文件的内容了,看反汇编也要看Targets/LS2K/compile/ls2k/下那个pmon 文件的反汇编了,从此程序地址都不再用0x81 开头的了,都用0x8010 开始的了。

/opt/gcc-4.4-gnu/bin/mipsel-linux-objdump -d pmon > objcode
pmon:
file format elf32-tradlittlemips
Disassembly of section .text:
8f010000 <_ftext>:
8f010000:
3c02bfff
lui
v0,0xbfff
8f010004:
34420225
ori
v0,v0,0x225
8f010008:
240300ff
li
v1,255
8f01000c:
a0430000
sb
v1,0(v0)
8f010010:
04110ed3
bal
8f013b60 <ls2k_version>
8f010014:
00000000
nop
8f010018:
14400019
bnez
v0,8f010080 <_ftext+0x80>
8f01001c:
00000000
nop
8f010020:
40087801
0x40087801

最后

以上就是义气水池为你收集整理的龙芯PMON(2K1000)启动流程(二、汇编部分)1、pmon 文件相关的地址问题2、start.S 在pmon 中的作用?3、为什么start.S 是在cpu 上电之后立即执行的代码?4、为什么gzrom.bin 的开头就是start.S?5、start.S 代码进行逐一分析6、initmips 代码分析的全部内容,希望文章能够帮你解决龙芯PMON(2K1000)启动流程(二、汇编部分)1、pmon 文件相关的地址问题2、start.S 在pmon 中的作用?3、为什么start.S 是在cpu 上电之后立即执行的代码?4、为什么gzrom.bin 的开头就是start.S?5、start.S 代码进行逐一分析6、initmips 代码分析所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(55)

评论列表共有 0 条评论

立即
投稿
返回
顶部