品川准教授のブログ

東京大学で准教授をしています。主に研究分野(OSや仮想化などのシステムソフトウェア)に関連したことを気まぐれに書きます。

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