模拟器中最核心的函数是simulate()函数,这个函数对模拟器进行周期级模拟,每次模拟中,会执行fetch()、decode()、execute()、accessMemory()和writeBack()五个函数,每个函数会以上一个周期的流水线寄存器作为输入,并输出到下一个周期的流水线寄存器。在周期结束时,新的寄存器的内容会被拷贝到作为输入的寄存器中。在执行过程中,每个函数都会处理有关数据、控制和内存访问冒险的内容,并且在适当的地方记录历史信息。由于之间的交互关系比较复杂,因此在上图中并没有画出。由于相关函数代码过长,不便于在此贴出,因此关于实现的更多细节请参见src/Simulator.cpp。
三、具体设计和实现 3.1 内存管理模块MemoryManagerMemoryManager的功能是为模拟器提供一个简单易使用的内存访问接口,必须支持任意内存大小、内存地址的访存,还要能检测到非法内存地址访问。事实上,这非常类似于操作系统中虚拟内存的机制。因此,MemoryManager的内部实现采用了类似x86体系结构中使用的二级页表的机制。具体地说,将32位内存空间在逻辑上划分为大小为4KB(2^12)的页,并且采用内存地址的前10位作为一级页表的索引,紧接着10位作为二级页表的索引,最后12位作为一个内存页里的下标。
页表结构可以如下声明
uint8_t **memory[1024];其中,memory指向一个长度为1024的一级页表数组,memory[i]指向长度为1024的二级页表数组,memory[i][j]指向具体的内存页,memory[i][j][k]可以取出内存地址为(i<<22)|(j<<12)|k的一个字节。可以在需要的时候对memory进行动态内存分配和释放。模拟器对memory的一个访存过程的示例如下
uint8_t MemoryManager::getByte(uint32_t addr) { if (!this->isAddrExist(addr)) { dbgprintf("Byte read to invalid addr 0x%x!\n", addr); return false; } uint32_t i = this->getFirstEntryId(addr); uint32_t j = this->getSecondEntryId(addr); uint32_t k = this->getPageOffset(addr); return this->memory[i][j][k]; }关于MemoryManager实现的更多信息,参见src/MemoryManager.cpp。
3.2 可执行文件的装载、初始化本模拟器的可执行文件加载部分采用了GitHub上的开源库ELFIO(https://github.com/serge1/ELFIO),由于这个库只有头文件,所以导入工程相当容易,相关头文件在include/文件夹下。
使用这个库进行ELF文件加载相当容易
// Read ELF file ELFIO::elfio reader; if (!reader.load(elfFile)) { fprintf(stderr, "Fail to load ELF file %s!\n", elfFile); return -1; }加载ELF文件进内存的代码如下,直接按照ELF文件头的信息将每个数据段拷贝到指定的内存位置即可,唯一需要注意的是文件内数据长度可能小于指定的内存长度,需要用0填充。值得一提的是本模拟器在设计时并未考虑支持32位以上的内存,因为内存占用如此之大的用户程序是比较罕见的,在我们用的测试程序中不会出现这种情况。
void loadElfToMemory(ELFIO::elfio *reader, MemoryManager *memory) { ELFIO::Elf_Half seg_num = reader->segments.size(); for (int i = 0; i < seg_num; ++i) { const ELFIO::segment *pseg = reader->segments[i]; uint64_t fullmemsz = pseg->get_memory_size(); uint64_t fulladdr = pseg->get_virtual_address(); // Our 32bit simulator cannot handle this if (fulladdr + fullmemsz > 0xFFFFFFFF) { dbgprintf( "ELF address space larger than 32bit! Seg %d has max addr of 0x%lx\n", i, fulladdr + fullmemsz); exit(-1); } uint32_t filesz = pseg->get_file_size(); uint32_t memsz = pseg->get_memory_size(); uint32_t addr = (uint32_t)pseg->get_virtual_address(); for (uint32_t p = addr; p < addr + memsz; ++p) { if (!memory->isPageExist(p)) { memory->addPage(p); } if (p < addr + filesz) { memory->setByte(p, pseg->get_data()[p - addr]); } else { memory->setByte(p, 0); } } } }最后,需要在模拟器初始化时手动设置PC的值。模拟器还需要很多其他的初始化操作,具体可以参考src/Main.cpp。
simulator.pc = reader.get_entry(); 3.3 指令语义的解析和控制信号的处理本小节中涉及代码由于普遍过长,且存在非常强的相互依赖,单独贴出可能难以理解,因此不会在此直接贴出代码,具体内容请参见src/Simulator.cpp。
指令的取值过程参见Simulator::fetch()函数,由于RV64I指令集都是4字节定长,所以实现起来非常简单。
指令的解码过程参见Simulator::decode()函数,其中绝大多数内容都是对RISC-V Specification 2.2中规定的指令编码的直接翻译。在解码过程中,为了便于调试,decode()函数会按照RISC-V汇编格式翻译出指令字符串。此外,decode函数会模仿硬件实现在指令中抽象出op1、op2、dest等几个共有的域。分支预测模块会在解码阶段做出预测判断。