riscv-semihosting

Introduce riscv semihosting

Overview

Semihosting提供了一种机制,让CPU上的代码与主机通信并借助主机侧的功能。其核心原理就是,在CPU侧执行特定序列的指令,在主机侧识别这些指令,并采集参数,调用主机侧响应功能。

RISCV 也支持Semihosting的功能。

RISCV Semihosting

riscv定义的特定指令序列是:

1
2
3
slli x0, x0, 0x1f       # 0x01f01013    Entry NOP
ebreak # 0x00100073 Break to debugger
srai x0, x0, 7 # 0x40705013 NOP encoding the semihosting call number 7

这个指令序列还有一些要求:

  • 必须是32 bit 指令码,不能是16 bit指令码
  • 如果paging使能了,这个序列必须不能跨页。也就是这个序列在同一个物理页上。因为host需要检查这个序列,而不想发生page fault。

因此,riscv semihosting trap function可以如下写:

1
2
3
4
5
6
7
8
9
10
        .option norvc
.text
.balign 16
.global sys_semihost
.type sys_semihost @function
sys_semihost:
slli zero, zero, 0x1f
ebreak
srai zero, zero, 0x7
ret

可以看出,这个指令序列都是固定的,那参数怎么传递?答案是寄存器 a0a1

  • operation number register. a0
  • parameter register. a1
  • return register. a0

在riscv-tests里面可以找到相关demo。

https://github.com/riscv-software-src/riscv-tests/blob/master/debug/programs/semihosting.c

函数定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static inline uintptr_t __attribute__((always_inline))
semihost_call_host(uintptr_t op, uintptr_t arg) {
register uintptr_t r0 asm("a0") = op;
register uintptr_t r1 asm("a1") = arg;

asm volatile(".option push \n"
".option norvc \n"
" slli zero,zero,0x1f \n"
" ebreak \n"
" srai zero,zero,0x7 \n"
".option pop \n"

: "=r"(r0) /* Outputs */
: "r"(r0), "r"(r1) /* Inputs */
: "memory");
return r0;
}

调用方法:

1
2
3
4
5
volatile semihostparam_t arg = {.param1 = (uintptr_t)name,
.param2 = (uintptr_t)semiflags,
.param3 = (uintptr_t)strlen(name)};

int ret = (int)semihost_call_host(SEMIHOST_SYS_OPEN, (uintptr_t)&arg);

其中operation参数定义:

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
28
// Semihosting operations.
enum Semihost_Sys_Op {
// Regular operations
SEMIHOST_SYS_CLOCK = 0x10,
SEMIHOST_SYS_CLOSE = 0x02,
SEMIHOST_SYS_ELAPSED = 0x30,
SEMIHOST_SYS_ERRNO = 0x13,
SEMIHOST_SYS_EXIT = 0x18,
SEMIHOST_SYS_EXIT_EXTENDED = 0x20,
SEMIHOST_SYS_FLEN = 0x0C,
SEMIHOST_SYS_GET_CMDLINE = 0x15,
SEMIHOST_SYS_HEAPINFO = 0x16,
SEMIHOST_SYS_ISERROR = 0x08,
SEMIHOST_SYS_ISTTY = 0x09,
SEMIHOST_SYS_OPEN = 0x01,
SEMIHOST_SYS_READ = 0x06,
SEMIHOST_SYS_READC = 0x07,
SEMIHOST_SYS_REMOVE = 0x0E,
SEMIHOST_SYS_RENAME = 0x0F,
SEMIHOST_SYS_SEEK = 0x0A,
SEMIHOST_SYS_SYSTEM = 0x12,
SEMIHOST_SYS_TICKFREQ = 0x31,
SEMIHOST_SYS_TIME = 0x11,
SEMIHOST_SYS_TMPNAM = 0x0D,
SEMIHOST_SYS_WRITE = 0x05,
SEMIHOST_SYS_WRITEC = 0x03,
SEMIHOST_SYS_WRITE0 = 0x04,
};

在openocd代码中也能找到相关处理。

https://github.com/openocd-org/openocd/blob/master/src/target/riscv/riscv_semihosting.c

1
2
3
4
5
6
7
8
9
10
uint32_t pre = target_buffer_get_u32(target, tmp_buf);
uint32_t ebreak = target_buffer_get_u32(target, tmp_buf + 4);
uint32_t post = target_buffer_get_u32(target, tmp_buf + 8);
LOG_DEBUG("check %08x %08x %08x from 0x%" PRIx64 "-4", pre, ebreak, post, pc);

if (pre != 0x01f01013 || ebreak != 0x00100073 || post != 0x40705013) {
/* Not the magic sequence defining semihosting. */
LOG_DEBUG(" -> NONE (no magic)");
return SEMIHOSTING_NONE;
}

解释一下工作过程:

  • CPU侧:将参数准备好放入a0和a1中,调用semihost_call_host函数。
  • CPU侧:第一条指令是nop,执行第二条ebreak指令,进入debug 模式
  • HOST侧:识别到CPU进入debug 模式,读取当前PC的指令码,和上一条PC指令码,和下一条PC指令码。判断是否符合semihost的magic number,由于这里固定了必须是32bit指令码,所以openocd代码中只需要判断这三条指令码的编码即可。
  • HOST侧:如果匹配编码,就根据a0和a1的值,来调用相应处理函数。a0作为类型,a1作为参数。因为只提供了一个参数,如果需要多个参数,CPU可以将多个参数的内存地址赋给a1,HOST从a1地址中拿多个参数。
  • HOST侧:执行完成之后,将返回值赋给a0,退出debug模式
  • CPU侧:从debug模式返回,执行第三条指令,也是个nop。然后返回。

Reference

https://github.com/riscv-software-src/riscv-semihosting/blob/main/riscv-semihosting-spec.adoc

https://www.cnblogs.com/Five100Miles/p/13757802.html

https://tomverbeure.github.io/2021/12/30/Semihosting-on-RISCV.html#:~:text=The%20semihosting%20call%20that%20runs%20on%20the%20embedded,is%20part%20of%20the%20following%20magic%20instruction%20sequence%3A