RubyでOSやGCを書く野望


現在、mmcというmrubyをC(に限らないのですが)に変換するプログラムを書いています。抽象実行とエスケープ解析で、GCのサポートや、selfが常に渡されることなどを除いて、手書きのCに近いコードが出てきます。これを使ってOSが書けないかと思い、色々試しています。
もちろん、普通のRubyでは書けないですけどいろいろ拡張して出来そうな気がしてきました。その拡張の詳細を忘れに用にメモしておきます。

拡張のためのモジュール

OSを書くための拡張はMMC, HALと2つのモジュールに全て閉じ込めてあります。名前空間は汚したくないですから。

MMCモジュール

 mmcのメタ情報やコード生成の制御をおこなうためのメソッドやクラスが入っています。

MMC::attribute

メソッドどの属性を設定する。gccのattributeやRustのメソッド定義の前にある#[...]みたいなやつ。今の所、sectionしかサポートしていない。生成したメソッドを特別なアドレスを割り当てるために使う。

  MMC::attribute(:section, "BOOT")
  def boot
    cpu = HAL::CPU.new(0)

MMC::class_sizeof(クラス) 未実装

そのクラスのインスタンスのサイズを返す。単位はバイト

MMC::instance_sizeof(オブジェクト) 未実装

オブジェクトのサイズを返す。単位はバイト

MMC::offsetof(オブジェクト, インスタンス変数) 未実装

オブジェクト内のインスタンス変数のオフセットを返す。インスタンス変数はシンボル

HALモジュール

 ハードウエアに依存したクラスなどを閉じ込めたモジュール

HAL::CPUクラス

 CPUを抽象化したクラス。インストラクションセットを生成するメソッドや、CPUに紐付けされたメモリやレジスタのオブジェクトを持つ。CPUクラスのインスタンスをえるにはCPU番号が必要。これはマルチCPU(しかもヘトロなシステム)に対応するため。

CPU#regsメソッド

 CPUのレジスタセットを抽象化したRegsクラスを返す。レジスタ1つではなくCPUにあるレジスタ全部であることに注意。

CPU#memメソッド

 CPUのアドレス空間を抽象化したMemクラスを返す。

CPU#jmp, CPU#labelメソッド

 ジャンプ命令やラベルの定義を生成するメソッド

Regsクラス

CPUのレジスタセットを抽象化したクラス。レジスタではなくレジスタ全体にするのは単なる代入ではフックできないが、[]=をオーバーライドするのは色々カスタマイズできるから。

Regs#[](name) メソッド

nameの名前のレジスタに格納された値を返す、のだが返しても大抵の場合意味が無いのでアセンブラのコード生成のために使う。次に説明する[]=()メソッドと一緒に使う

Regs#[]=(name、val) メソッド

nameの名前のレジスタに値を設定する。とても限定された場合しか使えないが、アセンブラを直接生成する。たとえば、

  regs[:eax] = regs[:eax] + regs[:ebx]

と書くと、

  add EAX, EBX

と生成される。(実際はgasのAT&Tフォーマットだけどなじみが多いだろうからIntelフォーマットで説明した)

Memクラス

CPUのメモリ(正確には論理アドレス空間)を抽象化したもの。あたかも配列のようにメモリを見せたり、メモリ空間の設定をしたりする。

Mem#[address, size = nil]

  addressが整数(またはアドレス用のクラスを用意する予定)ならそのアドレスの内容。sizeが指定されたらそのバイト数。デフォルトはintと同じ大きさ。
addressがRegクラス(Regs#[]メソッドの戻り値)ならアセンブラの命令を生成する。

  regs[:eax] = mem[regs[:eax] + 4]

だと、

  mov EAX, 4(EAX)

というコードが生成される。

Mem#[]=(address, size = nil, val)

addressとvalが整数ならaddressに示されるメモリを書きこむ。addressやvalがRegクラスだとアセンブラを生成する。

Mem#static_cast(address, klass)

addressに示されるメモリ領域をklassクラスのインスタンスとして返す。返って来たオブジェクトは普通のオブジェクトのように使えるけど(initializeメソッドは呼び出されない)、GCのことは全く考えていないので注意。

Mem#static_allocate(klass)

klassクラスのインスタンスを静的に確保して(Cレベルではグローバル変数を定義して)、そのオブジェクトを返す。返ってきたオブジェクトは普通のオブジェクトの様に使えるけど (以下同文)。

Mem#static_array_allocate(klass, num)

klassクラスのインスタンスnum個を静的に確保して(Cレベルではグローバル変数を定義して)、配列オブジェクトとして返す。返ってきたオブジェクトは普通のオブジェクトの様に使えるけど (以下同文)
MMCではエスケープしない配列はCレベルの配列と同じフォーマットです。

 プログラム例

例えばK&Rにあるような簡単なメモリアロケータを考えてみましょう。こんな感じになるかなと思います。(まだ動かないです)

  class Header
    def initiaize
       @size = 0
       @next = 0  # NULL pointer
    end
    attr_accessor :size
    attr_accessor :next
  end

class Allocator
  def initialize
    cpu = HAL::CPU.new(0)  # 0はCPU番号
    mem = cpu.mem

    arena = mem.static_array_allocate(Fixnum, 65536) # アロケートのためのメモリ領域
    header = mem::static_cast(arena, Header)
    header.size = 65536
    header.next = 0
    @root = header
  end

  def malloc(klass)  #このmallocはサイズではなくクラスのインスタンスを返す
    cpu = HAL::CPU.new(0)  # 0はCPU番号
    mem = cpu.mem

    size = MMC::sizeof(klass)
    cchunk = @root
    pchunk = nil
    while cchunk and cchunk.size < size then
      pchunk = cchunk
      cchunk = cchunk.next
    end

    result = nil
    if cchunk then
      result = mem::static_cast(cchunk, klass)   #チャンクの先頭を切りだす
      nchunk = mem::statioc_cast(cchunk + size, Header) #新しいヘッダを作る。前のチャンクは
    nchunk.size = cchunk.size - size #ヘッダのサイズを切りだし
      nchunk.next = cchunk.next     # 次のチャンクは変らない
      if pchunk then   #番兵を用意した方がいいかも
        pchunk.next = nchunk
      else
        @root = nchunk
      end
    end
    result
  end
end