ARM 学习报告003
——Hayden Luo BIOS 总体结构及部分源代码分析
杜云海( duyunhai@hotmail.com ,(wwww.seajia.com)
这段时间事情真的是很多,老板要文章,这已经要人命了,还要翻译成英文投到国际会议,还有本科生毕业设计… ,于是报告003 就这么被推迟了。不过, 鲁迅 先生说时间是海绵,的确有道理,终于还是基本上看完了Bootloader,并移植了uClinux,还在上面实现了一个有关安全的身份认证程序,呵呵,不然要投稿的那篇文章乱哈拉就不好了!!
ARM 学习是一个很令人兴奋的过程,可以在其中体会到在51 单片机世界里早已经淡薄的挑战和愉悦。在以前做过的51 片子的项目中,一直都没有好好深入到某些机理中,实在遗憾。现在把自己在ARM 学习过程中的一点一滴写下来,既是为了自我总结,也可以和ARM 同行交流讨论。
ARM 学习报告001 和002 在电子产品世界论坛上得到了大家的好评,也使我有了写好报告003 的动力,这篇文章基于以上两个学习报告,如果你是初学者或还没有看过以上两篇报告,那么推荐下载看看,也许对理解本篇文章中的一些论述很有帮助J(推销嫌疑)
l ARM 学习报告001:http://bbs.edw.com.cn/dispbbs.asp?boardID=20&ID=28310&page=1
l ARM 学习报告002:http://bbs.edw.com.cn/dispbbs.asp?boardID=20&ID=28649&page=1
写在文章前
在报告002 中我们阐述了GNU 工具集开发ARM 程序的过程和机理,重点讲了elf 文件格式和section 的来历和作用,并和ARM 的映象文件组织结构做了类比,主要起基础性的作用,因为不了解这些东西,看linux 下开发的ARM 程序只能囫囵吞枣,很难深入。不过,报告002 只是重点介绍了head.S、Makefile 和bios.ld 三个文件,对输入段,输出段的机理进行详细讲述,没有对整个BIOS 做系统描述,可能无法给人以全面印象,本篇将结合整个bios来重温并补充那些“机理”!
在报告003 中并没有对Hayden Luo Bios 进行详细的描述,没有这个必要,也没有时间,我觉得只要入门了,很多程序都很容易自己看懂。关键是入门难。所以在本篇报告中我用的例子程序bios-dyh 是我根据Hayden Luo Bios 缩减而来的,由于Hayden Luo Bios 的结构的特殊性,虽然是缩减,却丝毫没有改动bios 的大结构,只是功能上减弱一些,绝对不会影响到大家日后继续研究Hayden Luo Bios 或bios-lt。
特别声明
还有一点要阐述的,也是论坛上有弟兄问的,就是Hayden Luo Bios 哪里可以下?现在用google 搜,好像就只有liu tao 大侠的bios-lt74,当然bios-lt74 也是从Hayden Luo Bios 继承而来的,liu tao 大侠对其进行了大量的补充和增强,并将其发扬光大。如果大家要想真正使用功能强大的bios,可以推荐使用bios-lt74。本文之所以没有用bios-lt 做例子,是因为相对于原始的Hayden Luo Bios,bios-lt74 的数据结构和功能都变得复杂了,不利于初学者入门,而我这个bios-dyh 是为了本篇报告学习而特意制作的,比bios-lt74 的功能差一些,不过,麻雀虽小,学习正好!
开始我们的BIOS 旅程吧!
一 BIOS 的整体结构
1.1 BIOS 的“生成三阶段”整体图
在开始讲bios 整体结构之前,不如来看看一张图,图1 是根据bios-dyh 画的,完整的Hayden Luo Bios 还包括了setup、fdisk 两个文件夹(也就是setup 和fdisk 功能模块),缺少的两个模块是比较独立的,不影响整个系统结构。
图1 bios-dyh 的总体结构图
上图只是一个感性认识,从图1 可以看出一种层次感,就是图中标注的①②③阶段,还有“子文件夹和根目录”层次。再配合bios-dyh 根目录下的Makefile 文件,那就可以建立一个比较系统的bios 生成过程了。
1.2 Makefile 文件
/bios-dyh/Makefile
在这里不讨论Makefile 的语法了,如果你还看不懂这个文件的话,建议应该先补一补Linux 编程知识,否则将举步维艰。
看Makefile 当然从all:开始,
all:
make -C sysinit
make -C biosapi
make -C tftp
make -C uart_rec
make bios.bin
make -C imgtools/param
make -C imgtools/img
总的Makefile 可以分为三个部分,对应图1 的三个阶段:
l 前面四个make 对应图1 的①部分,也就是进入sysinit、biosapi、tftp、uart_rec 各个子文件夹进行相对较独立的编译连接,在子文件夹里生成“独立”的功能模块的可执行文件,并用bin2c 将子文件夹里可执行文件生成只包含一个数组的同名C文件,生成的C 文件在根目录下。
l 第四个make bios.bin 对应图1 的②部分,根据根目录下的各个C 程序,编译生成相应的.o文件,然后根据bios.ld 连接生成bios,并用objcopy 取出其中需要的section,生成bios.bin
l 最后两步是创建重要的“系统参数表”,对应图1 的③部分,并用combine 工具将这个表和bios.bin 以纯二进制形式合并在一起,最后在imgtools/img 文件夹中生成bios.img。
bios.img —— 一个的可烧入ROM 中的Bootloader 就这样出现的!!
以上只是一个大体的框架分析,以期建立一个大致的BIOS 的概念——“三阶段生成过程”,从而使BIOS 有章可循,不至于在脑子中零乱而无法入手。当然,这样一个这样的系统分析肯定讲不清楚很多细节问题,对于熟悉这个bootloader 的人来说,可能一看就明白;但是对于刚刚准备学习bootloader 的人,一定会存在很多不解的地方。没有关系,下面我们来具体分析。
分析源代码,特别是感觉是比较大的源程序,一个“分析顺序”很重要,否则写的人也乱,看的人更乱。本报告的分析顺序有两种:
l “BIOS 生成三阶段”顺序:就如图1 的三部分
l “BIOS 执行流程”顺序:以程序的执行流程为主线
第一种分析顺序的主要作用还是建立“机理性”的概念,讲究linux 下开发的BIOS 程序到底是如何一步一步生成的。这种分析就象在天上来分析地上迷宫的结构——面
而第二种分析顺序则跟着程序走,更接近于源代码分析,象亲自进入迷宫走一遭,作用和第一种是不同的——点
先来第一种吧,先建立一个“面”的概念,再来“由面到点”更符合学习逻辑。
二 生成BIOS 的 “ 三把斧”
首先分析BIOS 生成三阶段里的第一阶段,在这个阶段主要是6 个大字——“独立编译连接”。
在这个阶段里,各个功能模块进行了“独立编译连接”,注意是“独立编译连接”,也就是说,在各个子文件夹里,这三个功能模块都已经编译连接并生成了相应真正的可执行文件——sysinit、biosapi、tftp、urat_rec。也就是说,BIOS 还没有生成,sysinit、biosapi、tftp、urat_rec 已经生成了,并且是可执行的。这带来一个问题,这么早生成可执行文件,那最后和BIOS 整合在一起并使用它呢?
2.1 第一把斧:BIOS 功能模块——sysinit、biosapi、tftp、urat_rec
让我们来看看相应的Makefile 和.ld 连接脚本文件,体会一下“独立编译连接”过程。以sysinit 为例,其他两个类同。
/sysinit/Makefile
BIN2C = ../tools/bin2c
CFLAGS += -I..
AFLAGS += -I..
OBJ = head.o sysinit.o
all: ../sysinit.c
../sysinit.c: sysinit.bin
$(BIN2C) -c -s sysinit_data sysinit.bin ../sysinit.c
sysinit.bin: sysinit
$(OBJCOPY) -O binary --only-section=.init /
--only-section=.text /
--only-section=.rodata /
--only-section=.data /
--only-section=.bss sysinit /
sysinit.bin
sysinit: $(OBJ)
$(LD) -p -X -T sysinit.ld $(OBJ) /
-o sysinit
clean:
$(RM) -rf *.o sysinit sysinit.bin ../sysinit.c
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
%.o: %.S
$(CC) $(AFLAGS) -c -o $@ $<
从/sysinit/Makefile 可以看出,源程序sysinit.s 和head.S 已经经过了编译连接,生成了可执行文件sysinit(红色标识处)。可执行文件sysinit 和sysinit.o 有什么不同呢?为什么我老是强调“可执行文件”这五个字呢?肯定是有不同!!一说到这个东西,马上又是GNU/linux的编译连接机理。
还是让我们暂时先停下分析BIOS,来补一补在ARM 学习报告002 中没有讲完的GNU/linux 机理问题吧!!磨刀不误砍柴功阿!
有关机理问题的补充我写在附录1 中,不放在这了,省得打乱了BIOS 的分析
object 文件虽然已经编译,但是里面的变量和地址都没有重定位,库函数也没有连接,所以这种文件虽然已经是相应CPU 的二进制代码指令格式,却不能正常运行,只能等到连接重定位了,才算是真正的可执行程序。
那么接着上面分析,sysinit 文件夹中的独立编译连接后,sysinit 已经是可执行文件,连接的地址是0x03fe0000,这个下面会分析到,也就是说,只要能把sysinit 拷到0x03fe0000处,并将PC 寄存器设为0x03fe0000,那么sysinit 就可以正确运行起来,只要内存分配没有冲突,这个sysinit 和其他BIOS 程序没有什么直接关系。这就是所谓的“独立编译连接”。先看看Makefile 的其他部分:
/sysinit/Makefile 中其他部分的介绍如下:
sysinit.bin: sysinit
$(OBJCOPY) -O binary --only-section=.init /
--only-section=.text /
--only-section=.rodata /
--only-section=.data /
--only-section=.bss sysinit /
sysinit.bin
上面这段Makefile 命令是使用arm-elf-objcopy 工具将sysinit 这个可执行文件(elf 格式)中的几个需要的段(.init .text .rodata .data .bss)抽取出来,组成一个ARM 可执行文件, sysinit.bin,这个bin 文件是二进制的,已经不是elf 格式,仅仅包含了elf 文件中抽取出来的几个段,完全以二进制形式“拼”在一起,去除了elf 文件中系统使用的段(例如.systab .syntab等)。强调是“ARM 可执行文件”,也就是说sysinit.bin 甚至可以烧入flash 中运行,只要.text对应的地址正确(也就是相当于ro_base)。相关机理在报告001、002 和附录1 中已经讲述。
../sysinit.c: sysinit.bin
$(BIN2C) -c -s sysinit_data sysinit.bin ../sysinit.c
上面这两行命令是使用bin2c 工具将刚刚生成的sysinit.bin 可执行二进制文件转变成只包含一个数组sysinit_data[]的C 程序,这个C 程序生成到上一层目录,也就是根目录下,打开这个C 程序看看这个唯一的数组到底是什么内容?原来数组是个unsigned char sysinit_data[],里面包含的数据就是sysinit.bin 的二进制字符,大家可以用ultraedit 打开sysinit.bin 核对看看,这里就不贴图了。
转变为数组的目的就是想利用数组名来定位sysinit.bin 文件,因为根目录下的这个sysinit.c 还要被编译连接成.o 文件(这个sysinit.o 和sysinit 子文件夹里的完全不同了),在根目录下生成的sysinit.o 的symbol table 中只包含了一个变量,就是sysinit_data,数组首地址,那么BIOS 在第二次连接时就可以在其他程序定位到这个sysinit_data 变量,从而得到数组中的内容——sysinit.bin,达到直接调用sysinit.bin 的目的。好好琢磨一下,等会儿程序分析时还会讨论到这个问题。这种调用也是bios 中比较巧妙的技术之一。正是由于这样的定位方式,导致这些功能模块可以单独编译连接,而不必考虑最后生成bios 时的连接定位。如果你不能理解透,那么可能是GNU/linux 机理还是没有吃透.
再来看看sysinit 的ld 连接文件
/sysinit/sysinit.ld
OUTPUT_ARCH(arm)
ENTRY(stext)
SECTIONS
{
.text 0x03fe0000 : { /* Real text segment */
_text = .; /* Text and read-only data */
*(.text)
*(.rodata)
. = ALIGN(4);
_etext = .; /* End of text section */
}
.data : {
__data_start = .;
*(.data)
. = ALIGN(4);
_edata = .;
}
.bss : {
__bss_start = .; /* BSS */
*(.bss)
. = ALIGN(4);
_end = .;
}
}
这个连接脚本文件的格式在002 中已经讲述,不过这里还必须说明一点,连接具有局域性。就是在各个子文件夹中连接,只和连接需要的几个.o 文件有关,例如sysinit 只和head.o 、sysinit.o 有关,且这两个.o 文件都是从子文件夹的.c 或.S 源程序生成的,和上层根目录下的文件没有什么直接关系。从这个连接脚本可以看出三个重要信息:
l 该功能模块是单独编译连接,入口地址为stext,注意,这个stext 是在/sysinit/head.S中,不是根目录下的那个head.S。根据连接结果stext=0x03fe0000,也就是下面提到的代码段起始地址。
l 从.text 0x03fe0000 看出这个可执行文件的代码段起始地址(ro_base)是0x03fe0000,也就是说,只有把sysinit.bin 送到0x03fe0000 开始处的内存中,它才可以正确执行,而事实上,在后面我们要说的系统初始化时,sysinit.bin 的确“整个”被送到了0x03fe0000,这个地址是在哪里?呵呵,是在S3C4510B 的SRAM 中,等会儿再说。
l .data .bss 紧随.text 其后
形象来说,假设整个BIOS 是一个操作系统,那么sysinit、biosapi、tftp、urat_rec 就象可以运行在操作系统上的独立的应用软件一样,具有比较独立的功能,这个软件模块事先已经做好了,需要的时候只需将其装入,在windows 中我们是通过双击,在这个BIOS 中我们调用这个“软件模块”用的方法比较特殊,属于“Bootloader 系统”的特色用法——先用bin2c将可执行bin 文件转化为数组,然后调用数组名而得到其可执行代码。
所以我们在看BIOS 代码的时候,甚至可以很孤立地看这三个功能模块,因为他们和BIOS 其他部分交叉比较少!只是调用的时候比较特殊一些!
正是基于以上原因,所以bios-dyh 虽然比Hayden Luo Bios 少了几个功能模块,但是,对BIOS 没有造成很大影响,不过是“操作系统上少装了几个软件而已”!!当然,由于是嵌入式系统,所以,功能模块在内存分布上还是要满足整个BIOS 的“统筹规划”。而不是随心所欲,这是和通用计算机编程不同的地方!
biosapi 和tftp 功能模块的机理和sysinit 类似,这里就不赘述了,大家打开其子文件夹看看就清楚了。这里只讲一讲他们的内存分布: 其中biosapi 的连接地址为.text0x00001000,.data .bss 紧跟其后;tftp 的连接地址为.text 0x00300000,.data .bss 紧跟其后,关于具体的模块调用,到后面程序分析时再细说。
l sysinit:系统初始化代码 0x03fe0000
l biosapi:系统底层调用,主要是获得一些有关的参数 0x00001000
l tftp:当然是tftp 服务器模块 0x00300000
l urat_rec:串口接收模块,我自己添的 0x00400000
这四个模块只要用“bin2c+数组”的方法将其二进制代码(.bin)调到相应的连接地址处,该模块就可以正确运行了!!
2.2 第二把斧:bios.bin 的生成
第一阶段中,各个独立功能模块已经编译连接完成,并用bin2c 在根目录下生成只包含一个数组的c 程序,等待BIOS 调用。这时,第二部分可以启动了——make bios.bin,开进入真正的bios!也就是图1 的第二部分。
make bios.bin 到底要做多少事呢?来看看Makefile 吧
/Makefile
OBJ = head.o bios.o gunzip.o utils.o console.o bioscall.o /
sysinit.o biosapi.o tftp.o uart_rec.o
……
bios.bin: bios
$(OBJCOPY) -O binary /
--only-section=.init /
--only-section=.text /
--only-section=.rodata /
--only-section=.data /
--only-section=.bss bios bios.bin
bios: $(OBJ)
$(LD) -p -X -T bios.ld $(OBJ) /
-o bios
从Makefile 可以看出,bios.bin 是由bios 中的相应段section 组成的,用arm-elf-objcopy工具,这个已经在上面说过了。
那么bios 才是最实质的一个文件,一个经过连接的可执行文件,从上面Makefile 可以看出bios 是由几个.o 文件连接而成(OBJ = head.o bios.o gunzip.o utils.o console.o bioscall.o sysinit.o biosapi.o tftp.o,uart_rec.o,有顺序的阿),附录1 中已经讲述了.o 文件怎么连接生成可执行文件的机理。那么.o 文件怎么得到的呢?当然是通过arm-elf-gcc 编译各个c 程序得到的。
从图1的第二阶段可以看出,包括第一阶段生成的三个c 程序(只包含一个数组)在内,所有的c 程序都是“平等”的:
l head.S:这个是入口汇编程序,跳入bios_main
l bios.c:这个c 程序当然包括了bios 主函数bios_main(),也就是bios 的主体函数,这个函数中调用了其他模块函数
l gunzip.c:解压缩程序,有些功能模块都经过压缩,例如biosapi、tftp 等,要先将其解压缩到相应地址处,才可以正确执行
l utils.c:系统通用一些工具函数
l bioscall.c:主要是调用biosapi 的一些封装好的函数
l console.c:有关串口的一些函数,例如初始化,读写串口等等
l borad.h:开发板的寄存器地址以及其他一些宏定义
l config.h:系统配置头文件,主要是配置系统中ROM 或DRAM 的起始地址,各个独立功能的调用地址,系统参数表的地址等等
l 还有其他一些相关头文件
这些c 程序结合.h 文件被gcc 编译后,形成了相应的.o 文件,这时他们之间还是孤立的,等待连接,连接的机制已经在附录1 中说过,用rel<.section>和symbol table 来将各个.o 文件联系起来,并根据.ld 连接脚本,合并相应的段,从而形成了可执行文件bios(注,这个bios 不是笼统的bootloader,而是在根目录下生成的bios 文件)。大致步骤分以下两步:
第一步,所有的c 源程序都被arm-elf-gcc 编译生成.o 文件,从图1 的②可以看出,这时大家之间并没有什么联系,都是一个个独立的.o 文件。结合报告002 和附录1 的机理分析可知,每个.o 文件中除了包含.text .rodata .data .bss 段外,还包含了.rel<section>和.symtab 这两个上面提到的连接使用的系统段。大家之间仿佛独立,其实里面千丝万缕的联系,只是这些关系要有ld 来“牵”起来。
第二步,GNU ld 根据.ld 文件(连接脚本)合并各个输入.o 文件的各个相同的段(见报告002),主要合并.text .rodata .data .bss 这四个段,相当于RO、RW、ZI。合并后,开始形成一个新的symbol table,这个symbol table 包含了所有输入.o 文件的symbol 的值,并根据合并后的新地址更新了各个symbol 值,用这些新的symbol 值来更新.rel<section>中指定的要更新的变量,从而生成了bios。
从连接脚本看出:
/Makefile
SECTIONS
{
.text 0x01000000 : { /* Real text segment */
_text = .; /* Text and read-only data */
*(.text)
*(.rodata)
. = ALIGN(4);
_etext = .; /* End of text section */
bios 的代码段被定位在0x01000000,也就是ro_base=0x01000000,也就是16Mbyte 处,那么带来一个问题,就是烧入flash 中,这个可执行程序是否可以正确执行,按照常理和在ARM 学习报告001 中讲的那样,ro_base 定位在0x01000000,而烧入flash 却在0x00000000处,映象文件是不能正确执行的。(要提一下,烧入flash 的是bios.img,不过bios.img 的主体就是bios.bin,只不过后面加了一个“系统参数表”,而bios.bin 的主体也就是刚刚在第二阶段连接生成的bios 可执行文件,所以烧入flash 基本上99%就是bios 可执行文件了)那么烧入flash 时,这个bios.img,也就是bios 怎么运行呢?按道理bios 的连接地址要在
0x00000000 处才可以阿??!!怎么这样也可以正确运行呢?这里面牵涉到一个地址卷绕问题。
地址卷绕
上电复位时,S3C4510B的“存储器控制寄存器”未初始化,除了ROMCON0被初始化为0x20000060,其他的寄存器都是0x00000060,这时只有ROM0可以使用。以下是内存初始化图。
图 S3C4510B 内存初始化图
复位后,只有ROM0 可以用,一般我们都在ROM0 处接Flash,那么就只有第一个flash可以访问,访问地址为0x0~0x2000000,32M 字节,这是ROMCON0 所设置的,但事实上,由于外部地址线只有22 根(0:21),那么假如访问的数据宽度为32bits 的话,最大也只能达到4M×32bit=4M words=16M bytes。
这个就带来了一个“地址卷绕”问题了。
ROM0 初始化时为32Mbytes 最大空间(ROMCON0 中设置的初始值),而事实上在ROM0 处最大也只有16M 字节空间可以真正访问,如果超过了16M 字节呢?也就是地址超
过0x1000000 呢?
当PC 上为0x01000000 时,访问的地址范围已经超过了16M 字节了,也就是说,22根地址线可识别不出第23 位地址了;再往上的地址空间还没有初始化,不能使用;所以输出的地址就类似最上面的位“溢出”一样,第23 位无法“输出”,低22 根地址线就象从0x0又重新开始一样,发生了“地址卷绕”。但是,发生卷绕时,PC 的值的确是0x01000000,而不是0x0,这点很重要!!
假如ROM0 接的flash 为1M×16=2M 字节,外部只要接20 根地址线ADDR[19:0]即可,当上电复位后未初始化,访问的地址超过2M 时,由于复位后ROM0 的访问范围是32M 字节,所以即使已经超出flsh 的范围,片选依然在ROM0 处,而地址变化就加入了ADDR20,可是这些地址线没有连在flash 上,所以,对外界的表现就是依然是ADDR[19:0]在起作用,这样就在2M、4M、6M、8M… 16M、18M… 30M 处都发生“地址卷绕”。
所以,“地址卷绕”的原因就是赋予的地址空间太大,高位地址又不起作用,低位地址就老是重复着“一遍又一遍”的故事Bootloader 中的bios 可执行文件的连接地址为0x01000000,可是在刚刚开始执行程序时,却可以正确执行,究其原因,就是利用了这个“地址卷绕”的特点!!而且地址在16M处一定卷绕!
到此为止,我们已经讲述了BIOS 生成的第二阶段,也是最核心的一个阶段。这个阶段中的具体程序分析将在本文第三部分以“程序执行流程顺序”来详细分析!
2.3 第三把斧:bios.img 的生成
也许很多人以为bios.bin 烧入flash 中,结果还是不能正确执行,我刚刚开始也犯过这个错误,其实真正最后能正确执行的是/imgtools/img 文件夹中的bios.img 文件,也就是图1中的③部分。
图 bios、bios.bin、bios.img 对比图
这三个可不能混在一起,第一个bios 是连接后生成的elf 格式的可执行文件,不能直接烧入flash 中,但是其中包含了99%的Bootloader 的内容,就是要去除一些elf 格式中系统使用的东西,抽取出真正有用的段(.text .rodata .data .bss ),组成bios.bin,bin 文件按道理可以烧入flash 了,可是由于我们这个BIOS 比较特殊,还要在尾部加一个有绝对地址的“系统参数表”,所以又多了一步,最后bios.img 才是我们可以烧入flash 中的。
生成系统参数表——param.tbl 其实也是一个比较独立的过程,它虽然在整个bootloader中一直用到,但是生成以及整合是独立的过程,就如上图的“黄色块”,好像就是“贴”上在后面一样,而事实上,也就是贴在bios.bin 的后面,不过就是贴的位置是绝对的。也就是说,假如设置接在0xe000 处,那么在bios.img 中就要从0xe000 处开始,哪怕bios 很小,中间也要补上0,一直到0xe000,才是param.tbl,这就是绝对地址。用绝对地址的原因是这样在BIOS 中所有其他程序想取系统参数,就可以定位到这个绝对地址就可以了,这个绝对地址的设置在config.h 中—— #define SYSTEM_TABLE_OFFSET 0x0000e000,你可以根据需要更改。
在/imgtools/param 文件中,param.c 文件的作用就是生成一个param.tbl 和mbr.bin,这两个文件都是系统参数文件,尤其param.tbl 文件,是“系统配置表”,起到相当重要的作用。
/imgtools/param/param.c 部分源码:
int main()
{
FILE *f;
f = fopen("param.tbl", "wb");
fwrite((char *)(&system_table), 1, sizeof(struct system_table_struct), f);
fclose(f);
f = fopen("mbr.bin", "wb");
fwrite((char *)(&partition_table), 1, sizeof(struct partition_table_struct), f);
fclose(f);
return 0;
}
从上面函数可以看出,param.c 主要是生成param.tbl 和mbr.bin 文件,这个文件包含的内容就是system_table,就是param.c 的上面部分的定义,这些定义可以根据实际情况修改struct system_table_struct system_table这个system_table 是系统中十分重要的一个结构体,到后面的分析就可以知道, system_table_struct 在BIOS 的初始化中起到核心作用,包括地址重映射等等。
那么生成了这个param.tbl 后,怎么在BIOS 中使用呢?这个问题只有到了分析源程序时才可以真正理解,这里看个大概吧!
Bios.img 是在/imgtools/img/中生成的,来看看/imgtools/img/Makefile 吧
/imgtools/img/Makefile
bios:
../tools/combine ../../bios.bin -a 0x7f00 ../param/param.tbl bios.img
从Makefile 中看出,生成bios.img 要用到tools 文件夹里的combine 工具,这个工具就是合成文件作用,以二进制形式直接合成。也就是说,bios.img 是由bios.bin 和param.tbl 组成,不过param.tbl 被定位在一个绝对地址上(默认为0xe000),用ultraedit 打开就可以看到了,param.tbl 是从0x7f00 处开始。在bios 的其他程序中,可以直接用0x7f00 来读取这个“系统参数表”了,尤其在初始化的时候。
以上是以bios.img 生成三阶段的顺序来具体描述了bios 是怎么生成的!我想通过这样的分析,从某个“面”上的角度,对理解bios 是很有帮助的!尤其是理解了其中的编译连接机理,理解BIOS 的架构更是如鱼得水阿
第一种分析顺序完成后,大家对BIOS 整个框架应该有相当深度的了解。“面”上分析后,下面来“点”的分析。
三 BIOS 部分源码分析— — 跟着程序走
在这个部分,我将以“BIOS 的执行流程顺序”来分析BIOS,也就是具体到各个源程序中,当然,不可能对每一行程序都详细注释,没有必要的。就象一个带路人,在一条笔直的道路上,老是在一旁说:这一步往前走,下一步还是往前走… .,别人不烦死才怪!!其实只要在岔路口时,提醒一下就好了,这也是我写学习报告的原因,以自己的切身学习感受,使后来学习ARM 的人可以少走弯路!!
Let’s go!!
3.1 BIOS 的大门——head.S
以前论坛上有人写email 给我,问BIOS 从哪里开始?从哪里可以看出入口呢?看linux程序,主要看根目录下的Makefile 文件,Makefile 文件是“统领性文件”,起总指导作用。
/Makefile
OBJ = head.o bios.o gunzip.o utils.o console.o bioscall.o /
sysinit.o biosapi.o tftp.o uart_rec.o
bios: $(OBJ)
$(LD) -p -X -T bios.ld $(OBJ) /
-o bios
从连接机理可知,连接时的顺序就是Makefile 文件中.o 文件的顺序,在ADS 中也是这样。head.o 排在最前面,则生成的bios 中,head.S 中的指令也是排在最前的,这是连接的结果,也就是说head.o 中的.text 段是bios 中.text 段的最前头,这个原理在002 和附录1 中已经分析过了。所以,head.S 是入口文件,一般汇编文件也都作为入口文件。
head.S 中的指令格式在报告002 中已经很详细地讲解过了,head.S 的“与众不同的语法结构”给我留下了很深的印象。
这里大致解释一下其中部分指令。
.globl stext
.globl system_table_offset
.globl _rom_base
这三条定义使得stext、system_table_offset 、_rom_base 三个变量为全局变量,主要是连接时可以为其他源程序可见,用于重定位。下面两条指令是对该变量赋值:
system_table_offset:
.long SYSTEM_TABLE_OFFSET
SYSTEM_TABLE_OFFSET:
.long stext
其中SYSTEM_TABLE_OFFSET 在config.h 中定义,主要就是定义param.tbl 的绝对地址。_rom_base被定义为head.S 指令的第一条,其实也就是整个bios.img 的首地址,而bios.img是烧入flash 的,所以_rom_base 就是名副其实的ROM 首地址,在其他源程序,尤其是初始化程序中,对其进行了调用。
/*
* Load up the linker defined values for the static data copy
*/
ldr r0, =_etext
ldr r1, =__data_start
ldr r3, =_edata
上面这三条指令的大致含义在ARM学习报告002 中已经做了介绍,_etext、__data_start、_edata 在bios.ld 中定义的,连接脚本中的定义的变量在连接范围内可以使用,当然包括head.S了,这里不赘述,结合报告002 来看看吧,接下去的的程序是移动.data 段的数据到相应连接段,以及初始化.bss,所有这些作用可以参考报告002 和报告001,已经做了相应的解释,也可以参考电子产品世界论坛上的twentyone 的有关bootloader 的文章JJJ,这几段程序是bootloader 起始阶段很精华的部分,一定要好好理解。也就是关于移动RW 数据段到rw_base处的机理。
移动完RW 数据(.data)以及初始化好ZI(.bss),则开始设置cpsr,也就是设置ARM的运行模式为SVC,并初始化堆栈指针__stack。
接下来是有关cache 的操作
再来就是bios 的正题了:
bios_start:
bl bios_main
用跳转指令跳到bios_main 处,摆脱了汇编,进入c 代码阶段了,也就是说bios 就要开始真正上场了!!
Head.S 中,bl bios_main 后面的指令的用处是什么?可能很多人不一定看得全懂,当初我刚刚看的时候,也是很稀里糊涂的,多谢twentyone 的帮助,才终于知道了答案!让我们来看看:
/head.S
mov r3, r0
这条语句的作用是将r0 的值保存在r3 中,那么r0 里面到底是什么值呢?后面又是一段SYSCFG 和cache 的初始化代码,初始化中一直没有改变r3 的值,最后关键一句:
/head.S
/*
* Call sys_init, it should never return.
*/
sys_start:
mov pc, r3
将r3 送入PC,于是程序就从r3 寄存器所指的那个值开始的,而r3 的值是r0 的值,那r0是什么值呢?原来ATPCS 中规定了,子程序调用返回后,返回值放在r0 中,所以r0 中放的是bios_main()函数调用返回的值,bios_main()调用返回的是什么?让我们看看bios.c 文件中bios_main()中的最后一句:
/bios.c
return (unsigned char *)_rom_base;
呵呵,返回的是BIOS 的首地址!!!bootloader 程序又重新启动了!刚刚说head.S 中先是执行bl bios_main 语句跳入BIOS 的c 代码中开始真正执行的, bios_mian()函数在bios.c 中,至于怎么在汇编调用到c 的函数,还是属于linux/gnu 编译连接机理问题!
让我们顺着那个语句进入bios.c 吧
3.2 走进BIOS
在bios-dyh 根目录下的bios.c 的尾部,我们可以看到这段代码:
/bios.c
unsigned char *bios_main(void)
{
struct system_table_struct *system_table;
unsigned long startup_mode;
int i, ch;
system_table = (struct system_table_struct *)(_rom_base + system_table_offset);
sys_init(system_table, _rom_base, DRAM_BASE);
console_init();
printf("/r/n/r/n");
printf("Compex BIOS for SAMSUNG S3C4510B v1.20 (build 0801)/r/n/r/n");
printf("modified by duyunhai for 'ARM study report 003'/r/n");
biosapi_init();
printf("/r/n");
startup_mode = bios_startup_mode();
if (startup_mode == BOOT_MENU) {
startup_mode = menu(system_table);
}
return (unsigned char *)_rom_base; //return to head.S with _rom_base in r0
}
bl bios_main 语句正是跳到这里来执行的,这个函数也是bios 的主函数,通过这个主函数来进行各种判断,然后调用相应的程序模块从而完成bootloader 的操作。
函数首先定义了一个指向结构体变量struct system_table_struct 的指针system_table;
/bios.c
struct system_table_struct *system_table;
这个结构体顾名思义,也就是“系统参数表”——里面藏着所有初始化的系统参数,结构体struct system_table_struct 在bios.h 中定义,这个系统参数表的结构体我们必须先分析一下,比较重要。
/bios.h
struct system_table_struct {
unsigned short vendor_id;
unsigned short device_id;
unsigned short sub_vendor_id;
unsigned short sub_device_id;
unsigned long rev;
unsigned long bios_size;
unsigned long system_table_size;
unsigned long partition_table_offset;
unsigned long partition_table_size;
unsigned long sys_reg_base;
unsigned long sys_clock;
unsigned long ext_clock;
struct sys_rom_cfg rom_table[6];
struct sys_dram_cfg dram_table[4];
unsigned long ext_base;
struct sys_ext_cfg ext_table[4];
struct sys_iop_cfg iop;
struct sys_uart_cfg uart[2];
struct sys_eth_cfg eth;
struct sys_ne2000_cfg ne2000;
struct sys_uart16550_cfg uart16550;
struct sys_pc97338_cfg pc97338;
unsigned long startup_mode;
unsigned long tftp_ipaddr;
};
由于这个BIOS 兼容其他一些开发板上的器件,所以上面也许很多选项和我们并没有直接的关系,但有些选项却十分关键,例如rom_table,dram_table 等,那么这个系统参数表结构体中的这些参数是怎么被赋初始值且被调用的呢?
我们要回忆起在本文2.2 中我们曾讲过param.tbl,讲过它是在最后被combine“粘”在BIOS 尾部的一个固定位置。是的, param.tbl 其实就是一个赋好初始值的struct system_table_struct。
先让我们看看下面代码,这个代码在/imgtools/param/param.c 中,且在2.2 已经部分引用过了。
在这个param.c 文件的头部有关于这个系统参数表的初始赋值,这些初始值也就是bootloader 要用来初始化系统用的!!
/imgtools/param/param.c 部分源码:
#define COMPEX_VENDOR_ID 0x11F6
#define IRE201_DEVICE_ID 0x8000
#define NP15B_DEVICE_ID 0x8001
#define BTE201_DEVICE_ID 0x8002
#define NONE_DEVICE_ID 0x0000
struct system_table_struct system_table = {
bios_size: 0xc000,
#ifdef IRE201
vendor_id: COMPEX_VENDOR_ID,
device_id: IRE201_DEVICE_ID,
sub_vendor_id: COMPEX_VENDOR_ID,
sub_device_id: IRE201_DEVICE_ID,
#endif
#ifdef NP15B
vendor_id: COMPEX_VENDOR_ID,
device_id: NP15B_DEVICE_ID,
sub_vendor_id: COMPEX_VENDOR_ID,
sub_device_id: NP15B_DEVICE_ID,
#endif
#ifdef BTE201
vendor_id: COMPEX_VENDOR_ID,
device_id: BTE201_DEVICE_ID,
sub_vendor_id: COMPEX_VENDOR_ID,
sub_device_id: BTE201_DEVICE_ID,
#endif
#ifdef SNDS100
vendor_id: COMPEX_VENDOR_ID,
device_id: NONE_DEVICE_ID,
sub_vendor_id: COMPEX_VENDOR_ID,
sub_device_id: NONE_DEVICE_ID,
#endif
rev: 0,
sys_reg_base: 0x03ff0000,
sys_clock: fMCLK_MHz,
ext_clock: 0xffffffff,
#ifdef IRE201
rom_table: {{0x00200000, 16, 0x00000060}, //rom0 is flash with
2M=1M*16bit
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060}},
dram_table: {{0x01000000, 32, 0x00000380}, //sdram is 16M=4M*32bit
{0x00000000, 32, 0x00000398},
{0x00000000, 32, 0x00000398},
{0x00000000, 32, 0x00000398}},
#endif
#ifdef NP15B
rom_table: {{0x00080000, 8, 0x00000060},
{0x00080000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060}},
dram_table: {{0x00400000, 32, 0x00000398},
{0x00000000, 32, 0x00000398},
{0x00000000, 32, 0x00000398},
{0x00000000, 32, 0x00000398}},
#endif
#ifdef BTE201
rom_table: {{0x00100000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060}},
dram_table: {{0x00800000, 32, 0x00000398},
{0x00000000, 32, 0x00000398},
{0x00000000, 32, 0x00000398},
{0x00000000, 32, 0x00000398}},
#endif
#ifdef SNDS100
rom_table: {{0x00080000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060}},
dram_table: {{0x00400000, 32, 0x00000398},
{0x00000000, 32, 0x00000398},
{0x00000000, 32, 0x00000398},
{0x00000000, 32, 0x00000398}},
#endif
system_table_size: sizeof(struct system_table_struct),
partition_table_offset: 0x0000f000,
partition_table_size: sizeof(struct partition_table_struct),
ext_base: 0x03fd0000,
ext_table: {{ 32, 0x00000fff},
{ 32, 0x00000fff},
{ 32, 0x00000fff},
{ 32, 0x00000fff}},
iop: {0x00020070, 0x2ad00300, ~0x00020070},
uart: {{0x80000000, 0, 0, 0, 0, 0, 0}, {0x80000000, 1, 0, 0, 0, 0, 0}},
eth: {0x80000000, {0x00, 0x80, 0x48, 0x88, 0x00, 0x00}},
ne2000: {0x80000000, 0, {0x00, 0x80, 0x48, 0x88, 0x00, 0x01}},
uart16550: {0x80000000, 1, 13000000},
pc97338: {0x80000000, 1},
startup_mode: BOOT_MENU,
tftp_ipaddr: 0xd34156c0,//211.65.86.192
};
其实很多系统参数都必须在这里改动,所以要求熟悉这个“系统参数表”的结构和内容,其实我想对于设计过4510b 程序一段时间的人来说,看懂这个初始值设置是不难的,要我们设置的也就几个地方比较重要,例如sys_reg_base,rom_table,dram_table,iop,uart,eth, tftp_ipaddr 等,要结合4510b 的datasheet。
这里主要讲一下和地址重映射相关的ROM 和SDRAM 的初始化值部分:
#ifdef IRE201
rom_table: {{0x00200000, 16, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060},
{0x00000000, 8, 0x00000060}},
dram_table: {{0x01000000, 32, 0x00000380},
{0x00000000, 32, 0x00000398},
{0x00000000, 32, 0x00000398},
{0x00000000, 32, 0x00000398}},
#endif
由于这个BIOS 可以适合许多板子的不同资源,所以采用条件编译,我们这里用的是IRE201。
有关rom_table 的数据格式可以见bios.h
/bios.h
struct sys_rom_cfg {
unsigned long size;
unsigned long width;
unsigned long flags;
};
包括三个项:size(rom 大小),width(数据宽度),flags(其他标识);上面的定义是第一个flash 的大小是2M,数据宽度16 位,flags 是根据datasheet 中的相应项设置的,这里就不详细说了!其他5 个ROM BANK都是空的(这是按照我自己的板子资源设置的,如果你的开发板不一样,这里要进行一些修改)有关dram_table 的数据格式也是见bios.h,和rom_table 差不多!也是同样的三个项:我的开发板是两片16 位SDRAM 合作一片32 位的SDRAM 用,大小是16M,数据宽度32位,其他就是标志位的设置了。如果开发板资源不同,也要在这作相应修改!
这些值在后面的系统初始化的地址重映射中要马上用到了!
赋完初始值后,就将这个已经初始过的“系统参数表结构体”生成一个param.tbl 二进制文件,见下面代码,依然在/imgtools/param/param.c 中。
/imgtools/param/param.c 部分源码:
int main()
{
FILE *f;
f = fopen("param.tbl", "wb");
fwrite((char *)(&system_table), 1, sizeof(struct system_table_struct), f);
fclose(f);
f = fopen("mbr.bin", "wb");
fwrite((char *)(&partition_table), 1, sizeof(struct partition_table_struct), f);
fclose(f);
return 0;
}
上面这两段代码就完成了“系统初始化参数表”,应该说/imgtools/param/param.c 其实最主要的功能也就是完成这个初始化参数表。
这个表中的内容是以二进制的形式保存着一个struct system_table_struct,如果给出了这个表的地址,那么就可以用指针来访问这个表中的结构体元素,从而获得各个重要的初始化参数了!!
所以,问题终于归结到了系统初始化参数表的首地址上了!!这个首地址到底是多少呢?是固定的吗?
/bios.c
system_table = (struct system_table_struct *)(_rom_base + system_table_offset);
接下来的这条语句就给出了这个表的地址了——_rom_base + system_table_offset,就是BIOS 的首地址+系统参数表的偏移量。_rom_base 和system_table_offset 都在head.S 的开头部分被定义为全局变量,且被赋值。
/head.S
system_table_offset:
.long SYSTEM_TABLE_OFFSET
_rom_base:
.long stext
可以看出system_table_offset 等于宏变量SYSTEM_TABLE_OFFSET,而SYSTEM_TABLE_OFFSET 在config.h 中定义了,这个宏定义常量即为BIOS 中这个系统参数表到底粘在BIOS 中的绝对的位置上,以利于BIOS 寻找到这个重要的初始化参数表。个中情况,大家最好慢慢在看下面的程序中在慢慢体会吧!
定义好system_table 后就开始了下面最重要的一步了:
/bios.c
sys_init(system_table, _rom_base, DRAM_BASE);
一看就知道,系统开始初始化了……,调用的是bios.c 中的sys_init( )函数:
/bios.c
int sys_init(struct system_table_struct *system_table, unsigned long rom_base, unsigned long
dram_base)
{
typedef int (SYSINIT)(struct system_table_struct *, unsigned long, unsigned long);
SYSINIT *sys_init_ptr;
memcpy((char *)SRAM_BASE, sysinit_data, 1024 * 3);
sys_init_ptr = (SYSINIT *)SRAM_BASE;
sys_init_ptr(system_table, rom_base, dram_base);
return 0;
}
初始化传入三个参数——系统初始化参数表地址指针,BIOS 首地址,SDRAM 起始地址DRAM_BASE,其实很多初始化的参数都已经在初始化参数表system_table 中了,所以才可以传入这么少的参数进行初始化。
理解上面这段代码要和前面讲过的有关sysinit“独立编译连接模块”联系起来考虑了,有时感觉很难说清其中的关系,如果大家看了这里还是不好理解,那就得回头多琢磨琢磨,回想一下linux/gnu 机理吧!
其中牵涉到sysinit_data 这个数组名的调用,数组名当然就是数组的首地址,在bios.c的头部已经定义了extern const unsigned char sysinit_data[];而这个sysinit_data[]其实是在根目录下的sysinit.c 中,在连接的时候,可以把二者联系起来,所以这里的sysinit_data 在连接时就是指到了目录下的sysinit.c 中的sysinit_data[]里,也就是里面的“二进制文件”——独立编译好的sysinit 功能模块(前面已经大力讲述),所以刚刚sysinit 独立编译的时候,将.text的地址定位在0x03FE0000,也就是和这里配合!!
0x03FE0000 是什么地址?是SRAM_BASE,4510b 内部RAM,系统上电复位时SRAM就是这个地址!上面代码将sysinit_data[]复制到SRAM_BASE 中。
从独立编译到用bin2c 生成数组,再到数组名引用,做了这么多工作其实也就是定位sysinit 等独立编译好的功能模块的二进制代码。这种定位方式是这个BIOS 最巧的地方JJJ(我个人认为)
上面代码的其他语句就是使用一个函数指针sys_init_ptr,将sysinit 二进制代码复制到0x03FE0000 后,立即将0x03FE0000 赋给这个函数指针,然后跳入该函数指针所指的函数中。而这个函数指针sys_init_ptr 指的当然是0x03FE0000 处,前面已经阐述过,这里复制着sysinit 的编译连接好的二进制代码,这个二进制代码的第一条指令就是跳到sysinit 文件夹中的sysinit.c 中
以上是这个BIOS 的精髓所在!
3.3 系统初始化
刚刚那段代码将sysinit 的二进制代码复制到SRAM_BASE 中,然后跳到第一条指令,也就是sysinit 目录下的head.S 中
/sysinit/head.S
.globl stext
.section ".text"
stext:
b sys_init
这条指令当然是跳到sysinit.c 的中的sys_init( )函数中,这个函数主要起“系统初始化”的作用,让我们来看看代码的内容:
有关的程序编写方面就不多说了,系统初始化所需要的初始化值主要来自于system_table 指向的“系统参数表”结构体,最终归结到——param.tbl,这个前面已经描述过了。
设置extdbwth 的代码还是比较好理解的,结合datasheet 看应该没有问题。接下来的就是设置rom 和sdram 了,也就是初学者最怕的、传说中“地址重映射”!
首先来看看ROM 的设置:
/sysinit/sysinit.c
base = rom_base;
for (i = 0; i < 6; i++) {
sys_regs.romcon[i] = (system_table->rom_table[i].flags & ~0x3ffffc00) |
(((base + system_table->rom_table[i].size) & 0x03ff0000) << 4) |
((base & 0x03ff0000) >> 6);
base += system_table->rom_table[i].size;
}
6 个romcon 寄存器逐一设置,我们来理解一下设置中的代码吧!
设置中用到了两个变量:base 和system_table->rom_table[i].size,其中base = rom_base;也就是BIOS 的_rom_base=0x01000000,这时flash 地址已经要被设置为从16M 处开始了,而system_table->rom_table[i].size 这个值当然是由system_table 去param.tbl 中取到初始化的值。
从datasheet 中看到,10~29 是设置rom 地址的,所以首先用个~0x3ffffc00 与操作来对10~29 这些位清零,然后设置ROM Bank Next Pointer : (((base + system_table->rom_table[i].size) & 0x03ff0000) << 4)先将后16 位清零,(这是datasheet 中规定好的,这里设置的地址是64k 的整数倍,也就是原地址左移16 位),然后左移4 位,这是由于((base + system_table->rom_table[i].size) & 0x03ff0000)这个值指示的ROM Bank Next Pointer 是真正要设置的值是在16~25(低16 位已经被清零),可是按上图,寄存器中ROM Bank Next Pointer 的位置却在20~29,所以要左移4 位才符合寄存器设置的位置!
然后的循环操作不过是重复上面的“故事”!
接下来就是设置SDRAM 了:
/sysinit/sysinit.c
base = dram_base;
for (i = 0; i < 4; i++) {
sys_regs.dramcon[i] = (system_table->dram_table[i].flags & ~0x3ffffc00) |
(((base + system_table->dram_table[i].size) & 0x03ff0000) << 4) |
((base & 0x03ff0000) >> 6);
base += system_table->dram_table[i].size;
}
其实SDRAM 的设置和ROM 差不多,这里就不说了
在设置了其他寄存器后,用outl()函数将设置好的寄存器值写到相应的寄存器中:
/sysinit/sysinit.c
outl(sys_regs.extdbwth, EXTDBWTH);
outl(sys_regs.romcon[0], ROMCON0);
outl(sys_regs.romcon[1], ROMCON1);
outl(sys_regs.romcon[2], ROMCON2);
outl(sys_regs.romcon[3], ROMCON3);
outl(sys_regs.romcon[4], ROMCON4);
outl(sys_regs.romcon[5], ROMCON5);
outl(sys_regs.dramcon[0], DRAMCON0);
outl(sys_regs.dramcon[1], DRAMCON1);
outl(sys_regs.dramcon[2], DRAMCON2);
outl(sys_regs.dramcon[3], DRAMCON3);
outl(sys_regs.refextcon, REFEXTCON);
其中outl()函数的定义是在board.h 中,主要
/board.h
#define VPchar *(volatile unsigned char *)
#define outl(data, addr) (VPint(addr) = (data))
完成了“地址重映射”!
接下来是其他一些I/O 口寄存器的设置,从而完成了系统的初始化!!
3.4 串口初始化
上面讲了这么多,就完成了bios_main()中的这一句话,真是辛苦: sys_init(system_table, _rom_base, DRAM_BASE);
接下来的这句话是:
console_init();
这句话是跳到了console.c 中的函数里了:
/console.c
int console_init(void)
{
outl(0x03, ULCON0);
outl(0x09, UCON0);
outl(0x1A0, UBRDIV0); //57600,N,8
return 0;
}
console_init()函数其实就是对ULCON0,UCON0,UBRDIV0 这三个串口0寄存器进行设置,得到相应的串口设置,具体设置方法见datasheet。
设置好串口后,就可以向超级终端printf 了,这个printf 函数也是在console.c 中定义的,而不是c 语言下的库函数。有关串口读写这些函数这里就不介绍了!
3.4 biosapi 初始化
biosapi,顾名思义,就是提供类似于操作系统的API 接口,通过这些接口可以取得bios的相关信息,而不必知晓复杂的bios 底层的机制!这些API 操作其实大多是取得系统参数表中的参数。
biosapi 也是作为一个“单独编译连接的功能模块”进行使用的,所以调用的机理和前面说过的sysinit 基本上是一样的,不过,中间多了一个解压缩过程,让我们看看下面的代码:
/bios.c
int biosapi_init(void)
{
unsigned char *inbuf;
unsigned long insize;
unsigned char *outbuf;
unsigned long outsize;
struct biosapi_init_struct init_param;
BIOSCALL *fp;
inbuf = (unsigned char *)biosapi_data;
outbuf = (unsigned char *)BIOS_API_ADDR;
insize = 0x7fffffff;
outsize = 0x7fffffff;
gunzip(inbuf, &insize, outbuf, &outsize);
init_param.rom_base = _rom_base;
init_param.dram_base = DRAM_BASE;
init_param.system_table_offset = system_table_offset;
fp = (BIOSCALL *)outbuf;
return (*fp)(0, (unsigned long)&init_param); // Init BIOS call
}
其中:inbuf = (unsigned char *)biosapi_data;就是定位到“biosapi 功能模块的二进制代码”,这些二进制代码被压缩过;
至于如何被压缩,如何独立编译连接biosapi 功能模块,大家可以自己根据sysinit 中讲述的原理,打开biosapi 文件夹好好体会一下里面的.c、.ld、Makefile 等等文件!
outbuf = (unsigned char *)BIOS_API_ADDR;是二进制代码解压缩的目标地址,而后跳入该地址执行。和sysinit 的机制差不多,只不过sysinit 是直接复制到相应的内存中,而biosapi则是解压缩到该相应的地址,地址值为BIOS_API_ADDR,是个在config.h 中定义的宏变量。
BIOS_API_ADDR 其实就是biosapi 编译连接中.text 的地址,所以将biospai 的二进制代码解压到outbuf,也就是BIOS_API_ADDR 处,程序才可以正确执行!!
gunzip(inbuf, &insize, outbuf, &outsize); 这句话就是将二进制代码解压缩到outbuf 处。
然后将biospai 初始化需要的参数设置好,最后两行代码就是采用函数指针跳到outbuf处执行:
/bios.c
fp = (BIOSCALL *)outbuf;
return (*fp)(0, (unsigned long)&init_param);
跳到什么地方呢?
当然是biosapi 中的head.S 中的代码里,这是在biospai 这个功能模块编译连接时就已设定好的。
/biosapi/head.S
.globl stext
.section ".text"
stext:
b bios_call_internal
可以看出,是跳入biosapi 文件夹中的biosapi.c 中的bios_call_internal()函数中:
/biosapi/biosapi.c
int bios_call_internal(unsigned long id, unsigned long arg)
{
switch (id) {
case BIOSCALL_INIT:
bios_call_init((struct biosapi_init_struct *)arg);
break;
很容易看出这个函数就是判断参数中的ID,然后调用或返回不同的和bios 底层相关值,通过函数参数arg 传回!!!
传进来的ID 是0,也就是说调用的是BIOSCALL_INIT:
bios_call_init((struct biosapi_init_struct *)arg);
看看bios_call_init ()函数,其实很简单,就是在内存中建立一个config_table,这个config_table 其实就是一个动态的system_table 而已,下面就是将system_table 中的参数传入到config_table 中,这样做的目的就是可以修改系统参数,但是不修改原始的flash 中的系统参数。
那么系统是怎么调用API 的呢?通过bioscall.c 中转的,在根目录下的bioscall.c 中定义了其他程序可调用的函数,由这些函数来实现的API 调用。
/bioscall.c
int bios_call(unsigned long id, unsigned long arg)
{
BIOSCALL *bios_call_ptr = (BIOSCALL *)BIOS_API_ADDR;
return bios_call_ptr(id, arg);
}
在bioscall.c 中最核心的函数就是上面这个bios_call()函数,这个函数就是两个参数——id 和arg,该函数中首先定义一个函数指针,然后跳到这个函数指针指向的函数去执行,这种调用方法大家应该不陌生了,在这个BIOS 中比比皆是。
很显然,这个函数指针又指向了biosapi 的二进制代码中(BIOS_API_ADDR),也就是又到了biosapi.c 中的int bios_call_internal(unsigned long id, unsigned long arg)函数去了,然后又是判断id,接着就是调用…….
3.5 BIOS 菜单选择
让我们回到bios_main()中,在打印了四行信息后,接下去的代码是:
/bios.c
startup_mode = bios_startup_mode();
if (startup_mode == BOOT_MENU) {
startup_mode = menu(system_table);
}
return (unsigned char *)_rom_base; //return to head.S with _rom_base in r0
以上代码和原来的Hayden Luo BIOS 不太一样了,不过大致机制没有什么大变化,顶多是原来的BIOS 提供的功能多一些,强大一些。
这段代码根据启动模式的不同,转向了menu(system_table)函数,这个menu 函数是BIOS中最核心的函数,起到“打印菜单”——>“接收选择”——>“装入相应程序执行”,下面我们就来看看这个menu 函数,在bios-dyh 中也已经对其进行了相应的修改,但主要的东西没有变化!
/bios.c
int menu(struct system_table_struct *system_table)
{
int select, partition_num = 0;
unsigned long input_data;
unsigned long i;
unsigned long linux_origin=0x01010000;
while (1) {
select = main_menu(system_table);
if (select == BOOT_LOAD_FIRMWARE) {
for(i=0;i<1024*1024*2;i++)
{
*(volatile unsigned char*)(0x8000+i)=*(volatile unsigned
char*)(linux_origin+i);
}
asm( "
ldr r14, =0x00008000;
mov PC, r14; "
);
break;
}
switch (select) {
case BOOT_UPDATE_BIOS:
load_tftp(select, partition_num);
break;
case BOOT_UPDATE_FIRMWARE:
printf("Sorry,this function is not complement/r/n/n/n");
break;
case BOOT_LOAD_PROGRAM: {
uart_receive();
break;
}
case BOOT_BIOS_SETUP:
printf("Sorry,this function is not complement/r/n/n/n");
break;
default:
break;
}
}
return select;
}
这段代码首先当然是打印启动菜单,然后取得用户的选择值,这些都是通过main_menu(system_table)完成。取得选择值后,就根据选择值来判断该执行什么代码或那些功能模块。
由于bios-dyh 中功能模块已经减少了几个,同时我根据BIOS 的原理自己加上一个功能模块uart_rec,这个功能模块可以从串口接收可执行文件到dram 中,然后跳到这个可执行文件的入口处执行。这个uart_rec 是我根据BIOS的机理写的一个小模块,机理等同与sysinit、tftp 等独立编译连接的功能模块,供大家参考学习使用,价值不太大,虽然我用它也运行起来uClinux 了。
BOOT_LOAD_FIRMWARE:假定已经将image.ram 烧入flash 中,则直接从flash 中的0x01010000 处将image 复制到内存0x8000 处,然后用汇编语句跳到0x8000 处开始运行uClinux。
BOOT_UPDATE_BIOS:通过tftp 可直接将image.ram 装入sdram 的0x8000 处,然后跳转执行,利用ram 版调试比较方便。
BOOT_UPDATE_FIRMWARE:这个是用来烧写bootloader,不过在bios-dyh 中没有将其实现,不过大致机理都差不多!
BOOT_LOAD_PROGRAM:这个选项是我自己补充的,用来从串口接收可执行文件,送入sdram,然后执行,功能雷同于tftp,但是速度很有限,毕竟是串口,波特率是57600, 8 位,停止位1,无校验。而且需要一个可以发送串口文件的工具,推荐——串口调试助手V2.2,用过的都说不错!而且要输入文件的精确字节数和连接地址,便于串口接收二进制文件和运行该文件!!
BOOT_BIOS_SETUP:这个其实就是第四项的专门用于uClinux 的选项,所以没有具体实现!
再来看看main_menu(system_table)这个函数,这个函数其实就是起到打印菜单,取得选择值的作用,具体代码还是很好懂的!!
3.7 进入功能模块
根据选择值select 进行判断,然后分别跳到各个功能模块例如tftp,uart_rec 等等
/bios.c
switch (select) {
case BOOT_UPDATE_BIOS:
load_tftp(select, partition_num);
break;
case BOOT_UPDATE_FIRMWARE:
printf("Sorry,this function is not complement/r/n/n/n");
break;
case BOOT_LOAD_PROGRAM: {
uart_receive();
break;
}
case BOOT_BIOS_SETUP:
printf("Sorry,this function is not complement/r/n/n/n");
break;
}
至于怎么跳入功能模块执行,前面已经将了很多了,机理和sysinit 差不多,这里就不说了!!
其中tftp 的代码这里就不作分析了,因为实在没有时间和精力,我已经请twentyone 大侠来写有关tftp 的代码解释,到时大家将一睹何大侠的精彩文章。
其中uart_rec 功能模块里面的程序也比较简单,当初写出来只是检验一下自己对BIOS的一些思路是否正确,其功能主要是用来接收一段可执行文件,而后跳转执行,程序简单,这里也不多说了。用串口调试助手V2.2 发送命令的时候,一定要记得在输入字节数和连接地址时,后面加个回车键,然后再手动发出去!如果你想用它运行一下uClinux 的话也可以,在文件送入后,我对该程序延迟了几秒,便于修改波特率,默认波特率为57600,而uClinux的波特率是19200,所以要改过来,不然看不到开始界面!!
到此为止,BIOS 总体结构分析告一段落了!虽然没有对所有的代码进行逐字逐句地分析,但是通过这样总体结构分析和机理引导,相信可以给许多想学习bootloader 的同行带来一个好的开始!对于Hayden Luo Bios 中其他部分:tftp 将由twentyone 大侠执笔,其他我觉得大家应该可以在入门后自己消化了
文章错误难免,请同行多多指教,欢迎讨论!
附言:
我最近和几个ARM 同行在“嵌入式世界”网站(www.embedworld.com)组织成立了一个OPEN-JTAG 计划,其目的是开发一个开放的ARM JTAG 仿真器,为ARM 爱好者提供使用和学习ARM 仿真器的最佳平台。同时,也希望能吸引更多的ARM 爱好者加入到嵌入式开发的行列,一起学习、一起交流、一起提高。OPEN-JTAG 项目的宗旨是:为ARM 爱好者提供一个开放的、强大的ARM JTAG 仿真器使用和学习平台。
欢迎对ARM 及JTAG 仿真器感兴趣的嵌入式系统同行参加,为OPEN-JTAG 计划贡献一份力量!谢谢!
网址:http://www.embedworld.com/forum_list.asp?forum_id=18
附录1
GNU/Linux 下ARM 映象文件生成机理之二
这个附录中的机理是对ARM 学习报告002 的补充,在ARM 学习报告002 中只讲述了Object 文件的格式和其中包含的段,以及怎么由输入段生成输出段。在这个附录中,将对Object 文件连接生成可执行文件的机理进行阐述。
强调这些机理,因为嵌入式系统不同于其他通用计算机系统,通用系统中可以对某些东西视而不见,但嵌入式系统的每个过程的来去,我们都必须有所了解,才能完全理解嵌入式程序,才可以编出好的嵌入式应用程序。
编译程序的工作过程一般分为五个阶段:词法分析、语法分析、语义分析和中间代码生成、优化、目标代码生成。前面四个阶段我们只需要知道大致“做了什么”就好,对我们来说,最关心的就是目标代码——.o 文件。
目标代码的形式可以是绝对指令代码,也可以是可重定位的指令代码或汇编指令代码。
现代多数编译程序所生成的目标代码都是可重定位的指令代码,这种代码在运行前必须借助于一个连接装配程序把各个目标模块(包括系统的库模块)连接在一起,确定程序变量(或常数)在主存中的位置,装入内存中的指定起始地址,使之成为一个可以运行的绝对指令代码程序。以上这些都是经典编译原理书上说的。
在ARM 学习报告002 中我们已经对object 文件做了大规模的分析,并对elf 文件格式做了详细介绍,主要为下面的机理分析打基础。
对于可重定位的object 文件来说,在其elf 文件中一般都包含.rel<section name>段(section),例如head.o 中包含了.rel.text,该段中就包含了需要在连接中重定位的项(入口, entry),这些项在连接时将被修改,如图1 所示。
图1 readelf 中打开的head.o 的rel.text 段
其实就是这些relocation 项在连接前后要变
从概念上来说,链接器合并一个或多个可重定位文件来组成输出。它首先决定怎样合并、定位输入文件,然后更新符号值,最后进行重定位。对于可执行文件和共享的目标文件而言,重定位过程是相似的并有相同的结果。至于连接器在连接过程中如何根据这些项的属性(偏移量,类型等)来进行符号值更新,然后重定位,可以参考elf 文件格式文档。
上面我们已经知道了一个可重定位的object 文件在连接前后需要重定位哪些量,那么在重定位过程中,符号值的确定主要依据symbol table,symbol table 是动态变化的。这个symboltable 主要在elf 文件格式中的.symtab 段里,可以参见报告002 第9 页。
一个object 文件的符号表(symbol table)保存了一个程序在定位和重定位时需要的定义和引用的信息。一个符号表索引是相应的下标。让我们再来看看head.o 中的symol 值的图:
图2 readelf 读出的head.o 中的symol table 值
Symbol table 表在编译连接过程都非常重要,每个过程都要更新该表中的符号的信息,以便在连接时可以引用。并不是Symbol table 表中的每个符号都需要重定位(也就是都在rel<.section>出现),但是表中的每个符号在连接时都要用到!都有可能被更新!
如果该符号的值没有在可重定位的文件中定义,那么其值暂时为0。
总结来说,可重定位的object 文件包含两个段或表——.rel<section>和symbol table,两表中包含了连接时所用到的符号和信息,.rel<section>提供了需要定位哪些符号,怎么重定位;而symbol table 提供了这些连接时用到的符号的信息。在连接时,symbol table 被更新,.rel<section>指出的符号根据symbol table 的相应值被重定位!当然,最后生成的可执行文件也包含了这两个段或表!!
其实以上这些连接机理本来想单独做成一个学习报告的,其实还有很多很好的贴图可以来形象讲述连接过程,但是由于某些原因,只好合在这里讲了,可能不太好懂,大家自己多摸索一下吧
举几个例子,可能说的更明白一些!
例1:head.S 文件中bl bios_main 指令怎么定位到bios.c 中的bios_main()函数?
二者都经过编译生成相应的head.o 和bios.o 文件,这时他们还没有联系起来,就如图2,
这是head.o 中的bios_main 的值还是00000000,看到了吗?跳转指令还是不能跳到正确的地方阿?!!而此时,bios.o 的symbol table 中也有一个bios_main 变量,这个变量值也没有最后确定下来,只是暂时值。接着,连接工具LD 开始连接各个.o 文件了,它首先根据连接脚本决定如何合并.o 文件(bios 是由这些.o 文件合并起来的),等到各个.o 文件都“放好”位置了,开始更新整个symol table 值,这时的symol table 已经是bios 新生成的symol table了,是由各个.o 文件中的symol table 合并起来的(可以用readelf –a bios 命令看),里面当然包含了bios_main 这个需要重定位的变量,最后用更新的值重定位了head.o 文件中blbios_main 指令,从而达到准确跳转到该函数最后在可执行文件中的位置。
看看图3 中连接后的bios_main 的值,等于01000584。
图3 readelf 读出的bios 中的symol table 值
例2:bin2c 产生的只包含一个数组的c 源程序
这个例子讲述sysinit 这个文件夹中编译连接后,由bin2c 工具产生的只包含一个数组的c 源程序的定位。注意由于根目录和子文件夹里的文件名字相同,所以请分清!
该源程序中只包含一个数组,而这个数组中的字符元素其实就是sysinit 已经编译连接好的二进制代码,注意,是已经编译连接成功,也就是里面的所有变量都已经重定位过了,是可正常执行的,但是必须将其复制到连接的地址处。
BIOS 中最巧妙的地方就是引用该数组,从而达到引用数组中包含的已编译连接好的二进制代码。那么引用过程中是怎么定位的呢?
首先当然先看看由bin2c 生成的c 源代码开头部分:
/sysinit.c
* autogenerated file -- DO NOT EDIT! */
const unsigned char sysinit_data[]= {
0x1e, 0x00, 0x00, 0xea, 0x0d, 0xc0, 0xa0, 0xe1,
0x00, 0xd8, 0x2d, 0xe9, 0x04, 0xb0, 0x4c, 0xe2,
0x08, 0xd0, 0x4d, 0xe2, 0x10, 0x00, 0x0b, 0xe5,
0x10, 0x30, 0x1b, 0xe5, 0x10, 0x00, 0x53, 0xe3,
……
}
/* Note: 1460 characters output */
/* end of file */
可以看到这个源代码中就包含了一个数组sysinit_data[],在连接文件bios.ld 中可以看到,这个c 源程序又被编译连接生成最后的bios,也就是其中间还被编译一次,在根目录下生成了sysinit.o 文件,来看看这个object 文件的elf 吧!
图4 readelf 读出的sysinit.o 中的symol table 值
看出这个.o 文件只有一个sysinit_data 可重定位的符号,我们在bios.c 中用到这个符号,但是没有连接之前,就是用到了,也无法定位这个符号的位置!符号在什么时候被定位呢?
在生成bios 时被定位,从而使引用该符号的程序可定位到sysinit_data,也就定位到了其中的功能模块的二进制代码!!!定位后sysinit_data 见图5。
图5 readelf 读出的bios 中的symol table 值