FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

GCCのインラインアセンブラの構文について調査

xv6には、以下のような記述がある。

github.com

static inline void
insl(int port, void *addr, int cnt)
{
  asm volatile("cld; rep insl" :
               "=D" (addr), "=c" (cnt) :
               "d" (port), "0" (addr), "1" (cnt) :
               "memory", "cc");
}

インラインアセンブラだが、構文がいまいち意味が分からない。どういう意味だろう?

GCCインラインアセンブラについて

asm文を使うことで、インラインアセンブラを利用できる。さらに、volatileを付加することで、最適化による命令削除を抑止することができるのだが、それ以外の文法はどういう意味だ? 調査してみると、これは拡張アセンブリ構文というものであるらしい。以下のサイトが参考になった。

GCC Inline Assembler

__asm__ ( アセンブリテンプレート
        : 出力オペランド                    /* オプション */
        : 入力オペランド                    /* オプション */
        : 破壊されるレジスタのリスト        /* オプション */
);      

出力オペランド、入力オペランドを指定することで、インラインアセンブラC言語の変数を交互にやり取りさせることができる。さらに、破壊レジスタを指定することで、インラインアセンブラ内で破壊されるレジスタを指定し、コンパイラにあらかじめレジスタの退避を指定することができる。

簡単な例

ここで、理解を深めるために簡単な例を作ってみた。

int inline_test (int a, int b)
{
  int ret;
  asm volatile ("addw %0, %1, %2"
                : "=r"(ret)
                : "r"(a), "r"(b)
                :
                );
  return ret;
}

int inline_test2 (int a, int b)
{
  return a + b;
}

inline_testでは、インラインアセンブリを利用している。オペランドはすべて拡張インラインアセンブラで指定されている。それぞれ、0番目、1番目、2番目のオペランドとして記述されているのだが、どうやら拡張構文におけるオペランドリストの順に番号が付くらしい。ここでは、

  • %0 : ret
  • %1 : a
  • %2 : b

と対応がつけられた。コンパイルしてみよう。

$ riscv64-unknown-elf-gcc -O3 -c inline.c
$ riscv64-unknown-elf-objdump -D inline.o

inline.o:     file format elf64-littleriscv


Disassembly of section .text:

0000000000000000 <inline_test>:
   0:   00b5053b                addw    a0,a0,a1
   4:   00008067                ret

0000000000000008 <inline_test2>:
   8:   00b5053b                addw    a0,a0,a1
   c:   00008067                ret

ちゃんと対応が取られているね。

xv6における拡張インラインアセンブラの利用

では、改めてxv6のx86.hを見てみよう。以下のようなインラインアセンブリの記述がある。

static inline void
insl(int port, void *addr, int cnt)
{
  asm volatile("cld; rep insl" :
               "=D" (addr), "=c" (cnt) :
               "d" (port), "0" (addr), "1" (cnt) :
               "memory", "cc");
}

不思議なのは、ここでアセンブラとしてはオペランドの指定が一つもないことだ。 そもそも、rep inslという命令は、ECXで指定されたカウンタだけinslを繰り返すというものになる。破壊されるのは、メモリとccだ。あれ、ccって何だ? とりあえず、ここでコンパイルされたコードを見てみよう。

static inline void
insl(int port, void *addr, int cnt)
{
80102548:       55                      push   %ebp
80102549:       89 e5                   mov    %esp,%ebp
8010254b:       57                      push   %edi
8010254c:       53                      push   %ebx
  asm volatile("cld; rep insl" :
8010254d:       8b 55 08                mov    0x8(%ebp),%edx
80102550:       8b 4d 0c                mov    0xc(%ebp),%ecx
80102553:       8b 45 10                mov    0x10(%ebp),%eax
80102556:       89 cb                   mov    %ecx,%ebx
80102558:       89 df                   mov    %ebx,%edi
8010255a:       89 c1                   mov    %eax,%ecx
8010255c:       fc                      cld
8010255d:       f3 6d                   rep insl (%dx),%es:(%edi)
8010255f:       89 c8                   mov    %ecx,%eax
80102561:       89 fb                   mov    %edi,%ebx
80102563:       89 5d 0c                mov    %ebx,0xc(%ebp)
80102566:       89 45 10                mov    %eax,0x10(%ebp)
               "=D" (addr), "=c" (cnt) :
               "d" (port), "0" (addr), "1" (cnt) :
               "memory", "cc");
}
80102569:       5b                      pop    %ebx
8010256a:       5f                      pop    %edi
8010256b:       5d                      pop    %ebp
8010256c:       c3                      ret

cldが実行される前に、レジスタの退避をしている。それぞれ、オペランド制約で指定されたレジスタの退避と、もとに戻す作業が入っている。