12
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

RISC-V のカスタム命令をインラインアセンブリで出力する方法

Posted at

#はじめに
研究の過程で、githubで公開されているRISC-Vのtoolchain( https://github.com/riscv/riscv-gnu-toolchain )を使ってカスタム命令を出力することになりました。
が、そんな簡単に出力できるわけもなく、binutilsのソースコードの中身を見ることになりました。
がっつりコードの中身見て、きっちり理解する時間は無かったので、最低限の労力でカスタム命令を出力できるようになるまでの軌跡をまとめてみました。ちなみにコメントなどが多く、個人的に結構分かりやすいソースコードでした。
あと、読みやすいコードはこれくらいふんわりした理解でもそこそこ読めるなというのを体験してほしいなとも思います。

環境

インラインアセンブリでカスタム命令を記述する

とりあえず何も考えず、main関数にカスタム命令を1命令だけ記述してコンパイルしてみます。
今回は3オペランドのものを例にしています。

custom.c
int main(void){
    asm("custom a1, a2, a3");
}

残念ながら以下のエラーが出力されました。
まあ、当たり前ですがうまく行くはずはないので、今後どうすれば良いのか手がかりを得ていきたいと思います。

> riscv64-unkown-elf-gcc custom.c 
custom.c: Assembler messages: 
custom.c:2: Error: unrecognized opcode 'custom' 

エラーの内容は、「'custom' 命令なんて無いよ!」

RISC-Vの命令の定義ファイルを見る

色々調べた結果、どうやら、
riscv-gnu-toolchain/riscv-binutils/opcodes/riscv-opc.c
で命令の定義を行っているようです。
実際には構造体の配列に命令が定義されていて、中身はこんな感じになっています。(一部抜粋

riscv-opc.c
#include "sysdep.h"
#include "opcode/riscv.h"
#include <stdio.h>

...
const struct riscv_opcode riscv_opcodes[] =
{
/* name,     xlen, isa,   operands, match, mask, match_func, pinfo.  */
{"unimp",       0, {"C", 0},   "",  0, 0xffffU,  match_opcode, INSN_ALIAS },
{"unimp",       0, {"I", 0},   "",  MATCH_CSRRW | (CSR_CYCLE << OP_SH_CSR), 0xffffffffU,  match_opcode, 0 }, /* csrw cycle, x0 */
{"ebreak",      0, {"C", 0},   "",  MATCH_C_EBREAK, MASK_C_EBREAK, match_opcode, INSN_ALIAS },
{"ebreak",      0, {"I", 0},   "",    MATCH_EBREAK, MASK_EBREAK, match_opcode, 0 },
{"sbreak",      0, {"C", 0},   "",  MATCH_C_EBREAK, MASK_C_EBREAK, match_opcode, INSN_ALIAS },
{"sbreak",      0, {"I", 0},   "",    MATCH_EBREAK, MASK_EBREAK, match_opcode, INSN_ALIAS },
{"ret",         0, {"C", 0},   "",  MATCH_C_JR | (X_RA << OP_SH_RD), MASK_C_JR | MASK_RD, match_opcode, INSN_ALIAS|INSN_BRANCH },
{"ret",         0, {"I", 0},   "",  MATCH_JALR | (X_RA << OP_SH_RS1), MASK_JALR | MASK_RD | MASK_RS1 | MASK_IMM, match_opcode, INSN_ALIAS|INSN_BRANCH },
{"jr",          0, {"C", 0},   "d",  MATCH_C_JR, MASK_C_JR, match_rd_nonzero, INSN_ALIAS|INSN_BRANCH },
{"jr",          0, {"I", 0},   "s",  MATCH_JALR, MASK_JALR | MASK_RD | MASK_IMM, match_opcode, INSN_ALIAS|INSN_BRANCH },
{"jr",          0, {"I", 0},   "o(s)",  MATCH_JALR, MASK_JALR | MASK_RD, match_opcode, INSN_ALIAS|INSN_BRANCH },
{"jr",          0, {"I", 0},   "s,j",  MATCH_JALR, MASK_JALR | MASK_RD, match_opcode, INSN_ALIAS|INSN_BRANCH },
{"jalr",        0, {"C", 0},   "d",  MATCH_C_JALR, MASK_C_JALR, match_rd_nonzero, INSN_ALIAS|INSN_JSR },
{"jalr",        0, {"I", 0},   "s",  MATCH_JALR | (X_RA << OP_SH_RD), MASK_JALR | MASK_RD | MASK_IMM, match_opcode, INSN_ALIAS|INSN_JSR },
{"jalr",        0, {"I", 0},   "o(s)",  MATCH_JALR | (X_RA << OP_SH_RD), MASK_JALR | MASK_RD, match_opcode, INSN_ALIAS|INSN_JSR },
{"jalr",        0, {"I", 0},   "s,j",  MATCH_JALR | (X_RA << OP_SH_RD), MASK_JALR | MASK_RD, match_opcode, INSN_ALIAS|INSN_JSR },
{"jalr",        0, {"I", 0},   "d,s",  MATCH_JALR, MASK_JALR | MASK_IMM, match_opcode, INSN_ALIAS|INSN_JSR },
{"jalr",        0, {"I", 0},   "d,o(s)",  MATCH_JALR, MASK_JALR, match_opcode, INSN_JSR },
{"jalr",        0, {"I", 0},   "d,s,j",  MATCH_JALR, MASK_JALR, match_opcode, INSN_JSR },
{"j",           0, {"C", 0},   "Ca",  MATCH_C_J, MASK_C_J, match_opcode, INSN_ALIAS|INSN_BRANCH },
{"j",           0, {"I", 0},   "a",  MATCH_JAL, MASK_JAL | MASK_RD, match_opcode, INSN_ALIAS|INSN_BRANCH },
{"jal",         0, {"I", 0},   "d,a",  MATCH_JAL, MASK_JAL, match_opcode, INSN_JSR },
{"jal",        32, {"C", 0},   "Ca",  MATCH_C_JAL, MASK_C_JAL, match_opcode, INSN_ALIAS|INSN_JSR },
{"jal",         0, {"I", 0},   "a",  MATCH_JAL | (X_RA << OP_SH_RD), MASK_JAL | MASK_RD, match_opcode, INSN_ALIAS|INSN_JSR },
...

この配列にカスタム命令っぽいものを見つけようとしましたが、案の定ありませんでした。
ここにcustom命令を追加すれば良いのかな?と見当をつけて、構造体の各要素が何を表しているのかを調べます。
この構造体の定義は
riscv-gnu-toolchain/riscv-binutils/opcode/riscv.h
にあります。

riscv.h
...
struct riscv_opcode
{
  /* The name of the instruction.  */
  const char *name;
  /* The requirement of xlen for the instruction, 0 if no requirement.  */
  unsigned xlen_requirement;
  /* An array of ISA subset name (I, M, A, F, D, Xextension), must ended
     with a NULL pointer sential.  */
  const char *subset[MAX_SUBSET_NUM];
  /* A string describing the arguments for this instruction.  */
  const char *args;
  /* The basic opcode for the instruction.  When assembling, this
     opcode is modified by the arguments to produce the actual opcode
     that is used.  If pinfo is INSN_MACRO, then this is 0.  */
  insn_t match;
  /* If pinfo is not INSN_MACRO, then this is a bit mask for the
     relevant portions of the opcode when disassembling.  If the
     actual opcode anded with the match field equals the opcode field,
     then we have found the correct instruction.  If pinfo is
     INSN_MACRO, then this field is the macro identifier.  */
  insn_t mask;
  /* A function to determine if a word corresponds to this instruction.
     Usually, this computes ((word & mask) == match).  */
  int (*match_func) (const struct riscv_opcode *op, insn_t word);
  /* For a macro, this is INSN_MACRO.  Otherwise, it is a collection
     of bits describing the instruction, notably any relevant hazard
     information.  */
  unsigned long pinfo;
};
...

コメントアウトを見ながら各要素の意味を推測すると以下のようになりました。(ざっくりすぎる笑
name:命令名
xlen_requirement: アドレッシングのbit数(32,64bitなど; 指定しないなら0)
subset : 所属する命令のサブセット(I:基本命令, M:乗除算命令など)
args: オペランドの取り方
match: 命令を特定するためのもの
mask: 命令を特定するためのもの
match_func: 命令を特定するための関数
pinfo:何らかの情報を格納するbit?

命令定義の仕方を考える

なんとなく構造体の意味はわかったので他の命令がどう定義されているかを参考にカスタム命令を定義していきます。
今回出力するカスタム命令は以下の3オペランドのもので, レジスタrs1,rs2に対して何らか演算を行った結果をレジスタrdに格納します。

custom rd, rs1, rs2

add命令も第2,3オペランドの加算結果を第1オペランドに格納するという点で同じなので, add命令を参考にすることにします。
add命令の定義はこのようになっていました。

riscv-opc.c
/* name,     xlen, isa,   operands, match, mask, match_func, pinfo.  */
...
{"add",         0, {"C", 0},   "d,CU,CV",  MATCH_C_ADD, MASK_C_ADD, match_c_add, INSN_ALIAS },
{"add",         0, {"C", 0},   "d,CV,CU",  MATCH_C_ADD, MASK_C_ADD, match_c_add, INSN_ALIAS },
{"add",         0, {"C", 0},   "d,CU,Co",  MATCH_C_ADDI, MASK_C_ADDI, match_rd_nonzero, INSN_ALIAS },
{"add",         0, {"C", 0},   "Ct,Cc,CK", MATCH_C_ADDI4SPN, MASK_C_ADDI4SPN, match_c_addi4spn, INSN_ALIAS },
{"add",         0, {"C", 0},   "Cc,Cc,CL", MATCH_C_ADDI16SP, MASK_C_ADDI16SP, match_c_addi16sp, INSN_ALIAS },
{"add",         0, {"I", 0},   "d,s,t",  MATCH_ADD, MASK_ADD, match_opcode, 0 },
{"add",         0, {"I", 0},   "d,s,t,1",MATCH_ADD, MASK_ADD, match_opcode, 0 },
{"add",         0, {"I", 0},   "d,s,j",  MATCH_ADDI, MASK_ADDI, match_opcode, INSN_ALIAS },
...

なんとadd命令は普通に1つだけだと思っていたのですが、種類があるみたいです。(驚
落ち着いてみていきましょう。
まず2番目の要素xlenが0ということは32bit,64bit,128bitアドレッシングのどれでも使用されるということですね。
(ちなみに、今回は32bitアドレッシングを想定してコンパイルします。)
次に3番目の要素subset(//isa)では"C"と"I"が存在します。"C"は16bit短縮命令セットを表しており、"I"は基本命令セットを表しています。特に16bit短縮命令を参考にする理由もないので、基本命令を参考にします。
残ったのは以下のとおりですね。

riscv-opc.c
/* name,     xlen, isa,   operands, match, mask, match_func, pinfo.  */
...
{"add",         0, {"I", 0},   "d,s,t",  MATCH_ADD, MASK_ADD, match_opcode, 0 },
{"add",         0, {"I", 0},   "d,s,t,1",MATCH_ADD, MASK_ADD, match_opcode, 0 },
{"add",         0, {"I", 0},   "d,s,j",  MATCH_ADDI, MASK_ADDI, match_opcode, INSN_ALIAS },
...

4番目の要素args(//operands)を見るとそれぞれ違っています。
同じadd命令でもオペランドの書き方にバリエーションがあるということでしょうか。また、5,6番目の要素match, maskに関してはマクロで定義されているようです。
7番目の要素match_funcの関数はmatch_opcodeとなっています。I命令セットのmatch_funcは大体(というか全て?)match_opcode関数でした。他の命令セットだと、命令の判定に別の関数を使うみたいですね。
最後の要素pinfoはまだよくわかりませんが、0かINSN_ALIASのパターンがあります。

カスタム命令も基本命令に突っ込んじゃえということで、暫定のカスタム命令の定義は以下になりました。

{"custom",     32, {"I", 0},     "?", MATCH_CUSTOM, MASK_CUSTOM, match_opcode, 0}

64bitアドレッシングするので、xlenは64です。
match,mask のマクロは後で定義します。
I命令セットなので, match_funcはmatch_opcodeで良いと思われます。
pinfoはとりあえず0にしときます。

オペランド情報をパースする関数を眺める

先程の構造体の4番目の要素argsをパースして、オペランド情報を取得するであろう関数を発見しました。
プログラム自体はriscv-gnu-toolchain/riscv-binutils/opcodes/riscv-dis.cとなります。

riscv-dis.c
static void
print_insn_args (const char *d, insn_t l, bfd_vma pc, disassemble_info *info)
{
  struct riscv_private_data *pd = info->private_data;
  int rs1 = (l >> OP_SH_RS1) & OP_MASK_RS1;
  int rd = (l >> OP_SH_RD) & OP_MASK_RD;
  fprintf_ftype print = info->fprintf_func;

  if (*d != '\0')
    print (info->stream, "\t");

  for (; *d != '\0'; d++)
    {
      switch (*d)
	{
	case 'C': /* RVC */
	  switch (*++d)
	    {
	    case 's': /* RS1 x8-x15 */
	    case 'w': /* RS1 x8-x15 */
	      print (info->stream, "%s",
		     riscv_gpr_names[EXTRACT_OPERAND (CRS1S, l) + 8]);
	      break;
	    case 't': /* RS2 x8-x15 */
	    case 'x': /* RS2 x8-x15 */
	      print (info->stream, "%s",
		     riscv_gpr_names[EXTRACT_OPERAND (CRS2S, l) + 8]);
	      break;
	   ...
	    }
	  break;
	case 'j':
	  if (((l & MASK_ADDI) == MATCH_ADDI && rs1 != 0)
	      || (l & MASK_JALR) == MATCH_JALR)
	    maybe_print_address (pd, rs1, EXTRACT_ITYPE_IMM (l));
	  print (info->stream, "%d", (int)EXTRACT_ITYPE_IMM (l));
	  break;
        ...
    case 'd':
	  if ((l & MASK_AUIPC) == MATCH_AUIPC)
	    pd->hi_addr[rd] = pc + EXTRACT_UTYPE_IMM (l);
	  else if ((l & MASK_LUI) == MATCH_LUI)
	    pd->hi_addr[rd] = EXTRACT_UTYPE_IMM (l);
	  else if ((l & MASK_C_LUI) == MATCH_C_LUI)
	    pd->hi_addr[rd] = EXTRACT_RVC_LUI_IMM (l);
	  print (info->stream, "%s", riscv_gpr_names[rd]);
	  break;
        ...

's', 'w'はRS1レジスタを表しており, 't', 'x'はRS2レジスタを表しているようです。
また 'j' は即値で 'd' はrdレジスタなどと推測できます。
つまり、

custom rd, rs1, rs2

の場合では、

{"custom",     32, {"I", 0},     "d,s,t", MATCH_CUSTOM, MASK_CUSTOM, match_opcode, 0}

とすれば良さそうですね。
#MATCH と MASKのマクロ変数を定義する
MATCH_hoge と MASK_fuga を定義されている場所は、
riscv-gnu-toolchain/riscv-binutils/include/opcode/riscv-opc.hのようです。

riscv-opc.h
...
#define MATCH_SLLI_RV32 0x1013
#define MASK_SLLI_RV32  0xfe00707f
#define MATCH_SRLI_RV32 0x5013
#define MASK_SRLI_RV32  0xfe00707f
...
DECLARE_INSN(slli_rv32, MATCH_SLLI_RV32, MASK_SLLI_RV32)
DECLARE_INSN(srli_rv32, MATCH_SRLI_RV32, MASK_SRLI_RV32)
...

更に命令を宣言するマクロ関数もあるようです(DECLARE_INSN)
この MATCH と MASC のマクロ変数を眺めていると以下のような記述を見つけました。

#define MATCH_CUSTOM0 0x0b
#define MASK_CUSTOM0  0x707f
#define MATCH_CUSTOM0_RS1 0x200b
#define MASK_CUSTOM0_RS1  0x707f
#define MATCH_CUSTOM0_RS1_RS2 0x300b
#define MASK_CUSTOM0_RS1_RS2  0x707f
#define MATCH_CUSTOM0_RD 0x400b
#define MASK_CUSTOM0_RD  0x707f
#define MATCH_CUSTOM0_RD_RS1 0x600b
#define MASK_CUSTOM0_RD_RS1  0x707f
#define MATCH_CUSTOM0_RD_RS1_RS2 0x700b
#define MASK_CUSTOM0_RD_RS1_RS2 0x707f
...
#define MATCH_CUSTOM3 0x7b
#define MASK_CUSTOM3  0x707f
#define MATCH_CUSTOM3_RS1 0x207b
#define MASK_CUSTOM3_RS1  0x707f
#define MATCH_CUSTOM3_RS1_RS2 0x307b
#define MASK_CUSTOM3_RS1_RS2  0x707f
#define MATCH_CUSTOM3_RD 0x407b
#define MASK_CUSTOM3_RD  0x707f
#define MATCH_CUSTOM3_RD_RS1 0x607b
#define MASK_CUSTOM3_RD_RS1  0x707f
#define MATCH_CUSTOM3_RD_RS1_RS2 0x707b
#define MASK_CUSTOM3_RD_RS1_RS2  0x707f

カスタム命令のオペランドの取り方によってどのように定義すれば良いのかすでに書かれてありました。
カスタム命令のうち下記の上2行を拝借し、下2行に置き換えます。

//#define MATCH_CUSTOM0_RD_RS1_RS2 0x700b
//#define MASK_CUSTOM0_RD_RS1_RS2 0x707f
#define MATCH_CUSTOM 0x700b
#define MASK_CUSTOM 0x707f

また命令宣言のために以下の記述を書き加えます。

DECLARE_INSN(custom, MATCH_CUSTOM, MASK_CUSTOM)

#コンパイルしてみる
この時点で試しにカスタム命令を出力できるか試してみます。
その前に binutils のソースコードを書き換えたので、binutils 自体をビルドし直します。
riscv-gnu-toolchain/ で以下のコマンドを実行します。

make clean
make build-binutils

コンパイルします。

riscv64-unknown-elf-gcc custom.c

すると以下のようなエラーが出ました。

Assembler messages:
Error: internal: bad RISC-V opcode (bits 0xfffffffffe000000 undefined): custom d,s,t
Fatal error: Broken assembler.  No assembly attempted.

まさかうまく行かないとは。。。
泣きそう。。。
#エラー解消
このエラー文がどこで出ているのかを追求してみます。
すると、

tc-riscv.c
...
#undef USE_BITS
      if (used_bits != required_bits)
          {
              as_bad (_("internal: bad RISC-V opcode (bits 0x%lx undefined): %s %s"),
                      ~(unsigned long)(used_bits & required_bits),
                      opc->name, opc->args);
              return FALSE;
          }
      return TRUE;
...

この辺りで落ちていることがわかりました。
if文から、 bit定義のしかたが違うようですね。
ところでこのrequired_bitsとは

tc-riscv.c
required_bits = ~0ULL >> (64 - insn_width);

ULLはunsinged long longなので64bitです。~は否定なので64個の0が1に変わります。d
つまり、64bit命令なら全部1, 32bitなら下位32bitに1が入る感じですね。
used_bitsはここで定義されています。

tc-riscv.c
insn_t used_bits = opc->mask;

つまりCUSTOM_MASKですね。
その後、used_bitsに以下の記述でオペランドの情報から適切な位置にbitを立てています。

tc-riscv.c
...
#define USE_BITS(mask,shift)	(used_bits |= ((insn_t)(mask) << (shift)))
 while (*p)
      switch (c = *p++)
          {
          case 'C': /* RVC */
              switch (c = *p++)
                  {           
...       
          case 'd':	USE_BITS (OP_MASK_RD,		OP_SH_RD);	break;
          case 'm':	USE_BITS (OP_MASK_RM,		OP_SH_RM);	break;
          case 's':	USE_BITS (OP_MASK_RS1,		OP_SH_RS1);	break;
          case 't':	USE_BITS (OP_MASK_RS2,		OP_SH_RS2);	break;
...
}
        

以上から間違っているのはused_bitsの初期値に値するCUSTOM_MASKだと見当をつけます。
この辺りでrequired_bitsとused_bitsの値をダンプしてみたりして適切なCUSTOM_MASKを見つけていきます。
結論は、

#define MASK_CUSTOM  0xfe00707f

となりました。
#コンパイルしてみる(2回目)
先にビルドしてからコンパイルします。
その後、逆コンパイルしてカスタム命令が出力されているかを確認します。

cd riscv-gnu-toolchain
make clean 
make build-binutils
cd (workspace; ここは任意)
riscv64-unknown-elf-gcc custom.c
riscv64-unknown-elf-objdump -d a.out | grep custom

これでカスタム命令が確認できるはずです。

#最後に
私がカスタム命令を出力するための軌跡を追いかけてみました。binutilsの中身の理解を真面目にやらずともなんとかカスタム命令を出すことができました。また、binutilsの膨大なソースコードでも、中身が非常に読みやすい印象でした。コードの書き方など参考になる部分があると思うので読んでみるのも良いかもしれません。
ここまで読んで頂きありがとうございました。

12
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?