mrubyでブートローダを書けるようにしました

カーネルVM@関西7回目でお話したネタです。スライドはこちら:

既にmrubyはUEFIへ移植されています(覚え書き: mruby on EFI Shell)。

また、EFI StubカーネルUEFIアプリから実行するのは非常に簡単であることが分かっています(実際にソースコードを書いて実験しました)。

以上のことから、「mrubyにEFI Stubカーネルのファイル名を指定したら実行するAPIを足せば、mrubyでブートローダが実装可能になるに違いない」と考えました。

ただ面白いというだけでなく、mruby-socket・mruby-simplehttp-socketなどのmrbgemsを移植していけばブート処理を記述したmrubyスクリプトカーネルそのものをHTTPから取得することが出来るようになり、簡単なスクリプトで高度にカスタマイズされたブート処理をOSに依存せず行う事が出来るようになると思い、まずはたたき台として単純なブートメニューを実現するものを実装してみました。

以下に今回実装したブートローダのmrubyスクリプトを貼ります(Dirクラスのコードは一部省略しました。フルサイズのコードはこちら)。

base_dir = "kernels"
kernel_options = "root=/dev/sda3"
kernels = []

Dir.foreach(base_dir) do |ent|
	next if ent == "." || ent == ".."
	kernels << ent
end

puts "** Boot Menu **"
(0...kernels.length).each do |i|
	puts "[#{i}] #{kernels[i]}"
end
loop do
		puts "Input number:"
		key = Shell.gets.chomp
		idx = key.to_i
		if kernels.length <= idx
			next
		end
		puts "#{base_dir}\\#{kernels[idx]} #{kernel_options}"
		Shell.exec "#{base_dir}\\#{kernels[idx]} #{kernel_options}"
		break
end

ここでは、ブートローダを実現するために「Dir」と「Shell」クラスを実装しています。
(mruby_on_efi_shellでは殆どのモジュールが無効化されている上にmrbgemsのビルドが封じられており、更にLinuxと完全に同等のAPIを提供してはいないので、ビルドスクリプトを編集したりAPIを叩いて1クラスごとに定義していく必要がありました。)

Dirクラスはmruby-dirから最低限必要なコードを抜き出してきたものです。
mruby_on_efi_shell/dir.c at devel · syuu1228/mruby_on_efi_shell · GitHub
普通にreaddir()などが使えているのはEDK2のlibcを使用しているからです。今回の作業では基本的にlibcのAPIを使用しています。
(このlibcは実験版扱いで、UEFIアプリケーションの正式なAPIではなく実験的な実装です。アプリをUEFIへポーティングする際には便利で、mruby_on_efi_shellもこれを利用して移植されています。よって、今回はこれに頼ることにしました。)

Shellクラスは今回独自に実装したもので、libc関数であるsystem(3)・fgets(3)の単純なラッパ関数(それぞれShell.exec, Shell.gets)を提供します。
以下にコードを示します。

#include "mruby.h"
#include "mruby/class.h"
#include "mruby/data.h"
#include "mruby/string.h"
#include "error.h"
#include <stdlib.h>
#include <stdio.h>
#include "uefi.h"

mrb_value
mrb_shell_exec(mrb_state *mrb, mrb_value klass)
{
  mrb_value command;
  char *ccommand;
  int ret;

  mrb_get_args(mrb, "S", &command);
  ccommand = mrb_string_value_cstr(mrb, &command);
  ret = system(ccommand);
  return mrb_fixnum_value(ret);
}

mrb_value
mrb_shell_gets(mrb_state *mrb, mrb_value klass)
{
  char buf[65535];
  if (fgets(buf, 65535, stdin) == NULL)
	return mrb_nil_value();
  return mrb_str_new_cstr(mrb, buf);
}

void
mrb_init_shell(mrb_state *mrb)
{
  struct RClass *shell;

  shell = mrb_define_class(mrb, "Shell", mrb->object_class);
  MRB_SET_INSTANCE_TT(shell, MRB_TT_DATA);
  mrb_define_class_method(mrb, shell, "exec", mrb_shell_exec, ARGS_REQ(1));
  mrb_define_class_method(mrb, shell, "gets", mrb_shell_gets, ARGS_NONE());
}

本当はUEFIネイティブなAPIを使うともう少し複雑なAPIコールが必要になるのですが、ここではlibc関数でその部分をラップすることで関数コール1つだけの単純なコードになっています。

上述のmrubyスクリプトの通り、fgets(stdin)でキーボード入力を取って起動するカーネルのファイル名を決め、system()にカーネルファイル名と引数を渡して実行することでカーネルが起動しています。

というわけで、実に簡単なハックでブートローダがmrubyで書けるようになりました。
これだけではAPIが足りませんし、コードが極めてとっちらかっているので、もう少し整理していきたいと考えています。

現状のコードはこちらで公開しています:
GitHub - syuu1228/mruby_on_efi_shell at devel

※このコードではEFI StubカーネルLinux)や各種UEFIブートローダ(拡張子.EFIのPEバイナリ)のみがブート対象です。それ以外のものは例えUEFI対応カーネルでもブートできません。 多くのディストリビューションEFI Stubカーネルを提供しているのでこれで必要充分だろうと判断しました。