嵌入式裸机调试需要在有限资源的目标硬件上尽可能挖掘更多的信息,比如打印寄存器等等,但是即便看似很简单的串口打印,在有的情况下也是奢望,针对这种情况,能够有效利用主机资源协同调试的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种类型的半主机调用:
支持半主机的C库
- Newlibc,如上截图。
- Picolibc,fork from newlibc.但是更轻量。
- Arm CMSIS。
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