基于newlib为RISCV移植semihost ABI

semihosting背景介绍

semihosting是ARM提出的一种新的调试机制, 它允许运行在目标ARM架构上的代码与主机通信并借用主机侧的IO功能, 一般用于仿真环境/调试环境. 更多信息见ARM官方文档.
有意思的是, RISCV在今年也推出了基于自身架构的semihosting标准, 其文档见这里.

newlib背景介绍

newlib是一个轻量级的标准c库的实现, 其主要应用领域是嵌入式场景. 想对于glibc它有两大优势:

  1. 精简的库函数实现, 只保留必要的接口, 减少移植代码的工作量.
  2. 更友好的许可证, newlib本身是FreeBSD许可证, 只有少量引用的第三方代码是GPL许可证, 更适用商业闭源应用.
    newlib的官网见这里, 它使用独立的服务器发布/更新代码, 因此更推荐使用github上的每日更新的镜像.

newlib代码结构

关于newlib代码以后有空详细分析一下, 这里简要介绍下一共可以分为三部分:

  1. libc: 包含标准c的库函数, 依赖底层系统调用封装(不同架构的系统调用实现, 这部分代码在3.0.0后被抽取为一个新库libgloss).
  2. libm: 包含标准的math库函数, 同时也依赖libgcc / compiler-rt中的浮点运算的实现.
  3. 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;
}

代码修改

主要分为以下几块:

  1. 根据semihost abi规范修改__internal_syscall()接口, 上一节提到的内容.
  2. 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. 其它需要适配的代码:
    3.1. 部分上层库函数调用的接口, 但semihost不支持相应的实现或实现效果不一致.
    3.2. 构建工程修改, 支持两套abi并存, 提供编译选项控制abi选择.

注意事项

这里简要记录一下之前调试遇到的问题, 由于涉及代码原因这里以开源的AArch64为例介绍.

  1. 命令行解析
    由于软仿时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)
  1. 标准输入/输出/错误的使能
    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;
  }
}
  1. 文件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;
}
  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;
}
posted @ 2020-10-01 23:46  Five100Miles  阅读(2060)  评论(0编辑  收藏  举报