这篇博文的起因是需要写一个简单的riscv的指令模拟器(毕竟目前不需要模拟流水线和系统调用),然后记录一下自己在用C++实现过程中的踩坑。
目标是希望通过elf文件,能够根据函数名找到任意一个函数或者一个全局变量的地址或初始值。
主要利用section中的.symtab, .strtab, .shstrtab。后俩前者是记录目标文件中的符号名,后者是记录节名。因为在elfheader中记录了.shstrab的序号,所以首先可以找到.shstrab,然后再遍历section header,与.shstrtab中记录的节名比较,拿到.symtab, .strtab,再遍历符号表,与.strtab中记录的符号名进行比较,就可以拿到需要的符号对应的表项,从而获得需要的信息。
当然有一个小问题是C++会进行name mangling, 因此需要知道函数原型才能找到符号。不过这里我实际需要的是main函数的地址,有意思的事情是,C++中main函数比较特殊,不会进行name mangling。
Why name-mangling has no effect on main function in C++?
另外一个需要是通过elf文件,能够知道C库初始化gp register的值是多少。
首先是关于gp指针的作用
linker relaxation
主要是一个linker relaxation的概念,在CISC体系中通过全局指针gp访问全局变量,大概率可以减少一条汇编指令。
接下来的一个问题是riscv的C库如何初始化这个gp,这需要linker和C库的配合。
initialize gp
不过这个汇编代码还蛮奇怪的,参考下面的
RISC-V Assembler Modifiers
可以知道,上面C库中的初始化gp的方法是把符号__global_pointer$的地址放到了gp寄存器中。而__global_pointer$本身是由linker给的。源于下面的链接脚本。因此可以在elf文件中找到__global_pointer$符号,从而确定gp寄存器的值
`PROVIDE( __global_pointer$ = . + 0x800 );`
另外,下面还有一个stackoverflow上面的关于上面modifier的讨论
What do %pcrel_hi and %pcrel_lo actually do?
不过目前看得还不是特别明白,以及为什么加载的是__global_pointer$的地址而不是它的值?先mark一波。
目标是希望用一个数组把riscv的instruction type直接映射到相应种类的执行函数上(而不是用if,else)。
也就说希望达到这样一个目标
array[I type] = execItypeInstruction
array[R type] = execRtypeInstruction
......
array[instruction type](argument)
然而面临的第一个问题是如何声明这样一个指针数组,如下的stackoverflow链接回答了这个问题。
How do you declare a const array of function pointers?
再一次认识到typedef的作用
具体来说,我应该这样写
typedef void (riscvSimulator::*FP)(int, int, int);
const FP riscvSimulator::execPtr[typeNum] = {
&riscvSimulator::execR,
&riscvSimulator::execI,
&riscvSimulator::execS,
&riscvSimulator::execSB,
&riscvSimulator::execU,
&riscvSimulator::execUJ};
还有一个需要注意的事情是,这里必须要显式地取成员函数的地址。
Why must I use address-of operator to get a pointer to a member function
具体解释来说,以前声明函数指针没有取地址其实是不规范的,能够编译过是C++的implicit conversions,而对于非静态成员函数是没有这种隐式转换的。
那么,在调用的时候也需要显式地加上星号
>this->*array[i] || 类名.*array[i]
在解析riscv的4字节指令时,一个问题是如何快速提取出相应的位,直观想法是利用C结构体中的位域,但是会面临对齐,padding之类的问题,下面的链接表明可以用gcc的packed属性解决这个问题。
Is there any way to control the padding between struct members (incl. bit field) in C++?
其中还提到可以用C++11的alignof运算符解决这个问题,不过我没有查到具体应该怎么做。
目标是类似于3,希望利用unordered_map, 使(opcode, func3, func7)这个三元组直接映射到对应的指令执行函数。
发现的问题是标准的std::hash并不支持元组,下面的stackoverflow链接解释了这个问题。
Using tuple in unordered_map
具体来说,自定义hash只需要实现一个返回类型为size_t的函数对象就可以了。这里因为opcode, func3, func7的大小都是确定的。我采用了如下的hash方法。
struct tupleHash
{
size_t operator()(const tuple<uint, uint, uint> &t) const
{ // (opcode:7, func3:3, func7:7)
uint opcode, func3, func7;
tie(opcode, func3, func7) = t;
return (opcode & 0x7f) + ((func3 & 0x7) << opcodeSize) + ((func7 & 0x7f) << (opcodeSize + func3size));
}
};
const unordered_map<tuple<uint, uint, uint>, instruction, tupleHash> riscvSimulator::opcode2instruction;
定义unordered_map的时候显式地把tupleHash作为hash方法即可。
另外,上面的链接中还提到可以直接使用boost库中对tuple进行hash的源码,直接把该段源码粘贴到头文件中即可。
上面的代码声明unordered_map为const,因为这个映射关系也不需要改。然后在调用[] operator的时候编译出问题了。
passing const as this argument discards qualifiers only when using a unordered_map but not a vector
上面的链接解释了,问题出在[] operator并不是一个const 成员函数,主要是因为[] operator需要隐式地插入相应类型的零值(如果发现相应的key不在map中)。而vector的 [] operator则有一个const重载。对于const unordered_map,需要用at成员函数。
目标是把命令行的一行输入读到istringstream类中,然后再利用移位运算符读到相应的数据中。参考了如下链接。
c++ stringstream类 clear函数的真正用途
我遇到了一个类似的问题,发现istringstream的str函数没有像我预期的那样istringstream中的输入流。简单总结就是,用istringstream读到流结尾或者发生错误时,需要用clear函数刷新标记位,然后再用str读入新的字符串流。
调试时面临的一个问题是,执行一条riscv指令时,因为访存错误等原因,崩溃掉了。用coredump文件来帮助重现崩溃时的情景。
How do I analyze a program’s core dump file with GDB when it has command-line parameters?
在我的Ubuntu18.04上,用ullimit -a查看,发现默认是不生成coredump文件的。然后用ulimit -S -c unlimited命令临时开启(仅对当前shell有效)。
虽然回答中提到了很多调试coredump文件的方法,但目前我并不清楚coredump文件的内部结构是怎样的,以后有时间可以再慢慢看看。
目前觉得很有用的是以下两个:bt, 栈帧切换
这里的一个实例是,因为之前用at方法获取unordered_map的value,如果对应的key不在unordered_map中,就会抛出异常,转储为core文件。
$gdb ./simulator core
(gdb) bt
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1 0x00007f362b39a921 in __GI_abort () at abort.c:79
#2 0x00007f362ba000a9 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#3 0x00007f362ba0b506 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#4 0x00007f362ba0b571 in std::terminate() ()
from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#5 0x00007f362ba0b7f5 in __cxa_throw ()
from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#6 0x00007f362ba0291d in std::__throw_out_of_range(char const*) ()
from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#7 0x00005614250fe265 in std::__detail::_Map_base, std::pair const, void (riscvSimulator::*)(int, int, int)>, std::allocator const, void (riscvSimulator::*)(int, int, int)> >, std::__detail::_Select1st, std::equal_to >, tupleHash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits, true>::at (
this=0x561425306840 <riscvSimulator::opcode2instruction>,
__k=std::tuple containing = {...})
at /usr/include/c++/9/bits/hashtable_policy.h:769
#8 0x00005614250fd8e1 in std::unordered_map, void (riscvSimulator::*)(int, int, int), tupleHash, std::equa--Type for more, q to quit, c to continue without paging--
l_to<std::tuple<unsigned int, unsigned int, unsigned int> >, std::allocator<std::pair<std::tuple<unsigned int, unsigned int, unsigned int> const, void (riscvSimulator::*)(int, int, int)> > >::at (
this=0x561425306840 <riscvSimulator::opcode2instruction>,
__k=std::tuple containing = {...})
at /usr/include/c++/9/bits/unordered_map.h:1007
#9 0x00005614250f8b96 in riscvSimulator::execI (this=0x7fff5f1d6770)
at simulator.cpp:289
#10 0x00005614250f86cb in riscvSimulator::execInstruction (this=0x7fff5f1d6770)
at simulator.cpp:246
#11 0x00005614250f8606 in riscvSimulator::run (this=0x7fff5f1d6770)
at simulator.cpp:237
#12 0x00005614250fcaaf in main (argc=4, argv=0x7fff5f1d69d8)
at simulator.cpp:947
从这个长得要死的栈帧中可以看到,是在execI函数中抛出了out-of-range异常,这说明是执行一条I type指令时,相应的I type指令在模拟器中并没有实现。但是我希望更进一步知道具体是哪一条I type指令没有实现,但是这里已经陷入了C++库中,没办法查看源代码中相应变量的值。
(gdb) frame 9
#9 0x00005614250f8b96 in riscvSimulator::execI (this=0x7fff5f1d6770)
at simulator.cpp:289
289 (this->*opcode2instruction.at({(uint)now.opcode, (uint)now.func3, 0}))(now.rd, now.rs1, immediate);
进行栈帧切换,切换到execI函数对应的上下文中(呃,其实我并不理解gdb是如何做到这一点的)。可以看到,gdb显示了导致抛出异常的语句。
(gdb) print now.opcode
$1 = 27
(gdb) print now.func3
$2 = 1
(gdb) print pc
$3 = 67764
进一步,可以把对应的opcode, func3找出来,以及对应riscv指令的pc(这里的pc是simulator类的成员变量),这样便可以知道相应的riscv指令了。