以下内容摘自《步步惊芯——软核处理器内部设计分析》一书
上一节安装好了GNU编译工具,还设置了虚拟机与宿主机的共享文件夹,下面我们就小试牛刀,写一个简单的程序,需要读者朋友们了解的是本书所有的测试代码都是很简单的汇编程序,因为即使最简单C语言代码(如:HelloWorld)经过编译后也会得到大量目标代码,而我们的目的是了解处理器内部的工作过程,所以大量的目标代码容易分散我们的精力,为此示例代码一律采用汇编,有的甚至只有几条指令,但也能说明问题。在Ubuntu中新建一个Document,文件名可以为Example.S,输入下面的代码,我们的第一个汇编程序。
.section .text,"ax" #定义了一个Section,Section名为.text,并且是可执行Section .org 0x100 #OR1200处理器默认从地址0x100处开始执行,所以我们的代码从0x100开始 .global _start _start: l.andi r0,r0,0 #OR1200的r0寄存器应该始终为0,所以这里将其设置为0 l.extwz r1,r0 #下面两条指令是初始化寄存器r1、r2,使其都为0 l.extwz r2,r0 l.addi r1,r1,0x0A #r1的值加上0x0A存储到r1中,使得r1为0x0A l.add r2,r2,r1 #r1的值加上r2的值存储到r2中,使得r2为0x0A l.nop 0x0001 #空指令,但对模拟器有特殊含义
在终端中首先使用cd命令将路径调整到上述Example.S所在目录,然后使用如下指令编译代码:
or32-elf-as Example.S –o Example.o
图2.11 Example.o的开始部分
遇到什么学什么,这是笔者的一贯宗旨,下面就简单介绍一下ELF文件,读者朋友中如果谁对这不感兴趣或者希望尽快了解编译链接过程的可以跳过下面的分析,直接阅读2.2.3节。
ELF(Executable and Linkable Format)可执行链接格式,是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]; 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文件。此处以Example.o为例,介绍Elf header后面的字节含义,参考图2.11:
通过上述解释可以了解到这个文件是一个可重定向文件(Relocatable)文件,不是可执行文件,同时了解该文件包含的Program header table、Section header table信息。这里没有Program header table,按照给出的偏移信息,我们可以得到Section header table表的位置,通过Section header table得到每个Section的位置。
当然按照ELF header的内容以及Section header table,我们可以按图索骥地分析所有Section,但是这样效率太慢,借助于GNU工具链中的or32-elf-readelf,我们可以直接得到Section信息,如图2.12所示。
图2.12 利用程序or32-elf-readelf可以得到所有的Section信息
注意添加“-S”参数。这里列出了7个Section的信息,注意其中的“.text”这个Section,它的起始地址是0x34,长度是0x118,我们列出这个Section的内容如图2.13所示。
图2.13 Section .text的内容
在这0x118个字节中,前0x100个字节都是0x00,接下来的24个字节是什么?我们利用工具or32-elf-objdump对目标代码进行反汇编,得到指令与二进制代码的对应关系,如图2.14所示。
图2.14 使用Objdump查看反汇编结果
这里注意加上参数“-d”表示显示可执行Section的反汇编结果。显示出来的结果分为三栏,左边是指令执行时的地址,在程序中我们的第一条指令是从0x100开始的,中间一栏是对应的二进制代码,右边一栏是对应的汇编指令,对比一下图2.13与图2.14,可以发现Section .text的最后24个字节正是这6条汇编指令。
MEMORY { ram : ORIGIN = 0x00000000, LENGTH = 0x00005000 } SECTIONS { .text : { *(.text) } > ram .data : { *(.data) } > ram .bss : { *(.bss) } > ram } Entry(_start)
or32-elf-ld –T ram.ld Example.o –o Example.or32
图2.15 Example.or32的Elf header
在分析Example.o的Elf header时我们是手工分析的,主要是为了大家的理解,现在可以直接使用工具分析Elf header,在终端中输入如下命令:
or32-elf-readelf –h Example.or32
图2.16 Example.or32的Elf header信息
其中显示是一个可执行文件。读者朋友们可能注意到了,Example.or32比Example.o多了Program header,而这在Example.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;
我们还是使用or32-elf-readelf从Example.or32中分析出一个Program header,然后结合这个Program header解释上面各个各项的含义。使用如下命令得到Program header的信息:
or32-elf-readelf –l Example.or32
图2.17 Program header信息
借助上图介绍Program header各个字段的含义:
该Program header表示将Example.or32从地址0x2000开始的0x118个字节放置在内存的0x0处,打开Example.or32可以发现从0x2000开始的0x118个字节的内容与Example.o中Section .text的内容一样,所以当这个Program Section加载入内存后,会使得内存的0x100处存放的就是我们的第一条指令,而Example.or32的入口地址正是0x100。
分析到这里,大家是不是对我们的编译、链接过程有了比之前更深的了解?其实这些与我们分析OR1200关系不大,但是有时候剖析也会上瘾,拿到什么都想拆开看看里面是什么,所以笔者就情不自禁的讲了这么大一段,读者朋友如果没有这样的兴趣,那么只需要知道编译和链接的命令就可以了,很简单,重复如下:编译:or32-elf-as Example.S –o Example.o 链接:or32-elf-ld –T ram.ld Example.o –o Example.or32
为了得到可执行代码,我们需要输入两个指令:编译指令as、链接指令ld,还是有点麻烦,最好只输入一条指令就可以了,这需要使用到Makefile文件。还是先给出Makefile文件,然后作出解释。在Example.S所在目录下新建一个Document,文件名为Makefile,内容如下:
ifndef CROSS_COMPILE CROSS_COMPILE = or32-elf- endif CC = $(CROSS_COMPILE)as LD = $(CROSS_COMPILE)ld OBJECTS = Example.o export CROSS_COMPILE # ******************** # Rules of Compilation # ******************** all: Example.or32 %.o: %.S $(CC) $< -o $@ Example.or32: ram.ld $(OBJECTS) $(LD) -T ram.ld $(OBJECTS) -o $@ clean: rm -f *.o *.or32
目标:依赖文件 命令
$< 表示第一个依赖文件的名称 $@ 表示目标的完整名称
所以上述Makefile可以解读如下:
(1)用户输入make all,要求得到目标all,目标all的依赖文件是Example.or32,要先得到Example.or32
(2)要得到Example.or32,依赖文件$(OBJECTS),也就是Example.o
(3)要得到Example.o依赖于文件Example.S,这里已经提供了Example.S,满足依赖条件,然后通过执行命令$(CC) $< -o $@,实际就是or32-elf-as Example.S –oExample.o得到Example.o
(4)得到Example.o后就可以进一步得到Example.or32,通过执行命令$(LD) -T ram.ld $(OBJECTS) -o$@,实际就是or32-elf-ld-T ram.ld Example.o –o Example.or32得到Example.or32
(5)得到Example.or32,满足了目标all的依赖条件,从而实现目标all。
有了Makefile文件,我们在终端中输入“make all”就可以完成编译、链接的过程了。
sim –t Example.or32 –m1M > Example.trace
Seeding random generator with value 0x07204ad8
Or1ksim 2011-08-15
Building automata... done, num uncovered: 0/213.
Parsing operands data... done.
Resetting PIC.
loadcode: filename Example.or32 startaddr=00000000 virtphy_transl=00000000
Not COFF file format
ELF type: 0x0002
ELF machine: 0x005c
ELF version: 0x00000001
ELF sec = 5
Section: .text, vaddr: 0x00000000, paddr: 0x0 offset: 0x00002000, size: 0x00000118
S 00000100: a4000000 l.andi r0,r0,0 r0 = 00000000 flag: 0
S 00000104: e020004d l.extwz r1,r0 r1 = 00000000 flag: 0
S 00000108: e040004d l.extwz r2,r0 r2 = 00000000 flag: 0
S 0000010c: 9c21000a l.addi r1,r1,0xa r1 = 0000000a flag: 0
S 00000110: e0420800 l.add r2,r2,r1 r2 = 0000000a flag: 0
exit(0)
@reset : cycles 0, insn #0
@exit : cycles 5, insn #6
diff : cycles 5, insn #6
S: 表示出于特权模式 PC值: 0x00000100 指令的二进制编码:0xa4000000 指令的汇编代码: l.addi r0,r0,0 目的地址: r0 目的地址的新值: 0x00000000 flag的值: 0x0
原文:http://blog.csdn.net/leishangwen/article/details/21713357