RISC-V semi-hosting原理以及实践

文章介绍了Semi-Host技术,这是一种在资源有限的嵌入式硬件上进行调试的方法,允许目标处理器通过调试器与主机通信,使用主机的IO设施,如printf函数。ARM在1995年定义了半主机规范,RISC-V也有自己的实现。Semi-Host工作原理涉及特殊指令如ARM的svc和RISC-V的ebreak来触发主机的调试代理程序。文章还提到了newlibc和picolibc等C库对半主机的支持,以及如何在QEMU环境下搭建半主机测试环境。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

嵌入式裸机调试需要在有限资源的目标硬件上尽可能挖掘更多的信息,比如打印寄存器等等,但是即便看似很简单的串口打印,在有的情况下也是奢望,针对这种情况,能够有效利用主机资源协同调试的semi-host(半主机)技术应运而生。以下是关于semi-host的简单介绍。

  1. semi-host机制使用在目标处理器(arm,riscv等)上运行代的代码能够与正在运行调试器的主机进行通信,并使用其IO设施。
  2. 这些设施包括键盘,屏幕和磁盘IO.
  3. 嵌入式端使用C库中的函数(printf,scanf)能够使用主机的屏幕和键盘,而无需在目标系统上具备屏幕和键盘。
  4. semi-host最早由ARM在1995年定义,公开了ARM半主机规范,被很多调试器和C库支持,调试器比如jlink(rtti),trace32,以及ICE仿真器等,C库包括newlib,picolibc等。
  5. 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库
  1. Newlibc,如上截图。
  2. Picolibc,fork from newlibc.但是更轻量。
  3. 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的博客-优快云博客

这里使用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*)
	}
}

链接脚本中定义的符号概念不同于C语言层面定义的变量的概念,C语言对于变量的定义会为其分配存储空间,但是在链接脚本中,a=b(b已经在C中定义a已声明)却指定了a符号地址和b符号地址相同这一概念,在sizeof(a)==sizeof(b)的情况下,a就是b,类似于C++中的引用。

为什么这样做?可能是C中没有很好的别名机制。

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

总结

半主机技术提供了一个便捷的开发环境,加快开发速度,优点包括:

  1. 方便调试和开发。
  2. 节省资源。
  3. 快速开发和原型验证
  4. 灵活,可移植性好
参考文章

ARM Semihosting - native but slow Debugging - Code Inside Out


结束
### STM32 Semihosting 使用方法及常见问题解决 #### 什么是SemihostingSemihosting是一种允许嵌入式应用程序通过主机(通常是开发PC)访问资源的技术。这使得开发者可以在目标设备上运行程序的同时,利用主机的操作系统功能来处理输入/输出操作。 #### 如何配置STM32项目以支持Semihosting? 为了使STM32能够使用semihosting特性,在编译器设置中需要启用相应的选项。对于GCC工具链而言,可以通过链接脚本指定`--specs=rdimon.specs`参数,并确保启用了浮点单元(FPU),如果硬件具备的话[^1]。 另外,当采用ARM MDK (Keil)作为IDE时,则需在工程属性里勾选"Semihosting"选项并调整相关配置项以便正确工作[^2]。 #### 实现简单的Semihosting调用实例 下面给出一段基于STM32CubeMX初始化后的简单代码片段用于展示如何打印字符串到控制台: ```c #include "main.h" #include <stdio.h> int __io_putchar(int ch){ ITM_SendChar(ch); return ch; } void SystemClock_Config(void); int main(void) { HAL_Init(); SystemClock_Config(); printf("Hello from STM32 via Semihosting!\n"); while(1){} } ``` 这里重定义了标准I/O函数`__io_putchar()`指向ITM接口发送字符的方法,从而实现了通过SWDIO/SWCLK引脚向连接的调试器传输数据流的目的[^3]。 #### 解决常见的Semihosting问题 有时会遇到无法正常显示输出的情况,可能是因为未开启ITM通道或是调试器不支持该功能等原因造成。此时可以尝试以下措施: - 确认已安装最新版本的支持包; - 检查是否已经激活了ITM端口以及对应的TRACESWO管脚; - 尝试更换不同的调试探针或软件环境测试兼容性; 此外,某些情况下还需要注意关闭不必要的优化级别(-O0),因为过高的编译优化可能导致部分语句被裁剪掉而影响实际效果[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

papaofdoudou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值