ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

mruby/c 1.2 のコードを読んで理解するmruby VMの実装

mrubyファミリ (組み込み向け軽量Ruby) Advent Calendar 2024、23日目の記事です。メリークリスマス!

qiita.com

mrubyファミリAdvent Calendar、今年は1日目からyharaさんによる衝撃的な記事がありました。

yhara.jp

なぜ衝撃的だったかというと、僕も全く同じことをしてた(mrubyをRubyで実装しようとしていた)ので...。

で、その実装の下準備のために mruby/c 1.2 のコードリーディングをしていたわけです。今日はそのメモの内容を整理して、最低限のmruby VMの実装を理解する手助けとしようと思います。

ちなみに今回、以下はしません。

  • バイトコードのパース部分の読み込み
    • そもそもmruby 1と3でだいぶ違う点、あと基本的には仕様通りやるだけの実装なので省略
    • バイトコードフォーマットが気になる方は以下の記事を見てください:

udzura.hatenablog.jp

あと、筆者は言語実装の素人なのですが、しかし読みやすくするためにこの実装が何者なのかについて適当に言い切っちゃってる点もあります。ツッコミは優しく、事実確認は適宜ご自身でおなしゃす。

また、mruby/cがどのバージョンでどの使用をサポートしているかについての情報もアドベントカレンダーで公開されました。ぜひ併せてお読みください。この記事で色々な背景を残していただいているおかげで、筆者もコード上の不明点がいくつか明らかになり大変助かりました。

qiita.com

mruby/c 1.2の実装を読む

まず、 mruby/c release 1.0 は試したところコンパイルを通せなかったり、正直シンプルに完成していない箇所があると判断しました。release 1.2が1.x系統で最後にタグがついた実装なので、これをベースに実装を読んでいきます。

mruby/c 1.2 の段階ではさまざまな命令が未実装ではあります。以下の Implemented の項目にある通り、mruby 1系統の命令からさらに一部にのみ対応していました。

github.com

一方でとても基本的な命令は動作できているため、残りの実装も拡張しながら進めていくイメージは湧きそうです。では見ていきましょう。

VM 周りの構造体

まずVM構造体の抜粋:

typedef struct VM {
  mrbc_irep *irep;

  uint8_t        vm_id; // vm_id : 1..n
  const uint8_t *mrb;   // bytecode
  mrbc_irep *pc_irep;    // PC
  uint16_t  pc;         // PC
  mrbc_value    regs[MAX_REGS_SIZE];
  mrbc_value   *current_regs;
  mrbc_callinfo *callinfo_tail;
  mrbc_class *target_class;
  int32_t error_code;

  volatile int8_t flag_preemption;
  int8_t flag_need_memfree;
} mrbc_vm;
typedef struct VM mrb_vm;

mrbc_irep への残照が2つ(irep, pc_irep)、プログラムカウンタ(pc)、そして何やらレジスタへの参照が2つある...?(regs/current_regs)

mrbc_callinfo への参照と、mrbc_class への参照もあります。error_codeは実行後に保持しているエラーコードですかね。flag_preemptionは後で見ていきますが中断するしないのフラグです。flag_need_memfreeはそのまま名前の通りでしょう。

*mrb は生のバイトコードをそのまま保持しているようです。idはまあidですね。

typedef struct IREP {
  uint16_t nlocals;        //!< # of local variables
  uint16_t nregs;      //!< # of register variables
  uint16_t rlen;       //!< # of child IREP blocks
  uint16_t ilen;       //!< # of irep
  uint16_t plen;       //!< # of pool

  uint8_t     *code;       //!< ISEQ (code) BLOCK
  mrbc_object **pools;      //!< array of POOL objects pointer.
  uint8_t     *ptr_to_sym;
  struct IREP **reps;      //!< array of child IREP's pointer.

} mrbc_irep;
typedef struct IREP mrb_irep;

mrb_irep の定義はこうなっており、IREPのバイトコードを解釈した結果、実行コードの塊について必要な情報がまとまっています。変数の数、利用するレジスタの数など数値系はまあコメント通りかと思います。

codeとpoolsというメンバがあり、それぞれopcodeの列と、IREP内のstaticなオブジェクト(ほとんどの場合文字列です)のプールへの参照を保持しています。

ptr_to_symはどうやら生バイトコードのsymbol定義セクションへの参照で、毎回パースしながら参照していそうです(Matz mrubyではsymの情報への配列にしていた記憶も)。

また、IREPは親子関係があるので、子供のIREPへのポインタを持っています。ilen個の子供がいるという感じです。

で、メソッド(など)が呼ばれるたびにmrbc_callinfoというものが仮想的なstackにプッシュされるわけです*1

typedef struct CALLINFO {
  struct CALLINFO *prev;
  mrbc_sym mid;
  mrbc_irep *pc_irep;
  uint16_t  pc;
  mrbc_value *current_regs;
  mrbc_class *target_class;
  uint8_t   n_args;     // num of args
} mrbc_callinfo;
typedef struct CALLINFO mrb_callinfo;

自分の下層にあるcallinfo(prev)、メソッドなどの名前(mid)、当該メソッドやprocなどの引数カウント(n_args)を保持します。またその時の実行コンテクストでの実行情報を持っていて、具体的には:

  • IREPとpc
  • レジスタ
  • 対象class(selfの文脈みたいなやつ?/regs[0]がselfなので必要なんだっけとは思った)

これらが真にコード列を実行するために必要な情報で、callinfoがpushされるたびに切り替わり、脱出するときに元の状態に戻るべきものです。なのでFIFOのようなデータ構造をしばしば使うわけですね。callinfoの場合はprevへの参照を持ったリストって感じでしょうか。

レジスタや対象classについてはVMでも持っていますが、これが現在アクティブなレジスタやclassということで、 callinfo_tail には一つ前の情報が保持されているという設計のようです。 OP_SEND のコードを後ほど見ますが、そこと合わせるとわかりやすいです。

値を表す構造体

struct RObject {
  mrbc_vtype tt : 8;
  union {
    mrbc_int i;         // MRBC_TT_FIXNUM, SYMBOL
#if MRBC_USE_FLOAT
    mrbc_float d;       // MRBC_TT_FLOAT
#endif
    struct RClass *cls;        // MRBC_TT_CLASS
    struct RObject *handle;    // handle to objects
    struct RInstance *instance;    // MRBC_TT_OBJECT
    struct RProc *proc;        // MRBC_TT_PROC
    struct RArray *array;  // MRBC_TT_ARRAY
    struct RString *string;    // MRBC_TT_STRING
    const char *str;      // C-string (only loader use.)
    struct RRange *range;  // MRBC_TT_RANGE
    struct RHash *hash;        // MRBC_TT_HASH
  };
};
typedef struct RObject mrbc_object;

struct RObject はまあ言っていれば tagged union となっていて、vtypeに従ってオブジェクトへの参照を保持している趣です。少し気になるのはRObjectからRObjectへのハンドルも保持しているんですがどういうパターンでしょうかね?

typedef struct RClass {
  mrbc_sym sym_id;  // class name
#ifdef MRBC_DEBUG
  const char *names;  // for debug. delete soon.
#endif
  struct RClass *super;    // mrbc_class[super]
  struct RProc *procs; // mrbc_proc[rprocs], linked list

} mrbc_class;
typedef struct RClass mrb_class;

struct RClass は名前(sym_id)、スーパークラス、おそらくメソッド一覧(procs)を持っています。RProcが名前の情報も持っているので、これがメソッドテーブル、名前と手続きの紐付けです。

ちなみに、 global.cグローバル変数のテーブルも定義されています。またVMには定数のテーブルがないのですが、それもグローバルオブジェクトです。

static mrbc_kv_handle handle_const; //!< for global(Object) constants.
static mrbc_kv_handle handle_global;   //!< for global variables.

mrbc_kv_handle (struct RKeyValueHandle) は各所で使われている自前のKVという感じです。これはハッシュテーブルではく、後ろに値を追加しつつバイナリサーチで検索するデータ構造です*2

// See keyvalue.[ch]
typedef struct RKeyValue {
  mrbc_sym sym_id;  //!< symbol ID as key.
  mrbc_value value; //!< stored value.
} mrbc_kv;

typedef struct RKeyValueHandle {
  uint16_t data_size;  //!< data buffer size.
  uint16_t n_stored;   //!< # of stored.
  union {
    mrbc_kv *data;  //!< pointer to allocated memory.
    struct VM *vm; //!< pointer to VM (if data_size == 0)
  };
} mrbc_kv_handle;

インスタンスについても見てみます。

#define MRBC_OBJECT_HEADER \
  uint16_t ref_count; \
  mrbc_vtype tt : 8  // TODO: for debug use only.

typedef struct RInstance {
  MRBC_OBJECT_HEADER;

  struct RClass *cls;
  struct RKeyValueHandle ivar;
  uint8_t data[];

} mrbc_instance;
typedef struct RInstance mrb_instance;

MRBC_OBJECT_HEADER という、vtypeと参照カウントの共通のメンバの示すマクロがあります。

で、 struct RInstance は自分の所属するクラスと、 RKeyValueHandle を用いたインスタンス変数のKVを持っています。 data[] で任意のデータを持たせることもできそうな雰囲気ですが...。

typedef struct RProc {
  MRBC_OBJECT_HEADER;

  unsigned int c_func : 1;   // 0:IREP, 1:C Func
  mrbc_sym sym_id;
#ifdef MRBC_DEBUG
  const char *names;      // for debug; delete soon
#endif
  struct RProc *next;
  union {
    struct IREP *irep;
    mrbc_func_t func;
  };

} mrbc_proc;
typedef struct RProc mrb_proc;

struct RProc では、Cの関数かIREPで実現された関数かの区別(RProcはメソッドとprocの両方を表現できるものとして実装されているため)と、unionの形でそれぞれの関数本体への参照を保持しています。

また、 next として上位のRProcの参照を持たせることができますが、これは将来クロージャを表現するためかなと思われました(ただ、upvalueのようなものは1.2では未実装に見えた)。

ここまで代表的なものを確認しました。他のValueを表現した構造体については、どう実装するか想像したら割とその通りというシンプルなものなので省略しています。

初期化処理まわり

int mrbc_load_mrb(struct VM *vm, const uint8_t *ptr)
{
  int ret = -1;
  vm->mrb = ptr;

  ret = load_header(vm, &ptr); // ヘッダをパースしている
  while( ret == 0 ) {
    if( memcmp(ptr, "IREP", 4) == 0 ) {
      ret = load_irep(vm, &ptr); // vm->irep = load_irep_0(vm, &p); してる
    }
    else if( memcmp(ptr, "LVAR", 4) == 0 ) {
      ret = load_lvar(vm, &ptr); // 1.2では最初の4バイトで長さをとってその分スキップしてるだけ...
    }
    else if( memcmp(ptr, "END\0", 4) == 0 ) {
      break;
    }
  }

  return ret;
}

// load_irep() は vm->irep = load_irep_0(vm, &p); が本体処理
static mrbc_irep * load_irep_0(struct VM *vm, const uint8_t **pos)
{
  mrbc_irep *irep = load_irep_1(vm, pos); // 最初のが親
  if( !irep ) return NULL;

  int i;
  for( i = 0; i < irep->rlen; i++ ) { // あとは子供
    irep->reps[i] = load_irep_0(vm, pos);
  }

  return irep;
}

VMのための領域をmalloc() した後、 mrbc_load_mrb() を呼んで初期化していますが、基本的にはptrで渡されたmrubyバイナリデータをパースし、IREPの情報を vm->irep にセットしているのが主要な処理です。

void mrbc_vm_begin( struct VM *vm )
{
  vm->pc_irep = vm->irep;
  vm->pc = 0;
  vm->current_regs = vm->regs;
  memset(vm->regs, 0, sizeof(vm->regs));

  // clear regs
  int i;
  for( i = 1; i < MAX_REGS_SIZE; i++ ) {
    vm->regs[i].tt = MRBC_TT_NIL;
  }

  // set self to reg[0]
  vm->regs[0].tt = MRBC_TT_CLASS;
  vm->regs[0].cls = mrbc_class_object;

  vm->callinfo_tail = NULL;

  // target_class
  vm->target_class = mrbc_class_object;

  vm->error_code = 0;
  vm->flag_preemption = 0;
}

mrbc_vm_begin() ではその他の値(トップレベルの実行コンテクスト的な)を初期化します。 vm->pc_irep = vm->irep としているので vm->irep に入るIREPがエントリポイントというわけですね。

なお mrbc_class_objectグローバル変数で、prelude処理(init_static() など)をしたらObjectクラスのRClassインスタンスが入ります。他にもそういうグローバル変数があります。

static void mrbc_init_class_object(struct VM *vm)
{
  // Class
  mrbc_class_object = mrbc_define_class(vm, "Object", 0);
  mrbc_define_method(vm, mrbc_class_object, "initialize", c_ineffect);
  mrbc_define_method(vm, mrbc_class_object, "alias_method", c_object_alias_method);
  //...
}

VMの実行

ここまでのデータ構造をはっきり理解していれば、命令の実行は難しいわけではないと想像できます。pcに沿って一つ命令をフェッチして、評価して、pcを動かす... を繰り返すだけですね。

int mrbc_vm_run( struct VM *vm )
{
  int ret = 0;

  do {
    // get one bytecode
    uint32_t code = bin_to_uint32(vm->pc_irep->code + vm->pc * 4);
    vm->pc++;

    // regs
    mrbc_value *regs = vm->current_regs;

    // Dispatch
    int opcode = GET_OPCODE(code);
    switch( opcode ) {
    case OP_NOP:        ret = op_nop       (vm, code, regs); break;
    case OP_MOVE:       ret = op_move      (vm, code, regs); break;
    case OP_LOADL:      ret = op_loadl     (vm, code, regs); break;
//...
    case OP_ABORT:      ret = op_stop      (vm, code, regs); break;  // reuse
    default:
      console_printf("Skip OP=%02x\n", GET_OPCODE(code));
      break;
    }
  } while( !vm->flag_preemption );

  vm->flag_preemption = 0;

  return ret;
}

停止すべき命令に出会ったら(RETURNなど?) flag_preemption が 1 になると想像できます。なお、mruby/c 1.2の時点(mruby 1.0系統)では命令は固定長なので、フェッチも vm->pc_irep->code + vm->pc * 4 とシンプルに参照できています。mruby 3 系統では命令は可変長ですね。確か2から可変長だったっけ...。

本記事では全ての命令を確認することはしませんが、いくつか基本的な命令を読もうと思います。

単純な命令

まず単純な命令を見てみます。あるメソッドのバイトコード列と考えてください。

LOADI        R1  123 ;; 1, 123
LOADI       R2  456 ;; 2, 456
MOVE        R3  R1  ;; 3, 1
MOVE        R4  R2  ;; 4, 2
ADD     R3  R4      ;; 3
RETURN  R4          ;; 4

こういう命令列を評価するとどうなるでしょう。この命令が return 123 + 456 しているだけですよというのは読み取れるかなと思います。

OP_LOADI

 static inline int op_loadi( mrbc_vm *vm, uint32_t code, mrbc_value *regs )
 {
   int ra = GETARG_A(code);
 
   mrbc_release(&regs[ra]);
   regs[ra].tt = MRBC_TT_FIXNUM;
   regs[ra].i = GETARG_sBx(code);
 
   return 0;
 }

命令一つ一つも、基本的にやることが単純なので実装を負いやすいかなと思います。loadiではレジスタに値を入れているだけ。 GETARG_A|sBx() マクロはバイト列の規約に沿ってオペランドを取り出しています。

loadiの場合元々レジスタにあった値の参照が一つ減るので、 mrbc_release() で参照を減らし、参照がなくなったらメモリを解放しています。

OP_MOVE

 static inline int op_move( mrbc_vm *vm, uint32_t code, mrbc_value *regs )
 {
   int ra = GETARG_A(code);
   int rb = GETARG_B(code);
 
   mrbc_release(&regs[ra]);
   mrbc_dup(&regs[rb]);
   regs[ra] = regs[rb];
 
   return 0;
 }

これも、 R(A) の方は参照を減らす、 R(B) の方は参照を増やすということをしています。

OP_ADD

static inline int op_add( mrbc_vm *vm, uint32_t code, mrbc_value *regs )
 {
   int ra = GETARG_A(code);
 
   if( regs[ra].tt == MRBC_TT_FIXNUM ) {
     if( regs[ra+1].tt == MRBC_TT_FIXNUM ) { // in case of Fixnum, Fixnum
       regs[ra].i += regs[ra+1].i;
       return 0;
     }
   }

OP_ADDはオペランドが一つです。オペランドAがある時、 reg[A] に対して reg[A+1] を足してセットするという操作なのでこうなっています。なお、 MRBC_USE_FLOAT の定義により実装される分岐があり、整数と小数同士の演算にも対応しているのですが、そこは省略しています。

OP_RETURNOP_SEND と一緒に参照することにしましょう。

メソッド呼び出し周辺の命令

これら演算や代入系の命令はレジスタと参照、pc(ジャンプの時)を操作するだけなので比較的難しくないのですが、メソッド呼び出しと戻り値、SEND/RETURNの流れはVM実装に慣れていないと追いづらい*3ので少し取り上げます。

OP_SEND

長いので適当にCの関数を分割しています。

static inline int op_send( mrbc_vm *vm, uint32_t code, mrbc_value *regs )
{
  int ra = GETARG_A(code);
  int rb = GETARG_B(code);  // index of method sym
  int rc = GETARG_C(code);  // number of params
  mrbc_value recv = regs[ra];

  // Block param
  int bidx = ra + rc + 1;
  switch( GET_OPCODE(code) ) {
  case OP_SEND:
    // set nil
    mrbc_release( &regs[bidx] );
    regs[bidx].tt = MRBC_TT_NIL;
    break;

  case OP_SENDB: // 省略
    break;

  default:
    break;
  }

まずオペランドを取得します。この際、 R(A) が新しいself(sendの段階ではレシーバ)、 R(A+1),...,R(A+C) がメソッドの引数となるので、A+C+1番目のレジスタは使わないものとして解放されています。ブロック呼び出し(OP_SENDB)ではA+C+1番目のレジスタにRProcが入る決まりですが今回は省略。

  const char *sym_name = mrbc_get_irep_symbol(vm->pc_irep->ptr_to_sym, rb);
  mrbc_sym sym_id = str_to_symid(sym_name);
  mrbc_proc *m = find_method(vm, &recv, sym_id);

  if( m == 0 ) {
    mrb_class *cls = find_class_by_object( vm, &recv );
    console_printf("No method. Class:%s Method:%s\n",
           symid_to_str(cls->sym_id), sym_name );
    return 0;
  }

B番目のシンボルを取得して(この段階ではpc_irepは呼び出し前の状態です)、メソッドを探し出す処理です。 find_method() は本当に recv->procs からメソッドを探しているだけ。

  // m is C func
  if( m->c_func ) {
    m->func(vm, regs + ra, rc);

    extern void c_proc_call(mrbc_vm *vm, mrbc_value v[], int argc);
    if( m->func == c_proc_call ) return 0;

    int release_reg = ra+1;
    while( release_reg <= bidx ) {
      mrbc_release(&regs[release_reg]);
      release_reg++;
    }
    return 0;
  }

メソッド struct *RProc m がC関数ポインタであった場合そのまま呼び出しています。

c_proc_call との一致を判定している箇所がよくわからなかったのですが、多分 Proc#call を呼んだ時だけは、 c_proc_call の中でレジスタのクリーンアップ操作などをしているのでそのまま抜けてOKという感じでしょうか?

それ以外の普通のC関数ポインタの時は引数で使ったレジスタ A+1 .. A+C+1 をここで解放して、抜けます。

  // m is Ruby method.
  // callinfo
  mrbc_push_callinfo(vm, sym_id, rc);

IREPのメソッドを呼ぶ際は、 callinfo を一つ(仮想的な)call stackにpushします。

void mrbc_push_callinfo( struct VM *vm, mrbc_sym mid, int n_args )
{
  mrbc_callinfo *callinfo = mrbc_alloc(vm, sizeof(mrbc_callinfo));
  if( !callinfo ) return;

  callinfo->current_regs = vm->current_regs;
  callinfo->pc_irep = vm->pc_irep;
  callinfo->pc = vm->pc;
  callinfo->mid = mid;
  callinfo->n_args = n_args;
  callinfo->target_class = vm->target_class;
  callinfo->prev = vm->callinfo_tail;
  vm->callinfo_tail = callinfo;
}

vm->callinfo_tail にあるcallinfoは「呼び出し元のcallinfo」ですね。なので今のVMにある値がコピーされていきます。 callinfo->prev = vm->callinfo_tail としてさらに前のcallinfoへの参照も保持されて、リストとして動作していますね。

  // target irep
  vm->pc = 0;
  vm->pc_irep = m->irep;

  // new regs
  vm->current_regs += ra;

  return 0;
}

pushが終わったら pc はリセットされ、 pc_irepm のIREPに切り替えられます。これで次のeval loopからは新しいIREPを評価することになります。あと、 current_regs は今まで利用してきたレジスタがどんどん後ろに追加されることを想定していて、親のレジスタのうち A .. A+C は新しいcallinfoの 0 .. Cレジスタになるので、参照を移動させています。

ところで、ブロックが考慮されると使うべき引数レジスタC+1 までとなり、 op_send() 関数が使いまわされています。ただ呼び出し周りを素直に読めるよう、今回の記事ではバッサリ説明をカットします。

OP_RETURN

SENDで遷移したIREPからはいつかRETURNしないといけません。

static inline int op_return( mrbc_vm *vm, uint32_t code, mrbc_value *regs )
{
  // return value
  int ra = GETARG_A(code);

RETURNで戻すべき値は R(A) にあります。

  mrbc_value *regs0 = regs;
  // return value stored in original regs[0] if return in block
  mrbc_callinfo *ci = vm->callinfo_tail;
  if( ci && ci->current_regs[1].tt == MRBC_TT_PROC ){
    regs0 = regs - 2;
  } 
  mrbc_release(regs0);
  *regs0 = regs[ra];
  regs[ra].tt = MRBC_TT_EMPTY;

少しわかりづらいのですが、戻り値は呼び出し先のコンテクストでいう R(0) 、すなわち呼び出し元で言えば R(A)レジスタに戻されないといけません。なので、今の R(0) の位置を一旦保存して、呼び出し先での regs[ra] の値をそこにコピーするということをしています *4

  // nregs to release
  int nregs = vm->pc_irep->nregs;

  // restore irep,pc,regs
  mrbc_callinfo *callinfo = vm->callinfo_tail;
  vm->callinfo_tail = callinfo->prev;
  vm->current_regs = callinfo->current_regs;
  vm->pc_irep = callinfo->pc_irep;
  vm->pc = callinfo->pc;
  vm->target_class = callinfo->target_class;

  // clear stacked arguments
  int i;
  for( i = 1; i < nregs; i++ ) {
    mrbc_release( &regs[i] );
  }

vm->callinfo_tail にある情報を戻しています。で、 mrbc_release( &regs[i] ); で呼び出し先の 1 .. 使った分レジスタを解放しています。解放するのでIREP毎に利用しているレジスタの数を保持する必要があるんですね。このコードで、 regs で持っている参照自体は呼び出し先のものなので読み違えないようにします。

  // release callinfo
  mrbc_free(vm, callinfo);

  return 0;
}

最後に callinfo のメモリを解放します。ここまでで、基本的な命令の実行と、メソッド呼び出しと戻る操作(すなわりIREP間での移動)の実装を追ってみました。

他にも、クラスやメソッドを定義する命令もサポートされていますが、流石に長くなってしまったので省略します。


最後なので書く個人的な感想

yhara さんが自作してしまったように、mruby VMの仕様/実装は非常に面白く、実はRubyコミュニティのアイデア埋蔵金なのではと思っています。例えば、mruby VMバイトコードコンパイルできる別言語を作ってみる等は興味深いテーマかもしれません。

ちょっと前にWebAssembly Runtimeを自作したりしたこともあって、あらためて言語VMとは興味深い存在だなと思い直しました。

僕もmrubyで一発当てるぞ!

では、明日は24日目の記事です。

*1:なお、このcallinfoには実行時コンテクストのような情報がありません、従ってクロージャやUPVALUE的な挙動をサポートできないのですが、これは設計思想のようです。ref: https://qiita.com/HirohitoHigashi/items/14ffd29e1c23e6989191#%E3%82%AF%E3%83%AD%E3%83%BC%E3%82%B8%E3%83%A3 UPVALUEの実装を確認するにはmrubyのコードリーディングが必要です

*2:ちなみにHashも実は内部はハッシュテーブルではありませんでした。背景は https://qiita.com/HirohitoHigashi/items/14ffd29e1c23e6989191#hash の通りかと思います

*3:筆者が慣れているとは言っていない

*4:ブロックから戻る時?に regs をさらに2つたどっている背景はちゃんと読めてないですね...