问题
目前riscv的硬件往往落后于spec,比如我们手上的板子支持gc,并不支持b扩展,如果软件已经使用了b扩展指令,我们怎么把它跑起来? 直接运行的话可能是收到sigill后退出,我们需要在软件上想办法。
可能方法
我们大致有如下方法:
- 通过qemu执行,肯定是可行的,但是overhead很大,而且依赖库等等也需要处理
- 通过其他二进制翻译工具,也是可行的,同样可能引入额外的overhead,如果我们期望支持的指令能够native执行
- 在内核模拟,甚至是更高优先级mode的软件来模拟,也是可行的,内核里面有相应模拟指令的逻辑,但放到内核总归是不方便,如果要模拟很多指令,内核也不容易集成其他的开源方案
- 在用户态模拟指令,如果能行是个不错的选择
这样我们的重点放在用户态模拟指令,硬件支持的指令native执行,不支持的指令模拟执行。
实现逻辑
即使是用户态模拟也有不同的实现方法,我们选取一个简单能够快速实现的方法进行讨论。
- 实现指令模拟的逻辑。我们使用unicorn来模拟指令,unicorn是源于qemu的库,能够支持不同架构的指令,这样就不需要从头写指令模拟的逻辑
- 使用已有的逻辑来执行指令模拟,好处是不用重新发明一套指令模拟的框架。在执行非法指令时,内核会捕获该异常,应用程序最终会收到sigill信号,可以考虑在signal handler里面处理该指令
- 现在的问题就退化为怎么在应用程序中注入signal handler,首先一点我们不能改动应用程序,可能连源码都没有
我们使用LD_PRELOAD机制来注入signal handler,这种方案实现非常简单,也有一定的局限,比如需要是动态编译的,应用程序不能在注册sigill handler,但不失为快速支持新指令的好办法。这里我们替换掉_libc_start_main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void handle_ill(int signo, siginfo_t *info, void *context)
{
// call to unicorn
}
int _libc_start_main(int *(main)(int, char **, char **),
int argc, char **ubp_av,
void (*init)(void), void(*fini)(void),
void (*rtld_fini)(void),
void (*stack_end))
{
struct sigaction act = {0};
act.sa_flags = SA_SIGINFO;
act.sa_sigaction = &handle_ill;
if (sigaction(SIGILL, &act, NULL) == -1) {
perror("sigaction");
return 1;
}
int (*orig)(int *(main)(int, char **, char **),
int argc, char **ubp_av,
void (*init)(void), void (*fini)(void),
void (*rtld_fini)(void),
void (*stack_end));
orig = dlsym(RTLD_NEXT, "__libc_start_main");
return orig(main, argc, ubp_av, init, fini, rtld_fini, stack_end);
}
unicorn的使用参考它的手册,这里不再列出。