mruby を Linux カーネル内で動作させる
mruby を Loadable Kernel Module (LKM) として Linux カーネル内動作させて printk する方法をメモしておきます。
1年以上前に書きかけていたものを完成させたので、mrubyのバージョンが古いです。最近のバージョンだとどうなるのかは分かりません。
mruby のビルド
まず GitHub から mruby のソースコードをダウンロードします。今回試したのは f65a39f4d19b1de019b0b805ccfb081757e5b7b5 (Wed Sep 18 20:00:35 2013 -0700) です。
$ git clone https://github.com/mruby/mruby.git $ cd mruby $ git checkout f65a39f4d19b1de019b0b805ccfb081757e5b7b5
次にカーネル内で動作するバージョンをクロスビルドするための設定を追加します。これにより、ホスト上で動く通常の mruby とは別に、build/kernel 以下にカーネル内動作用の mruby がビルドされます。
$ cat >> build_config.rb MRuby::CrossBuild.new('kernel') do |conf| toolchain :gcc conf.cc.flags << "-Iinclude/kernel -mcmodel=kernel -mno-red-zone -mfpmath=387 -mno-sse -mno-sse2 -mno-mmx -mno-3dnow -msoft-float -fno-asynchronous-unwind-tables -fno-omit-frame-pointer" conf.cc.defines << %w(DISABLE_STDIO) conf.cc.defines << %w(DISABLE_FLOAT) conf.cc.defines << %w(MRB_INT64) end (ctrl-d)
次にカーネル内で動作させるためのヘッダファイルを作成します。
stdlib.h は必要最小限のものに置き換えます。これにより浮動小数点関係のエラーが減ります。
$ mkdir include/kernel $ cat > include/kernel/stdlib.h typedef unsigned long size_t; void free(void *ptr); void *realloc(void *ptr, size_t size); int abs(int j); unsigned long int strtol(const char *nptr, char **endptr, int base); unsigned long int strtoul(const char *nptr, char **endptr, int base); void exit(int status); #define EXIT_SUCCESS 0 #define EXIT_FAILURE (-1) void abort(void); int atoi(const char *nptr); # define strtod(p,e) strtol(p,e,10) (ctrl-d)
stdarg.h は vsnprintf 関係のものだけを gcc の builtin 関数で記述します。
$ cat > include/kernel/stdarg.h typedef unsigned long size_t; typedef __builtin_va_list __gnuc_va_list; typedef __gnuc_va_list va_list; #define va_start(v,l) __builtin_va_start(v,l) #define va_end(v) __builtin_va_end(v) #define va_arg(v,l) __builtin_va_arg(v,l) int vsnprintf(char *str, size_t size, const char *format, va_list ap); (ctrl-d)
浮動小数点は使わないので、全部ごまかします。
$ cat > include/kernel/math.h # define fmod(x,y) (x) # define pow(x,y) (x) # define log10(x) (x) # define floor(x) (x) # define ceil(x) (x) # define isinf(x) 0 # define isnan(x) 0 (ctrl-d)
内部のfloat型(mrb_float)をlongで置き換えます。
$ cat > value.h.patch --- include/mruby/value.h.orig 2013-09-19 13:24:11.378647350 +0900 +++ include/mruby/value.h 2013-09-19 16:15:33.647687793 +0900 @@ -7,7 +7,13 @@ #ifndef MRUBY_VALUE_H #define MRUBY_VALUE_H -#ifdef MRB_USE_FLOAT +#if defined(DISABLE_FLOAT) + typedef long mrb_float; +# define double long +int sprintf(char *str, const char *format, ...); +# define mrb_float_to_str(buf, i) sprintf(buf, "%d", i) +# define str_to_mrb_float(buf) strtol(buf, NULL, 10) +#elif defined(MRB_USE_FLOAT) typedef float mrb_float; # define mrb_float_to_str(buf, i) sprintf(buf, "%.7e", i) # define str_to_mrb_float(buf) strtof(buf, NULL) (ctrl-d) $ patch -p0 < value.h.patch
浮動小数点演算をしているところを片っ端から潰します。計算結果のことはここでは考えません。
$ cat > numeric.c.patch --- src/numeric.c.orig 2013-09-19 13:24:11.389647270 +0900 +++ src/numeric.c 2013-09-19 16:34:56.098316936 +0900 @@ -209,7 +209,7 @@ if (m < 0) { m -= 1; } - n = n / pow(10.0, m); + n = n / pow((mrb_float)10.0, m); m = 0; } else { @@ -222,15 +222,15 @@ /* puts digits */ while (max_digit >= 0) { - mrb_float weight = pow(10.0, m); - digit = (int)floor(n / weight + FLT_EPSILON); + mrb_float weight = pow((mrb_float)10.0, m); + digit = (int)floor(n / weight + (mrb_float)FLT_EPSILON); *(c++) = '0' + digit; n -= (digit * weight); max_digit--; if (m-- == 0) { *(c++) = '.'; } - else if (m < -1 && n < FLT_EPSILON) { + else if (m < -1 && n < (mrb_float)FLT_EPSILON) { break; } } @@ -324,7 +324,7 @@ mrb_float div; mrb_float mod; - if (y == 0.0) { + if (y == (mrb_float)0.0) { div = str_to_mrb_float("inf"); mod = str_to_mrb_float("nan"); } @@ -336,7 +336,7 @@ div = (x - mod) / y; if (y*mod < 0) { mod += y; - div -= 1.0; + div -= (mrb_float)1.0; } } @@ -457,7 +457,7 @@ d = (mrb_float)mrb_fixnum(num); /* normalize -0.0 to 0.0 */ - if (d == 0) d = 0.0; + if (d == 0) d = (mrb_float)0.0; c = (char*)&d; for (hash=0, i=0; i<sizeof(mrb_float);i++) { hash = (hash * 971) ^ (unsigned char)c[i]; @@ -615,10 +615,10 @@ mrb_get_args(mrb, "|i", &ndigits); number = mrb_float(num); - f = 1.0; + f = (mrb_float)1.0; i = abs(ndigits); while (--i >= 0) - f = f*10.0; + f = f*(mrb_float)10.0; if (isinf(f)) { if (ndigits < 0) number = 0; @@ -630,13 +630,13 @@ else number *= f; /* home-made inline implementation of round(3) */ - if (number > 0.0) { + if (number > (mrb_float)0.0) { d = floor(number); - number = d + (number - d >= 0.5); + number = d + (number - d >= (mrb_float)0.5); } - else if (number < 0.0) { + else if (number < (mrb_float)0.0) { d = ceil(number); - number = d - (d - number >= 0.5); + number = d - (d - number >= (mrb_float)0.5); } if (ndigits < 0) number *= f; @@ -662,8 +662,8 @@ { mrb_float f = mrb_float(num); - if (f > 0.0) f = floor(f); - if (f < 0.0) f = ceil(f); + if (f > (mrb_float)0.0) f = floor(f); + if (f < (mrb_float)0.0) f = ceil(f); if (!FIXABLE(f)) { return mrb_float_value(mrb, f); (ctrl-d) $ patch -p0 < numeric.c.patch
make して build/kernel/lib/libmruby.aがビルドできればOKです。その他のコンパイルエラーは放置します。
$ make ... AR build/kernel/lib/libmruby.a ar: /home/shina/mruby-kernel/mruby/build/kernel/lib/libmruby.a を作成しています ...
ホスト環境での mruby での Hello World
まずはホスト環境(Linux)で mruby の動作を確認します。host というディレクトリを mruby の隣に作って試します。
$ cd .. $ mkdir host $ cd host
下記の main.c は mruby の実行環境を呼び出すプログラムです。今回は "Kernel" というモジュールと "printk" というクラスメソッドを用意します。実体は printf しているだけです。
$ cat > main.c #include "mruby.h" #include "mruby/proc.h" #include "mruby/string.h" extern uint8_t code[]; static mrb_value kernel_printk(mrb_state *mrb, mrb_value self) { mrb_value retval; mrb_value str; mrb_get_args(mrb, "S", &str); printf("%s", RSTRING_PTR(str)); retval.value.i = 0; return retval; } int main(int argc, char **argv) { mrb_state *mrb; struct RClass *kernel; mrb_value ret; mrb = mrb_open(); kernel = mrb_define_module(mrb, "Kernel"); mrb_define_class_method(mrb, kernel, "printk", kernel_printk, ARGS_REQ(1)); ret = mrb_load_irep(mrb, code); return ret.value.i; } (ctrl-d)
次に実行する ruby のプログラム hello.rb を用意します。Kernel クラスの printk メソッドを呼び出すだけです。
$ cat > hello.rb Kernel.printk "Hello World!\n" (ctrl-d)
ビルドするための Makefile です。先ほどビルドした mruby のホスト環境を使います。mrbc は ruby のコードを mruby のバイトコード(のC言語の配列表現)に変換するプログラムです。
$ cat > Makefile CFLAGS = -I../mruby/include -g LDFLAGS = -lm OBJS = main.o hello.o LIBS = ../mruby/build/kernel/lib/libmruby.a main: $(OBJS) $(LIBS) $(CC) $(OBJS) $(LIBS) $(LDFLAGS) -o $@ hello.c: hello.rb ../mruby/bin/mrbc -Bcode $< (ctrl-d)
make して実行して動作を確認します。
$ make cc -I../mruby/include -g -c -o main.o main.c ../mruby/bin/mrbc -Bcode hello.rb cc -I../mruby/include -g -c -o hello.o hello.c cc main.o hello.o ../mruby/build/kernel/lib/libmruby.a -lm -o main $ ./main Hello World!
Linux カーネル内での mruby での Hello World
次に Linux で動作するカーネルモジュールを作成します。kernel というディレクトリを mruby の隣に作って試します。
$ cd .. $ mkdir kernel $ cd kernel
まずはカーネルモジュールの初期化・終了をおこなうコードです。
$ cat > lkm.c #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/ctype.h> extern int mruby_main(void); static int lkm_init(void) { printk(KERN_INFO "LKM: init\n"); return mruby_main(); } static void lkm_exit(void) { printk(KERN_INFO "LKM: exit\n"); } module_init(lkm_init); module_exit(lkm_exit); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("mruby"); MODULE_AUTHOR("Takahiro Shinagawa"); (ctrl-d)
次に mruby を呼び出すコードです。先ほどのホストで動作するものとほとんど同じです。
$ cat > main.c #include <linux/kernel.h> #include "mruby.h" #include "mruby/irep.h" #include "mruby/string.h" extern uint8_t code[]; static mrb_value kernel_printk(mrb_state *mrb, mrb_value self) { mrb_value retval; mrb_value str; mrb_get_args(mrb, "S", &str); printk(KERN_INFO "mruby: %s\n", RSTRING_PTR(str)); retval.value.i = 0; return retval; } int mruby_main(void) { mrb_state *mrb; struct RClass *kernel; mrb_value ret; mrb = mrb_open(); kernel = mrb_define_module(mrb, "Kernel"); mrb_define_class_method(mrb, kernel, "printk", kernel_printk, ARGS_REQ(1)); ret = mrb_load_irep(mrb, code); printk("mruby: ret = %d\n", ret.value.i); return 0; } (ctrl-d)
次に libc をエミュレーションするライブラリです。必要最低限しかエミュレーションしていないので、ちゃんと定義されていない関数が呼び出されたら正常に動作しないでしょう。
$ cat > libc.c #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/slab.h> typedef unsigned long size_t; void *realloc(void *ptr, size_t size); void free(void *ptr); typedef int jmp_buf[6]; int setjmp(jmp_buf env); void longjmp(jmp_buf env, int val); int * __errno_location(void); const unsigned short * * __ctype_b_loc (void); long int strtol(const char *nptr, char **endptr, int base); unsigned long int strtoul(const char *nptr, char **endptr, int base); void abort(void); void exit(int status); void *realloc(void *ptr, size_t size) { return krealloc(ptr, size, GFP_KERNEL); } void free(void *ptr) { kfree(ptr); } int _setjmp(jmp_buf env) { return __builtin_setjmp(env); } void longjmp(jmp_buf env, int val) { __builtin_longjmp(env, 1); } int * __errno_location(void) { static int errno; return &errno; } const unsigned short * * __ctype_b_loc (void) { printk("%s\n", __FUNCTION__); return NULL; } const unsigned short * * __ctype_tolower_loc (void) { printk("%s\n", __FUNCTION__); return NULL; } const unsigned short * * __ctype_toupper_loc (void) { printk("%s\n", __FUNCTION__); return NULL; } int isspace(int c) { return (c == 0x20) | (0x09 <= c && c <= 0x0d); } int isdigit(int c) { return ('0' <= c && c <= '9'); } int isupper(int c) { return ('A' <= c && c <= 'Z'); } int islower(int c) { return ('a' <= c && c <= 'z'); } int isalpha(int c) { return isupper(c) || islower(c); } long int strtol(const char *nptr, char **endptr, int base) { printk("%s: %s\n", __FUNCTION__, nptr); return 0; } unsigned long int strtoul(const char *nptr, char **endptr, int base) { printk("%s: %s\n", __FUNCTION__, nptr); return 0; } void abort() { printk("%s\n", __FUNCTION__); } void exit(int status) { printk("%s\n", __FUNCTION__); } (ctrl-d)
最小限のヘッダファイルを作成します。
$ cat > stdint.h typedef unsigned char uint8_t; (ctrl-d) $ echo > inttypes.h
最後に Makefile です。上記のコードに加えて、先ほど hello.rb をバイトコードに変換したものと、mruby のライブラリをリンクします。
$ cat > Makefile ccflags-y += -DDISABLE_STDIO -I$(PWD)/../mruby/include -I$(PWD) obj-m := mruby.o mruby-objs := lkm.o main.o libc.o ../host/hello.o ../mruby/build/kernel/lib/libmruby.a all: 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 (ctrl-d)
make すると少し警告が出ますが、mruby.ko が生成されるはずです。
$ make ... $ ls mruby.ko mruby.ko
出来たカーネルモジュールをロードすると、カーネルのログに Hello World! が出力されるはずです。
$ sudo insmod mruby.ko $ dmesg | grep mruby ... [ 75.744877] mruby: Hello World! [ 75.744879] mruby: ret = 0
※GitHub にコードを置きました.https://github.com/utshina/mruby-lkm