esm アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

アジャイル事業部運営を支える技術

こんにちは、平田です。

普段私はマネージャーとして、事業部の運営やお客様との折衝を担当しています。業務でプログラミングをすることはほとんどないのですが、運営の中で必要な業務の自動化や省略化のためにコードを書くことがあります。そのいくつかを紹介したいと思います。

勤怠承認の省力化

メンバーの勤怠を月末に確認して承認する必要があります。以前、あまり使い勝手の良くない勤怠管理ソフトが導入されていた時代に、30名近くのメンバーの勤怠確認をしやすくするために、 CSV ファイルを読み込んで、ターミナル上で確認できるように作ったプログラムです。

その後、全社の勤怠管理ツールが変更になった後もその変更に追随したり、社内ルールに対応した形で重点チェックすべきデータのハイライト機能などを地味に追加したりしています。

Gem としては、対話型で確認できるようにするために pry 、ターミナル上でハイライトするために color_echo を使ったりしています。

全社で同じ勤怠管理ソフトを使っていることもあり、隣の部署のマネージャーにもプレゼントしようとしたのですが、普通の管理職は手元に Ruby が入っていないらしく、不要と言われてしまいました。

今後の計画としては、自分が暗黙的に注意して確認しているところを、プログラムレベルでチェックできるようにして、人間の確認にかかる時間をもっと短くしたいと思います。

収益情報の共有

管理部門が作ってくれている収益に関する Excel から情報を抽出して、見やすい形でメンバーにスプレッドシートで共有するスクリプトです。

roo を使って読み込んだ Excel ファイルから、自部署の計画値や実績値を抽出して、 Google スプレッドシートに書き出します。書き出した先のスプレッドシートでは、他の KPI 数値と一緒に収益情報もグラフ化して表示されるようにしています。

今後の計画としては、まずは直近で管理部門作成の Excel が Google スプレッドシートに変わるということでそれに対応していきたいと考えています。さらに KPI 情報の共有場所を Google データポータルのようなものに移行することができれば、プログラムなしでデータが共有できるようになるかもしれません。

その他にも事業部内の日報の盛り上がりを確認するために esa の記事数をカウントするためのスクリプト(設計が悪くてすぐにAPI の limit を超えてしまうので運用停止中…)等を書いたり捨てたりしています。 引き続き、コンピュータに任せられる部分は任せていって、楽しい事業部運営に注力していきたいと思っています。


最後に、株式会社永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと共生しながら成長しつつ、マネージャの書くコードにツッコミを入れてくれるエンジニアを絶賛募集しています。

agile.esm.co.jp

RSpecで引数に特定の値が渡された時だけスタブしたい

はじめに

こんにちは。入社して4年目になりました、wai-doi です。

お仕事でRSpecでテストを書いていて、

「引数に特定の値が渡された時だけスタブしたい」

ということがありました。そのときどのように書けばよいか分からなかったので、今回は調べたこととその方法を書きます。

実行環境

  • Ruby 3.1.2
  • RSpec 3.11.0

サンプルコード

例えば以下の Shopper#buy_fruits メソッドのテストをしたいとします。単純に配列の ['apple', 'banana', 'orange'] を返します。

class Shopper
  def buy_fruits(shop)
    basket = []
    basket << shop.sell('apple')
    basket << shop.sell('banana')
    basket << shop.sell('orange')
    basket
  end
end

class Shop
  def sell(fruit)
    fruit
  end
end

普通に RSpec でテストを書くなら以下のようになります。

describe 'Shopper#buy_fruits' do
  let(:shop) { Shop.new }

  it do
    shopper = Shopper.new
    expect(shopper.buy_fruits(shop)).to eq ['apple', 'banana', 'orange']
  end
end

引数に 'banana' が渡される呼び出しだけをスタブする

ここからが本題です。

このとき、Shop#sell メソッドの引数に 'banana' が渡される呼び出しだけをスタブしたいとします。スタブして 'banana' ではなく nil を返すようにしてみます。

次のようにテストを書くとうまくできます。

describe 'Shopper#buy_fruits' do
  let(:shop) { Shop.new }

  before do
    allow(shop).to receive(:sell).and_call_original
    allow(shop).to receive(:sell).with('banana').and_return(nil)
  end

  it do
    shopper = Shopper.new
    expect(shopper.buy_fruits(shop)).to eq ['apple', nil, 'orange']
  end
end

引数に 'bannana' が渡される呼び出しを特定するために with('banana') を使用しています。

ポイントは、先に and_call_original を書いて、そのあと with('banana').and_return(nil) を書く順番です。逆だとうまくいきません。

今回の方法はこちらの Stack Overflow のページを参考にしました。

stackoverflow.com

上記の方法は柔軟には使えない

ただし、上記の方法が使える場合は限られています。以下の場合には使えません。

同じ引数の呼び出しすべてに影響してしまう

例えば次のような、 Shop#sell の 2 番目と 3 番目の呼び出しが同じ引数の場合です。

class Shopper
  def buy_fruits(shop)
    basket = []
    basket << shop.sell('apple')
    basket << shop.sell('banana')
    basket << shop.sell('banana')
    basket
  end
end

この場合

allow(shop).to receive(:sell).and_call_original
allow(shop).to receive(:sell).with('banana').and_return(nil)

と書いてしまうと、2 番目だけでなく 3 番目の Shop#sell の呼び出しも nil を返してしまいます。 例えば 2 番目の呼び出しだけをスタブしたいといったことはできません。

引数を持たないメソッドには使えない

スタブ対象のメソッド(今回の Shop#sell)が引数をもつ必要があります。引数を持たないメソッドの場合は with でスタブしたい呼び出しを特定できないため、この方法は使えません。 そのため同じメソッド呼び出しが複数ある場合、この行の呼び出しの時だけスタブしたいといったことはできません。

まとめ

RSpecで「引数に特定の値が渡された時だけスタブしたい」場合について書きました。 and_call_originalwith でそのスタブしたい呼び出しを特定することで実現できました。

この記事がどなたかのお役に立てば幸いです。


最後に、株式会社永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと共生しながら成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

esm.fm コンサル x EM 対談『テーマ: 育成』を開催します

esm.fm コンサル x EM 対談『テーマ: 育成』を 2022年7月5日(火) 12:30-13:00 に開催します。

esminc.doorkeeper.jp

アジャイルソフトウェア開発の老舗『永和システムマネジメント』のアジャイルコンサルタントの @fkino とエンジニアリングマネージャーの @koic のオンライン対談イベントです。

今回取り上げるテーマは『育成』です。"アジャイルマニフェスト"から20年後の現代において「いまの時代にはもうないだろう」と思っているような困りごとや価値観での実践が現実世界では起きているようです。アジャイルコンサルタントが現場で見ている課題と、そのような課題に対して永和の開発とコンサルタントがどのような価値観で解決しているか取りあげるインターネット番組です。

ランチタイムの 12:30 ~ 13:00 に開催するカジュアルなイベントです。お昼の賑わいにご活用ください。

また、ESM エンジニア ショートトーク『失敗から学ぶ技術』を 2022年6月30日(木) 12:30-13:00 に開催します。こちらもぜひご視聴ください。

esminc.doorkeeper.jp


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

ESM エンジニア ショートトーク『失敗から学ぶ技術』を開催します

ESM エンジニア ショートトーク『失敗から学ぶ技術』を 2022年6月30日(木) 12:30-13:00 に開催します。

esminc.doorkeeper.jp

Ruby とアジャイルソフトウェア開発の老舗『永和システムマネジメント』のエンジニア @9sako6@color_box 登壇のオンラインプレゼンイベントです。

今回取り上げるテーマは『失敗』です。

プロジェクトや個人でのソフトウェア開発で「失敗」はつきものです。このイベントでは、弊社エンジニアが汎用ツールを作成する過程で踏んださまざまな失敗や、逆に、実プロジェクトで遭遇した失敗をきっかけにして、同じ失敗を起こさないよう実装したツールについて紹介します。

ランチタイムの 12:30 ~ 13:00 に開催するカジュアルなイベントです。お昼の賑わいにご活用ください。


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

Rails / OSS パッチ会オンライン 2022年6月のお知らせ

2022年6月の Rails / OSS パッチ会を 6月15日(水)に Discord でオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS について、upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。

Discord の Rails/OSS パッチ会サーバーへの招待 URL は以下です👇

discord.gg

以下、前回の活動が関わる成果です。

kamipo: Rails

github.com

koic: RuboCop

github.com

先日新たに Rails コアコミッター、コミッターに就任されたレギュラーメンバーの活動や RubyKaigi 2022 に向けた話題などあるかもしれません。

rubyonrails.org

顧問の a_matsuda は RubyKaigi のチーフオーガナイザーでもあるため、RubyKaigi 2022 の CFP へのプロポーザルについて悩みを持ち寄っても良さそうです。

これからパッチ会に参加してみようという方も、ぜひどうぞ。Discord でお会いしましょう。


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

Rubyでできる!RISC-Vシミュレータの作りかた 〜 From 4649 To HELLO WORLD 〜

HELLO WORLD〜

はじめに

こんにちは、永和システムマネジメントの自作CPUおじさん、はたけやまたかし( @htkymtks )です。

今回はRubyを使った小さなRISC-Vシミュレータの作り方をご紹介します(以前もシミュレータの記事を書いたのですが、シミュレータに大幅に手を入れたので、それに対応したHDリマスター版です)

リポジトリ

(今回ご紹介するシミュレータのリポジトリはこちら)

RISC-Vとは

RISC-VはCPUの命令セットアーキテクチャ(ISA)のひとつで、使用料のかからないオープンソースライセンスで提供されていることや、命令セットの美しさから最近注目を集めています。

仕様

RISC-Vの仕様にはワード幅(32ビット、64ビット)や浮動小数点数サポートの有無など、いくつかのバリエーションがありますが、今回は32ビット整数演算のみをサポートする「RV32I」のサブセットを実装します。

メモリ

では早速作っていきましょう。まずは命令やデータを格納するメモリを作ります。Memoryクラスは、バイナリデータが格納されたStringオブジェクトをメモリイメージとして受け取り、メモリイメージに対しての1ワードの読み込み(readメソッド)と1ワードの書き込み(writeメソッド)を提供します。

ちなみに「ワード」というのはCPUがデータの読み込み・書き込みを行う単位です。今回作成する32ビットRISC-Vは32ビット単位でデータの読み書きを行うため、1ワードは32ビット(4バイト)となります。

class Memory
  WORD_SIZE = 4

  attr_accessor :data

  def initialize(data = "\0" * 512)
    # バイナリデータとして扱いたいので ASCII-8BIT エンコーディングへ変換
    @data = data.b
  end

  # 指定したアドレスから1ワード(4バイト)のデータを読み込む
  def read(addr)
    word = @data.slice(addr, WORD_SIZE)

    # 「signed int32」でメモリの内容を読み込む
    # see: https://docs.ruby-lang.org/ja/latest/doc/pack_template.html
    word.unpack1("l")
  end

  # 指定したアドレスへ1ワード(4バイト)のデータを書き込む
  def write(addr, word)
    # 「signed int32」でメモリへ書き込む
    @data[addr, 4] = [word].pack("l")
  end
end

Memoryの使い方はこんな感じです。

RISC-VはリトルエンディアンなCPUなので、4バイトの 0x01010093 をメモリの 0x00000000 番地へ書き込むと「0x00000000 = 0x93, 0x00000001 = 0x0, 0x00000002 = 0x01, 0x00000003 = 0x01」のような順番に格納されます。

# メモリを初期化
memory = Memory.new("\x93\x00\x01\x01\x94\x00\x01\x01")

# アドレスを指定して読み込み
printf "0x%08x\n", memory.read(0)
#=> 0x01010093
printf "0x%08x\n", memory.read(4)
#=> 0x01010094

# アドレス0番地へ書き込み
memory.write(0, 0x00ABCDEF)
printf "0x%08x\n", memory.read(0)
#=> 0x00abcdef

# アドレス4番地へ書き込み
memory.write(4, -1)
printf "0x%08x\n", memory.read(4)
#=> 0xffffffff

命令デコーダ

次は命令デコーダです。

RISC-Vの命令は以下のようなフォーマットになっています。「opcode」「funct3」「funct7」は命令の判別に、「rd」「rs1」「rs2」は操作対象のレジスタ番号の指定に、「imm」は即値に利用されます。

命令デコーダは命令のビット列からこれらパラメータの切り出し処理を行います。

( https://github.com/jameslzhu/riscv-card/blob/master/riscv-card.pdf より)

class Decoder
  attr_reader :opcode, :rd, :funct3, :rs1, :rs2, :funct7, :i_imm, :s_imm, :b_imm

  def initialize
    @opcode = nil
    @rd = nil
    @funct3 = nil
    @rs1 = nil
    @rs2 = nil
    @funct7 = nil
    @i_imm = nil
    @s_imm = nil
    @b_imm = nil
  end

  def decode(inst)
    @opcode = (inst & 0x0000007f)
    @rd = (inst & 0x00000f80) >> 7
    @funct3 = (inst & 0x00007000) >> 12
    @rs1 = (inst & 0x000f8000) >> 15
    @rs2 = (inst & 0x01f00000) >> 20
    @funct7 = (inst & 0xfe000000) >> 25
    @i_imm = (inst & 0xfff00000) >> 20
    @s_imm = ((inst & 0xfe000000) >> 20) | ((inst & 0x00000f80) >> 7)
    @b_imm = ((inst & 0x80000000) >> 19) |
             ((inst & 0x00000080) << 4) |
             ((inst & 0x7e000000) >> 20) |
             ((inst & 0x00000f00) >> 7)
  end

  # NOP命令(no operation 何も行わない命令)かどうかを判定(あとで使う)
  def nop?
    @opcode == 0b0010011 &&
      @funct3 == 0 &&
      @rd == 0 &&
      @rs1 == 0 &&
      @i_imm == 0
  end
end

命令デコーダの使い方はこんな感じです。

# 適当なバイト列を読み込ませて正しくデコードできるか確認
decoder = Decoder.new
decoder.decode(0b1000001_10111_10011_101_10001_0110011)
printf "%07b\n", decoder.opcode
#=> 0110011
printf "%05b\n", decoder.rd
#=> 10001
printf "%03b\n", decoder.funct3
#=> 101
printf "%05b\n", decoder.rs1
#=> 10011
printf "%05b\n", decoder.rs2
#=> 10111
printf "%07b\n", decoder.funct7
#=> 1000001

CPU

次はCPUです。今回作成するCPUは以下の4つの部品から構成されます。

  1. メモリ(さっき作ったMemoryクラス)
    • プログラムやデータの保存領域
  2. プログラムカウンタ(PC)
    • メモリ中の次に実行する命令のアドレスを指し示す
  3. 命令デコーダ(さっき作ったDecoderクラス)
  4. x0からx31まで32個のレジスタ
    • RV32Iはx0からx31までの32ビットの整数レジスタを持ちます
    • RISC-Vの「x0レジスタ」は常に0を返す特殊なレジスタになります

INST_TABLEoptcodefunct3funct7 をキーにした命令テーブルで、デコーダから返ってきた値を使って INST_TABLE を引くことでどの命令を呼び出すかを決定します。

また、 _add_sub は実行される命令の本体になります。

class Cpu
  INST_TABLE = {
    [0b0110011, 0x0, 0x00] => :_add,
    [0b0110011, 0x0, 0x20] => :_sub,
    [0b0110011, 0x6, 0x00] => :_or,
    [0b0110011, 0x7, 0x00] => :_and,
    [0b0010011, 0x0]       => :_addi,
    [0b0010011, 0x1]       => :_slli,
    [0b1100011, 0x0]       => :_beq,
    [0b0000011, 0x2]       => :_lw,
    [0b0100011, 0x2]       => :_sw
  }

  attr_accessor :pc
  attr_reader :x_registers, :memory

  def initialize
    @pc = 0                   # プログラムカウンタ
    @x_registers = [0] * 32   # レジスタ
    class << @x_registers
      # x0は常に0を返す
      def [](nth)
        nth == 0 ? 0 : super
      end
    end

    @decoder = Decoder.new
    @memory = Memory.new(     # メモリ
      ("\x00" * 512).b
    )
    @nop_count = 0
  end

  def init_memory(data)
    @memory.data[0, data.size] = data
  end

  def run
    inst = fetch

    decode(inst)

    # NOPが5回来たら処理を終える
    return false if @nop_count >= 5

    execute
    true
  end

  def fetch
    @memory.read(@pc)
  end

  def decode(inst)
    @decoder.decode(inst)
    if @decoder.nop?
      @nop_count += 1
    else
      @nop_count = 0
    end
  end

  def execute
    op_f3_f7 = [@decoder.opcode, @decoder.funct3, @decoder.funct7]
    op_f3 = [@decoder.opcode, @decoder.funct3]
    inst_symbol = INST_TABLE[op_f3_f7] || INST_TABLE[op_f3]
    send inst_symbol
  end

  ### Instructions

  def _add
    rd = @decoder.rd
    rs1 = @decoder.rs1
    rs2 = @decoder.rs2
    @x_registers[rd] = @x_registers[rs1] + @x_registers[rs2]
    @pc = @pc + 4
  end

  def _sub
    rd = @decoder.rd
    rs1 = @decoder.rs1
    rs2 = @decoder.rs2
    @x_registers[rd] = @x_registers[rs1] - @x_registers[rs2]
    @pc = @pc + 4
  end

  def _or
    rd = @decoder.rd
    rs1 = @decoder.rs1
    rs2 = @decoder.rs2
    @x_registers[rd] = @x_registers[rs1] | @x_registers[rs2]
    @pc = @pc + 4
  end

  def _and
    rd = @decoder.rd
    rs1 = @decoder.rs1
    rs2 = @decoder.rs2
    @x_registers[rd] = @x_registers[rs1] & @x_registers[rs2]
    @pc = @pc + 4
  end

  def _slli
    rd = @decoder.rd
    rs1 = @decoder.rs1
    i_imm = @decoder.i_imm
    @x_registers[rd] = @x_registers[rs1] << (i_imm & 0b11111)
    @pc = @pc + 4
  end

  def _addi
    rd = @decoder.rd
    rs1 = @decoder.rs1
    i_imm = @decoder.i_imm

    minus_flg = (i_imm & 0b100000000000) >> 11
    imm = if minus_flg == 1
            # TODO もっといい感じに書きたい
            imm = (0b1000000000000 - i_imm) * -1
          else
            imm = i_imm
          end
    @x_registers[rd] = @x_registers[rs1] + imm
    @pc = @pc + 4
  end

  def _beq
    rd = @decoder.rd
    rs1 = @decoder.rs1
    rs2 = @decoder.rs2
    b_imm = @decoder.b_imm

    minus_flg = (b_imm & 0b1000000000000) >> 12
    imm = if minus_flg == 1
            # TODO もっといい感じに書きたい
            imm = (0b10000000000000 - b_imm) * -1
          else
            imm = b_imm
          end
    @pc = if @x_registers[rs1] == @x_registers[rs2]
            @pc + imm
          else
            @pc + 4
          end
  end

  def _lw
    rd = @decoder.rd
    rs1 = @decoder.rs1
    imm = @decoder.i_imm
    @x_registers[rd] = @memory.read(@x_registers[rs1] + imm)
    @pc = @pc + 4
  end

  def _sw
    rs1 = @decoder.rs1
    rs2 = @decoder.rs2
    imm = @decoder.s_imm
    @memory.write(@x_registers[rs1] + imm, @x_registers[rs2])
    @pc = @pc + 4
  end
end

CPUの使い方はこんな感じです。インスタンスを生成してメモリへプログラムをセット、runメソッドが呼ばれると「メモリから命令をフェッチ」→「命令のデコード」→「命令の実行」が行われ、実行結果がレジスタに反映されます。

# 命令組み立て用のユーティリティメソッド
def _add(rd, rs1, rs2)
  0b0110011 |
    (rd << 7) |
    (0x0 << 12) |
    (rs1 << 15) |
    (rs2 << 20) |
    (0x00 << 25)
end

cpu = Cpu.new
cpu.x_registers[1] = 10
cpu.x_registers[2] = 20
cpu.x_registers[3] = 30

# 命令メモリをセット
mem = [
  _add(4, 1, 2), # x4 = x1 + x2
  _add(5, 4, 3), # x5 = x4 + x3
].pack("l*")

cpu.init_memory(mem)

# 実行前
puts cpu.pc
#=> 0
puts cpu.x_registers[4]
#=> 0
puts cpu.x_registers[5]
#=> 0

cpu.run # 1つめの命令を実行
cpu.run # 2つめの命令を実行

# 実行後
puts cpu.pc
#=> 8
puts cpu.x_registers[4]
#=> 30
puts cpu.x_registers[5]
#=> 60

シミュレータ

最後はシミュレータです。渡されたプログラムファイルを読み込んでCPUを初期化した後、CPUのrunメソッドを回し続けます。

本物のコンピュータではCPUは止まることなく回り続けますが、シミュレータのCPUはいつかは止める必要があります。CPUの止め方はシミュレータによってマチマチで「サポート外の命令を呼び出す」とか「nop命令(no operation なにもしない命令)を5回連続で呼び出す」など、いろいろな方法があります。今回はnop命令を5回連続で呼び出したら停止することにします。

class Simulator
  def initialize
    @cpu = Cpu.new
  end

  def init_memory(data)
    @cpu.init_memory(data)
  end

  def start
    loop do
      @cpu.run || break
    end
  end

  def dump_registers
    puts "-" * 80

    for i in 0..7
      for j in 0..3
        print "\t" unless j == 0
        n = (i * 4) + j
        print sprintf "x%02d = 0x%x (%d)", n, @cpu.x_registers[n], @cpu.x_registers[n]
      end
      print "\n"
    end

    puts "-" * 80
    puts sprintf "pc = 0x%x (%d)", @cpu.pc, @cpu.pc
  end
end

if $0 == __FILE__
  sim = Simulator.new
  mem = File.binread(ARGV.shift)
  sim.init_memory(mem)
  sim.start
  sim.dump_registers
end

シミュレータを実行

それではシミュレータを実行してみましょう。rv32simのリポジトリ内にサンプルプログラムが用意されているのでそちらを利用します。

以下のサンプルプログラムはフィボナッチ数の第10項の値を求めるプログラムです。 x1 レジスタに第10項のフィボナッチ数である 55 が格納されています。

プログラムファイルの作り方

シミュレータが完成したので、次はオリジナルのプログラムを作成してみましょう。

今回はgccを利用してプログラムをビルドするため、RISC-V向けのGNUツールチェインをインストールします。

macOSではHomebrewを使って以下の手順でインストールできます(macOS Monterey ではバイナリがインストールされますが、そうでない場合はソースからビルドされるためインストール完了するまで数時間かかります…)

$ brew tap riscv-software-src/riscv
$ brew install riscv-tools

macOS以外の環境では以下を参考にすると良いかも。

RISC-V 用のクロスコンパイラを使ってみる | Hassy's Tech Blog

(追記)Dockerもあるよ

GNUツールチェイン構築済みのDockerイメージを使う方法もあります(後述の「【おまけ】Dockerを使ったビルド手順」をご覧ください)

gccを使ってアセンブル

GNUツールチェインをインストールしたらgccを使ってアセンブルしてみましょう。アセンブルするソースファイルを用意して、

# 4649
# https://github.com/thata/rv32sim/blob/master/sample/4649.S

  .text
  .globl _start
  .type _start, @function
_start:
  addi x1, x0, 0x46 # x1 = ヨロ(46)
  addi x2, x0, 0x49 # x2 = シク(49)
  # シミュレータを停止させるための nop
  nop
  nop
  nop
  nop
  nop

アセンブルを行い実行ファイル(ELFファイル)を生成、ELFファイルを機械語のみのバイナリ形式(ROMファイル)に変換、作成したROMファイルを引数にシミュレータを実行します。

レジスタ x01x02 に「ヨロシク (0x46, 0x49)」がセットされていれば成功です。

$ cd sample
$ riscv64-unknown-elf-gcc -march=rv32i -mabi=ilp32 -Wl,-Ttext=0x00 -nostdlib -o 4649.elf 4649.S
$ riscv64-unknown-elf-objcopy -O binary 4649.elf 4649.rom
$ cd ..
$ ruby rv32sim.rb sample/4649.rom

4649〜

【おまけ】Dockerを使ったビルド手順

GNUツールチェイン構築済みのDockerイメージ(15GBくらいあるよ...)を使ってビルドする手順は以下の通り。「riscv64-unknown-elf-gcc」が「/opt/riscv/bin/riscv32-unknown-elf-gcc」になってるのでご注意ください。

# Dockerイメージをpullしてくる
$ docker pull kamiyaowl/riscv-gnu-toolchain-docker

# ビルド
$ cd sample
$ docker run --rm \
  -v $PWD:/usr/src/myapp \
  -w /usr/src/myapp kamiyaowl/riscv-gnu-toolchain-docker \
  /opt/riscv/bin/riscv32-unknown-elf-gcc \
  -march=rv32i -mabi=ilp32 -Wl,-Ttext=0x00 -nostdlib -o 4649.elf 4649.S
$ docker run --rm \
  -v $PWD:/usr/src/myapp \
  -w /usr/src/myapp kamiyaowl/riscv-gnu-toolchain-docker \
  /opt/riscv/bin/riscv32-unknown-elf-objcopy -O binary 4649.elf 4649.rom

# シミュレータで実行
$ cd ..
$ ruby rv32sim.rb sample/4649.rom

シミュレータ外部とのデータのやりとり

これでRISC-Vシミュレータが完成したわけですが、これだけだとレジスタに値を書き込むことしかできず、ちょっと物足りないですよね。

ということで、シミュレータへ仮想シリアルデバイスを導入し、仮想シリアルデバイスを通じて標準入出力へアクセスできるようにシミュレータを拡張してみましょう。

メモリマップドI/O

メモリのアドレス空間の一部を外部デバイスに割り当てて、メモリのアクセスと同じ方法で外部デバイスへアクセスする方法を「メモリマップドI/O」と呼びます。

今回は仮想シリアルデバイスの入出力インターフェースをメモリの0x10000000番地へ割り当てて、0x10000000番地へ書き込みが行われたら標準出力へ書き込み、0x10000000番地から読み込みを行ったら標準入力から読み込みを行えるようにします。

仮想シリアルデバイスの組み込み

まずはSerialクラスを作成します。このクラスを介して標準入出力とのデータのやり取りを行います。

class Serial
  def initialize(input = $stdin, output = $stdout)
    @input = input
    @output = output
  end

  def write(word)
    @output.putc(word & 0xFF)    
  end

  def read
    c = @input.getc until c
    # getc は String が返ってくるので、ASCIIコードに変換
    c.ord
  end
end

次にCpuクラスを以下のように修正します。lw(Load Word)命令で 0x10000000 番地からのデータの読み込みが指示された場合はSerialからデータを読み込み、sw(Store Word)命令で 0x10000000 番地へデータの書き込みが指示された場合はSerialへデータを書き込むようにします。

class Cpu
  def initialize
    ...
    #--- 追加ここから ---
    @serial = Serial.new
    #--- 追加ここまで ---
  end

  #--- 追加ここから ---
  SERIAL_ADDRESS = 0x1000_0000
  def serial_address?(address)
    address == SERIAL_ADDRESS
  end
  #--- 追加ここまで ---

  #--- _lwの修正(ここから) ---
  def _lw
    rd = @decoder.rd
    rs1 = @decoder.rs1
    imm = @decoder.i_imm
    address = @x_registers[rs1] + imm
    @x_registers[rd] =
      if serial_address?(address)
        # 標準入力から読み込む
        @serial.read
      else
        @memory.read(address)
      end
    @pc = @pc + 4
  end
  #--- _lwの修正(ここまで) ---
  
  #--- _swの修正(ここから) ---
  def _sw
    rs1 = @decoder.rs1
    rs2 = @decoder.rs2
    imm = @decoder.s_imm
    address = @x_registers[rs1] + imm
    if serial_address?(address)
      # 標準出力へ書き込む
      @serial.write(@x_registers[rs2])
    else
      @memory.write(address, @x_registers[rs2])
    end
    @pc = @pc + 4
  end
  #--- _swの修正(ここまで) ---
end

標準出力へ出力

まずは標準出力へ文字を出力してみましょう。

出力したい文字をt0レジスタへセットして0x10000000番地へ書き込むことで標準出力への出力を行います。

# hello
# https://github.com/thata/rv32sim/blob/master/sample/hello.S

  .text
  .globl _start
  .type _start, @function
_start:
  // シリアル通信の送受信レジスタのアドレス ( 0x10000000 ) を gp レジスタにセット
  // 1024を18ビットシフトさせて 0x10000000 を作成する
  addi gp, zero, 1024
  slli gp, gp, 18

  addi t0, zero, 'H'
  sw t0, 0(gp)
  addi t0, zero, 'E'
  sw t0, 0(gp)
  addi t0, zero, 'L'
  sw t0, 0(gp)
  addi t0, zero, 'L'
  sw t0, 0(gp)
  addi t0, zero, 'O'
  sw t0, 0(gp)
  addi t0, zero, ' '
  sw t0, 0(gp)
  addi t0, zero, 'W'
  sw t0, 0(gp)
  addi t0, zero, 'O'
  sw t0, 0(gp)
  addi t0, zero, 'R'
  sw t0, 0(gp)
  addi t0, zero, 'L'
  sw t0, 0(gp)
  addi t0, zero, 'D'
  sw t0, 0(gp)
  addi t0, zero, '!'
  sw t0, 0(gp)
  addi t0, zero, '!'
  sw t0, 0(gp)
  addi t0, zero, '\n'
  sw t0, 0(gp)
  nop
  nop
  nop
  nop
  nop

上記のソースをアセンブルして、

$ riscv64-unknown-elf-gcc -march=rv32i -mabi=ilp32 -Wl,-Ttext=0x00 -nost
dlib -o sample/hello.elf sample/hello.S
$ riscv64-unknown-elf-objcopy -O binary sample/hello.elf sample/hello.rom

実行してみます。「HELLO WORLD!!」が出力されればOKです。

標準入力から入力

次は標準入力から入力を受け取り、それをそのまま標準出力へ出力してみます。

# loopback
# https://github.com/thata/rv32sim/blob/master/sample/loopback.S

  .text
  .globl _start
  .type _start, @function
_start:
  // シリアル通信の送受信レジスタのアドレス ( 0x10000000 ) を gp レジスタにセット
  // 1024を18ビットシフトさせて 0x10000000 を作成する
  addi gp, zero, 1024
  slli gp, gp, 18
loop:
  // 標準入力から入力を受け取り
  lw a0, 0(gp)
  // 標準出力へ出力する
  sw a0, 0(gp)
    // 無限ループなので、終了させる場合は Ctrl-c で
  beq zero, zero, loop

上記のソースをアセンブルして、

$ riscv64-unknown-elf-gcc -march=rv32i -mabi=ilp32 -Wl,-Ttext=0x00 -nostdlib -o sample/loopback.elf sample/loopback.S
$ riscv64-unknown-elf-objcopy -O binary sample/loopback.elf sample/loopback.rom

実行してみます。入力した文字がそのまま画面へ表示されれば成功です。

C言語で書きたいんだけど...

今回はソースコードの短さを優先したためいくつか命令が足りず、C言語でプログラムを書くことができませんでした。次回は足りない命令を追加してC言語を使ったベアメタルプログラミングに挑戦する予定です。

終わりに

以上、Rubyを使ったシンプルなRISC-Vシミュレータの作り方のご紹介しました。

皆さんも梅雨の時期のおうち時間にお好きなプログラム言語でCPUシミュレータ自作なんていかがですか?

参考

フィヨルドブートキャンプのオンライン合同会社説明ドリンクアップでお話してきました

ワドルディ集めにいそしんでいる nsgc です。

2022年6月2日(木)に、フィヨルドブートキャンプさん主催のオンライン合同会社説明ドリンクアップで、@fugakkbnと一緒にお話させていただきました。

今年3月入社の@fugakkbnは、フィヨルドブートキャンプ卒業生で入社後ちょうど3ヵ月というタイミングでした。

身近なOBの先輩のふりかえりを通して、どんな感じの会社か、どういうことを学べるのか、どんな不安があってそれは解消されるか、といった生の声をライブ感を持ってお伝えできればという想いから、弊社で開発運用しており、日頃現場のプロジェクトでも活用している『Continuous KPTA (CKPTA) 』を使ったふりかえり実演をしました。

kpta.agile.esm.co.jp

発表

成果物はこちら。

ふーがメンのKPTA

当日は、それぞれの項目について、@fugakkbn からの回答を、私が付箋に入力するという流れで行ないました。

普段のふりかえり司会進行と同じ感覚で行なえたため、プレゼン発表よりは緊張は少なく感じました。また、@fugakkbn の相槌やコメントフォロー、タイピングのタイミングに合わせた回答にだいぶ救われました。

打鍵音は聞いてる方によってはうるさく感じたかもしれないので、次回行なう場合は単一指向性のマイク変えるなど改善したいです。

フリートークタイム

この後、他の2社様の発表のあと各テーブル毎でフリートークタイムでした*1
フリートークでは興味を持っていただけた多くの方と、受託開発に関する疑問や、採用フロー、会社の技術イベントといった参加された受講生の気になりポイントについてお酒を飲みながらざっくばらんに話せて良かったです。

最後に

最後になりましたがフィヨルドの皆さん素晴しい機会をありがとうございました。 そして、ご参加されたフィヨルドブートキャンプの皆さんありがとうございました!


株式会社永和システムマネジメント アジャイル事業部では、エンジニアを絶賛募集しています。

さまざまな背景を持つ方々も歓迎です。 応募エントリお待ちしております!

agile.esm.co.jp

*1:会場は Remo というオンラインイベントサービスで会社毎にテーブルが分かていました。便利。