参考pmon源码,将start.S、Makefile和链接脚本移植到裸机程序,实现纯粹的真正的裸机程序。这样就不再需要pmon,上电后直接运行裸机程序。
本文涉及的异常和地址空间的相关知识,需要结合《龙芯1c的芯片手册》、《see mips run》和《北京龙芯的龙芯1c开发板手册》。这几个文档都已经放到龙芯1c库的git上了,最新最完整的代码也请移步到git查看。龙芯1c库的git地址是https://gitee.com/caogos/OpenLoongsonLib1c
背景知识
使用mipsel-linux-objdump反汇编
为什么需要使用反汇编
为什么这里首先讨论使用objdump反汇编呢?可能大家习惯了仿真,单步调试。很少单独使用反汇编。可是目前龙芯1c是不能仿真和单步调试的(至少目前我不知道),所以手动反汇编就有必要了,通过查看反汇编,可以很清楚的查看程序的运行流程,可以看到上电后CPU运行的第一条汇编指令是什么。
举个例子吧,在调试上电初始化这部分汇编程序的过程中,发现汇编源码和pmon中的差不多,可是串口没有打印helloworld。经过一番排除,最后用objdump反汇编发现,链接后执行的第一条语句不是汇编,而是c程序。原因是ld链接时,c文件放在了依赖文件列表的前面,改为汇编文件在前面,就可以了。
怎样反汇编
为了能在反汇编的结果中同步显示源码,在编译时,需要增加选项” -g ”,
例如“make cfg all tgt=rom DEBUG=-g”,
使用mipsel-linux-objdump反汇编,
例如
root@ubuntu:/home/develop/loongson1-pmon-master/Targets/LS1X/compile/ls1c# mipsel-linux-objdump -S pmon.gdb > /mnt/hgfs/VmShare/pmon-gdb-objdump.S
比如,pmon反汇编后,得到如下内容
pmon.gdb: file format elf32-tradlittlemips
Disassembly of section .text:
80010000 <_ftext>:
80010000: 40806000 mtc0 zero,$12
80010004: 40806800 mtc0 zero,$13
80010008: 3c080040 lui t0,0x40
8001000c: 40886000 mtc0 t0,$12
80010010: 3c1d8001 lui sp,0x8001
80010014: 27bdc000 addiu sp,sp,-16384
80010018: 3c1c800c lui gp,0x800c
8001001c: 279c6cf0 addiu gp,gp,27888
80010020: 3c08bfe8 lui t0,0xbfe8
80010024: 24090017 li t1,23
80010028: a1090004 sb t1,4(t0)
8001002c: 24090005 li t1,5
80010030: a1090006 sb t1,6(t0)
80010034: 3c04bfd0 lui a0,0xbfd0
80010038: 348411c0 ori a0,a0,0x11c0
8001003c: 8c850040 lw a1,64(a0)
80010040: 34a50001 ori a1,a1,0x1
80010044: ac850040 sw a1,64(a0)
80010048: 041101b9 bal 80010730
8001004c: 00000000 nop
...
反汇编结果是如何与源码一一对应的
这里主要讨论一下,反汇编得到的汇编代码,与start.S中的汇编代码的对应关系
左边为start.S中的汇编源码,右边为反汇编的结果。图中用线将其一一对应了。
异常入口点(CP0的SR寄存器的BEV)
上电运行的第一条指令在什么地方,地址是多少
mips系列cpu的异常和中断是两个不同的概念,中断一般指外设中断,所有外设中断共用一个异常入口,即外设中断是一种特定类型的异常。《龙芯1c的芯片手册》中目前几乎没怎么讲这部分内容,而《see mips run》中却讲得很详细,专门用一章来讲异常。
本文不是要讨论上电初始化那部分汇编代码吗?怎么这里研究异常呢?在mips系列cpu上,上电(冷复位)也属于一种异常,异常入口固定为ROM入口点0xBFC00000,如下图
《龙芯1c的芯片手册》中也有对应描述,如下
图中,明确说了,
根据系统启动方式将内存地址0xBFC00 0000 -- )XBFCF FFFF映射到SPI或NAND,即从地址0xBFC0 0000处取出的指令,就是SPI或NAND的地址0处的指令。也是上电后运行的第一条指令。刚上电时,把BEV置1
截图中,讲清楚了,在cpu刚上电时,cache还未初始化之前,只能使用不经过cache的kseg1。
初始化完成后,把BEV清零
(内存,cache等)初始化完成后,就可以使用cache了,通过把协处理器0的SR寄存器中的BEV清零,使所有异常入口从ROM入口点(0xBFC0 0000)改为RAM入口点(BASE + 0x180),其中BASE为寄存器EBase的值。《see mips run》中的描述为
Pmon中对应的代码为
地址空间的划分
see mips run中关于程序地址空间的划分情况
see mips run中kseg0和kseg1的详细描述
因为kseg0(0x8000 0000 - 0x9fff ffff)和kseg1(0xa000 0000 - 0xBfff ffff)实际上是映射到低端同一块512M的物理地址上,只是kseg1不需要cache,而kseg0必须等cache初始化后才能使用。
所以,把固件拷贝到kseg1上,等等于拷贝到了kseg0上。
龙芯1c芯片手册中关于地址空间分配的描述
链接时的起始地址(代码段的首地址)
标号start的值为0x80010000
代码段是从0x8001 0000处开始的,初始化完成后,异常入口改为RAM入口,即BASE+0x180=0x8000 0000+0x180
栈空间在什么地方
在代码段之前有0x4000大小的栈空间,代码如下
把固件本身从ROM(SPI nor flash)拷贝到内存RAM中
上电时,CPU映射了1M的Boot内存到SPI(NOR FLASH)或NAND(FLASH),可是这部分内存是只读的,并且不经过cache。当内存和cache初始化完成后,需要将ROM(位于kseg1地址段)上的代码拷贝到kseg0地址段内,kseg0上的内存可写,同时经过cache,还有利于提高性能。
链接时指定的起始地址为0x80010000,而上电后运行的起始地址是0xBFC00000,所以在拷贝时需要做地址修正。
汇编代码(start.S)详解
主要参考《北京龙芯的1c开发板用户手册》v0.60,其中对start.S的注解非常好。如下
参考这个文档,我讲文档中的讲解以注释的形式添加到了代码中,并增加了一些我的理解。下面以start.S的程序执行流程来讲解,可能和北京龙芯的1c开发板手册中讲解的顺序有点不一样。
其中,pmon中汇编代码初始化后,跳转到函数initmips,而1c库中是直接跳转到main函数。Pmon中有压缩解压固件的功能,我认为裸机程序中不需要这个功能,所以1c库中没有移植这个功能,如果有需要的自行移植。开发板手册中的描述如下
初始化基础寄存器
主要是初始化协处理器0的STATUS寄存器和CAUSE寄存器、sp寄存器和gp寄存器。代码如下
.set noreorder
.set mips32
.globl _start
.globl start
.globl __main
_start:
start:
/*
设置栈指针为start地址之前0x4000的位置
mips架构堆栈寄存器实际只是通用寄存器,并没有规定生长方向,但软件约定“堆栈指针向下生长”
*/
.globl stack
stack = start - 0x4000 /* Place PMON stack below PMON start in RAM */
/* NOTE!! Not more that 16 instructions here!!! Right now it's FULL! */
/*
根据“《see mips run》第5.3节——异常向量:异常处理开始的地方”中的描述,
异常向量间的距离为128字节(0x80),可容纳32条指令(每条指令4字节)。
而这里原来的英文注释为“ Not more that 16 instructions here!!!”,即最大16条指令
我认为需要进一步斟酌,到底是最大16字节,还是32字节
*/
mtc0 zero, COP_0_STATUS_REG // 清零cp0 status寄存器
mtc0 zero, COP_0_CAUSE_REG // 清零cp0 cause寄存器
/*
设置启动异常向量入口地址为ROM地址(0xbfc00000)
将寄存器cp0 status的BEV置1,使CPU采用ROM(kseg1)空间的异常入口点
*/
li t0, SR_BOOT_EXC_VEC /* Exception to Boostrap Location */
mtc0 t0, COP_0_STATUS_REG
la sp, stack // 加载栈地址
la gp, _gp // 加载全局指针gp
如果是SPI启动,设置SPI控制寄存器
spi初始化代码如下
/* initialize spi */
li t0, 0xbfe80000 //地址0xbfe80000为SPI0的寄存器基地址
li t1, 0x17 // div 4, fast_read + burst_en + memory_en double I/O 模式 部分SPI flash可能不支持
sb t1, 0x4(t0) // 设置寄存器sfc_param
li t1, 0x05
sb t1, 0x6(t0) // 设置寄存器sfc_timing
其实,我认为这步或许可以省略掉,因为cpu上电后,能从spi nor flash执行代码,说明上电后默认就能正常读SPI NOR FLASH,没必要再次初始化。
设置PLL和各级时钟(包括CPU和SDRAM的时钟)
/* config pll div for cpu and sdram */
#define PLL_MULT (0x54) // 晶振为24Mhz时,PLL=504Mhz
#define SDRAM_DIV (0) // SDRAM为CPU的2分频
#define CPU_DIV (2) // CPU为PLL的2分频
li t0, 0xbfe78030 // 地址0xbfe78030为PLL/SDRAM频率配置寄存器的地址
/* 设置PLL倍频 及SDRAM分频 */
li t2, (0x80000008 | (PLL_MULT << 8) | (0x3 << 2) | SDRAM_DIV)
/* 设置CPU分频 */
li t3, (0x00008003 | (CPU_DIV << 8))
/* 注意:首先需要把分频使能位清零 */
li t1, 0x2
sw t1, 0x4(t0) // 清零CPU_DIV_VALID,即disable
sw t2, 0x0(t0) // 写寄存器START_FREQ
sw t3, 0x4(t0) // 写寄存器CLK_DIV_PARAM
DELAY(2000)
初始化调试串口
等调试串口初始化完成后,就可以打印调试信息了。
可是为什么没有把串口初始化再提前一点呢?因为串口的波特率计算需要用到时钟,所以把串口初始化放在了PLL初始化之后。这个位置(初始化串口)已经是非常靠前了。
调用汇编函数initserial
/* initialize UART */
li a0, 0
bal initserial // 初始化串口
nop
PRINTSTR("
asm uart2 init ok!
"); // 打印一条提示信息,表示串口初始化成功了
汇编函数initserial的实现
除了计算波特率稍微复杂点,串口初始化函数其实很简单,只需要设置几个串口控制寄存器和引脚复用就可以了。
其实计算波特率本身并不复杂,只是下面的代码是使用汇编函数实现的,并且用汇编函数读取PLL频率和各个分频系数,这样代码的行数就看起来就显得有点多,其实并不复杂,我已经添加了注释。计算波特率的那二三十行代码可以用一条汇编语句替代,如下
li v1, ((APB_CLK / 4) * (PLL_MULT / CPU_DIV)) / (16*CONS_BAUD) / 2
这行代码默认被注释了。
完整的汇编函数initserial的源码如下
LEAF(initserial)
move AT,ra // 把返回地址暂时保存在寄存器AT中
la v0, UART_BASE_ADDR // 加载串口基地址到寄存器v0中
#ifdef HAVE_MUT_COM
bal 1f
nop
li a0, 0
la v0, COM3_BASE_ADDR
bal 1f
nop
jr AT
nop
#endif
1:
li v1, FIFO_ENABLE|FIFO_RCV_RST|FIFO_XMT_RST|FIFO_TRIGGER_4 // 清空Rx,Tx的FIFO,申请中断的trigger为4字节
sb v1, LS1C_UART_FCR_OFFSET(v0) // 写FIFO控制寄存器(FCR)
li v1, CFCR_DLAB // 访问操作分频锁存器
sb v1, LS1C_UART_LCR_OFFSET(v0) // 写线路控制寄存器(LCR)
/* uart3 config mux 默认第一复用 */
#if (UART_BASE_ADDR == 0xbfe4c000)
li a0, 0xbfd011c4
// lw a1, 0x00(a0)
// and a1, 0xfffffff9
// sw a1, 0x00(a0)
lw a1, 0x10(a0)
ori a1, 0x06
sw a1, 0x10(a0)
// lw a1, 0x20(a0)
// and a1, 0xfffffff9
// sw a1, 0x20(a0)
// lw a1, 0x30(a0)
// and a1, 0xfffffff9
// sw a1, 0x30(a0)
/* li a0, 0xbfd011f0
lw a1, 0x00(a0)
ori a1, 0x03
sw a1, 0x00(a0)*/
#elif (UART_BASE_ADDR == 0xbfe48000)
/* UART2 使用gpio36,gpio37的第二复用*/
li a0, LS1C_CBUS_FIRST1 // 加载复用寄存器CBUS_FIRST1的地址到寄存器a0
lw a1, 0x10(a0) // 加载复用寄存器CBUS_SECOND1的值到寄存器a1
ori a1, 0x30 // a1 |= 0x30,即GPIO36,GPIO37配置为第二复用
sw a1, 0x10(a0) // 将寄存器a1的值写入寄存器CBUS_SECOND1中
#elif (UART_BASE_ADDR == 0xbfe44000)
/* UART1 */
li a0, 0xbfd011f0
lw a1, 0x00(a0)
ori a1, 0x0c
sw a1, 0x00(a0)
#endif
// 设置波特率
// 计算pll频率
li a0, 0xbfe78030 // 0xbfe78030为PLL/SDRAM频率配置寄存器START_FREQ,将地址0xbfe78030加载到寄存器a0中
lw a1, 0(a0) // 加载寄存器START_FREQ的值到寄存器a1中
srl a1, 8 // a1 >>= 8
andi a1, 0xff // a1 &= 0xff,即a1=PLL_MULT(PLL倍频系数)
li a2, APB_CLK // a2 = APB_CLK = 24Mhz(外部晶振频率)
srl a2, 2 // a2 = a2 >> 2 = APB_CLK/4
multu a1, a2 // hilo = a1 * a2 = PLL_MULT * APB_CLK /4
mflo v1 // v1 = lo,将a1 * a2的结果的低32位放到v1中,即v1为pll频率
// 判断是否对时钟分频
lw a1, 4(a0) // 加载寄存器CLK_DIV_PARAM的值到寄存器a1中
andi a2, a1, DIV_CPU_SEL // a2 = a1 & DIV_CPU_SEL,即读取位CPU_SEL的值,如果=1,则分频时钟;如果=0,则晶振输入时钟(bypass模式)
bnez a2, 1f //if (a2 != 0) 则跳转到下一个标号1处
nop
li v1, APB_CLK // v1 = APB_CLK,即cpu时钟为晶振频率
b 3f
nop
1: // 判断cpu分频系数是否有效
andi a2, a1, DIV_CPU_EN // a2 = a1 & DIV_CPU_EN,即读取位CPU_DIV_EN的值,判断配置参数是否有效
bnez a2, 2f // if (a2 != 0) 则跳转到下一个标号2处
nop
srl v1, 1 //v1 >>= 1,即v1 = APB_CLK/4 * PLL_MULT / 2
b 3f
nop
2: // 计算cpu频率
andi a1, DIV_CPU // a1 &= DIV_CPU
srl a1, DIV_CPU_SHIFT // a1 >>= DIV_CPU_SHIFT,即a1为cpu分频系数
divu v1, a1 // lo = v1 / a1; hi = v1 % a1
mflo v1 // v1 = lo,即v1为cpu频率
3:
// li v1, ((APB_CLK / 4) * (PLL_MULT / CPU_DIV)) / (16*CONS_BAUD) / 2
li a1, 16*CONS_BAUD // a1 = 16 * 波特率
divu v1, v1, a1 // v1 = v1 / a1
srl v1, 1 // v1 >>= 1,即v1 /= 2
sb v1, LS1C_UART_LSB_OFFSET(v0) // 将低8位写入分频锁存器1
srl v1, 8 // v1 >>= 8
sb v1, LS1C_UART_MSB_OFFSET(v0) // 将低8位写入分频锁存器2
li v1, CFCR_8BITS // 8个数据位,1个停止位,无校验
sb v1, LS1C_UART_LCR_OFFSET(v0) // 写线路控制寄存器(LCR)
// li v1, MCR_DTR|MCR_RTS // 使能DTR和RTS
// sb v1, LS1C_UART_MCR_OFFSET(v0) // 写MODEM控制寄存器(MCR)
li v1, 0x0 // 关闭所有中断
sb v1, LS1C_UART_IER_OFFSET(v0) // 写中断使能控制寄存器(IER)
j ra
nop
END(initserial)
其中使用了两个宏
// 配置调试串口
#define UART_BASE_ADDR LS1C_UART2_BASE // 串口2作为调试串口
#define CONS_BAUD B115200 // 波特率115200
配置内存SDRAM
配置SDRAM的代码不多,只需要设置一个64位的寄存器。因为寄存器长度为64位,一条汇编只能修改其中的(高或低)32位,为了保证正确设置,要求写3次寄存器,最后一次才使能。代码如下
/* 配置内存 */
li msize, MEM_SIZE
#if !defined(NAND_BOOT_EN)
/*
手册建议,先写寄存器SD_CONFIG[31:0],然后再写寄存器的SD_CONFIG[63:32],
即先写低32位,再写高32位。
写三次寄存器,最后一次将最高位置一,即使能
*/
// 写第一次
li t1, 0xbfd00410 // 寄存器SD_CONFIG[31:0]的地址为0xbfd00410
li a1, SD_PARA0 // 宏SD_PARA0在sdram_cfg.S中定义的
sw a1, 0x0(t1) // 将宏SD_PARA0的值写入寄存器SD_CONFIG[31:0]
li a1, SD_PARA1
sw a1, 0x4(t1) // 同理,将宏SD_PARA1的值写入寄存器SD_CONFIG[63:32]
// 写第二次
li a1, SD_PARA0
sw a1, 0x0(t1)
li a1, SD_PARA1
sw a1, 0x4(t1)
// 写第三次
li a1, SD_PARA0
sw a1, 0x0(t1)
li a1, SD_PARA1_EN // 使能
sw a1, 0x4(t1)
// DELAY(100)
#endif
其中使用的宏如下
// 配置内存大小
#define MEM_SIZE (0x02000000) // 32MByte
//#define SD_FREQ (6 * PLL_M) / (2 * SDRAM_PARAM_DIV_NUM)
#define SD_FREQ (((APB_CLK / 4) * (PLL_MULT / CPU_DIV)) / SDRAM_PARAM_DIV_NUM)
/*
以型号为EM63A165TS的SDRAM为例,
物理参数为,
容量:32MB
行宽:13位,即2的13次方,即8K
列宽:9位,即2的9次方,即512
位宽:16位
所以,
颗粒的行数=ROW_8K
颗粒的列数=COL_512
颗粒的位宽=WIDTH_16
再结合宏SD_PARA0和芯片手册中寄存器SD_CONFIG,相信一看就能明白
*/
/* 颗粒行数 */
#define ROW_1K 0x7
#define ROW_2K 0x0
#define ROW_4K 0x1
#define ROW_8K 0x2
#define ROW_16K 0x3
/* 颗粒列数 */
#define COL_256 0x7
#define COL_512 0x0
#define COL_1K 0x1
#define COL_2K 0x2
#define COL_4K 0x3
/* 颗粒位宽 */
#define WIDTH_8 0x0
#define WIDTH_16 0x1
#define WIDTH_32 0x2
#define TRCD 3
#define TCL 3
#define TRP 3
#define TRFC 8
#define TRAS 6
#define TREF 0x818
#define TWR 2
#define DEF_SEL 0x1
#define DEF_SEL_N 0x0
#define HANG_UP 0x1
#define HANG_UP_N 0x0
#define CFG_VALID 0x1
/* mem = 32MByte */
#define SD_PARA0 (0x7f<<25 |
(TRAS << 21) |
(TRFC << 17) | (TRP << 14) | (TCL << 11) |
(TRCD << 8) | (WIDTH_16 << 6) | (COL_512 << 3) |
ROW_8K)
#define SD_PARA1 ((HANG_UP_N << 8) | (DEF_SEL_N << 7) | (TWR << 5) | (TREF >> 7))
#define SD_PARA1_EN ((CFG_VALID << 9) | (HANG_UP_N << 8) |
(DEF_SEL_N << 7) | (TWR << 5) | (TREF >> 7))
如果是两片SDRAM,需要设置片选
/* 设置sdram cs1复用关系,开发板使用ejtag_sel gpio_0引脚(第五复用)作为第二片sdram的片选
注意sw2拨码开关的设置,使用ejtag烧录pmon时需要调整拨码开关,烧录完再调整回来 */
li a0, 0xbfd011c0
lw a1, 0x40(a0)
ori a1, 0x01
sw a1, 0x40(a0)
初始化CACHE
初始化Cache这部分代码没有仔细研究
调用汇编函数cache_init
do_caches:
/* Init caches... */
li s7, 0 /* no L2 cache */
li s8, 0 /* no L3 cache */
bal cache_init // 调用汇编函数cache_init
nop
mfc0 a0, COP_0_CONFIG // 将协处理器0的config寄存器的值加载到寄存器a0
and a0, a0, ~((1<<12) | 7) // a0 = a0 & ~((1<<12) | 7)
or a0, a0, 2 // a0 |= 2
mtc0 a0, COP_0_CONFIG // 将寄存器a0的值写入协处理器0的config寄存器
汇编函数cache_init的具体实现
.ent cache_init
.global cache_init
.set noreorder
cache_init:
move t1, ra
####part 2####
cache_detect_4way:
.set mips32
mfc0 t4, CP0_CONFIG,1 // 将cp0的config1寄存器的值加载到寄存器t4中
lui v0, 0x7 // v0 = 0x7 << 16
and v0, t4, v0 // v0 = t4 & v0
srl t3, v0, 16 // t3 = v0 >> 16 Icache组相联数 IA
li t5, 0x800 //32*64
srl v1, t4,22 //v1 = t4 >> 22
andi v1, 7 //Icache每路的组数 64x2^S IS
sll t5, v1 //InstCacheSetSize
sll t5, t3 //t5 InstCacheSize
andi v0, t4, 0x0380
srl t7, v0, 7 //DA
li t6, 0x800 // 32*64
srl v1, t4,13
andi v1, 7 //DS
sll t6, v1 // DataCacheSetSize
sll t6, t7 // t5 DataCacheSize
####part 3####
# .set mips3
lui a0, 0x8000 //a0 = 0x8000 << 16
addu a1, $0, t5
addu a2, $0, t6
cache_init_d2way:
/******************************/ //lxy
// addiu t3, t3, 1
// li t4, 0
//5:
/******************************/
// a0=0x80000000, a1=icache_size, a2=dcache_size
// a3, v0 and v1 used as local registers
mtc0 $0, CP0_TAGHI
addu v0, $0, a0 //v0 = 0 + a0
addu v1, a0, a2 //v1 = a0 + a2
1: slt a3, v0, v1 //a3 = v0 < v1 ? 1 : 0
beq a3, $0, 1f //if (a3 == 0) goto 1f
nop
mtc0 $0, CP0_TAGLO
cache Index_Store_Tag_D, 0x0(v0) // 1 way
4: beq $0, $0, 1b
addiu v0, v0, 0x20
1:
cache_flush_i2way:
addu v0, $0, a0
addu v1, a0, a1
1: slt a3, v0, v1
beq a3, $0, 1f
nop
cache Index_Invalidate_I, 0x0(v0) // 1 way
4: beq $0, $0, 1b
addiu v0, v0, 0x20
1:
cache_flush_d2way:
addu v0, $0, a0
addu v1, a0, a2
1: slt a3, v0, v1
beq a3, $0, 1f
nop
cache Index_Writeback_Inv_D, 0x0(v0) // 1 way
4: beq $0, $0, 1b
addiu v0, v0, 0x20
/******************************/ //lxy
// addiu t4, t4, 1
// addiu a0, a0, 1
// slt t5, t4, t3
// bne t5, $0, 5b
// nop
/******************************/
// .set mips0
1:
cache_init_finish:
jr t1
nop
.set reorder
.end cache_init
搬运固件到内存
内存和cache初始化之后,就可以把固件搬运到内存,这样代码就可以在内存运行了。
拷贝text和data段
拷贝固件分为两步:
一,先将执行拷贝pmon到内存任务的代码,拷贝到内存0xa0000000;
二,将固件拷贝到起始地址为0xa0010000的内存空间。
已经在代码中添加了详细的注释,直接看代码吧
DEBUGGING AND COPY SELF TO RAM***********************/
//#include "newtest.32/mydebug.S"
bootnow:
/* copy program to sdram to make copy fast */
/* 先将执行拷贝pmon到内存任务的代码,拷贝到内存0xa0000000 */
/* 先确定需要拷贝的代码段为标号121到标号122之间的代码
* 由于链接时指定的起始地址是0x80010000,
* 而目前正在ROM(SPI NOR FLASH,起始地址为0xBFC00000)运行
* 所以需要用寄存器s0来修正一下地址
*/
la t0, 121f // 将下一个标号121所在地址,加载到寄存器t0
addu t0, s0 // 使用寄存器s0修正t0中的(标号121的)地址
la t1, 122f // 将下一个标号122所在地址,加载到寄存器t1
addu t1, s0 // 使用寄存器s0修正t1中的(标号122的)地址
li t2, 0xa0000000 // 将立即数0xa0000000(起始地址)加载到寄存器t2
1:
lw v0, (t0) // 将寄存器t0所指的内存地址开始4字节的数据加载到寄存器v0
sw v0, (t2) // 将寄存器v0的内容保存到寄存器t2所指的内存中
addu t0, 4 // 寄存器t0向后移4字节
addu t2, 4 // 寄存器t2向后移4字节
ble t0, t1, 1b // 如果t0 <= t1,则跳转到上一个标号1处,继续拷贝后面的4字节
nop
li t0, 0xa0000000 // 将立即数0xa0000000加载到寄存器t0
jr t0 // 跳转到起始地址0xa0000000处开始执行(拷贝任务)
nop
121:
/* Copy PMON to execute location... */
/* 将固件拷贝到起始地址为0xa0010000的内存空间
由于kseg0(0x8000 0000 - 0x9FFF FFFF)和kseg1(0xA000 0000 - 0xBFFF FFFF)是映射到物理内存的相同区域
即拷贝到0xA000 0000开始的kseg1,就相当于拷贝到0x8000 0000开始的kseg0
这就是为什么链接时,指定的地址是0x8001 0000,而拷贝的目标起始地址是0xA001 0000
*/
la a0, start // 加载符号start所在地址0x80010000加载到寄存器a0中
addu a1, a0, s0 // 使用寄存器s0修正寄存器a0中的地址,a1=0xBFC00000
la a2, _edata // 加载_edata(链接脚本中的一个符号)到寄存器a2
or a0, 0xa0000000 // a0 = a0 | 0xa0000000 = 0xa0010000
or a2, 0xa0000000 // a2 = a2 | 0xa0000000,修正地址_edata
subu t1, a2, a0 // t1 = a2 - a0,即计算从start到_edata之间的长度(字节数)
srl t1, t1, 2 // t1 >>= 2,即t1除以4。(和前面类似,每次拷贝4字节,所以除以4)
// 似乎t1计算结果没有被使用,马上就被后面的覆盖了
move t0, a0 // t0 = a0 = 0xa0010000 (目标起始地址)
move t1, a1 // t1 = a1 = 0xBFC00000 (start在ROM中的地址,源起始地址)
move t2, a2 // t2 = a2 (_edata在ROM中的地址,源结束地址)
/* copy text section */
1: and t3, t0, 0x0000ffff // t3 = t0 & 0x0000ffff,取低16位
bnez t3, 2f // 如果t3不等于0,则跳转到下一个标号2处继续执行,t3的计算结果似乎没被使用,就被后面的覆盖了
nop
2: lw t3, 0(t1) // 从源地址t1处加载4字节到寄存器t3中
nop
sw t3, 0(t0) // 将寄存器t3中的4字节数据保存到目标地址t0处
addu t0, 4 // 目标地址t0后移4字节
addu t1, 4 // 源地址t1 后移4字节
bne t2, t0, 1b // 如果t2不等于t0,则跳到上一个标号1处继续拷贝,总的来说就是判断拷贝是否结束
nop
/* copy text section done. */
初始化BSS
程序编译链接后,会生成三段:text段,data段和bss段。
其中,text段为代码段,用于存放程序执行代码;Data段为数据段,用于存放程序中已初始化的全局变量;bss段也是数据段,不过存放的是程序中未初始化的全局变量。
三段中,text段和data段在前面已经拷贝到内存了,而bss段是不需要拷贝的,因为存放的是程序中未初始化的全局变量。只需要将这片内存区域全部清零即可。如下所示
/* Clear BSS */
/* BSS段为未初始化的全局变量的内存区间,这部分不需要从ROM中拷贝,也就不需要做地址修正 */
la a0, _edata // 加载_edata的地址到寄存器a0
la a2, _end // 加载_end的地址到寄存器a2
2: sw zero, 0(a0) // 将寄存器a0所指的4字节清零
bne a2, a0, 2b // 如果a2不等于a0,则跳到上一个标号2处,继续清零下一个4字节
addu a0, 4 // a0 += 4,注意,这条汇编在延迟槽内,所以仍然会被执行到
/* Copy PMON to execute location done */
跳转到main函数
在基础寄存器(SP,GP等)初始化后,内存和cache也都初始化了,并且把固件搬运到内存后,c语言运行环境就已经具备了,这时就直接跳到main函数,执行c语言程序了。至此汇编初始化任务就完成了。
/* 将内存大小(单位M)作为入参,放在寄存器a0中 */
move a0, msize // a0 = msize(内存大小)
srl a0, 20 // a0 >>= 20,将单位转换为M
/* 调用函数main */
la v0, main // 将main()函数地址加载到寄存器v0中
jalr v0 // 调用寄存器v0所指的函数
nop
跳转到main函数的代码很简单,就一两条汇编语句就可以了。Pmon中将内存的大小作为入参传递给了main函数,这里也没删,其实在裸机程序中内存大小就是一个宏,需要用的时候,直接读取宏的值。换句话说就是内存大小是否以入参形式传递给main函数,其实不重要。
Makefile详解
pmon编译时,会将使用的命令打印出来,这里将其重定向到一个文本文档。下面就以这个编译输出作为参考,详细分析纯粹的裸机程序的编译链接的参数等。
首先,需要获取编译pmon时的打印信息,这个可以通过在命令后面跟一个重定向即可,即把打印信息重定向到一个文本文档,比如“pmon_build.log”,假设现在已经有了这个文档。
编译参数有哪些
汇编语言的编译参数
在编译log文档中搜索关键字“mipsel-linux-gcc”,得到的第一条命令就是编译start.S的,如下图所示
从中提取出编译参数为“-mno-abicalls -fno-pic -G 0 -mips2 -Wall -mno-abicalls -fno-builtin”,如下图所示
c语言的编译参数
紧接着start.S的就是c文件的编译,所有c文件的编译参数都是一样的,这里就以紧接着start.S的那个c文件为例,编译记录如下
从中提取出的编译参数为“-mno-abicalls -fno-pic -g -Wall -Wstrict-prototypes -Wno-uninitialized -Wno-format -Wno-main -O2 -G 0 -mips2 -fno-builtin”,如下图所示
链接参数有哪些
和编译类似,以关键字“mipsel-linux-ld”搜索,得到的第一个结果就是链接pmon的,如下所示
从中提取出的链接参数为“-m elf32ltsmip -G 0 -static -n -nostdlib -N”,如下所示
链接脚本
从前面的链接命令中,可以得到使用的链接脚本为“ld.script”。那么就以“ld.script”为关键字搜索,得到
从上图可知,最终的“ld.script”是由“ld.script.S”生成的,生成的“ld.script”所在目录为“../Targets/LS1X/conf/ld.script”。那么直接将其拷贝出来即可,完整的链接脚本文件如下
OUTPUT_FORMAT("elf32-tradlittlemips", "elf32-tradbigmips",
"elf32-tradlittlemips")
OUTPUT_ARCH(mips)
ENTRY(_start)
SECTIONS
{
. = 0xffffffff80010000;
.text :
{
_ftext = . ;
*(.text)
*(.rodata)
*(.rodata1)
*(.reginfo)
*(.init)
*(.stub)
*(.gnu.warning)
} =0
_etext = .;
PROVIDE (etext = .);
.fini : { *(.fini) } =0
.data :
{
_fdata = . ;
*(.data)
. = ALIGN(32);
*(.data.align32)
. = ALIGN(64);
*(.data.align64)
. = ALIGN(128);
*(.data.align128)
. = ALIGN(4096);
*(.data.align4096)
CONSTRUCTORS
}
.data1 : { *(.data1) }
.ctors :
{
__CTOR_LIST__ = .;
LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2)
*(.ctors)
LONG(0)
__CTOR_END__ = .;
}
.dtors :
{
__DTOR_LIST__ = .;
LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2)
*(.dtors)
LONG(0)
__DTOR_END__ = .;
}
_gp = ALIGN(16) + 0x7ff0;
.got :
{
*(.got.plt) *(.got)
}
.sdata : { *(.sdata) }
.lit8 : { *(.lit8) }
.lit4 : { *(.lit4) }
_edata = .;
PROVIDE (edata = .);
__bss_start = .;
_fbss = .;
.sbss : { *(.sbss) *(.scommon) }
.bss :
{
*(.dynbss)
*(.bss)
. = ALIGN(32);
*(.bss.align32)
. = ALIGN(64);
*(.bss.align64)
. = ALIGN(128);
*(.bss.align128)
. = ALIGN(4096);
*(.bss.align4096)
*(COMMON)
}
_end = . ;
PROVIDE (end = .);
.stab 0 : { *(.stab) }
.stabstr 0 : { *(.stabstr) }
.debug 0 : { *(.debug) }
.debug_srcinfo 0 : { *(.debug_srcinfo) }
.debug_aranges 0 : { *(.debug_aranges) }
.debug_pubnames 0 : { *(.debug_pubnames) }
.debug_sfnames 0 : { *(.debug_sfnames) }
.line 0 : { *(.line) }
.gptab.sdata : { *(.gptab.data) *(.gptab.sdata) }
.gptab.sbss : { *(.gptab.bss) *(.gptab.sbss) }
}
Makefile文件原文
# 虚拟机里的交叉编译工具链
#CROSS_COMPILE =mipsel-linux-
#COPY = cp
# windows下的交叉编译工具链
CROSS_COMPILE =mips-linux-gnu-
COPY = copy
#
# Include the make variables (CC, etc...)
#
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm
STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
SIZE = $(CROSS_COMPILE)size
HEADERS = $(wildcard lib/*.h example/*.h app/*.h)
SRC_S = $(wildcard lib/*.S)
SRC_C = $(wildcard lib/*.c example/*.c app/*.c)
SRCS = $(SRC_S) $(SRC_C)
OBJS = $(patsubst %.S, %.o, $(SRC_S)) $(patsubst %.c, %.o, $(SRC_C)) # 注意汇编文件一定要在前面
#头文件查找路径
INCLUDES = -Ilib -Iexample -Iapp
#链接库查找路径
LIBS =
#编译参数
CCFLAGS = -mno-abicalls -fno-pic -g -Wall -Wstrict-prototypes -Wno-uninitialized -Wno-format -Wno-main -O2 -G 0 -mips2 -fno-builtin
AFLAGS = -mno-abicalls -fno-pic -G 0 -mips2 -Wall -mno-abicalls -fno-builtin
#链接参数
LDFLAGS = -m elf32ltsmip -G 0 -static -n -nostdlib -N
# 最终的目标文件
OUTPUT = OpenLoongsonLib1c.elf
all:$(OUTPUT)
$(OUTPUT):$(OBJS)
$(LD) $(LDFLAGS) -T ld.script -e start -o $@ $^
$(COPY) $(OUTPUT) OpenLoongsonLib1c_debug.elf
$(STRIP) -g -S --strip-debug $(OUTPUT)
$(OBJCOPY) -O binary $(OUTPUT) OpenLoongsonLib1c.bin
$(SIZE) $(OUTPUT)
# cp $(OUTPUT) /tftpboot/
.c.o:
$(CC) $(CCFLAGS) $(INCLUDES) -c -o $@ $^
.S.o:
$(CC) $(AFLAGS) $(INCLUDES) -c -o $@ $^
clean:
# rm -f $(OBJS) $(OUTPUT) OpenLoongsonLib1c.bin
del lib*.o example*.o app*.o $(OUTPUT) OpenLoongsonLib1c.bin