2010年12月31日

LinuxのカーネルモジュールをC++で書く

C++ Advent Calendar 参加記事です。

LinuxのカーネルモジュールをC++で書く  ~C言語er(アンチC++)との仁義なき戦い(と敗北)~

注:この記事はどようびが日ごろのうっぷんを晴らすべく心赴くまま書きなぐったモノです。理不尽なDISりとか投げやりな実装とか多数含まれますが、笑って許してください...m(__)m ... お願いします....

* はじめに

研究室でLinuxのソースを触ることになったのだが、そこには生粋のVB厨C++厨であるどようびにとっては信じられない世界が広まっていた!

既に絶滅したかに思われていたさまざまなコードがどようびを襲う!

そして、カーネルいじりでもC++使えたらなぁ~。と思いぐぐってみたら次の記事が引っかかった!

Linux: C++ In The Kernel? | KernelTrap

Linus曰く
The fact is, C++ compilers are not trustworthy. They were even worse in 1992, but some fundamental facts haven't changed:

  1. the whole C++ exception handling thing is fundamentally broken. It's _especially_ broken for kernels.
    例外が嫌
  2. any compiler or language that likes to hide things like memory allocations behind your back just isn't a good choice for a kernel.
    メモリ確保勝手するのが嫌
  3. you can write object-oriented code (useful for filesystems etc) in C, _without_ the crap that is C++.
    OOはCでもできる

つまり、例外とnew/deleteを使わなければ良いんじゃね?良いよね!!!!new,deleteも自前で定義すればいいよね!


* 既存のコードに対する不満点など

** 継承

次のLinuxカーネルのコードを見てくれ!
(include/linux/device.h)

struct device {
  struct device *parent;
  struct device_private *p;
  (略)
  void *platform_data; /* Platform specific data, device
  (略)
};

任意のデータをvoid*で受けるのはC++erとしてはあんましやりたくない。というか型エラーの
チェックはできるだけ全てコンパイラにやらせたいのがC++erの真理だと思う。(void*)にキャスト
しちゃうと目でチェックするしかない。
で、この場合platform_dataの子クラスのみ格納できるようにしたい。
実際にデバイスドライバ側で件のデータを使うときは、『platform_data ⇒ その子クラス』ダウンキャストになるが、
polymorphic_downcast という神キャストが存在するのでそれを使うべし!

デバッグ時とリリース時で違う動きするなだと!?、両方で使えるテストを書こう!!!

** 継承2

もう継承でやっちゃえよ!直したいー!!

(include/linux/fs.h)

struct file_operations {
  struct module *owner;
  loff_t (*llseek) (struct file *, loff_t, int);
  ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
  ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
  ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
  ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
  int (*readdir) (struct file *, void *, filldir_t);
  unsigned int (*poll) (struct file *, struct poll_table_struct *);
  (略)
};

↓直したい!!!

class file_operations {
  module *owner;
  virtual loff_t llseek(file *, loff_t, int) {
    ....
  }
  (略)
};


** RAII


次のコードを見てくれ......
(kernel/sched.c)

static void migrate_nr_uninterruptible(struct rq *rq_src)
{
  struct rq *rq_dest = cpu_rq(cpumask_any(cpu_active_mask));
  unsigned long flags;

  local_irq_save(flags);
  double_rq_lock(rq_src, rq_dest);
  rq_dest->nr_uninterruptible += rq_src->nr_uninterruptible;
  rq_src->nr_uninterruptible = 0;
  double_rq_unlock(rq_src, rq_dest);
  local_irq_restore(flags);
}

次の関数が対応し、割り込み無効化/有効化、ロック/アンロックを行う
local_irq_save/local_irq_restore
double_rq_lock/double_rq_unlock
こういうセットで使う関数は、C++erとしては、RAII化せざるを得ない。
コードのメンテ時に、片方の位置を変えたらもう片方の位置も変える、とかめっちゃめんどくさい

** goto文

RAIIの続きになるが、件の対応する関数があると、どうしてもgoto文に頼らざるを得なくなる
(kernel/sched.c)

static int init_rootdomain(struct root_domain *rd, bool bootmem)
{
  gfp_t gfp = GFP_KERNEL;
  memset(rd, 0, sizeof(*rd));
  if (bootmem)
    gfp = GFP_NOWAIT;

  if (!alloc_cpumask_var(&rd->span, gfp))
    goto out;
  if (!alloc_cpumask_var(&rd->online, gfp))
    goto free_span;
  if (!alloc_cpumask_var(&rd->rto_mask, gfp))
    goto free_online;

  if (cpupri_init(&rd->cpupri, bootmem) != 0)
    goto free_rto_mask;
  return 0;

free_rto_mask:
  free_cpumask_var(rd->rto_mask);
free_online:
  free_cpumask_var(rd->online);
free_span:
  free_cpumask_var(rd->span);
out:
  return -ENOMEM;
}

やっぱしC++erとしてはgoto文を使わないで、RAIIやscope_exitで固めた状態で
途中returnだよね!!!goto文に比べて、コードを目で追っかける量が減るよ!

** for each

みんな待ってたforeach、GCC4.6で遂に実装!wktk,カーネル内には以下のような
foreachがあったけど、できれば標準のを使いたいよね!
(mm/page_alloc.c)

static void drain_pages(unsigned int cpu)
{
  unsigned long flags;
  struct zone *zone;

  for_each_populated_zone(zone) {
    struct per_cpu_pageset *pset;
    struct per_cpu_pages *pcp;

    local_irq_save(flags);
    pset = per_cpu_ptr(zone->pageset, cpu);

    pcp = &pset->pcp;
    free_pcppages_bulk(zone, pcp->count, pcp);
    pcp->count = 0;
    local_irq_restore(flags);
  }
}

他にも
for_each_migratetype_order(order, ...)
list_for_each(curr, ...)
for_each_zone_zonelist_nodemask(zone, ...)
とか。

** static_assert

こんなコードがあった

(mm/slab.c)

static __always_inline int index_of(const size_t size)
{
  extern void __bad_size(void);

  if (__builtin_constant_p(size)) {
    int i = 0;
    if (size <= 32) return i;
    i++;
    if (size <= 64) return i;
    i++;
    if (size <= 128) return i;
    i++;
    (略)
    if (size <= 131072) return i;
    i++;

    __bad_size();
  } else {

    __bad_size();

  }

  return 0;
}

1. 最適化オプションをつけてコンパイル

2. __bad_size関数をわざと定義しないでおく

上記の条件を満たし、かつsizeが定数でないか131072より大きい場合は

__bad_sizeがundefined refereceとなるリンク時エラーが起きる。という関数。

当然のことながら、訓練されたC++erとしては、static_assertを使わざるを得ない


* 実装 ~そして敗北へ~

とりあえずは、カーネルモジュールのキャラクタデバイスを作ってみます。
具体的には、
  cat /dev/hellocxx
コマンドを打つと、
  Hello, C++
と表示させるものを。


** 準備1 Makefile

とりあえずカーネルモジュールを作る準備をしなければいけませんね!
hellocxx.c がカーネルモジュール向けにコンパイルされるMakefileになります!

KERNEL_DIR = /root/linux
VERBOSE = 0
ARCH = x86

EXTRA_CFLAGS := -isystem $(KERNEL_DIR)/include -isystem $(KERNEL_DIR)/arch/$(ARCH)/include

obj-m := hellocxx.o
CFLAGS_hellocxx.o := -x c++ -std=gnu++0x -fno-operator-names -fno-rtti -fpermissive -fno-exceptions

all:
make -C $(KERNEL_DIR) M=$(PWD) V=$(VERBOSE) modules


ダウンロードはこちら>> https://gist.github.com/760992

基本事項はぐぐってください!詳しいサイトがいっぱいあります!
重要なオプション解説

*** -fpermissive
Linuxのヘッダ内に、C++では許されないポインタ同士の変換がいっぱいあるため、
そいつがエラーにならないようにするオプション。正直いきなりくじけそうだッ

*** -fno-operator-names
andとかがC++の予約語とかぶるので、それを抑制

*** -isystem
warningがうざすぎるので、linuxのヘッダのものに関しては全部抑制

*** -fno-exception
例外を使おうとするとC++ランタイムが必要になるっぽくてめんどくさそうなので使わないように

*** -fno-rtti
rttiもめんどくさそうなので切る

*** -x c++
このソースはC++だッ

*** -std=gnu++0x
C++0xに、gnuの拡張を入れたやつだと思う


** 準備2 マクロとかで置換

C言語用に書かれたヘッダファイルをC++で読もうとすると、いろいろ不都合があるよね!
1. 予約語が.....
2. 名前の修飾が.....

ということで、とりあえず以下のようになる

extern "C" {

#define alignof alignof_REPLACED
#define bool bool_REPLACED
#define catch catch_REPLACED
.....

#include <linux/kernel.h>

#undef alignof
#undef bool
#undef catch
......

}

これは基本だね!

とくに凶悪だったコードはこちら

(include/linux/kernel.h)

/* Force a compilation error if condition is true, but also produce a
 result (of value 0 and type size_t), so the expression can be used
e.g. in a structure initializer (or where-ever else comma expressions
 aren't permitted). */
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))
#define BUILD_BUG_ON_NULL(e) ((void *)sizeof(struct { int:-!!(e); }))

コードが読めない...コメントにものすごい助けられました。で、こんな感じに適当に定義しなおす

#define BUILD_BUG_ON_ZERO(e) ((size_t)0)
#define BUILD_BUG_ON_NULL(e) ((void*)NULL)

完成したファイルはこちら
>> https://gist.github.com/760993



** 準備3 Linuxソース書き換え

しかし、マクロ置換だけでは対応できない個所が多々あることが解ってしまった。
とくに多かったのがこういうやつ

static inline foo(int ret)
{   return (pte_t) { .pte = ret };
}

中カッコを使って構造体の右辺値を、メンバの値を設定しつつ作る奴。
この書き方はC++でも採用して欲しかったなぁ。

そこでなんだが、どおおおおおしても僕の実力では対応しきれないため、カーネルソースの

static inlineな関数の中身を変えるだけなら、今動いているカーネルを変更しなくてもいいはず!

と踏み、書き換えに走ることにした。だいたいもうすでに#define文でlinuxのヘッダの内容はだいぶかわっちゃってるしね

まぁ、仕方がないと自分を納得させる。

手元のカーネルのバージョンは2.6.32、おおむね以下のような書き換えになりました

-    return (pte_t) { .pte = ret };
+    {
+        pte_t r = {0};
+        r.pte = ret;
+        return r;
+    }

まぁ、修正前と後は処理内容的には同じです。
変更内容全体はこちら>> https://gist.github.com/761000

しかし、カーネルのソースいじくった時点でもう負けているのだあおあいaoidsjjkfks('A`)......


** カーネルモジュール書きます!
さっき書いたヘッダファイル、さすがにひどくてあんまり見たくないので stdafx.h (なにかとトラウマなヘッダファイル名) として分離
そして、カーネルモジュール本体を書き始める。キモは次の関数

static ssize_t hellocxx_read(struct file* f, char* buf, size_t count, loff_t* pos)
{
  if (*pos != 0) {
    return 0;
  }
  char message[] = "Hello, C++\n";
  ssize_t copy_count =
  count < sizeof(message) ? count : sizeof(message);
  copy_to_user(buf, message, copy_count);
  *pos += copy_count;
  return copy_count;
}

ユーザーから読み込みのリクエストがあったら、"Hello, C++"という文字列を返す
全体はこちら>> https://gist.github.com/761004


* 動作確認

ドライバのメジャー番号は適当に204にしました。

おおっ、予想通り今動いてるカーネルも変更なしで動いた!

result.png



* そして.....

platform_device化も行けそうです!ひどいコードばかりだし、カーネルのコード変えちゃったけど、まぁ動いたから良しとしましょう。

ここまで読んでいただき、ありがとうございました!

これらのファイルをまとめたモノはこちら >> http://dl.dropbox.com/u/458517/20101231/hellocxx.zip

 

コメント(3)

へー。foreachをC/C++がサポートするんだ。自分的にはラベルつきbreakが欲しかったな。あとは多重ポインタであっても機能するアロー演算子(T** pに対してp-->xというようなことをしたい)があればなぁと思ってます。

リンク時static assertとか-cで作られたら最後まで分からない・・・最強魔法リンカスクリプトで回避!(orz

そういえばrange based forはまた議論されてるっぽいのでGCCで実装したからと言って油断できない・・・

あけましておめでとうございます

> hoge
ラベル付きbreakは僕もたまに欲しくなります。結局、goto怖いので終了条件の変数を新設して対応しちゃってるけど。あとは、-->があるんだったらもう『-』が一個増えるごとに逆参照一回増やすとかにしたいw

> Flast
せめてBOOST_STATIC_ASSERTの結果みたいに、変数名を__argument_is_bad_size_or_not_constant みたいにしてくれれば、わかりやすかったんすけどねー
>> range based forはまた議論
マジすか.... まだ当分はboost foreachに頼る日々か..

コメントする





トラックバック(0)

トラックバックURL: http://www.lunaport.net/mt/mt-tb.cgi/29

るなぽブログ

最近のブログ記事

最近のコメント

  • どようび: あけましておめでとう (more)
  • Flast: リンク時static (more)
  • hoge: へー。foreach (more)

その他