0から作るソフトウェア開発

日々勉強中。。。

Tips
リンク

ファイルシステム >

0から作るLinuxプログラム Ext2ファイルシステムその2 ファイルシステムへのアクセス

Ext2ファイルシステム

Linuxのファイルシステムのうち基本的なファイルシステムであるExt2について
みていきます。

それでは、順番に見ていきます。

目次 Ext2ファイルシステム

  1. ファイルシステムの概要

  2. (現在のページ) ファイルシステムへのアクセス

  3. カーネルサイドから見たシステムコール

  4. ファイルシステム関連のシステムコール

  5. 仮想ファイルシステム(VFS)

  6. ステップ0 Ext2ファイルシステムと簡単なモジュールの実装

  7. ステップ1 Ext2ファイルシステムタイプとマウントメソッド

  8. ステップ2 Ext2スーパーブロックの読み込み

  9. ステップ3 スーパーブロック管理情報

  10. ステップ4 マウントとスーパーブロックオブジェクト

  11. ステップ5 スーパーブロックオブジェクトと管理情報のセットアップ

  12. ステップ6 ブロックグループディスクリプターの読み込み

  13. ステップ7 ルートディレクトリのinode読み込み

  14. ステップ8 簡単なディレクトリの読み込み

  15. ステップ9 ディレクトリ読み込みとページキャッシュとaddress_space

  16. ステップ10 inodeオブジェクトのlookupメソッド

  17. ステップ11 inodeオブジェクトのmkdirメソッドとper-cpuカウンター

  18. ステップ12 簡単なブロックの新規割り当て処理

  19. ステップ13 スーパーブロックオブジェクトのwrite_inodeメソッド

  20. ステップ14 Orlov方式による新規ディレクトリのinodeを割り当てるブロックグループ選択

  21. ステップ15 inodeオブジェクトのrmdirメソッドとunlinkメソッド

  22. ステップ16 inodeオブジェクトのrenameメソッド

  23. ステップ17 inodeオブジェクトのcreateメソッドとファイルオブジェクトの汎用メソッド

  24. ステップ18 inodeオブジェクトのlinkメソッド

  25. ステップ19 シンボリックとinodeオブジェクトのsymlinkメソッド

  26. ステップ20 inodeオブジェクトのmknodメソッドとtmpfileメソッド

  27. ステップ21 スーパーブロックオブジェクトのevict_inodeメソッド

  28. ステップ22 スーパーブロックオブジェクトのsync_fsメソッド

  29. ステップ23 スーパーブロックオブジェクトのstatfsメソッドとメモリーバリアー/フェンス

  30. ステップ24 スーパーブロックオブジェクトのremount_fsメソッドとマウントオプション解析

  31. ステップ25 スーパーブロックオブジェクトのshow_optionsメソッドとprocfs

  32. ステップ26 カーネルオブジェクトとsysfs

  33. ステップ27 スーパーブロックオブジェクトのfreeze_fs/unfreeze_fsメソッド

  34. ステップ28 ファイルオブジェクトのioctlメソッド

  35. ステップ29 ファイルオブジェクトのsetattrメソッド

  36. ステップ30 address_spaceのdirect_IOメソッド

  37. ステップ31 リザベーションウィンドウ

  38. ステップ32 拡張アトリビュートその1 ハンドラーの呼び出し

  39. ステップ33 拡張アトリビュートその2 コア処理

  40. ステップ34 POSIX ACLその1 ハンドラーの呼び出し

  41. ステップ35 POSIX ACLその2 コア処理

  42. ステップ36 ディスククォータ

ファイルシステムへのアクセス

ここでは、私たちがどのようにファイルシステムを利用しているのかを、その入り口である

システムコールについて見ていきます。



普段作業しているなかで、特にファイルシステムということは意識せずに使っている

かと思います。ファイルからデータを読み込みしたり書き込みしたり、ファイルに対して

作業をしています。そのような例として、ファイルシステムの概要で、テキストファイルを

編集する作業を行う例をあげました。そのときに、例えば次のようにコマンドを

実行させたりするかと思います。


$ pwd
/home/user_name

$ ls
Desktop Documents Downloads Pictures Public Templates Videos

$ cd Documents

$ ls
File1 File2

$ vi File1



ここで、pwdコマンドでカレントディレクトリを確認して、lsコマンドで

ファイルを確認、cdコマンドで作業ディレクトリに移動して、viコマンドで

File1ファイルを編集作業を行う例となります。



このようにコマンドを利用してファイルを操作することができることはすでに

ご存知のことかと思います。しかし、これらコマンドは一体どうやってファイル

システムにアクセスしているのでしょうか。



ファイルシステムはOSの機能の一部で、ブロックデバイスはOSが管理する

リソースとなります。そして、OSの機能やデバイスなどのリソースはユーザー

プログラムからは直接利用することができません。OSの機能やリソースは

貝のように貝殻で閉じ込められています。しかし、唯一カーネルが守られて

いる貝の殻をこじ開ける゛ゲート”が存在します。システムコールです。

システムコール

システムコールは、カーネル世界に入ることができる唯一のゲートです。

システムコールを呼び出すことではじめてOSの機能やリソースを利用する

ことができます。さきほど見てきました、lsコマンドやviコマンドも

例外なくシステムコールを呼び出すことで、ファイルシステムにアクセスしています。

また、これらのコマンドに限らずファイルシステムなどOSの機能が必要となる

プログラムは最終的にシステムコールを呼び出すことになります。これは

システムコールのことを知らないプログラマーでさえも呼び出していることがあります。

(そもそもプログラマーだからといって知っている必要は無いと思います。また

扱っている言語のライブラリーがシステムコールをラッパーしていますので、

存在していることを知らなくてもそもそもプログラミング可能です。)



システムコールはOSの機能呼び出しとなりますので、カーネルの機能である

ファイルシステムを利用することができますが、システムコール以外の方法での

カーネルへアクセスは禁止されています(例外が発生してプロセスが停止します)。

システムコール呼び出し時のみカーネルの機能・リソースを使用することができる

システムコールの呼び出し(C言語)

システムコールとして身近なものにwriteシステムコールがあります。

ここではwriteシステムコール呼び出しについて体験していきましょう。

writeシステムコールは次の例のように呼び出します。


#include <unistd.h>

int main( int argc, char *argv[ ] )
{
    int        err;
    const char *text = "abc\n";

    write( STDOUT_FILENO, ( const void* )text, sizeof( text ) - 1 );

    return( 0 );
}




上記プログラムを"my_write.c"に保存して次のようにコンパイルします。


$ gcc -o my_write my_write.c



そして、実行します。


$ ./my_write
abc



と表示されます。表示されない場合補足説明を参照してください。



ここでwriteシステムコールの第1引数は書き込むファイルのファイル

ディスクプリター、第2引数は書き込むデータのアドレス、第3引数は書き込む

データのサイズです。この例ではファイルディスクプリターにはプロセス起動時に

オープンされる標準出力のファイルディスクプリター(STDOUT_FILENO)です。

(STDOUT_FILENOはunistd.hに定義されていて通常1です。)



補足
この例ではwriteシステムコールを呼び出して直ぐに終了して

います。writeシステムコールでは、カーネル内のバッファーに

書き込みしたらすぐに処理が終了します。カーネル内のバッファーから

標準出力に書き出されるまで時間がかかり、先にプロセスが終了して

しまうかもしれません。



補足
GCCなどのライブラリーではwriteシステムコールはラップされています。

実際には、ライブラリーのwrite関数の最終段で実際のwriteシステムコールが

呼び出されます。


ライブラリーでのシステムコール呼び出しはラッパー関数で、実際にシステムコールを呼び出す

前に引数のチェック(アライメントなどのチェック)を行うものもあります。効率良く実装されて

いますが、ここではもう少しだけ直接的に呼び出してみましょう。syscall関数を使用します。

syscall関数では第一引数にsys/syscall.hに定義されている

システムコール番号を指定します。システムコール番号はプレフィクスSys_の後に

呼び出したいシステムコール名を付けたものとなります。例えば、writeシステム

コールの場合Sys_writeとなります。そのあとの引数は呼び出すシステムコール

の引数と同じ引数ぶんだけ指定します。syscall関数によるwriteシステムコール

呼び出しの例は次のようになります。


#include <unistd.h>
#include <sys/syscall.h>

int main( int argc, char *argv[ ] )
{
    long        ret;
    const char  *text = "abc\n";

    ret = syscall( SYS_write, STDOUT_FILENO, text, sizeof( text ) - 1 );

    return( 0 );
}



思ったより簡単に実装できたのではないでしょうか。このsyscall関数も実際の

システムコール呼び出しのラッパーとなります。しかし、ライブラリーよりもすごく簡素に

実装されています。次にもっと直接的にアセンブラーでの呼び出しについて見ていきます。

システムコールの呼び出し(アセンブラー)

システムコールに関してこれ以降はより詳細な説明となります。本質的には

ファイルシステムと関係がありません。なるべく読んで頂きたいのですが、、興味が

無い方は読み飛ばしてファイルシステム関連のシステムコールに進んでください。



システムコールはソフトウェア割り込み(例外)を発生させる必要があるため、

CPUの専用命令が必要となります。ですので、実際の呼び出し部分は

アセンブラーで記述することになります。パソコンでは32ビットCPUは

SYSENTER命令を、64ビットCPUではSYSENTER命令/SYSCALL命令を

使います。



補足
歴史的にはシステムコールの呼び出しにはINT命令を使用していました。

(Linuxでは割り込み番号128を指定してINT 0x80を実行します。)

しかし、INT命令の呼び出しではIntel CPUの仕様上オーバーヘッド

が大きくなるため、呼び出し処理に時間がかかっていました。システム

コールはほぼすべてのプロセスで頻繁に呼び出されるので、改善を行い

Intel P6ファミリープロセッサーでSYSENTER命令が設けられました。

LinuxではINT命令によるシステムコール呼び出しにも対応していますが

SYSENTER命令を使用するようにします。SYSENTER命令に対応している

プロセッサーかどうかはCPUID命令で判断することができます。





補足
Intel P6ファイミリープロセッサー以降でSYSENTER命令が導入されました。

SYSCALL命令はAMDの64ビットプロセッサーで仕様が決められました。先に

AMD主導で開発されてたため、パソコンの64ビットCPUは事実上AMD仕様が

デファクトスタンダードとなっています。64ビットOSではSYSENTER命令も

SYSCALL命令にも対応しています。アプリケーションプログラムがどちらを

使用するのかはコンパイラー次第となりますが、基本的に新しく用意された

SYSCACLL命令を使用します。



システムコールの呼び出した後はCPUは特権モードで動作します。このときに、システム
コールの引数をどう渡すのかが問題となってきます。例えば、システムコールの引数を
スタックで渡す場合を考えてみます。スタックで引数を渡す場合、ユーザーアプリケーション
で引数をスタックに積みあげて、システムコールを発行します。システムコールが発行
されるとカーネルはスタックから引数を取り出すという手順が発生します。このようにスタック
で引数を渡す場合は、メモリーアクセスが必要となってきます。システム上で動作する
アプリケーションはたくさんあり、そして各アプリケーションは頻繁にシステムコールを発行
しています。このような状況でスタックで引数で渡すとそれだけでかなりのオーバーヘッド
となってしまいます。そこで、現在の汎用OSではスタックではなく、汎用レジスターで
システムコールの引数をカーネルに渡します。

レジスターはメモリーほど潤沢にあるわけではなく、数は限られていますし、CPUによって

その数が異なります。ですので、パソコンでは32ビットのLinuxでは5個までのレジスター、

64ビットのLinuxでは6個を使ってシステムコールの引数を渡します。使用するレジスター

はカーネルの仕様(作り)で決まっています。また、コンパイラーはカーネルの仕様に

合わせてシステムコールの呼び出しをライブラリーにあらかじめ定義しています。



引数に使用するレジスターは次のように決まっています。

(32ビットのLinuxと64ビットのLinuxで使用するレジスターは異なります。)



32ビットのLinuxに対しては次のレジスターを使用します。

32ビットLinuxのシステムコール呼び出しで使用するレジスター
レジスター 説明
EAXシステムコールの番号
EBXシステムコールの第1引数
ECXシステムコールの第2引数
EDXシステムコールの第3引数
ESIシステムコールの第4引数
EDIシステムコールの第5引数


64ビットのLinuxに対しては次のレジスターを使用します。

64ビットLinuxのシステムコール呼び出しで使用するレジスター
レジスター 説明
EAXシステムコールの番号
RDIシステムコールの第1引数
RSIシステムコールの第2引数
RDXシステムコールの第3引数
R10システムコールの第4引数
R8システムコールの第5引数
R9システムコールの第6引数


それでは、実際にシステムコールを呼び出す手順について見ていきましょう。

  1. システムコールの呼び出しには対応するシステムコールの番号を汎用レジスターのEAX

    レジスターに格納してから呼び出します。システムコール番号はシステムコールが登録

    されているテーブルのインデックスとなります。


  2. そして、SYSENTER/SYSCALL/INT 0x80命令を実行すると、CPUは例外を検知して

    あらかじめ登録されているシステムコールハンドラーを呼び出そうとします。もう少し詳しく見ると、

    • SYSCALL命令の場合

    • MSR(Model Specific Register)のIA32_LSTARにハンドラーのアドレスが登録されて

      います。SYSCALL命令による例外発生時、CPUはここからハンドラーのアドレスを取得し

      ハンドラーを実行します。


    • SYSENTER命令の場合

    • MSRのIA32_SYSENTER_EIPにハンドラーのアドレスが登録されています。SYSENTER命令

      による例外発生時、CPUはここからハンドラーのアドレスを取得してハンドラーを実行します。


    • INT 0x80命令の場合

    • IDT(Interrupt Descriptor Table)の128番目の割り込みディスクプリターにハンドラーの

      アドレスが登録されています。INT 0x80命令による例外発生時、CPUはここからハンドラーの

      アドレスを取得してハンドラーを実行します。



  3. ハンドラーはsystem_callがその実体となります。

    32ビットプロセッサーであればarch/x86/kernel/entry_32.Sに、

    64ビットプロセッサーであればarch/x86/kernel/entry_64.Sに

    定義されています。CPUはMSRまたはIDTからsystem_callのアドレスを取得して処理を開始します。


  4. ハンドラーでは、EAXレジスターの値からシステムコールが登録されているテーブル(sys_call_table)の

    システムコール番号目の関数が呼び出されて、対応するシステムコールの処理が始まります。例えば、

    32ビットLinuxのwriteは4番目のシステムコール番号となりますので、sys_call_tableの4番目に登録

    されているsys_writeの処理が始まります。


SYSENTER/SYSCALL/INT 0x80実行時のシステムコール呼び出し

補足
Linuxカーネルのソースで、システムコールはsys_をシステムコール名の前に

付けたものがsystem_callハンドラーから呼ばれます。例えば、writeシステム

コールの場合はsys_write関数が呼び出されます。



補足
MSRはModel Specific Registerの略で、プロセッサーごとに固有の情報が格納されて

いたり、テスト機能・実効トレース・性能モニタリング・マシンチェックエラー機能を制御したり

することができます。MSRには専用命令でアクセスします。読み込みはRDMSR命令を、

書き込みはWRMSR命令を使用します。



補足
IDTはInterrupt Descriptor Tableの略で、割り込みハンドラーのアドレスが登録されて

いるテーブルとなります。INT 0x80命令を実行すると例外が発生してIDTの128番目のに登録

されている割り込みハンドラーが呼び出されます。



SYSENTER命令とSYSCALL命令とINT 0x80命令に対応しているかどうかは、プロセッサーの種類と

OSによって異なります。どのプロセッサーとどのOSがどのシステムコールの命令に対応しているかを次の

表にまとめました。現在のプロセッサーでは全部の命令に対応していますが、古いプロセッサーが

載っているパソコンをお使いの場合注意してください。

システムコールの命令とプロセッサーの種類とOSの対応
  64ビットプロセッサー
(x86-64)
32ビットプロセッサー
(P6ファミリー以降)
32ビットプロセッサー
(P6ファミリーより前)
64ビットLinux 32ビットLinux 32ビットLinux 32ビットLinux
SYSCALL命令 × ×
SYSENTER命令 ×
INT 0x80命令


SYSCALL命令によるwriteシステムコール呼び出し

それでは、自分自身でwriteシステムコールを呼び出す体験をしていきましょう。

まずは、SYSCALL命令での呼び出し体験です。64ビットカーネルでやってみましょう。

ここでは、インラインアセンブラーで実装してみました。ここでは、my_write.cという

ファイル名に次の内容を保存しているとして話を進めます。

[my_write.c]


#define STDOUT_FILENO   1
#define WRITE_SYS_NUM   1

int main( int argc, char *argv[ ] )
{
    int         err;
    const char  *text = "abc\n";

    /* write system call # = 1      */
    asm volatile( "mov %0, %%rax" : : "i"( WRITE_SYS_NUM ) );
    /* 1st argument (fd=stdout)     */
    asm volatile( "mov %0, %%rdi" : : "i"( STDOUT_FILENO ) );
    /* 2nd argument ( *text )       */
    asm volatile( "mov %0, %%rsi" : : "m"( text ) );
    /* 3rd argument ( size )        */
    asm volatile( "mov %0, %%rdx" : : "i"( sizeof( text ) - 1 ) );
    asm volatile( "syscall" : "=a"( err ) );

    return( 0 );
}



上記プログラムを"my_write.c"に保存して次のようにコンパイルします。


$ gcc -o my_write my_write.c



そして、実行します。


$ ./my_write
abc



SYSCALL命令によりwriteシステムコールが呼び出されて、標準出力に文字列が

表示されました。単純なプログラムですが、やっていることはC言語によるwriteシステム

コール呼び出しと同じことになります。(ただし、ライブラリーのwriteシステムコール呼び出し

と実際には動作が異なります。ライブラリーではもう少し複雑な処理を行っています。)



アセンブラーが苦手な方のために説明します。インラインアセンブラーの詳細な記述方法

については、ファイルシステムの本質から少し離れてしまいますし、他に説明されている文書が

たくさんありまので、その都度ご参照ください。

アセンブラーについてわかっている方は読み飛ばしてください。



補足
インラインアセンブラーについては"GCC インラインアセンブラー"や"GCC 拡張インラインアセンブラー"

などで検索を行うとさまざまな説明サイトが出てくるかと思います。



最初の命令から見ていきましょう。

    /* write system call # = 1      */
    asm volatile( "mov %0, %%rax" : : "i"( WRITE_SYS_NUM ) );
    /* 1st argument (fd=stdout)     */
    asm volatile( "mov %0, %%rdi" : : "i"( STDOUT_FILENO ) );
    /* 2nd argument ( *text )       */
    asm volatile( "mov %0, %%rsi" : : "m"( text ) );
    /* 3rd argument ( size )        */
    asm volatile( "mov %0, %%rdx" : : "i"( sizeof( text ) - 1 ) );
    asm volatile( "syscall" : "=a"( err ) );



ここでは、writeシステムコールの番号としてWRITE_SYS_NUM=1を指定

して、RAXレジスター(64ビットのレジスターです。下位32ビットがEAXレジスターと同じ

になります)にmov命令で格納しています。拡張フォーマットのiは定数を

指定して%0に出力しています。次の命令を実行しているのと同じことになります。


    mov $1, %rax



これで第1引数の準備ができました。次を見ていきましょう。


    /* write system call # = 1      */
    asm volatile( "mov %0, %%rax" : : "i"( WRITE_SYS_NUM ) );
    /* 1st argument (fd=stdout)     */
    asm volatile( "mov %0, %%rdi" : : "i"( STDOUT_FILENO ) );
    /* 2nd argument ( *text )       */
    asm volatile( "mov %0, %%rsi" : : "m"( text ) );
    /* 3rd argument ( size )        */
    asm volatile( "mov %0, %%rdx" : : "i"( sizeof( text ) - 1 ) );
    asm volatile( "syscall" : "=a"( err ) );



先ほどの同じような命令となります。writeシステムコールの第1引数に

STDOUT_FILENO=1を指定して、RDIレジスターに格納しています。次に行きます。


    /* write system call # = 1      */
    asm volatile( "mov %0, %%rax" : : "i"( WRITE_SYS_NUM ) );
    /* 1st argument (fd=stdout)     */
    asm volatile( "mov %0, %%rdi" : : "i"( STDOUT_FILENO ) );
    /* 2nd argument ( *text )       */
    asm volatile( "mov %0, %%rsi" : : "m"( text ) );
    /* 3rd argument ( size )        */
    asm volatile( "mov %0, %%rdx" : : "i"( sizeof( text ) - 1 ) );
    asm volatile( "syscall" : "=a"( err ) );



writeシステムコールの第2引数に"text"変数のアドレスを指定して、

RSIレジスターに格納しています。拡張フォーマットのmは出力元がメモリーである

と言うことを意味しています。ここではメモリー上にある"text"のアドレスを%0に出力し

ています。


    /* write system call # = 1      */
    asm volatile( "mov %0, %%rax" : : "i"( WRITE_SYS_NUM ) );
    /* 1st argument (fd=stdout)     */
    asm volatile( "mov %0, %%rdi" : : "i"( STDOUT_FILENO ) );
    /* 2nd argument ( *text )       */
    asm volatile( "mov %0, %%rsi" : : "m"( text ) );
    /* 3rd argument ( size )        */
    asm volatile( "mov %0, %%rdx" : : "i"( sizeof( text ) - 1 ) );
    asm volatile( "syscall" : "=a"( err ) );



writeシステムコールの第3引数に"text"の文字列長を指定してRDXレジスターに格納

しています。拡張フォーマットのiは出力元が定数である"sizeof(text)-1"の計算結果を

%0に出力しています。ここで"-1"しているのは、文字リテラルの最後にNULLが格納されて

いるので、NULL以外の文字の長さを指定するためとなります。


    /* write system call # = 1      */
    asm volatile( "mov %0, %%rax" : : "i"( WRITE_SYS_NUM ) );
    /* 1st argument (fd=stdout)     */
    asm volatile( "mov %0, %%rdi" : : "i"( STDOUT_FILENO ) );
    /* 2nd argument ( *text )       */
    asm volatile( "mov %0, %%rsi" : : "m"( text ) );
    /* 3rd argument ( size )        */
    asm volatile( "mov %0, %%rdx" : : "i"( sizeof( text ) - 1 ) );
    asm volatile( "syscall" : "=a"( err ) );



最後にSYSCALL命令を実行しています。拡張フォーマットの=aは入力をRAX(EAX)レジスター

に格納しています。"err"変数を指定しているのは、err変数としてRAXレジスターを使用することを

指示するためとなります。



次にINT 0x80命令によるwriteシステムコール呼び出しについて見ていきましょう。

INT 0x80命令によるwriteシステムコール呼び出し

INT 0x80命令によるシステムコール呼び出しは基本的にSYSCALL命令による呼び出しと同じに

なります。ここでは、32ビットLinuxでの例となります。


#define STDOUT_FILENO   1
#define WRITE_SYS_NUM   4

int main( int argc, char *argv[ ] )
{
    int         err;
    const char  *text = "abc\n";

    /* write system call # = 1      */
    asm volatile( "mov %0, %%eax" : : "i"( WRITE_SYS_NUM ) );
    /* 1st argument (fd=stdout)     */
    asm volatile( "mov %0, %%ebx" : : "i"( STDOUT_FILENO ) );
    /* 2nd argument ( *text )       */
    asm volatile( "mov %0, %%ecx" : : "m"( text ) );
    /* 3rd argument ( size )        */
    asm volatile( "mov %0, %%edx" : : "i"( sizeof( text ) - 1 ) );
    asm volatile( "int $0x80" : "=a"( err ) );

    return( 0 );
}




レジスターが32ビットのレジスターになっているのと、最後にINT 0x80命令を実行している

箇所が違うだけとなります。



次にSYSENTER命令によるwriteシステムコール呼び出しについて見ていきましょう。

SYSENTER命令によるwriteシステムコール呼び出し

SYSENTER命令によるシステムコール呼び出しは命令の仕様から言って結構むずかしいです。

SYSENTER命令が呼び出された後カーネルはSYSEXIT命令で呼び出しもとに復帰します。

゛呼び出しもとに復帰”と書いていますが、実際にはそうではなく近い動作を想定してカーネル

を作っています。SYSENTER命令では、INT 0x80やSYSCALL命令のように実行時に゛呼び

出しもと”のアドレスをスタックやレジスターに退避することはありません。(゛呼び出しもと”を

スタックに入れるのは、INT 0x80命令やSYSCALL命令実行後に元のプログラムに戻って

くるために必要です。この仕組み自体は通常の関数呼び出しと同じ原理となります。)

元に戻る場所をカーネルに知らせることなく処理が開始します。SYSEXIT命令からユーザー

プログラムに復帰するには、SYSEXIT命令を実行する前にカーネルで、EDXレジスターに

戻り先のアドレスを、ECXレジスターにユーザープログラムのスタックポインターを設定する

必要があります。つまり、カーネルは戻り先のユーザープログラムのアドレスを把握しおく

必要があります。これにはいくつか方法があります。レジスターの1つを使ってカーネルに通知

するなどが考えられますが、いずれにせよオーバーヘッドがかかってしまい、なんのための

SYSENTER命令かわからなくなってきます。そこで、Linuxでは事前に決められたアドレスに

復帰する方針をとっています。つまり、ユーザー空間の固定アドレスに復帰するという手段を

とります。

補足
Linuxでは従来SYSENTER命令からの復帰するアドレスは固定アドレスでしたが、

セキュリティーの要請からそうはいかないようになってきました。時代の要請から

なるべくランダムなアドレスに復帰するようになっています(実際にはSYSENTER

命令の呼び出しもとがランダムなアドレスになります)。これについては後述して

いきます。



このため、システムコールを呼び出すときにユーザープログラムのどのアドレスに帰ってくるのか

を知っておく必要がありますが、これはカーネルとコンパイラーのライブラリーが知っていれば

よいだけの話となります。通常のプログラムをしていくうえでこのようなことを知っている必要は

ありませんが、ちょっと説明していきたいと思います。

vDSOとvsyscall

カーネルではvDSO(Virtual Dynamic Shared Object)という仮想的な動的ライブラリー

を用意しています。vDSOは全てのプロセス起動時にカーネルが共有ライブラリーとしてリンクを

行います。vDSOは通常Cライブラリーから呼び出しされますので、意識することはありません。

vDSOには頻繁に呼び出されるシステムコールが実装されていてます。頻繁に呼び出される

システムコールとしてgettimeofdayがあります。タイムスタンプの更新や、ウェイト

の時間測定などの使用しますが、アプリケーションからもライブラリーからもよく呼び出されます。

このため、なるべくオーバーヘッドを少なくしたいためvDSOに実装されています。なるべく

処理が速くなるように、システムコール自体の発行は行わず、カーネルがvDSO領域で更新

する時刻情報を取得するだけの軽い処理になっています。

補足
64ビットLinuxでは、そもそもvDSOによるシステムコール呼び出し自体のオーバーヘッドが

問題となっていたため、現在ではCライブラリーからvDSOの呼び出しは行わず、カーネルが

更新する情報を取得するだけの処理となっています。古いライブラリーにリンクされた

プログラムとの互換性を確保するために、存在自体は残してあります。



vDSOには次のシステムコールが実装されています。

そして、vDSOには上記以外のシステムコールを呼び出す関数も用意されています。32ビット

Linuxでは__kernel_vsyscallで、64ビットLinuxではvsyscallといいます。

これを仮想システムコール(virtual system call)と言います。このvsyscall(これ以降は

特に注意書きが無い限り__kernel_vsyscallvsyscallと記載します)も含めて、

vDSOは全てのプロセスにリンクされていて、ユーザー空間で動作しますが、実際にはカーネルに

実装されているプログラムとなります。このため、カーネルはプロセス起動時にvDSO領域の

ページフレームを共有ページとしてプロセスに割り当てます。これを共有ライブラリーとしてリンク

します。ユーザー空間では次のようにリンクされていることが確認できます。(次の例では/bin

ディレクトリに移動してからコマンドを実行しています)



[32ビットLinuxの場合]
				
$ ldd cat
    linux-gate.so.1 =>  (0xb77cb000)
    libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb760d000)
    /lib/ld-linux.so.2 (0xb77cc000)



32ビットLinuxではlinux-gate.so.1がvDSOとなります。



[64ビットLinuxの場合]
				
$ ldd cat
    linux-vdso.so.1 =>  (0x00007fff1faae000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2211a07000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f2211de3000)



64ビットLinuxではlinux-vdso.so.1がvDSOとなります。

ここで、何回かlddコマンドを実行してみます。


$ ldd cat
    linux-vdso.so.1 =>  (0x00007fff9c389000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4d5b22b000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f4d5b607000)
$ ldd cat
    linux-vdso.so.1 =>  (0x00007fff817fe000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffd2b981000)
    /lib64/ld-linux-x86-64.so.2 (0x00007ffd2bd5d000)
$ ldd cat
    linux-vdso.so.1 =>  (0x00007fff0f1fe000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcc3198a000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fcc31d66000)



このようにリンクされているアドレスがその都度変わることが確認できます。これは、セキュリティからの

要求でプロセスを起動するごとにランダムなアドレスにリンクしていることがわかります。これにより、

悪意あるプログラムからの攻撃にはある程度有効な対策となります。



SYSEXIT命令ではカーネル空間から復帰するアドレスが固定であると言っていましたが、

まさに、このvDSOのアドレスに帰ってくるようにカーネルは作られています。32ビットLinuxでは

__kernel_vsyscallの中でSYSENTER命令を呼びだします。そしてカーネルは

SYSEXIT時に__kernel_vsyscall中のアドレスを指定してユーザー空間に

戻ってきます。

補足
従来SYSENTER命令から戻ってくるときのvDSOへの戻り先アドレスは固定でした。

しかし、先ほど見てきましたように、セキュリティの要請からvDSOのアドレスはランダム

となります。このため、スレッドの構造体に戻り先アドレスを憶えておく変数が追加

されています。プロセス起動時にランダムなアドレスにvDSOを置き、その時の

vsyscallのアドレスをこの変数に入れておきます。そして、SYSEXIT

するときに戻り先アドレスを設定して、またvsyscallに戻ってきます。



64ビットLinuxでは、固定アドレスにvsyscallがリンクされています。次のように

/proc/self/mapsで確認することができます。


$ cat /proc/self/maps
00400000-0040b000 r-xp 00000000 08:01 262168             /bin/cat
0060a000-0060b000 r--p 0000a000 08:01 262168             /bin/cat
0060b000-0060c000 rw-p 0000b000 08:01 262168             /bin/cat
02363000-02384000 rw-p 00000000 00:00 0                  [heap]
7fcba3925000-7fcba40f5000 r--p 00000000 08:01 1712146    /usr/lib/locale/locale-archive
7fcba40f5000-7fcba42b0000 r-xp 00000000 08:01 681882     /lib/x86_64-linux-gnu/libc-2.19.so
7fcba42b0000-7fcba44b0000 ---p 001bb000 08:01 681882     /lib/x86_64-linux-gnu/libc-2.19.so
7fcba44b0000-7fcba44b4000 r--p 001bb000 08:01 681882     /lib/x86_64-linux-gnu/libc-2.19.so
7fcba44b4000-7fcba44b6000 rw-p 001bf000 08:01 681882     /lib/x86_64-linux-gnu/libc-2.19.so
7fcba44b6000-7fcba44bb000 rw-p 00000000 00:00 0 
7fcba44bb000-7fcba44de000 r-xp 00000000 08:01 681883     /lib/x86_64-linux-gnu/ld-2.19.so
7fcba46c4000-7fcba46c7000 rw-p 00000000 00:00 0 
7fcba46db000-7fcba46dd000 rw-p 00000000 00:00 0 
7fcba46dd000-7fcba46de000 r--p 00022000 08:01 681883     /lib/x86_64-linux-gnu/ld-2.19.so
7fcba46de000-7fcba46df000 rw-p 00023000 08:01 681883     /lib/x86_64-linux-gnu/ld-2.19.so
7fcba46df000-7fcba46e0000 rw-p 00000000 00:00 0 
7fff3ae6d000-7fff3ae8e000 rw-p 00000000 00:00 0          [stack]
7fff3afc0000-7fff3afc2000 r-xp 00000000 00:00 0          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0  [vsyscall]



初期の64ビットLinuxではこの固定アドレスのvsyscall呼び出しを想定して作られて

いました。ですので、Cライブラリーも固定アドレスのvsyscall呼び出しを行うようにして

いました。しかし、本質的にvsyscallよりもSYSCALL命令によるシステムコール呼び出し

の方が速いため、使われなくなりました。Cライブラリーについても現在ではvsyscall

呼び出さずにSYSCALL命令を使用するようになっています。現在では不要となっていますが、

初期のユーザープログラム用にこのvsyscallが残されています。現在の64ビット

Linuxでは、処理効率の問題から直接vsyscallを呼び出せないようになっています。

現在のvsyscallではトラップ命令(INT命令でソフトウェア割り込みを発生させます)

vsyscallをエミュレートしています。64ビットLinuxで32ビットアプリケーションを

実行させたときに、vsyscallを呼び出すと引数などのチェックを行ってから、実際の

システムコールとして動作を行います。

32ビットLinuxでのSYSENTER命令

実際に32ビットLinuxの__kernel_vsyscallを見てみましょう。ライブラリーは

システムコールを呼び出す必要があるときにレジスターに必要な値を格納してから

__kernel_vsyscallを呼び出します。__kernel_vsyscallは次のようにカーネルに

実装されています(ユーザー空間で実行されます)。


        .text
        .globl __kernel_vsyscall
        .type __kernel_vsyscall,@function
        ALIGN
__kernel_vsyscall:
.LSTART_vsyscall:
        push %ecx
.Lpush_ecx:
        push %edx
.Lpush_edx:
        push %ebp
.Lenter_kernel:
        movl %esp,%ebp
        sysenter

        /* 7: align return point with nop's to make disassembly easier */
        .space 7,0x90

        /* 14: System call restart point is here! (SYSENTER_RETURN-2) */
        int $0x80
        /* 16: System call normal return point is here! */
VDSO32_SYSENTER_RETURN: /* Symbol used by sysenter.c via vdso32-syms.h */
        pop %ebp
.Lpop_ebp:
        pop %edx
.Lpop_edx:
        pop %ecx
.Lpop_ecx:
        ret



SYSENTER命令が呼び出された後のSYSEXIT命令では、ECXは呼び出しもとのスタック

ポインターを復帰用に、EDXは呼び出し元のアドレスの復帰用に使用する仕様となっています。

このため、ユーザープログラムがシステムコールを呼び出す前と同じ状態にするという考えのもと、

SYSENTER命令を実行する前に、ECXとEDXをスタックに退避しています。これは、普通の

関数呼び出しと同じ考え方となります。カーネルはSYSEXIT命令を実行する前にEDXに

VDSO32_SYSENTER_RETURNのアドレスを設定してから実行します。つまり、SYSENTER

命令から戻ってきた後は、VDSO32_SYSENTER_RETURNから処理を開始することになり

ます。



それでは、ここでvsyscall(32ビットでは__kernel_vsyscallと言います)を呼び出す

体験をしていきましょう。次のようなプログラムとなります。


#define STDOUT_FILENO   1
#define WRITE_SYS_NUM   4

int main( int argc, char *argv[ ] )
{
    long    err;
    char    *text = "abc\n";

    /* write system call #                  */
    asm volatile( "mov %0, %%eax" : : "i"( WRITE_SYS_NUM ) );
    /* 1st argument (fd=stdout)             */
    asm volatile( "mov %0, %%ebx" : : "i"( STDOUT_FILENO ) );
    /* 2nd argument ( *text )               */
    asm volatile( "mov %0, %%ecx" : : "m"( text ) );
    /* 3rd argument ( size )                */
    asm volatile( "mov %0, %%edx" : : "i"( sizeof( text ) - 1 ) );

    /* call vsyscall                        */
    asm volatile( "call *%%gs:0x10" : "=a"( err ) );

    return( 0 );
}



この例ではgsレジスターが指しているセグメントの開始アドレスからのオフセット0x10

を呼び出しています。Cライブラリーでは、プロセス起動後にvDSOとvsyscallを探してきてくれます。

vDSOはライブラリーですので、その実態はELFとなります。Cライブラリーはvsyscallのアドレスを

ELFヘッダーから探してきてくれて、gsセグメントに置いておいてくれます(これは

32ビット限定の話となります)。vDSOのアドレスはプロセスのスタック、つまりAuxiliary Vector

AT_SYSINFO_EHDRに格納していてくれます。AT_SYSINFO_EHDRにはvDSOの開始アドレス

が格納されていて(ELFのヘッダーを指しています)、ELFを解析することによりvsyscall

アドレスを取得します。取得した後、Auxiliary VectorのAT_SYSINFOに格納します。

そして、同様にvsyscallのアドレスをgsセグメントのオフセット0x10が指すアドレスに

格納します。そしてmain関数が動き出します。



なぜ、gsセグメントのオフセット0x10なのでしょうか。



スレッドを動作させるときに、各スレッドで変数などのデータを局所的に使用したい場合が

あります。このため、Cライブラリーでは、スレッドごとに個別に使用する変数などのデータを

管理するための領域であるTLS(Thread Local Storage)セグメントを用意しています。

スレッドごとの局所的なデータはスレッド管理ブロックTCB(Thread Control Block)で

管理していきます。GCCでは、この局所的なデータが格納されるのがgsセグメントとなります。

gsセグメントの一番最初にmainスレッドのスレッド管理ブロックTCBが格納されています。

TCBでは管理する局所データの情報とあわせてシステム情報sysinfoも管理しています。

このsysinfovsyscallのコードが格納されている領域のアドレスが格納されています。

さきほど見てきましたAuxiliary VectorのAT_SYSINFOの値と同じになります。

TCBの構造体は次のようになっています。この構造体では、sysinfoは構造体の

先頭から16バイト(0x10バイト)目に配置されます。ですので、gsセグメントの0x10バイト

目がsysinfo(=vsyscallのアドレス)となりますので、オフセット値を指定して

gs:0x10vsyscallにアクセスすることができます。


typedef struct
{
    void *tcb;        /* Pointer to the TCB.  Not necessary the
                         thread descriptor used by libpthread.  */
    dtv_t *dtv;
    void *self;       /* Pointer to the thread descriptor.  */
    int multiple_threads;
    uintptr_t sysinfo;
    uintptr_t stack_guard;
    uintptr_t pointer_guard;
} tcbhead_t;



TCBの構造とsysinfo

補足
Auxiliary VectorのAT_SYSINFOとAT_SYSINFO_EHDRは環境変数などの同じ

ようにスタックに積まれています。32ビットのライブラリーではAuxiliary Vector

取得するインターフェースが用意されていません。メイン関数のスタックをたどるなどして自分で

探してくる必要があります。64ビットライブラリーではgetauxval関数が用意されて

います。



補足
Auxiliary VectorのAT_SYSINFOは廃止予定のパラメーターとなります。

今後作成するプログラムではなるべく使わないようにしておきます。



とこのようにSYSENTER命令を使うことはできるのですが、なんだか自分で使っているような

気にはなりません。そこで、自分のプログラムからSYSENTER命令を使う例を見ていきたいと

思います。

この例では、vsyscallの後半部分を利用します。カーネルはSYSEXIT命令の戻り先を

vsyscallのアドレスを指定して実行していました。SYSEXIT命令から戻ってきたときに

自分のプログラムに直に戻ってくるようにスタックを操作します。



vsyscallでは、SYSENTER命令を実行する前に、ECXレジスターとEDXレジスターと

EBPレジスターをスタックに退避させていました。このため、呼び出し前のスタック操作と呼び出し

後のスタック操作が一見してわかりにくくなっています。この例では32ビットLinuxの例となります。


#include <stdio.h>
#define STDOUT_FILENO   1
#define WRITE_SYS_NUM   4

unsigned int    ebp_before = 0;
unsigned int    ebp_after  = 0;
unsigned int    esp_before = 0;
unsigned int    esp_after  = 0;
unsigned int    ebp_after1 = 0;
unsigned int    esp_after1 = 0;
unsigned int    ebp_after2 = 0;
unsigned int    esp_after2 = 0;
unsigned int    ebp_after3 = 0;
unsigned int    esp_after3 = 0;

int main( int argc, char *argv[ ] )
{
    int         err;
    const char  *text = "abc\n";

    /* write system call # = 4      */
    asm volatile( "mov %0, %%eax" : : "i"( WRITE_SYS_NUM ) );
    /* 1st argument (fd=stdout)     */
    asm volatile( "mov %0, %%ebx" : : "i"( STDOUT_FILENO ) );
    /* 2nd argument ( *text )       */
    asm volatile( "mov %0, %%ecx" : : "m"( text ) );
    /* 3rd argument ( size )        */
    asm volatile( "mov %0, %%edx" : : "i"( sizeof( text ) - 1 ) );
    
    asm volatile( "mov %%ebp, %0" : "=m"( ebp_before ) : );
    asm volatile( "mov %%esp, %0" : "=m"( esp_before ) : );

    /* ------------------------------------------------------------------------ */
    /* 1 __start : do nothing                                                   */
    /* ------------------------------------------------------------------------ */
    asm volatile( "__start:" );

    /* ------------------------------------------------------------------------ */
    /* 2 before_sysenter -> execute_sysenter                                    */
    /* 4 before_sysenter -> exit                                                */
    /* ------------------------------------------------------------------------ */
    asm volatile( "before_sysenter:" );
    asm volatile( "    mov %%ebp, %0" : "=m"( ebp_after1 ) : );
    asm volatile( "    mov %%esp, %0" : "=m"( esp_after1 ) : );
    asm volatile( "    call execute_sysenter" );
    asm volatile( "    mov %%ebp, %0" : "=m"( ebp_after3 ) : );
    asm volatile( "    mov %%esp, %0" : "=m"( esp_after3 ) : );

    asm volatile( "    jmp exit" );

    /* ------------------------------------------------------------------------ */
    /* 3 execute_sysenter -> before_systenter                                   */
    /* ------------------------------------------------------------------------ */
    asm volatile( "execute_sysenter:" );
    asm volatile( "    mov %%ebp, %0" : "=m"( ebp_after2 ) : );
    asm volatile( "    mov %%esp, %0" : "=m"( esp_after2 ) : );
    asm volatile( "    push %ecx\n"
                  "    push %edx\n"
                  "    push %ebp\n"
                  "    mov  %esp, %ebp\n" );

    asm volatile( "    sysenter" );

    /* ------------------------------------------------------------------------ */
    /* 5 finish                                                                 */
    /* ------------------------------------------------------------------------ */
    asm volatile( "exit:" );
    asm volatile( "    mov %%ebp, %0" : "=m"( ebp_after ) : );
    asm volatile( "    mov %%esp, %0" : "=m"( esp_after ) : );

    printf( "\n" );

    printf( "[1 __start]---------------------------------\n" );

    printf( "ebp before = %8X\n", ebp_before );
    printf( "esp before = %8X\n", esp_before );

    printf( "[2 before_sysenter -> execute_sysenter]-----\n" );

    printf( "ebp after1 = %8X\n", ebp_after1 );
    printf( "esp after1 = %8X\n", esp_after1 );

    printf( "[3 execute_sysenter -> before_sysenter]-----\n" );

    printf( "ebp after2 = %8X\n", ebp_after2 );
    printf( "esp after2 = %8X\n", esp_after2 );

    printf( "[4 before_sysenter -> exit ]----------------\n" );

    printf( "ebp after3 = %8X\n", ebp_after3 );
    printf( "esp after3 = %8X\n", esp_after3 );

    printf( "[5 finish]----------------------------------\n" );

    printf( "ebp after  = %8X\n", ebp_after );
    printf( "esp after = %8X\n", esp_after );

    return( 0 );
}



SYSENTER命令によるシステムコール呼び出しは、スタック操作がわかりにくいため、

このプログラム例では各処理時点のESPレジスターとEBPレジスターの値を保存して

おき、最後にその値を表示しています。ぜひ、実行させてスタックがどのように操作

されているのかを確認してみてください。



SYSENTER命令によるシステムコール呼び出しは、ほかの命令と同様にレジスターに

引数を入れていきます。ここまでは同じです。この後にcall execute_sysenter命令で

実際にSYSENTER命令を実行する処理を呼び出します。CALL命令で処理を呼び出すことで、

CALL命令の次の命令(ここではmov %%ebp, %0" : "=m"( ebp_after3 ) : );となります)が

格納されているメモリーアドレスをスタックにプッシュしてから、CALL命令で呼び出した処理を

行っていきます。そして、SYSENTER命令を開始する前にECXレジスターとEDXレジスターを

スタックにプッシュしておきます。次にESPレジスターの値をEBPレジスターに入れ、スタックの

一番積みあがった場所をSYSENTER命令が呼び出された後のベーススタックとしています。

呼び出し元のアドレスをスタックにプッシュして関数が使用するレジスターの値を退避、

そしてスタックの一番上をベーススタックとしているということはつまり、この例では

SYSENTER命令を実行するということは通常の関数呼び出しと同じスタック操作を

していることになります。



SYSENTER命令呼び出し前には次のようにスタックを操作しておく必要があります。

SYSENTER命令呼び出し前のスタック

最後にSYSENTER命令の実行例です。EBPとESPがどのように変わっているのかを確認して

みてください。先ほどの例をwrite_sysenter.cに保存してコンパイル後実行

した結果となります。


$ ./write_sysenter
abc
[1 __start]---------------------------------
ebp before = BFCE5CE8
esp before = BFCE5CC0
[2 before_sysenter -> execute_sysenter]-----
ebp after1 = BFCE5CE8
esp after1 = BFCE5CC0
[3 execute_sysenter -> before_sysenter]-----
ebp after2 = BFCE5CE8
esp after2 = BFCE5CBC
[4 before_sysenter -> exit ]----------------
ebp after3 = BFCE5CE8
esp after3 = BFCE5CC0
[5 finish]----------------------------------
ebp after  = BFCE5CE8
esp after = BFCE5CC0



64ビットLinuxでのSYSENTER命令

64ビット環境ではvsyscallを呼び出してSYSENTER命令を実行する

ことはできなくなっています。しかし、64ビット環境で32ビットアプリケーションを動作

させるときはカーネルは32ビット用のvDSOをリンクしますので、SYSENTER命令を

使用することができます。



64ビット環境ではSYSCALL命令を使用するようにしてください。



ここまでで、ファイルシステムにアクセスするシステムコールについてわかりました。

次にカーネルサイドから見たシステムコールについて見ていきましょう。

PR 説明

ディベロッパー・エクスペリエンス Linux Ext2ファイルシステム
0から作るLinuxプログラム Linux Ext2ファイルシステムの完全説明版(Kindleのみ)となります。実装するステップ数は36ステップあります。少しずつステップを実装して、実際に動かして学習していきます。ファイルシステムの機能を呼び出すシステムコールの実装から、仮想ファイルシステムが呼び出すExt2ファイルシステムの実装を行っていきます。ファイルシステムの開発体験を通すことで、実際にカーネルが行っているファイルシステムの基礎的な動作を理解することができます。ぜひお試しください!

inserted by FC2 system