基于newlib为RISCV移植semihost ABI
semihosting背景介绍
semihosting是ARM提出的一种新的调试机制, 它允许运行在目标ARM架构上的代码与主机通信并借用主机侧的IO功能, 一般用于仿真环境/调试环境. 更多信息见ARM官方文档.
有意思的是, RISCV在今年也推出了基于自身架构的semihosting标准, 其文档见这里.
newlib背景介绍
newlib是一个轻量级的标准c库的实现, 其主要应用领域是嵌入式场景. 想对于glibc它有两大优势:
- 精简的库函数实现, 只保留必要的接口, 减少移植代码的工作量.
- 更友好的许可证, newlib本身是FreeBSD许可证, 只有少量引用的第三方代码是GPL许可证, 更适用商业闭源应用.
newlib的官网见这里, 它使用独立的服务器发布/更新代码, 因此更推荐使用github上的每日更新的镜像.
newlib代码结构
关于newlib代码以后有空详细分析一下, 这里简要介绍下一共可以分为三部分:
- libc: 包含标准c的库函数, 依赖底层系统调用封装(不同架构的系统调用实现, 这部分代码在3.0.0后被抽取为一个新库libgloss).
- libm: 包含标准的math库函数, 同时也依赖libgcc / compiler-rt中的浮点运算的实现.
- libgloss: 包含架构相关代码, i.e. 系统调用(syscall), 启动代码(crt0.s).
我们需要修改的代码就在libgloss目录下.
系统调用区别
标准Gnu/Linux系统调用指令为scall, 参数寄存器分别为a0-a5, 系统调用号保存在a7中(eabi标准下使用t0替代), 返回寄存器同样是a0, 其代码见libgloss/riscv/internal_syscall.h:
static inline long
__internal_syscall(long n, long _a0, long _a1, long _a2, long _a3, long _a4, long _a5)
{
register long a0 asm("a0") = _a0;
register long a1 asm("a1") = _a1;
register long a2 asm("a2") = _a2;
register long a3 asm("a3") = _a3;
register long a4 asm("a4") = _a4;
register long a5 asm("a5") = _a5;
#ifdef __riscv_32e
register long syscall_id asm("t0") = n;
#else
register long syscall_id asm("a7") = n;
#endif
asm volatile ("scall"
: "+r"(a0) : "r"(a1), "r"(a2), "r"(a3), "r"(a4), "r"(a5), "r"(syscall_id));
return a0;
}
相比之下, semihosting使用一个指令序列实现系统调用.
.option norvc
.text
.balign 16
.global sys_semihost
.type sys_semihost @function
sys_semihost:
slli zero, zero, 0x1f
ebreak
srai zero, zero, 0x7
ret
其传参方式如下:
32bit | 64bit | |
---|---|---|
syscall number | a0 | a0 |
param register | a1 | a1 |
return register | a0 | a0 |
data block size | 32bit | 64bit |
注意到semihost abi中只包含一个参数寄存器, 对于超过一个参数的系统调用均以结构体指针方式传递参数, 其结构体中每个成员的大小由data block size指定. | ||
另外由于防止page fault原因导致指令序列识别失败, 该指令序列要求不得使用压缩格式, 且必须在同一物理页内. 所以可以看到汇编代码中添加了norvc选项, 且二进制对齐到16字节. | ||
再看下__internal_syscall的调用者, 以open系统调用为例, 代码见libgloss/riscv/sys_open.c. |
#include <machine/syscall.h>
#include "internal_syscall.h"
/* Open a file. */
int
_open(const char *name, int flags, int mode)
{
return syscall_errno (SYS_open, name, flags, mode, 0, 0, 0);
}
_open()被libc中内部函数_open_r()(defined in newlib/libc/reent/openr.c)调用, 此处我们只关注系统调用, 更多调用链暂不展开.
int
_open_r (struct _reent *ptr,
const char *file,
int flags,
int mode)
{
int ret;
errno = 0;
if ((ret = _open (file, flags, mode)) == -1 && errno != 0)
ptr->_errno = errno;
return ret;
}
代码修改
主要分为以下几块:
- 根据semihost abi规范修改__internal_syscall()接口, 上一节提到的内容.
- libgloss/riscv/目录下若干系统调用的实现, semihost支持的系统调用, 这里粗粗分为几类讨论.
2.1. 文件IO, 包括open/close/read/write/readc/writec/istty/seek/flen/remove/rename.
其中open/close/read/write/istty作用与Gnu/Linux类似, 几乎不用修改, 但是对于seek/rename由于关键信息/返回值缺失, 需要额外修改.
2.2. 时间, 包括clock/elapsed/tickfreq/time.
semihost的时间相关接口比Gnu/Linux更多, 实际对应Gnu/Linux的time/gettimeofday的只要需要time一个接口.
2.3. 内存请求, getheapinfo.
在simulator/debugger看来target内存是flat模型, 因此getheapinfo只是返回可用的内存地址上下界, 用户需要自己实现brk/mmap系统调用.
2.4. 其它命令, exit / exit_extended / errno / getcmdline.
不同与linux下tls存储的errno, semihost需要errno调用用于访问并返回系统调用错误码. 另外getcmdline用于参数传递, 这块需要在启动代码中实现支持(否则main函数入参是空的). - 其它需要适配的代码:
3.1. 部分上层库函数调用的接口, 但semihost不支持相应的实现或实现效果不一致.
3.2. 构建工程修改, 支持两套abi并存, 提供编译选项控制abi选择.
注意事项
这里简要记录一下之前调试遇到的问题, 由于涉及代码原因这里以开源的AArch64为例介绍.
- 命令行解析
由于软仿时simulator不会将参数配置好, 需要newlib在_start()中main()运行前先调用getcmdline()获取并配置参数, 具体分为以下几步:
1.1. 调用getcmdline()获取参数字符串.
1.2. 顺序遍历将连续的字符串按空格截断成子串, 并记录每个子串的首地址.
1.3. 在调用main()前正确设置参数寄存器.
以AArch64为例, .Lcmdline是全局数组, 用于保存从getcmdline返回的字符, 然后遍历数组将空格转换为结束符, 并将其地址保存在栈上.
由于指针存在栈上, 所以遍历后需要将指针数组reverse成FIFO形式(低地址存首指针). 最后将a0与a1分别设置为参数个数与指针数组的首地址.
这里注意的几个细节:
a. 在遍历完字符串后需要在指针数组结尾添加null作为指针数组的结束符标识数组的结束, 否则llvm testsuit中的bison测试用例会失败(其参数解析代码依赖与解析到空指针结束的假设).
b. 在调用main()前对栈做对齐到16字节, 否则printf在打印long long数据时出错, 原因是vfprintf访问的栈地址未对齐.
#ifdef ARM_RDI_MONITOR
/* Fetch and parse the command line. */
ldr x1, .Lcmdline /* Command line descriptor. */
mov w0, #AngelSVC_Reason_GetCmdLine
AngelSVCAsm AngelSVC
ldr x8, .Lcmdline
ldr x8, [x8]
mov x0, #0 /* argc */
mov x1, sp /* argv */
ldr x2, .Lenvp /* envp */
/* Put NULL at end of argv array. */
str PTR_REG (0), [x1, #-PTR_SIZE]!
/* Skip leading blanks. */
.Lnext: ldrb w3, [x8], #1
cbz w3, .Lendstr
cmp w3, #' '
b.eq .Lnext
mov w4, #' ' /* Terminator is space. */
/* See whether we are scanning a quoted string by checking for
opening quote (" or '). */
subs w9, w3, #'\"'
sub x8, x8, #1 /* Backup if no match. */
ccmp w9, #('\'' - '\"'), 0x4 /* FLG_Z */, ne
csel w4, w3, w4, eq /* Terminator = quote if match. */
cinc x8, x8, eq
/* Push arg pointer to argv, and bump argc. */
str PTR_REG (8), [x1, #-PTR_SIZE]!
add x0, x0, #1
/* Find end of arg string. */
1: ldrb w3, [x8], #1
cbz w3, .Lendstr
cmp w4, w3 /* Reached terminator? */
b.ne 1b
/* Terminate the arg string with NUL char. */
mov w4, #0
strb w4, [x8, #-1]
b .Lnext
/* Reverse argv array. */
.Lendstr:
add x3, x1, #0 /* sp = &argv[0] */
add x4, x1, w0, uxtw #PTR_LOG_SIZE /* ep = &argv[argc] */
cmp x4, x3
b.lo 2f
1: ldr PTR_REG (5), [x4, #-PTR_SIZE] /* PTR_REG (5) = ep[-1] */
ldr PTR_REG (6), [x3] /* PTR_REG (6) = *sp */
str PTR_REG (6), [x4, #-PTR_SIZE]! /* *--ep = PTR_REG (6) */
str PTR_REG (5), [x3], #PTR_SIZE /* *sp++ = PTR_REG (5) */
cmp x4, x3
b.hi 1b
2:
/* Move sp to the 16B boundary below argv. */
and x4, x1, ~15
mov sp, x4
#else
mov x0, #0 /* argc = 0 */
mov x1, #0 /* argv = NULL */
#endif
bl FUNCTION (main)
- 标准输入/输出/错误的使能
stdin/stdout/stderr文件句柄的打开与一般文件略有不同, 其标准见文档描述. 其AArch64的实现见libgloss/aarch64/syscalls.c中initialise_monitor_handles().
注意该接口需要在调用main()函数前调用, 由于stdin/stdout/stderr在上层的文件句柄固定是1/2/3, 如果先打开其它文件后再打开这三个文件会导致句柄不一致.
void
initialise_monitor_handles (void)
{
int i;
param_block_t block[3];
block[0] = POINTER_TO_PARAM_BLOCK_T (":tt");
block[2] = 3; /* length of filename */
block[1] = 0; /* mode "r" */
monitor_stdin = do_AngelSVC (AngelSVC_Reason_Open, block);
for (i = 0; i < MAX_OPEN_FILES; i++)
openfiles[i].handle = -1;;
if (_has_ext_stdout_stderr ())
{
block[0] = POINTER_TO_PARAM_BLOCK_T (":tt");
block[2] = 3; /* length of filename */
block[1] = 4; /* mode "w" */
monitor_stdout = do_AngelSVC (AngelSVC_Reason_Open, block);
block[0] = POINTER_TO_PARAM_BLOCK_T (":tt");
block[2] = 3; /* length of filename */
block[1] = 8; /* mode "a" */
monitor_stderr = do_AngelSVC (AngelSVC_Reason_Open, block);
}
/* If we failed to open stderr, redirect to stdout. */
if (monitor_stderr == -1)
monitor_stderr = monitor_stdout;
openfiles[0].handle = monitor_stdin;
openfiles[0].flags = _FREAD;
openfiles[0].pos = 0;
if (_has_ext_stdout_stderr ())
{
openfiles[1].handle = monitor_stdout;
openfiles[0].flags = _FWRITE;
openfiles[1].pos = 0;
openfiles[2].handle = monitor_stderr;
openfiles[0].flags = _FWRITE;
openfiles[2].pos = 0;
}
}
- 文件IO
由于semihost接口不支持stat, 且seek返回值仅标识成功或失败, 所以需要在libgloss中创建数据结构记录IO时的偏移. 每次对文件的lseek操作后都需要记录其绝对偏移并作为返回值返回.
参见AArch64实现如下:
off_t
_swilseek (int fd, off_t ptr, int dir)
{
int res;
struct fdent *pfd;
/* Valid file descriptor? */
pfd = findslot (fd);
if (pfd == NULL)
{
errno = EBADF;
return -1;
}
/* Valid whence? */
if ((dir != SEEK_CUR) && (dir != SEEK_SET) && (dir != SEEK_END))
{
errno = EINVAL;
return -1;
}
/* Convert SEEK_CUR to SEEK_SET */
if (dir == SEEK_CUR)
{
ptr = pfd->pos + ptr;
/* The resulting file offset would be negative. */
if (ptr < 0)
{
errno = EINVAL;
if ((pfd->pos > 0) && (ptr > 0))
errno = EOVERFLOW;
return -1;
}
dir = SEEK_SET;
}
param_block_t block[2];
if (dir == SEEK_END)
{
block[0] = pfd->handle;
res = checkerror (do_AngelSVC (AngelSVC_Reason_FLen, block));
if (res == -1)
return -1;
ptr += res;
}
/* This code only does absolute seeks. */
block[0] = pfd->handle;
block[1] = ptr;
res = checkerror (do_AngelSVC (AngelSVC_Reason_Seek, block));
/* At this point ptr is the current file position. */
if (res >= 0)
{
pfd->pos = ptr;
return ptr;
}
else
return -1;
}
- rename实现
semihost没有link/unlink系统调用, 但是有与之对应的remove/rename. 因此适配时可以使用remove实现unlink功能, rename实现link功能.
但是要注意的是rename()(defined in newlib/libc/reent/renamer.c)函数的实现: 其首先调用link创建一个硬链接, 再调用unlink删除原文件.
而rename作用是将原文件重命名为新文件, 因此unlink会失败. 解决办法有两个:
a. 使能HAVE_RENAME宏, 直接调用rename系统调用.
b. 使用open/read/write手动实现一个文件拷贝函数作为link的实现.
int
_rename_r (struct _reent *ptr,
const char *old,
const char *new)
{
int ret = 0;
#ifdef HAVE_RENAME
errno = 0;
if ((ret = _rename (old, new)) == -1 && errno != 0)
ptr->_errno = errno;
#else
if (_link_r (ptr, old, new) == -1)
return -1;
if (_unlink_r (ptr, old) == -1)
{
/* ??? Should we unlink new? (rhetorical question) */
return -1;
}
#endif
return ret;
}