嵌入式裸机调试需要在有限资源的目标硬件上尽可能挖掘更多的信息,比如打印寄存器等等,但是即便看似很简单的串口打印,在有的情况下也是奢望,针对这种情况,能够有效利用主机资源协同调试的semi-host(半主机)技术应运而生。以下是关于semi-host的简单介绍。
- semi-host机制使用在目标处理器(arm,riscv等)上运行代的代码能够与正在运行调试器的主机进行通信,并使用其IO设施。
- 这些设施包括键盘,屏幕和磁盘IO.
- 嵌入式端使用C库中的函数(printf,scanf)能够使用主机的屏幕和键盘,而无需在目标系统上具备屏幕和键盘。
- semi-host最早由ARM在1995年定义,公开了ARM半主机规范,被很多调试器和C库支持,调试器比如jlink(rtti),trace32,以及ICE仿真器等,C库包括newlib,picolibc等。
- RISCV基于ARM版本也定义了自己的半主机规范。
semihost工作原理
semihosting=semi+hosting,表明了半主机操作一半在目标设备上执行,另一半在主机上执行,半主机通过一组特殊的软件指令序列来陷入主机,例如ARM的svc指令,RISCV的ebreak指令,这些指令会出发CPU进入异常执行流,在异常执行流中,执行调试代理程序处理异常,这些调试代理程序包括解析半主机命令并实现和主机通信的逻辑实现。
下图表示在不依赖串口的情况下,资源受限的目标平台将调试信息打印到主机的过程。
流程如下:
- 目标设备上的应用程序调用标准库函数printf.
- 在库的底层不会将调用重定向到串口,而是准备号要发送的数据后,使用特殊指令ebreak通知调试器。
- 调试器收到通知后,检测到SEMI-HOST请求,然后执行对应的处理程序,将字符串显示出来。
下图是newlibc中 RISCV架构下触发SEMI-HOST请求的代码,在libgloss/riscv/semihost_syscall.h文件中。
由于CPU没有为semi-host保留异常号,为了将semihost触发的异常和其它异常区分开,在ebreak指令周围添加额外的指令来帮助调试器区分“半主机ebreak"和“常规ebreak".
其它约定包括,半主机调用号通过寄存器a0传递,调用参数的指针地质,通过a1寄存器传递,返回值放在a0中。
规范一共定义了24种类型的半主机调用:
picolibc中对方法的定义
支持半主机的C库
- Newlibc,如上截图。
- Picolibc,fork from newlibc.但是更轻量。
- Arm CMSIS。
如前面所谓分析,半主机主要是为了解决资源有限平台上的协同调试开发问题,glibc,musl libc等面向LINUX等大型应用场景的LIBC库不需要支持半主机,因为后者主要运行于RICH OS上,无需支持半主机机制。
QEMU半主机测试环境搭建
测试应用基于picolibc,编译方法如下:
$ sudo apt install gcc-riscv64-unknown-elf meson
$ git clone https://github.com/picolibc/picolibc.git
$ cd picolibc && mkdir build && cd build
$ ../scripts/do-riscv-configure
$ ninja
$ suso ninja install
编译生成了测试用例test/semihost/semihost-exit-extended-failure_rv64imafdc_lp64d, 之后安装
编译RISCV 64 QEMU:
参考博客Qemu在ARM和X86平台上的运行机制初探_papaofdoudou的博客-CSDN博客
这里使用8.0.0的QEMU,编译配置:
$ ./configure --target-list=arm-softmmu,aarch64-softmmu,i386-softmmu,x86_64-softmmu,riscv32-softmmu,riscv64-softmmu,aarch64-linux-user,arm-linux-user,riscv64-linux-user,x86_64-linux-user --audio-drv-list=alsa,sdl,pa --enable-system --enable-user --enable-linux-user --enable-sdl --enable-vnc --enable-virtfs --enable-kvm --enable-fdt --enable-debug --disable-strip --enable-debug-tcg --enable-debug-info --enable-debug --disable-strip --enable-vnc --prefix=/home/zlcao/semihost/install
$ make
$ make install
测试用例:
#include <stdio.h>
int main(void)
{
printf("%s line %d, hello semihosting.\n", __func__, __LINE__);
return 0;
}
编译
$ riscv64-unknown-elf-gcc --specs picolibc.specs --oslib=semihost -march=rv64imac -mabi=lp64 -mcmodel=medany -static main.c -o main
--oslib=semihost告诉编译器使用picolibc的semihost版实现,因为正常情况下它也有一个实现。使用picolibc.specs将程序连接到0x80000000运行,这是大部分RISCV及其内存的起始地址,也是QEMU “virt”及其的内存基址。由于使用的工具链是裸机工具链,这个地址是物理地址。
使用 qemu "virt" -serial stdio查看没有任何输出,因为我们用的是半主机的printf调用,没有用到串口终端,所以这里没有任何输出。
./../install/bin/qemu-system-riscv64 -M virt -bios main -display none -serial stdio
但是当我们加上-serial null -semihosting运行时,输出便出现了,-serial null的目的是禁止标准输出,这样如果有输出,一定是半主机机制的,方便对照,不加也行。
$ ./../install/bin/qemu-system-riscv64 -M virt -bios main -display none -serial null -semihosting
-bios目的是禁止opensbi启动。
手搓裸金属半主机用例
前面的用例使用了picolibc的半主机版printf实现,并且仅仅依赖于其实现,所以我们完全可以模仿printf手搓一份输出字符串调用,传入a0 4号半主机调用,调用名称SYS_WRITEO,该调用将目标机上一个以NULL结尾的字符串在主机侧的终端上输出。
实现代码:
测试用例源码:
static void smh_puts(char *str)
{
asm volatile("addi a1, %0, 0\n"
"addi a0, zero, 4\n"
".balign 16\n"
".option push\n"
".option norvc\n"
"slli zero, zero, 0x1f\n"
"ebreak\n"
"srai zero, zero, 0x7\n"
".option pop\n"
: : "r"(str) : "a0", "a1", "memory");
}
int main(void)
{
smh_puts("hello semihosting.\n");
return 0;
}
C启动源码:
.text
.globl _start
_start:
li sp, 0x80100000
tail main
链接脚本semihosting.ld
OUTPUT_ARCH("riscv")
ENTRY(_start)
SECTIONS
{
. = 0x80000000;
.text : {
crt0.o (.text)
main.o (.text)
}
.rodata : {
*(.rodata*)
}
.data : {
*(.data*)
}
.bss : {
*(.bss*)
}
}
makefile
all:
riscv64-unknown-elf-gcc -nostdlib -march=rv64imac -mabi=lp64 -mcmodel=medany -static -c main.c -o main.o
riscv64-unknown-elf-gcc -nostdlib -march=rv64imac -mabi=lp64 -mcmodel=medany -static -c crt0.S -o crt0.o
riscv64-unknown-elf-ld -Tsemihosting.ld -static crt0.o main.o -o main
sim:
../install/bin/qemu-system-riscv64 -M virt -bios main -display none -serial null -semihosting
测试,可以看到,字符串通过SEMI HOST机制正常打印出来。
qemu semihost实现
相对于运行用例,qemu站在上帝视角,相当于SEMIHOST架构中的主机方,其核心SEMI HOST实现逻辑是用函数do_common_semihosting实现的。
以SEMI HOST的文件操作命令为例,最终其调用的世纪上也是通过HOST机的OPEN函数实现的。
fopen的semihost实现
总结
半主机技术提供了一个便捷的开发环境,加快开发速度,优点包括:
- 方便调试和开发。
- 节省资源。
- 快速开发和原型验证
- 灵活,可移植性好
参考文章
ARM Semihosting - native but slow Debugging - Code Inside Out