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