ARMのセミホスティング(--specs=ridmon.specsオプション)

https://qiita.com/takahashim/items/120d69a44a80d08b70e7から始めよう。この記事によると、ridmonとはRDI(Remote Debug Interface) MONitorの略らしい。そしてこの記事の中にあるリンク[ https://sourceware.org/git/gitweb.cgi?p=newlib-cygwin.git;a=blob;f=libgloss/arm/syscalls.c;hb=HEAD ]を辿る。このコードの中の、writeシステムコールに対応すると思われる_writeサブルーチンがどう実行されるのかを調べる。

/* fd, is a user file descriptor. */
int __attribute__((weak))
_write (int    fd,
     const void * ptr,
     size_t    len)
{
    int res;
    struct fdent *pfd;

    pfd = findslot (fd);
    if (pfd == NULL)
    {
       errno = EBADF;
       return -1;
    }

    res = _swiwrite (pfd->handle, ptr,len);

    /* Clearly an error. */
    if (res < 0)
        return -1;

    pfd->pos += len - res;

    /* We wrote 0 bytes? 
      Retrieve errno just in case. */
    if ((len - res) == 0)
        return error (0);

    return (len - res);
}

fdとはFile Descriptor、fdentはFile Descriptor ENTryのことだろう。fdent構造体は、以下のように定義されている。

/* Struct used to keep track of the file position, just so we
    can implement fseek(fh,x,SEEK_CUR).  */
struct fdent
{
    int handle;
    int pos;
};

findslotサブルーチンは、

/* Return a pointer to the structure associated with
the user file descriptor fd. */ 
static struct fdent*
findslot (int fd)
{
    CHECK_INIT(_REENT);

    /* User file descriptor is out of range. */
    if ((unsigned int)fd >= MAX_OPEN_FILES)
        return NULL;

    /* User file descriptor is open? */
    if (openfiles[fd].handle == -1)
        return NULL;

    /* Valid. */
    return &openfiles[fd];
}

となっているが、openfiles配列が何を格納しているかわからないと読み解けない。openfiles配列は、

void
initialise_monitor_handles (void)
{
    int i;

    /* Open the standard file descriptors by opening the special
    * teletype device, ":tt", read-only to obtain a descritpor for
    * standard input and write-only to obtain a descriptor for standard
    * output. Finally, open ":tt" in append mode to obtain a descriptor
    * for standard error. Since this is a write mode, most kernels will
    * probably return the same value as for standard output, but the
    * kernel can differentiate the two using the mode flag and return a
    * different descriptor for standard error.
    */

    int volatile block[3];

    block[0] = (int) ":tt";
    block[2] = 3;     /* length of filename */
    block[1] = 0;     /* mode "r" */
    monitor_stdin = do_AngelSWI (AngelSWI_Reason_Open, (void *) block);

    for (i = 0; i < MAX_OPEN_FILES; i ++)
        openfiles[i].handle = -1;

    if (_has_ext_stdout_stderr ())
    {
        block[0] = (int) ":tt";
        block[2] = 3;     /* length of filename */
        block[1] = 4;     /* mode "w" */
        monitor_stdout = do_AngelSWI (AngelSWI_Reason_Open, (void *) block);

        block[0] = (int) ":tt";
        block[2] = 3;     /* length of filename */
        block[1] = 8;     /* mode "a" */
        monitor_stderr = do_AngelSWI (AngelSWI_Reason_Open, (void *) 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].pos = 0;

    if (_has_ext_stdout_stderr ())
    {
        openfiles[1].handle = monitor_stdout;
        openfiles[1].pos = 0;
        openfiles[2].handle = monitor_stderr;
        openfiles[2].pos = 0;
    }
}

どうやら、do_AngelSWIでファイルを開いているようだ。標準入出力および標準エラー出力のfile descriptorは0、1、2となっていて、普通のunixと同じだ。また、それぞれにはfdent構造体のインスタンスが割り当てられ、そのhandleはdo_AngelSWIの返り値となっている。do_AngelSWIについては、後で中身を追って見る。

話を戻そう。_writeのなかには、

pfd = findslot (fd);
...
res = _swiwrite (pfd->handle, ptr,len);

という部分がある。どうやらここがwriteの処理のようだ。今までの話から、pdf->handleはfdによって示されたファイルのhandlerであると思われる。ptrはおそらく書き込む内容が格納されているアドレス、lenは書き込むバイト長だろう。これを頭において、_swiwriteの中身を見て見る。

/* fh, is a valid internal file handle.
    Returns the number of bytes *not* written. */
int
_swiwrite (
            int    fh,
            const void * ptr,
            size_t    len)
{
    int block[3];

    block[0] = fh;
    block[1] = (int) ptr;
    block[2] = (int) len;

    return checkerror (do_AngelSWI (AngelSWI_Reason_Write, block));
}

またしても、do_AngelSWIが現れた。do_AngelSWIのSWIとは、ARMの命令セットから推測するに、SoftWare Interruptの略だろう。このように仮定すると、block配列はおそらくdo_AngelSWIによって呼び出される割り込みハンドラに渡す引数を格納しているのだろう。

さて、do_AngelSWIを読むとしますか。do_AngelSWIは別ファイル[ https://chromium.googlesource.com/native_client/nacl-newlib/+/590577e/newlib/libc/sys/arm/syscalls.c#106 ]にある。

static inline int
do_AngelSWI (int reason, void * arg)
{
    int value;
    asm volatile ("mov r0, %1; mov r1, %2; " AngelSWIInsn " %a3; mov %0, r0"
         : "=r" (value) /* Outputs */
         : "r" (reason), "r" (arg), "i" (AngelSWI) /* Inputs */
         : "r0", "r1", "r2", "r3", "ip", "lr", "memory", "cc"
          /* Clobbers r0 and r1, and lr if in supervisor mode */);
                  /* Accordingly to page 13-77 of ARM DUI 0040D other registers
                     can also be clobbered.  Some memory positions may also be
                     changed by a system call, so they should not be kept in
                     registers. Note: we are assuming the manual is right and
                     Angel is respecting the APCS.  */
    return value;
}

インラインアセンブリが出てきた。この書き方はgcc拡張[ https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html ]だ。先ほど見た呼び出し部によると、reason = AngelSWI_Reason_Write, arg = blockだった。

アセンブリの内容を見る前に、AngelSWIInsnとAngelSWIが何を指しているのか調べる。その定義は[ http://jsdkk.net/download/jde/jde5x/JDE544/samples/Checked_by_v54/H8S2215-P10/printf/include/swi.h ]に書いてある。thumbを使っていないとすると、

#define AngelSWI_ARM            0x123456
#define AngelSWI           AngelSWI_ARM

だそうだ。つまり、AngelSWI = 0x123456。またおかしな数字が出てきた。AngelSWIInsnの方は、

#define AngelSWIInsn            "swi"

だった。同じファイルにAngelSWI_Reason_Writeも定義してあり、

#define AngelSWI_Reason_Write       0x05

であった。つまり、実際に実行されるアセンブリは、

mov r0, 0x05
mov r1, block
swi 0x123456
mov value, r0

といった感じか。そして、value (=r0)が返される。これはおそらく、割り込みルーチンの返り値だろう。実際、この値は_swiwrite内で、

return checkerror (do_AngelSWI (AngelSWI_Reason_Write, block));

というように、checkerrorに渡される。checkerrorは、[ https://sourceware.org/git/gitweb.cgi?p=newlib-cygwin.git;a=blob;f=libgloss/arm/syscalls.c;hb=HEAD ]において、

/* Check the return and set errno appropriately. */
static int
checkerror (int result)
{
    if (result == -1)
        return error (-1);
    return result;
}

と定義されているので、割り込みルーチンの返り値が、慣例的にエラーを表す-1でないことを確かめている。さて、次はswi命令について見ていこう。ARMのマニュアル[ http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0210c/CIHFJDDH.html ]によると、

The Software Interrupt instruction (SWI) is used to enter Supervisor mode, usually to request a particular supervisor function.

また、ARMの命令セットのマニュアル[ http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0068b/BABFCEEG.html ]によると、

Syntax SWI immed_8 where: immed_8 is a numeric expression evaluating to an integerin the range 0-255. Usage The SWI instruction causes a SWI exception. This means that the processor state changes to ARM, the processor mode changes to Supervisor, the CPSR is saved to the Supervisor Mode SPSR, and execution branches to the SWI vector (see the Handling Processor Exceptions chapter in ADS Developer Guide). immed_8 is ignored by the processor. However, it is present in bits[7:0] of the instruction opcode. It can be retrieved by the exception handler to determine what service is being requested.

とのことだ。文中のCPSRとは、カレントプログラム状態レジスタ、SPSRとは、セーブドプログラム状態レジスタのことだ([ http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0204ij/CEGIBCCG.html ]参照)。swiの詳しい実行過程については、[ http://www.riscos.com/support/developers/prm/swis.html ]にかいてある(以下)。

Although in general you don't need to know how a SWI is decoded and executed, there are some more advanced cases where you will need to know more. This is what happens:

  1. The contents of R15 are saved in R14_svc (the SVC mode subroutine link register).
  2. The M0 and M1 bits of R15 are set (the processor is forced to SVC mode) and the I bit is also set (IRQ is disabled).
  3. The PC bits of R15 are forced to &08.
  4. The instruction at &08 is fetched and executed. It is normally a branch to the code that RISC OS uses to decode SWIs.

0x8番アドレスには、NMI_Handleが置かれている。--ridmon.specsオプションをつけると、NMI_Handleの定義がリンクされて、SWIがうまくハンドルされるのだろう。先ほど値を格納していたr0、r1は、割り込みルーチンの第一、第二引数となるようだ。割り込みルーチンの仕様は、[ http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0471c/Bgbjhiea.html ]に書いてある通り、r0ではoperation typeを渡す。また、r1にblockを格納する理由は、[ http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0471c/Bgbjhiea.html ]に書いてあるように、semi-hostingの仕様のためである。

The semihosting interface is common across all debug agents provided by ARM.

(from) [ http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0471c/Bgbjhiea.html ]

なんか、中途半端な感じになってしまったが、ここで今日は終わり。詳しい情報は、newlibとかsemi-hostingとかでググると得られる。