SimpleScalar源码学习
alpha架构下,从sim-safe 开始,用gdb调试,学习源码,采用tests-alpha目录内的二进制文件执行。
main.c
main.c里定义了主函数和在主函数内调用的一些函数封装,只需要分析主函数即可。
主函数内部:上来掉了了一堆没见过的库和函数,还复习了一下函数指针,先是signal标准库和setjmp标准库
C标准库 setjmp.h 定义类型库变量、库宏和库函数
C标准库 signal.h
选项配置部分 主要应用 options.[c,h] 文件里面的结构体和函数,处理命令行的参数-h -v等。
调用 opt_reg_flag 函数初始化各个命令行参数,设置拥有的命令行参数以及对应配置的变量(通过地址指向实现),函数内部就是先用 calloc 初始化一个变量结构体变量然后根据入口参数赋值,最后调用 add_option 函数把opt结构体加入到odb结构体里(option data base),后面的 opt_reg_xxx都是类似的
sim_reg_options 是注册每个模拟器的单独的选项。
用 opt_process_options 处理命令行输入参数,即argc和argv。一个简单的判断之后就调用了另一个函数 __opt_process_options 。
这个 __opt_process_options 里面用了几个goto语句,判断处理完所有的数据后会goto到函数最后然后跳出。
首先判断了config、dumpconfig等选项,处理完这些调用 process_option 处理用户自定义的选项。
该函数执行完成后以上配置的所有参数的变量也对应修改,根据修改的变量用一个个 if 依次判断。
调用 banner 函数输出声明等。
然后调用 sim_check_options 检查具体的参数设置,sim-safe文件里面没有要求。
以上就是对命令行参数的处理。
接下来就是模拟器的关键部分,先是根据指令集初始化解码,调用 md_init_decoder 函数,以alpha架构为例。原理就是调用函数定义宏定义,还在函数里展开了一个头文件(?WTF)。
调用 sim_init 函数初始化模拟器,函数在sim-safe.c文件中,函数内部包含 regs_init 和 mem_init 两个初始化函数,初始化寄存器和内存单元。这两个函数分别在regs.[c,h]和memory.[c,h]定义。初始化就是把所有的变量赋零指针清空。
下一步是 sim_load_prog 根据索引顺序加载程序到模拟器。
stat_new 函数,创建统计数据库变量,和上面的odb类似,都是用结构体实现,函数内部初始化之后还创建了一个评估用的结构体。紧接着调用 sim_reg_stats 把要统计的数据存入数据库,这个也是在 sim-safe里具体实现的。
用 time 函数记录开始运行的时间。
fprintf 输出命令行参数、运行时间,调用 opt_print_options 输出以上配置的参数。
sim_aux_config 函数内未实现任何功能。
置标志位 running 表示模拟器开始运行。
进入 sim_main 函数,在模拟器模块内部具体实现,主函数内容到此结束,在 sim_main 函数中死循环不断执行。
sim-safe.c
sim-safe.c 文件中所有的函数都是在main.c中调用,比如 sim_reg_options 等。
主要分析 sim_main 函数。
MD_FETCH_INST 宏定义取指令,具体是由几个宏的嵌套组合实现,部分代码如下。
1 | /* get the next instruction to execute */ |
MEM_PAGE 完成了一个虚拟地址与物理地址转换的过程:
ptab 是页表,它被定义为结构体数组,其内部有两个成员 tag 和 page ;
tag 用于判断当前页表是否与该虚拟地址对应,不对应替换当前页表;
page 是物理地址页号,而数组的序号是虚拟地址页号,以此来实现由虚拟地址向物理地址的转换。
sim_num_insn 变量累加,这个变量配置在了统计结构体里面。
置标志位 addr 、 is_write 和 fault 。
调用 MD_SET_OPCODE 宏定义解码。
1 | /* inst -> enum md_opcode mapping, use this macro to decode insts */ |
然后开始执行程序,根据指令执行,通过几个宏定义定义指令格式,然后展开 machine.def 文件(这个文件是在 make config-alpha
的时候生成的软链接,链接到 alpha.def ),这个文件中定义了指令的具体内容,即32位中每位属于哪个部分,也是通过宏定义实现,通过 switch-case 选择指令,case的每一个选项都是宏定义的展开。
具体代码如下
1 | switch (op) |
op的类型为枚举,同样用宏定义和展开 machine.def 文件实现。
1 | /* global opcode names, these are returned by the decoder (MD_OP_ENUM()) */ |
以上步骤完成后这一次的指令就执行完成,最后为将 PC 指向下一条指令。
sim-fast.c
fast里和safe有部分不同,用了更多的宏定义,取指时用的alpha架构文件里的函数。
sim_load_prog 函数中多一条对alpha架构的宏定义判断,具体内容是预编码,然后把存到另一块内存中。
1 | #ifdef TARGET_ALPHA |
对应的,因为提前解码了指令,在执行取指步骤中不需要再解码,直接获取即可,
1 | /* load predecoded instruction */ |
regs.[c,h]
对寄存器模型定义,在头文件定义了结构体
1 | struct regs_t { |
所有的类型都是在 alpha.h 文件里单独定义的,因为类型由架构决定。
这个结构体就是表明模拟器的寄存器组包含哪些寄存器和寄存器的位宽。
在 alpha.h 文件中的定义如下
1 | typedef qword_t md_gpr_t[MD_NUM_IREGS]; |
可以看出alpha架构的寄存器都是64位宽,有整型寄存器堆、浮点寄存器堆、控制寄存器堆、PC寄存器和NPC寄存器。
整数寄存器堆和浮点寄存器堆的深度由宏定义控制,都是32。
loader.[c,h]
用于加载执行程序用的部分,主体是 ld_load_prog 函数,在 main.c 的 sim_load_prog 函数中执行调用,代码比较长不全贴了,只记录关键部分。
函数的声明及原有注释如下
1 | ld_load_prog(char *fname, /* program to load */ |
函数内部先用 eio_valid 函数判断文件名的有效性,然后调用 fopen 打开文件, FILE 指针赋值给变量 fobj,然后调用 fread 函数先读取文件头赋值给 ecoff_filehdr 结构体变量 fhdr ,sizeof为24,根据这个文件头判断程序的大小端模式和指令集架构。
文件头判断通过后,类似地调用 fread 读取sizeof为80的数据到 ecoff_aouthdr 结构体变量 ahdr,这个包含了代码大小等数据,读取完成后把结构体的数据赋值给下面的几个变量。
以上操作完成后调用 fseek 函数设置 fobj 偏移值,即把前两次读取过的数据移出。
调用 debug 函数,这个不知道是做什么的,暂时跳过。
之后进入循环,循环次数为 fhdr 结构体的变量 fhdr.f_nscns ,循环中每次将一个 sections 里面的数据转移到虚拟memory中,具体的操作由每次循环开始读取的结构体 shdr 决定。这部分应该是一种文件协议 ecoff 但是网上没有找到合适的资料,只有 coff 格式的说明,个人认为这两种类似。
循环结束后释放 fobj 指针。
进行一个简单的有效性判断之后判断memory的大小端。
计算各种参数,如堆栈基地址、代码基地址等。
依次是(原代码注释
初始化sp指针
写 argc 变量入栈
跳过argv数组和空指针(sp值增加)
为envp数组和空指针保留空间(循环实现)
填充argv指针数组和数据(循环实现)
用空指针结束argv数组
写envp指针数组
检验sp是否溢出
初始化堆底到数据段
初始化最低栈指针变量为初始栈值
最后初始化了 regs 结构体的值
1 | regs->regs_R[MD_REG_SP] = ld_environ_base; |
memory.[c,h]
loader文件加载程序就一定对应着程序存储的过程,即memory.[c,h]文件的作用。
memory.h 文件中定义了内存结构体和对 memory 操作的宏定义,类似寄存器,部分结构由alpha架构决定。
在 ld_load_prog 函数中调用memory的相关函数,将从文件读取的数据存入memory,在处理指令的时候直接从memory中存储取数据。
alpha.[def,c,h]
def文件中定义了指令集的具体指令定义,通过各种宏定义实现。
取一部分代码如下
1 | DEFLINK(CALL_PAL, 0x00, "call_pal", 0, 0xff) |
alpha.h 文件中定义了大量宏,主要是为编写指令操作时更加清晰,部分代码如下
1 | /* integer register specifiers */ |
这些部分在 def 文件出现过,本质上还是对32位指令码处理。
alpha.c 文件中的 md_init_decoder 函数就是初始化指令的过程,主要目的是为后续将机器码inst转换成操作码op
1 | md_init_decoder(void) |
syscall.[c,h]
系统调用,在C程序里面调用系统的接口,对应处理器的特权模式之类的。
比如调用 fopen 函数等。
以open为例:
1 | case OSF_SYS_open: |
用寄存器做参数传递,类似C和汇编相互调用的过程。
cache.[c,h]
cache文件是对cache的建模和描述,头文件定义了cache的相关结构体和宏。
主要函数是 cache_access 执行流程如下(用了好多goto):
- 先获得tag、set和bofs变量(宏操作 移位
- 其他变量初始化以及参数检查
- 检查是否快速命中,即这个地址就是上一次命中的,检查通过则goto到对应位置,然后return,函数内其他部分均不执行
- 检查是否用哈希查表,然后有各自对应的命中检查方法,goto到hit的位置然后return同样不执行其他部分
- 没有命中的话就首先选择cache策略,然后处理没有命中的情况
参考链接
- https://www.cnblogs.com/qiaoshanzi/archive/2013/04/27/3047203.html
- https://www.runoob.com/cprogramming/c-standard-library-setjmp-h.html
- https://www.zybuluo.com/yiltoncent/note/231062
- https://www.runoob.com/cprogramming/c-function-signal.html
- https://www.runoob.com/cprogramming/c-function-fseek.html
- 本文作者: Zheng Yuchen
- 本文链接: https://zycccccc.top/2021/01/19/simplescalar/simplescalar03-源码学习 copy/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!