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カーネルを提供しているのでこれで必要充分だろうと判断しました。