mruby を Linux カーネル内で動作させる(2017 ver)
このエントリは KLab Advent Calendar 2017(兼 mruby Advent Calendar 2017)の 12 日目の記事です。
今年は(前半は)Keepalived にフルタイムでコントリビュートしていたり(後半は)ひたすら mruby をいじっていたりと、実に OSS 充な一年だった @pandax381 です。
タイトルにある試みについては、2015 年の時点で東京大学の品川先生が「mruby を Linux カーネル内で動作させる」という素晴らしい記事を書かれていて、Kernel module に組み込んだ mruby が動作することを実証されています。
mruby をカーネル内で動作させるために大きく問題となるのが、浮動小数点演算です。mruby は Float を標準的なクラスとして提供していると共に VM 内部でも浮動小数点演算を行っている一方で、Kernel 内では原則として浮動小数点演算を行ってはならないという制約があります。そのため、先人の試みでは、全ての float を long に置き換えたり浮動小数点演算にまつわる関数をダミーに置き換えることで辻褄を合わせながらコンパイルを通す手法を取っています。そのため、mruby のコードで浮動小数点演算を行った場合、どのような挙動をするかは一切保証されないものとなっています。
それから数年が経過し、2017 年現在の mruby には MRB_WIHTOUT_FLOAT という「ビルド時に Float クラスをはじめとする全ての浮動小数点数を取り除く」ためのオプションが追加されています、というか僕が追加しました。
このエントリでは、MRB_WIHTOUT_FLOAT オプションを使って最新の mruby を Kernel module に組み込んでみようという内容です。
下準備
まず、mruby と Kernel module をビルドするためのツール群をインストールします。
$ sudo apt install build-essential git-core ruby bison
$ sudo apt install linux-headers-$(uname -r)
kmruby というディレクトリを作成し、以降はこの中で作業します。
$ mkdir ~/kmruby
mruby のクロスビルド
GitHub から mruby のコードを持ってきます。
最新のコミットで試してみたところ SEGV を吐いてしまったため、MRB_WITHOUT_FLOAT がマージされた直後のコミット(7ae20e0785704bce1a5bf7183fc9202336ad8676)をチェックアウトします(あまり時間が取れずエントリの公開までに間に合わなかったので時間があるときに原因を探ります)。
$ git clone https://github.com/mruby/mruby.git ~/kmruby/mruby
$ cd ~/kmruby/mruby
$ git checkout 7ae20e0785704bce1a5bf7183fc9202336ad8676
Kernel 内で動作させるための mruby(libmruby.a)をクロスビルド用するためのコンフィグファイルを作成します。
$ vi ~/kmruby/build_config.rb
MRuby::Build.new do |conf|
toolchain :gcc
enable_debug
conf.gembox 'default'
end
MRuby::CrossBuild.new('kernel') do |conf|
toolchain :gcc
conf.cc.include_paths << "../stub/include"
conf.cc.flags << "-mcmodel=kernel -fno-PIE -mno-red-zone -fno-asynchronous-unwind-tables -fno-omit-frame-pointer"
conf.cc.defines << %w(DISABLE_STDIO)
conf.cc.defines << %w(MRB_WITHOUT_FLOAT)
conf.cc.defines << %w(MRB_64BIT)
conf.bins = []
end
次に、クロスビルド用にいくつかのヘッダファイルを作成します。
$ mkdir -p ~/kmruby/stub/include
setjmp / longjmp を GCC のビルトイン関数を利用するために、簡素な setjmp.h を作成します。
$ vi ~/kmruby/stub/include/setjmp.h
#ifndef SETJMP_H
#define SETJMP_H
typedef int jmp_buf[6];
#define setjmp(env) __builtin_setjmp(env)
#define longjmp(env, val) __builtin_longjmp(env, val)
#endif
標準の stdlib.h には float を返す関数などが含まれているため、代わりに最低限必要な関数のみを定義したものを用意してあげます。
$ vi ~/kmruby/stub/include/stdlib.h
#ifndef STDLIB_H
#define STDLIB_H
#include <stddef.h>
#define EXIT_FAILURE (-1)
void free(void *ptr);
void *realloc(void *ptr, size_t size);
void exit(int status);
void abort(void);
#endif
現状は上記の二つのヘッダファイルを用意してあげればクロスビルドできるようになります。
build_config.rb は標準のものではなくはじめに作成したものを指定して make します。
$ cd ~/kmruby/mruby
$ MRUBY_CONFIG=../build_config.rb make
Kernel module の作成
mruby のクロスビルドができたら、それを組み込むための Kernel module を作成します。
以下が Kernel module 本体のコードです。サンプルとして、Linux クラスを定義して printk というメソッドを実装していますが、これは後に作成する mruby のスクリプトから呼び出します。
$ vi ~/kmruby/main.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/ctype.h>
#include <mruby.h>
#include <mruby/irep.h>
#include <mruby/string.h>
#include "mrbcode.h"
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("kmruby");
MODULE_AUTHOR("YAMAMOTO Masaya");
static mrb_value
linux_printk (mrb_state *mrb, mrb_value self) {
mrb_value str;
int ret;
mrb_value retval;
mrb_get_args(mrb, "S", &str);
ret = printk(KERN_INFO "kmruby: %s\n", RSTRING_PTR(str));
retval.value.i = ret;
return retval;
}
static int
kmruby_init (void) {
mrb_state *mrb;
struct RClass *Linux;
mrb_value ret;
pr_info("kmruby: init\n");
mrb = mrb_open();
Linux = mrb_define_class(mrb, "Linux", mrb->object_class);
mrb_define_class_method(mrb, Linux, "printk", linux_printk, MRB_ARGS_REQ(1));
ret = mrb_load_irep(mrb, code);
pr_info("kmruby: ret = %d\n", (int)ret.value.i);
mrb_close(mrb);
return 0;
}
static void
kmruby_exit (void) {
pr_info("kmruby: exit\n");
}
module_init(kmruby_init);
module_exit(kmruby_exit);
動作確認用にシンプルな mruby のスクリプトを作成します。このスクリプトは、後々 mrbc でコンパイルしてバイトコード(main.c が include している mrbcode.h)にします。
$ vi ~/kmruby/example.rb
Linux.printk "Hello World!"
このままだと、本来は libc 内に含まれている関数の実態が存在しないため、それっぽい何かを雑に実装してあげる必要があります。
$ mkdir ~/kmruby/stub/libc
$ vi ~/kmruby/stub/libc/libc.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
void *
realloc(void *ptr, size_t size) {
return krealloc(ptr, size, GFP_KERNEL);
}
void
free (void *ptr) {
kfree(ptr);
}
int *
__errno_location (void) {
static int errno;
return &errno;
}
void
abort (void) {
panic("libc stub: abort()");
}
void
exit (int status) {
panic("libc stub: exit(%d)", status);
}
また、ビルド時にいくつかのヘッダファイルが参照できないとエラーが出るため、ダミーのファイルを用意します。
$ mkdir ~/kmruby/stub/include/dummy
$ touch ~/kmruby/stub/include/dummy/{inttypes,limits,stdint}.h
最後に Makefile を作成します。
$ vi ~/kmruby/Makefile
ccflags-y += -DMRB_64BIT -DDISABLE_STDIO -DMRB_WITHOUT_FLOAT -I$(PWD)/mruby/include -I$(PWD)/stub/include/dummy
obj-m := kmruby.o
kmruby-objs := main.o stub/libc/libc.o mruby/build/kernel/lib/libmruby.a
kmruby: libmruby.a mrbcode.h
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) CFLAGS_MODULE=$(CFLAGS) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
mrbcode.h: example.rb
$(PWD)/mruby/build/host/bin/mrbc -B code -o $@ $^
libmruby.a:
cd mruby; MRUBY_CONFIG=../build_config.rb make
これでビルドすれば、kmruby.ko が生成されます。
$ cd ~/kmruby
$ make
動作確認
生成された kmruby.ko を insmod でロードしてみましょう。成功すればカーネルログに出力があるはずです。言うまでもありませんが、カーネルモジュールなので何かミスっていると最悪の場合カーネルごとお亡くなりになるので実行の際は十分注意してください。
$ sudo insmod ./kmruby.ko
$ sudo dmesg
[21323.454671] kmruby: init
[21323.455421] kmruby: Hello World!
[21323.455423] kmruby: ret = 20
おわりに
まだいくつかフォローしてあげる必要はあるものの、MRB_WITHOUT_FLOAT のおかげで 2017 年版はだいぶシンプルになったのではないでしょうか。まだ、最低限のコードが動くだけですが、本格的に mruby でカーネルモジュールの開発ができるようになると面白そうですね。