基於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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM