カーネルおさんぽまっぷ超入門

Kernel/VM Advent Calender 2013 7日目

カーネル/VMはておくれなすごい人がけっこう集って、ネタとか披露しているようです.
今年のAdvent Calenderも既に濃い内容が集っています.

でも、私達パンピーにはカーネルとかちょっと難しすぎてそもそも
カーネル/VMという主題自体敬遠しちゃう……

ということで本当にカーネルの第一歩をやってみよう、というのがこの記事です.
記事タイトルはえりっく氏(@siritori)から無断で肖ってます.ありがとうございます


今年の他の記事が高度な内容なので
この記事が低レベル(低レイヤということでなく本当に程度が低いという意味で)ですいません.
普段からカーネル/VMに参加してる皆様、さようなら.
カーネル/VM界隈わいわいしてておもしろそうだけど怖くて敬遠してる方々、こんにちは.

前置きはこのぐらいで本文行きます. 少々長め.


1. イントロ

まず、いきなりカーネルのソースを読むのは無理です.どこから読んでいいやら、
よしんば読み始めてもどう読んで良いやらでさっぱりです.完全に未知の領域なわけですから.

最初は自分の興味のある場所から読んでいくのが良いかもしれません.

今回のネタはこれで行きましょう.ついさっきTLに流れてきました.
 実装が気になる? なら見てみればいいじゃない提督!

2. はじめかた

ということで、今回はsuコマンドがどうやってコマンドを発行したユーザーを判別しているか、
追ってみます

まずsuコマンドのソースについて.
ArchLinuxで、
$pacman -Qo /usr/bin/su
とすると、util-linuxパッケージに入ってることがわかります.
util-linuxで検索すると、ここからソースのtarballが入手できることが判明.
あ、ArchならabsのPKGBUILDに対してmakepkg -o、Gentooならportageのebuildに対してebuild unpackでソースが簡単に読める状態になります、良いですね!

こんな感じで解凍したファイル達が並びます.今回見たいのはログイン関連のコマンド(su)なので、login-utilsを見てみましょう.


su.cがありました.  まさにこれです.
中身は、こう
ひどい、これじゃなにもわからない

しかし、su.cの近くにsu-common.cというのがありましたね.これを読みましょう
https://gist.github.com/orumin/7837125
わー、長いナー. さすがにブログに埋め込むのも躊躇います.
とはいっても1000行ぐらいなので、ふつうといえばふつうです.
とりあえず、su.cにあったエントリポイントになっているsu_main(742行目)から読みましょう.

3. ユーザー空間からカーネル空間へ
けっこうこの時点で記事長くなってしまいましたがこれから本番なのでしばしのお付き合いを

su_main、850行目まではsuに渡されたオプションの処理とか前準備とか変数宣言やらで
興味深い場所はあまりないので、飛ばします.

ここで851行目に次の文が表れました
restricted = evaluate_uid ();
uidを評価する関数のようですね、おもしろそう.
uidとはUNIXのユーザーを識別する一意な値で、つまりuser idです
これは732行目に関数の実体が存在してて、
このような非常に短い関数です. ユーザーがrootかどうか判別するようですね.
おっ、 目的である、ユーザーの判別が出てきましたよ.
しかもgetuid();だなんてそのままな関数まで出てきました!

しかしこのgetuid()、utils-linuxのどのソースコードにも関数の実体はあらわれません.
manページでgetuidを引いてみると、getuid(2)はシステムコール、つまり、
OSに処理を依頼する特別な関数(OSのAPI)であることが判明します.

そう、ここがユーザー空間からカーネル空間への入口なのです!

4. うさぎの穴から不思議の国へご招待

OSにシステムコールを発行する時は、まずユーザー関数としてglibcのような
Cの標準ライブラリに実装されてるシステムコール関数を呼んでいます.
たとえば、glibc-2.18なら、sysdeps/unix/sysv/linux/i386/getuid.cを見ると、
INTERNAL_SYSCALLマクロでシステムコールが発行されていて、
これはsysdeps/unix/sysv/linux/i386/sysdep.SにアセンブリでINTERNAL_SYSCALLの実体が
書いてあります.
このアセンブリ命令がプロセッサに割り込み命令を出して割り込みベクタとか割り込みハンドラがどーたらこーたらって話は割愛します.

もし、興味があるのならば上記のglibcなどを読むのも良いでしょうが、
私達が知りたいのはgetuid(2)がLinuxでどう実装されてるかであって、どう呼ばれるかではありません.そこらへんは、OSの教科書を読むほうがわかりやすいでしょう.

さて、Linuxカーネルのソースコードは/usr/src/linux/にあったりします.
もしなければ、https://www.kernel.org/からダウンロードしましょう
今回私が読むのはLinux-3.12.2です.

LinuxやBSDといった現代のUNIXライクOSのソースコードはきちんとディレクトリにソースが分かれていて、目的のものを見つけやすいです.
これはLinuxカーネルのソースディレクトリですが、だいたいディレクトリ名で何が入ってるか
想像が付くとおもいます.
わかりにくいのは省略された名称であるmmやipc、archとかですが、mmはメモリ管理 archはCPUの命令の違いとかを吸収するためのアーキテクチャ依存のコード、ipcはプロセス間通信といった具合です.

重要なのは、ユーザー空間の普通のアプリの常識はカーネル空間ではそのまま通用するわけじゃありません
2つの空間はメモリアドレスで分離されてたりするため、別の空間のアドレスをそのままCで読みにはいけないです.
また、カーネル空間に標準Cライブラリがあるわけではありません
(だってカーネル空間の機能をAPI(システムコール)で呼びだして、glibcとかを実装していますから)


では今回はシステムコールを読みたいので、kernelディレクトリに移動しましょう.


さすがに、ここらへんになるとソースが膨大で、目的のソースを勘や総当たりで読むだけじゃ
目的の関数に到達できません
そこで、Linuxの便利なコマンドを紹介します
find ./ -name \*.c -or -name \*.h -exec grep 目的の関数名 {} /dev/null \;
これで目的の関数(今回はgetuid)を探せますね.
最近は
grep -R '目的の関数名' *.c
 でも再帰的に探してくれそうな気がしますし、-execより-printでパイプを使ってxargsのほうが速いかもしれません.


また、VimやEmacsのユーザーならばctags, etags, gtags等々のソフトを使ってクロスリファレンスをまず作成すると簡単に目的の関数にアクセスできます


とにかくこれで探してみると
kernel/sys.c:831の中にこういう関数がみつかります.
このSYSCALL_DEFINE0というマクロを、またまたgrepで探すと
include/linux/syscall.h:170にそのマクロを発見できます.
引数で渡した文字列(この場合getuid)をつかってsys_渡した文字列(今回はsys_getuid)という名前をリンカに渡すようにするマクロのようで、実装とはあんまり関係ありません.

関数のなかでuidを取得する実装に関係ありそうなのは、current_uid()な気がしますね


grepによるとcurrent_uid()はinclude/linux/cred.h:331に定義してあり、
#define current_uid()        (current_cred_xxx(uid))
 また、current_cred_xxx(uid)は同ファイル:326にて
#define current_cred_xxx(xxx)            \
({                        \
    current_cred()->xxx;            \
})
と定義されています.current_cred_xxx(uid)はcurrent_cred()の返り値のメンバuidを参照してるわけです.
同ファイル:252によると
ということらしいので、currentというのは現在のタスクを示しているようです.

ここででてくるRCUというのは、Read Copy Updateの略で、
読み込み、コピー、更新の動作を排他的に行うAPIらしいです リード・コピー・アップデート[Wikipedia ja]
ここで使われてるマクロ関数はinclude/linux/rcupdata.hに定義がありますが、
つまるところ引数で渡されたcurrent->credというポインタ値が指す先の値を取得してるようです.

そして、取得した構造体のメンバにあるuidを
curent_cred_xxx(uid) ( == curent_uid())で取得することで、
現在動作してるタスクのuidが判明し、
それが、getuidシステムコールの中で使われていることが判明しました.

ここでcred構造体は、include/linux/cred.hに戻って79行目をみると
と、たしかにuidというメンバを持っています.
currentの定義については、arch/x86/include/asm/current.hやarch/x86/include/asm/percpu.hに
あって、currentはカレントタスクを取得するget_current()の別名であることがわかりますが、
ここにはx86のアセンブリがマクロを多用しつつ書かれており、主題から逸れるので
興味のある人は読んでみてください


さて、もうあとはラストスパートです

一度kernel/sys.cの831行目に帰ってみると、先に判明したcurrent_uid()ほか
current_user_ns()を引数にfrom_kuid_munged()の返り値を、システムコールの
返り値にしてますね.

このfrom_kuid_mungedはkernel/user_namespace.cに定義されており、
これらがその関数です.
コメントを読むに、current_uid()で取得したのはカーネル空間のid(kid)なので、
それをユーザーの名前空間に変換してやってるみたいで、
実際にその処理をしているのがmap_id_up()のようです.
ここで使われてるmapという構造体の型はstruct uid_gid_mapで、
定義はgrepするとinclude/linux/user_namespace.hに発見できます

getuid(2)がfrom_kuid_mungedの引数としてcurrent_user_ns()の返り値を渡しているので、
current_user_ns()がstruct uid_gid_mapを返り値にしててnsはname spaceの略であることに
気がつけるかとおもいます.
current_user_nsについては今回は追いません(わたしが疲れましたし記事も長くなりました).

5. カーネル空間からの帰還

かくして、カーネル空間での仕事を終えたgetuid(2)は値を返してまたsu_common.cの
851行目に帰ってきます.
ここからさらにsuの仕事は続きます(カンジンの権限昇格はまだしてないですね)

しかし、これでカーネルに潜っていって、カーネルのソースを"読む"ことを体験できたのではないでしょうか.
正直最後のほうはちょっと丁寧さがなくなってて雑になってる解説でしたが、ここまで読んだ方は
ご苦労さまでした.

私はパンピーなので(大事なことなので二回(ry)今回読んだソースの解釈には
誤りがあるかもしれません.しかし、考え推測しながら読むことで、
一歩か二歩ぐらいはOSの仕組みへの理解に近づけたことでしょう.
そして、読んでいく上でどういう法則でソースコードが分割してあるかもなんとなく
想像が付くかもしれません


これから、current_uid()の返り値を常に0に置き換えてみたりとか(常にrootで動作!)
そういった改変や遊びをしたり、今回端折ったアーキテクチャ依存の部分や割り込みの仕組みに
ついても調べてみると、いろいろと楽しかったり理解が深まったりするんじゃないかな!
そしてようこそカーネル空間の不思議な国へ!

以上でした