首页 > 代码库 > 自己动手写CPU之第四阶段(3)——MIPS编译环境的建立

自己动手写CPU之第四阶段(3)——MIPS编译环境的建立

将陆续上传本人写的新书《自己动手写CPU》(尚未出版),今天是第13篇,我尽量每周四篇

4.4 MIPS编译环境的建立

      OpenMIPS处理器在设计的时候就计划与MIPS32指令集架构兼容,所以可以使用MIPS32架构下已有的GNU开发工具链。本节将说明如何安装使用GNU开发工具链以及如何制作Makefile文件,从而以更加方便、快捷、自动的方式对测试程序进行编译,并得到指令存储器ROM的初始化文件inst_rom.data。

4.4.1 VisualBox的安装与设置

      GNU工具链要安装在Linux环境下,大多数读者使用的可能都是Windows平台,可以首先安装Linux虚拟机,再在Linux虚拟机中安装GNU工具链。笔者推荐使用OpenCores站点上提供的一个Linux虚拟机镜像,该虚拟机预装的是Ubuntu系统。

      在浏览器中输入地址:ftp://openrisc.opencores.org/virtualbox-image/,FTP的用户名和密码都是openrisc,登录后会出现如图4-14所示界面。


      下载最新的那个文件就可以了,笔者使用的是2011-12-15版。下载完成后解压该文件,大约4GB左右。此时还需要下载VisualBox才可以打开该文件。VisualBox是一款开源的虚拟机软件,本书使用的是4.1.22版。下载完成后安装VisualBox,安装完成后打开VisualBox,界面如图4-15所示。


      点击“新建”出现“新建虚拟机”向导,点击“下一步”,出现如图4-16所示界面。

 

      此处操作系统选择Linux,版本选择Ubuntu,点击下一步,设置内存大小,如图4-17所示。


      内存大小依据计算机情况设置,本人设置的是512M,已经够用了,毕竟我们需要编译的程序都是十分简单的,点击下一步,选择“使用现有的虚拟硬盘”,然后选择解压后的虚拟机文件,如图4-18所示。


      点击“下一步”,VisualBox会将用户刚才的设置都列出来,确认无误后,点击“创建”,这样虚拟机就创建好了。启动虚拟机,显示如图4-19所示。


      至此Linux虚拟机就已经安装好了,还需要多做一步工作,就是设置虚拟机与Windows宿主机之间的共享,这样方便以后在两个系统之间传递文件。先关闭Ubuntu虚拟机,然后打开VisualBox中虚拟机的设置界面,选择“共享文件夹”,如图4-20所示。


      点击界面右边的添加文件夹按钮,出现如图4-21所示界面:


      在其中选择共享文件夹的路径,设置名称,参考图4-21所示设置。设置完成后,可以启动虚拟机,打开终端,输入命令:

sudo mount –t vboxsf UbuntuShareFolder /mnt/

      该命令的作用是将共享文件夹挂载在/mnt/目录下,sudo表示以Root用户身份执行该命令,终端会提示输入密码,Ubuntu虚拟机默认Root用户的密码是openrisc。这样就实现了虚拟机与宿主机的文件共享,对虚拟机而言共享文件放在/mnt/路径下,对宿主机而言共享文件放在图4-21所示的F盘UbuntuShareFolder文件夹下。

4.4.2 GNU工具链的安装

      在本书附带光盘的tools目录下提供了GNU工具链安装文件,文件名是mips-sde-elf-i686-pc-linux-gnu.tar.tar,将该文件复制到上一小节设置的共享文件夹下,即可通过Ubuntu虚拟机访问该文件。将安装文件复制到Ubuntu的/opt目录下,打开Ubuntu的终端,使用如下命令解压缩:

cd /opt
tar vfxj mips-sde-elf-i686-pc-linux-gnu.tar.tar

      然后打开用户主目录Home文件夹,在窗口菜单栏中选择View->Show Hidden Files,以显示所有文件,这样可以找到一个隐藏文件.bashrc,在此文件的最后加入 PATH 的设置,如下。

export PATH=”$PATH:/opt/mips-4.3/bin”

      重新启动Ubuntu系统。

      重启后,打开终端,在其中输入mips-sde-elf-,然后按两次Tab键,会列出刚刚安装的,针对MIPS平台的所有编译工具,如图4-22所示,表示GNU工具链安装成功。

      GNU工具链包含很多工具,但我们需要使用的不多,主要的几个工具如下。此处使用的是通用名,针对MIPS平台的工具,会在名称前增加“mips-sde-elf-”前缀。

  •  as:GNU汇编器,通常也称为GAS (GNU Assembler),as对汇编源程序进行编译产生目标文件。
  •  ld:GNU链接器,as产生的目标文件需要由ld进行链接、重定位数据产生可执行文件。
  •  objcopy:用于将一种格式的目标文件复制成另外一种格式。
  •  objdump:用于列出关于二进制文件的各种信息。
  •  readelf:类似于objdump,但是它只能处理ELF格式的文件。

4.4.3 使用GNU工具进行编译

      本小节就使用GNU工具编译4.3节的测试程序。首先在Ubuntu虚拟机中新建一个文件,文件名为inst_rom.S,内容如下。相比4.3节的测试程序,多了三条编译指导语句。

.org 0x0          // 指示程序从地址0x0开始
.global _start    // 定义一个全局符号_start
.set noat         // 允许自由使用寄存器$1 
_start:
   ori $1,$0,0x1100        # $1 = $0 | 0x1100 = 0x1100
   ori $2,$0,0x0020        # $2 = $0 | 0x0020 = 0x0020
   ori $3,$0,0xff00        # $3 = $0 | 0xff00 = 0xff00
   ori $4,$0,0xffff        # $4 = $0 | 0xffff = 0xffff

      对“set noat”做进一步说明,这是一个汇编控制伪操作。在第1章介绍MIPS32架构中的通用寄存器时已经提到MIPS32中的通用寄存器都有约定名称,其用法也遵循一些约定,比如:寄存器$1,编程时的约定名称为at,一般留给汇编器使用,程序中不直接使用。如果直接使用,汇编器会发出警告,此处设置“set noat”就是表示可以自由使用寄存器$1,汇编器不会发出警告。

      在Ubuntu中打开终端,使用cd命令将路径调整到上述inst_rom.S所在目录,然后使用如下命令编译代码。其中添加了“-mips32”选项,表示按照MIPS32指令集架构进行编译。

mips-sde-elf-as –mips32 inst_rom.S –o inst_rom.o

      上述命令会得到目标代码inst_rom.o。打开inst_rom.o文件,可以发现其最初的四个字节是:0x7F、0x45、0x4C、0x46,这说明inst_rom.o是一个ELF文件。

      为了便于读者理解,下面将简单介绍一下ELF文件,读者朋友如果对这不感兴趣或者希望尽快了解编译链接过程的可以跳过下面的介绍,直接阅读4.4.4节。

      ELF(Executable and LinkableFormat)可执行链接格式,是UNIX系统实验室(USL)作为应用程序二进制接口(ABI:Application Binary Interface)而开发和发布的。ELF目标文件有三种类型。

      (1)可重定位(Relocatable)文件:保存着代码和适当的数据,用来和其他Object文件一起创建一个可执行文件或共享文件。

      (2)可执行(Executable)文件:保存着一个用来执行的程序,该文件指出了如何来创建程序进程映象。

      (3)共享目标文件:包含了在两种使用环境中链接的代码和数据。首先链接器(ld)可以将它和其余可重定位文件和共享目标文件一起处理,生成另外一个目标文件(比如:编译器和链接器把*.o和*.so一起装配成一个*.exe文件)。其次,动态链接器(Dynamic Linker)可将它与某个可执行文件以及其它共享目标文件组合在一起创建进程映像(比如:动态加载器把*.exe程序和*.so加载进内存执行)。

      无论何种类型的ELF文件,其结构都是相同的。ELF文件由四部分组成:ELF header、Program header table、Sections、Section header table。其最开始的部分就是ELF header,定义如下:

#define EI_NIDENT 16
typedef struct{
       unsigned char     e_ident[EI_NIDENT]; //占用16个字节
       Elf32_Half        e_type;       //Elf32_Half表示是2个字节大小
       Elf32_Half        e_machine;
       Elf32_word        e_version;    //Elf32_Word表示是4个字节大小
       Elf32_Addr        e_entry;      //Elf32_addr也表示4个字节大小
       Elf32_Off         e_phoff;      //Elf32_Off也表示4个字节大小
       Elf32_Off         e_shoff;
       Elf32_Word        e_flags;
       Elf32_Half        e_ehsize;
       Elf32_Half        e_phentsize;
       Elf32_Half        e_phnum;
       Elf32_Half        e_shentsize;
       Elf32_Half        e_shnum;
       Elf32_Half        e_shstrndx;
}Elf32_Ehdr;

      开始四个字节是固定不变的:0x7F,紧接着是ELF三个字符的ASCII码,这四个字节表明这个文件是一个ELF文件。此处以inst_rom.o为例,介绍e_ident字段后面字节的含义,参考图4-23。

  •  e_type是01,表示是可重定位文件
  •  e_machine表示运行该程序需要的体系结构,此处为0x08,表示MIPS R3000
  •  e_version表示文件版本,此处是1
  •  e_entry表示程序的入口地址,此处是0x0
  •  e_phoff是Program header table在文件中的偏移量(以字节计数),此处是0x0
  •  e_shoff是Section header table在文件中的偏移量(以字节计数),此处为0x98
  •  e_flags为保存着相关文件的特定处理器信息,此处为0x50000000,表示MIPS32
  •  e_ehsize表示ELF header的大小,此处为0x34
  •  e_phentsize表示Program header table中每一个条目(一个Program header)的大小,此处为0x0
  •  e_phnum表示Program header table中有多少个条目,此处为0
  •  e_shensize表示Section header table中每一个条目(一个Section header)的大小,此处为0x28
  •  e_shnum表示Section header table中有多少个条目,此处为0x09
  •  e_shstrndx保存着字符表相关入口的节区头部表索引,此处为0x06

     通过上述解释可以了解到这个文件是一个可重定位(Relocatable)文件,不是可执行文件,同时了解到该文件包含的Program header table、Section header table信息。对inst_rom.o而言,没有Program header table。按照给出的偏移信息,我们可以得到Section header table表的位置,通过Section header table得到每个Section的位置。

      当然,按照ELF header的内容以及Section header table,我们可以按图索骥地分析所有Section,但是这样效率太慢,借助于GNU工具链中的mips-sde-elf-readelf,我们可以直接得到Section信息,如图4-24所示。


      注意添加“-S”选项。这里列出了9个Section的信息,注意其中的“.text”这个Section,它的起始地址是0x34,长度是0x10,我们列出这个Section的内容如图4-25所示。


      参考4.3.3节可知,这0x10个字节正是测试程序中的4条指令对应的二进制字。

4.4.4 使用GNU工具进行链接

      通过编译得到了一个可重定位ELF文件,但这个文件还不能执行,需要通过链接转化为可执行文件,然后才能执行。使用链接工具mips-sde-elf-ld完成这项工作,在mips-sde-elf-ld的参数中需要声明一个链接描述脚本,链接描述脚本描述了输入文件的各个Section如何映射到输出文件的各个Section中,并控制输出文件中Section和符号的内存布局。可以通过新建一个Document作为链接描述脚本,文件名为ram.ld,内容如下。
MEMORY
        {       
        ram    : ORIGIN = 0x00000000, LENGTH = 0x00001000
        }

SECTIONS
{
	.text :
        {
        *(.text)
        } > ram

        .data :
        {
        *(.data)
        } > ram

        .bss :
        {
        *(.bss)
        } > ram
}

Entry(_start)

      其中定义了一个存储块——ram,其起始地址是0x0,长度是0x1000,然后指示链接器输出文件包含三个Section,分别是.text、.data、.bss,其中.text从ram的起始地址开始存放,后面跟着.data、.bss,并且输入文件的Section .text存放在输出文件的.text中,输入文件的Section .data存放在输出文件的.data中,输入文件的Section .bss存放在输出文件的.bss中。最后的Entry指定程序的入口地址,也就是第一条执行的指令地址是_start符号的值,从汇编代码中可知_start符号就是0x0。现在就可以使用链接器了,在Ubuntu虚拟机的终端中输入如下命令。

mips-sde-elf-ld –T ram.ld inst_rom.o –o inst_rom.om

      得到链接后的文件inst_rom.om,这也是一个ELF格式的文件,其ELF header如图4-26所示。


      上一小节是手工分析inst_rom.o的ELF header,主要是为了帮助读者理解,其实可以直接使用工具分析ELF header,在终端中输入如下命令将自动分析inst_rom.om的ELF header。

mips-sde-elf-readelf –h inst_rom.om
      其中加上参数“-h”表示只读取ELF header,得到结果如图4-27所示。

      从中可知inst_rom.om是一个可执行文件。读者朋友也许已经注意到了,inst_rom.om比inst_rom.o多了Program header,而这在inst_rom.o里面是没有的,与Section header一样,Program header也可以使用一个结构体描述,如下。

typedef struct{
       Elf32_Word          p_type;
       Elf32_Off           p_offset;
       Elf32_Addr          p_vaddr;
       Elf32_Addr          p_paddr;
       Elf32_Word          p_filez;
       Elf32_Word          p_memsz;
       Elf32_Word          p_flags;
       Elf32_Word          p_align;
}Elf32_Phdr;
       我们还是使用工具mips-sde-elf-readelf从inst_rom.om中分析出一个Program header,然后结合这个Program header解释上面各个各项的含义。使用如下命令得到Program header的信息。

mips-sde-elf-readelf –l inst_rom.om
      其中加上“-l”参数,表示列出Program header的信息,显示如图4-28所示。

      借助上图介绍Program header各个字段的含义:

  •  p_type为LOAD,表示可加载
  •  p_offset表示段的第一个字节在文件inst_rom.om中的偏移,此处为0x10000
  •  p_vaddr表示段的第一个字节在内存中地址,此处为0
  •  p_paddr为0,在物理地址定位有关联的系统中,该成员是为该段的物理地址而保留的
  •  p_filez表示段在文件中的长度,此处为0x10
  •  p_memsz表示段在内存中的长度,此处为0x10
  •  p_flags为RE,表示可读、可执行
  •  p_align为0x10000,根据此项确定段在文件以及内存中如何对齐

      该Program header表示将inst_rom.om文件中从偏移0x10000开始的0x10个字节放置在内存的0x0处,打开inst_rom.om可以发现从偏移0x10000开始的0x10个字节的内容与inst_rom.o中Section .text的内容一样,所以当这个Program Section加载入内存后,会使得内存从地址0x0开始的0x10个字节存放的就是测试程序的4条指令。

      分析到这里,读者是不是对编译、链接过程有了比之前更深的了解?其实这些背景知识与OpenMIPS处理器关系不大,但是知道这些有助于理解编译链接的过程。总结一下,编译链接的过程很简单,只需要两步,如下。

编译:mips-sde-elf-as –mips32 inst_rom.S –o inst_rom.o
链接:mips-sde-elf-ld –T ram.ld inst_rom.o –o inst_rom.om

4.4.5 得到ROM初始化文件

      上一小节得到的inst_rom.om是一个ELF格式的可执行文件,与我们希望的指令存储器ROM初始化文件inst_rom.data的格式有很大区别,需要进行格式转化。在GNU工具链中提供了另一个工具mips-sde-elf-objcopy,用于将一种格式的目标文件转化成另外一种格式。在这里,可以使用mips-sde-elf-objcopy得到inst_rom.om的二进制(Binary)形式,使用方法如下。得到的二进制文件inst_rom.bin的内容如图4-29所示。

mips-sde-elf-objcopy –O binary inst_rom.om inst_rom.bin

      从图4-29可以发现,bin文件的内容正是测试程序中4条指令对应的二进制字,现在只需要编写一个小程序将bin文件转化为ModelSim中存储器初始化文件的格式。这个小程序很简单,此处不再列出代码,在本书附带的光盘中可以找到源程序,程序名为Bin2Mem.exe,使用方法如下。得到的inst_rom.data文件如图4-30所示。

./Bin2Mem.exe –f inst_rom.bin –o inst_rom.data

      好了,现在回忆一下从源代码得到ModelSim仿真时可以使用的指令存储器ROM初始化文件一共需要4步:编译、链接、得到bin文件、格式转化,如下。

编译:         mips-sde-elf-as –mips32 inst_rom.S –o inst_rom.o
链接:         mips-sde-elf-ld –T ram.ld inst_rom.o –o inst_rom.om
得到bin文件:  mips-sde-elf-objcopy –O binary inst_rom.om inst_rom.bin
格式转化:      ./Bin2Mem.exe –f inst_rom.bin –o inst_rom.data

未完待续,后续将上传第4阶段的代码!