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

日々勉強中。。。

Tips
リンク

ファイルシステム >

0から作るLinuxプログラム Ext2ファイルシステムその3 カーネルサイドから見たシステムコール

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 ディスククォータ

これまで見てきましたようにシステムコールを呼び出すとカーネルの処理に移り、sys_call_table

に登録されているシステムコールの実体の処理が始まります。例えば、writeシステムコールでは、

sys_write()関数が呼び出されていました。システムコール処理の実体はカーネルで次のように

定義されています。ここでは、同じく例としてsys_write()の例となります。

			
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)



このように、システムコールはSYSCALL_DEFINE定義で宣言されています。SYSCALL_DEFINE

後ろの数字はシステムコールの引数の数となります。writeシステムコールは引数が3つありました

ので3がつきます。その他の例として、accessシステムコールは引数が2となりますので、次のように

なります。


SYSCALL_DEFINE2(access, const char __user *, filename, int, mode)



注意
通常の関数定義では変数の型と変数名の間に゛,”はありませんが

SYSCALL_DEFINEでは型と変数名の間に゛,”をつけて区切ります。



ここで、SYSCALL_DEFINEの定義を見ていきましょう。SYSCALL_DEFINEはLinuxのソースの

linux/syscall.hに定義されています。


#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)



SYSCALL_DEFINExが定義されています。ここで、"_##name" の#は前のリテラルと後ろのリテラルを連結

します。例えば、"name"に"write"が渡されたときに、"_##name"は"_write"となります。"__VA_ARGS__"は

可変引数となります。例えば、ユーザー空間のprintf関数の引数は任意の個数の引数を受け取ることが

できますが、同じように実装されていて"VA_ARGS"マクロを使っています。



更に、SYSCALL_DEFINExは次のように定義されています。


#define SYSCALL_DEFINEx(x, sname, ...)              \
    SYSCALL_METADATA(sname, x, __VA_ARGS__)         \
    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)



ここで、SYSCALL_METADATA(sname, x, __VA_ARGS__)はトレース情報を定義していますが、カーネルの

コンフィグで"CONFIG_FTRACE_SYSCALLS"がyになっている場合のみトレース情報を定義することになります。

通常は、SYSCALL_METADATA(sname, x, __VA_ARGS__)については定義無しとなりますので、実際には

次のように定義されていることになります。


#define SYSCALL_DEFINEx(x, sname, ...)  __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)



例えば、writeシステムコールの場合は次のようになります。


__SYSCALL_DEFINEx(3, _write, __VA_ARGS__)



ここで、__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)は次のように定義されています。


#define __SYSCALL_DEFINEx(x, name, ...)                             \
    asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))       \
        __attribute__((alias(__stringify(SyS##name))));             \
    static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__));  \
    asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__));      \
    asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))       \
    {                                                               \
        long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__));      \
        __MAP(x,__SC_TEST,__VA_ARGS__);                             \
        __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));           \
        return ret;                                                 \
    }                                                               \
    static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))



かなり難しいですね。。。一体何をどうやったらこのような定義となるのでしょうか。一つ一つ

見ていきましょう。



まず最初に

    asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))       \
        __attribute__((alias(__stringify(SyS##name))));             \



について見ていきます。asmlinkageは関数呼び出しのときに、引数をレジスターで

渡すようにコンパイラーに指示しています。システムコール呼び出したときには、ユーザー空間から

カーネル空間に移ります。このとき、スタックもカーネルスタックに切り替わるため、レジスターで

引数を渡す必要がありました。これを明示的にコンパイラーに指示するためasmlinkage を使用

します。asmlinkageは次のように定義されています。regparm属性に0を指定する

ことでコンパイラーに引数がレジスター渡しであることを指示します。

	
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))

ifdef __cplusplus
#define CPP_ASMLINKAGE extern "C"
#else
#define CPP_ASMLINKAGE
#endif





この定義に使用されている__MAPは次のように定義されています。


#define __MAP0(m,...)
#define __MAP1(m,t,a) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)



__MAP0から__MAP6まであり、引数の個数分の定義があります。この定義で

使用しているmは通常マクロ(macro)となります。つまり、各引数に対してマクロmを適用

する定義となっています。



__MAPに対して__SC_DECLマクロを指定しています。__SC_DECLは次のように定義されています。


#define __SC_DECL(t, a) t a



taを分解しているだけとなります。つまり、__VA_ARGS__で展開される引数を1つ1つ分解

していきます。



__attribute__((alias(__stringify(SyS##name))));について見ていきます。最初の__attribute__

関数宣言に対して属性(アトリビュート)をコンパイラーに指示することができます。ここでは

alias属性を使っています。alias属性は関数宣言をweakシンボルであることをコンパイラーに

指示しています。weakな関数シンボルはその他のファイルから参照することができない名前

となり、隠ぺいすることができます。その代わりに、ここではalias属性で別名をつけて外部ファイルに

公開したい名前を定義しています。



次に__stringifyは、通常defineのパラメーターは最初に

文字リテラルに変換されることはありませんが、文字列を連結する"#"を使用している場合に、

最初に文字リテラルに変換するようにコンパイラーに指示していることになります。つまり、

プリプロセッサーの処理段階で、SyS##nameを文字リテラルに置き換えます。例えば、

通常は引数と文字リテラルは結合することができませんが、__stringifyを指定することで、

プリプロセッサ−の段階で連結して1つの文字列といったことができます。



alias属性と__stringifyでやっていることは__stringify(SyS##name)関数名を

別名のsys##nameで外部ファイルから参照できるようにしています。



この定義をwriteシステムコールに当てはめると

asmlinkage long sys_write( unsigned int fd, const char __user *buf, size_t count)
                __attribute__((alias( SyS_write ))); 



となります。ここではつまり、内部のSys_write関数を外部ファイルからはsys_write関数名

で参照できるようにしているということになります。



次に、

				
asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__));



について見ていきます。これは関数のプロトタイプ宣言をしています。writeシステムコールの場合、

				
asmlinkage long SyS_write( unsigned int fd, const char __user *buf,
        size_t count)



の宣言と同じになります。このプロトタイプの実体が次のように定義されていきます。

				
    asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))       \
    {                                                               \
        long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__));      \
        __MAP(x,__SC_TEST,__VA_ARGS__);                             \
        __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));           \
        return ret;                                                 \
    }                                                               \



ここで__MAP(x,__SC_TEST,__VA_ARGS__);__SC_TESTは次のように展開します。

				
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && \\
                        sizeof(t) > sizeof(long))



となります。ここで何をしているのかと言うと、引数がレジスターの大きさを越えていないか

コンパイル時にチェックできるようにしています。レジスターの大きさはこのコンパイラーでは

long型であるとしています。long型より大きい引数は当然レジスター渡しができませんので、

このようなチェック機構を設けています。



__PROTECTは次のように定義されています。

				
#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)



asmlinkage_protectはテールコール問題を解消するために定義されています。

テールコール問題について、システムコールを例にとりますと、

例えば、writeシステムコールではvfs_write処理が呼び出されます。このとき

asmlinkage_protectが無いとvfs_systemcallを直接呼び出すようにコンパイルされることが

あります。コンパイラーによる最適化が行われるためです。通常vfs_systemcall関数は

通常の関数と同じように引数の受け渡しにスッタクが用いられますが、システムコールでは

レジスターで渡すことになります。レジスターで渡すので、通常の関数のようにスタックに引数が

積まれているように動作すると問題が発生します。。システムコールの場合スタックには引数が積まれていませんので、

通常の関数呼び出しすると不定義な動作となってしまいます。これをテールコール問題と言い、

この問題を防ぎvfs_systemcallに確実に引数をレジスター渡しできるようにasmlinkage_protect

しておきます。

asmlinkage_protectは次のように定義されています。拡張インラインアセンブラーで各引数

に入力としてレジスターが使われるように("=r"がその意味となります)コンパイラーに指示します。

		
#define asmlinkage_protect(n, ret, args...) \
        __asmlinkage_protect##n(ret, ##args)
#define __asmlinkage_protect_n(ret, args...) \
        __asm__ __volatile__ ("" : "=r" (ret) : "" (ret), ##args)





ここまでで、システムコールのSYSCALL_DEFINE定義でした。めちゃめちゃ難しいですね。



少しだけ簡単に見ていきます。



最初に挙げた次のwriteシステムコールの処理がありました。

				
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)



これを難しい部分と、コンパイル後に実行コードにならない部分を省略すると

writeシステムコールは結局次のような形に落ち着きます。


    asmlinkage long sys_write( unsigned int fd,
                              const char __user *buf,
                              size_t count)
        __attribute__((alias( SyS_write )));
    asmlinkage long SyS_write( unsigned int fd,
                               const char __user *buf,
                               size_t count)
    {
        long ret = SYSC_write( unsigned int fd,
                               const char __user *buf,
                               size_t count)
        return ret;
    }
    static inline long SYSC_write( unsigned int fd,
                                   const char __user *buf,
                                   size_t count)
{
	// 実際のwriteシステムコールの処理
}



結局はwriteシステムコールは、sys_write関数名で呼び出すことができ、

その実体はSYSC_write関数を呼び出すSyS_write内部関数となります。



ここまでで、カーネル内でのシステムコールの宣言はSYSCALL_DEFINEで宣言されていて

SYSCALL_DEFINEの第1引数にsys_をつけたものがシステムコールの関数

となることがわかりました。カーネルのソースをSYSCALL_DEFINEgrepすると、簡単に

Linuxで定義されているシステムコールがわかります。



補足
Linuxのシステムコールを探すのにsys_で検索してもシステムコールの実装は

探せません。SYSCALL_DEFINEで検索してみてください。

ファイルシステム関連のシステムコールを探すには、Linuxのソースの

fsディレクトリ以下をSYSCALL_DEFINEgrepするとわかります。


システムコールテーブルsys_call_table

SYSCALL_DEFINEで宣言されているシステムコールのアドレスはsys_call_tableに格納されて

いることになります。sys_call_tableはカーネルのビルド時に生成されます。カーネルをビルドするときに

パソコン用のLinuxであればarch/x86/syscalls/のmakeが行われます。このときに、32ビット用カーネル

であれば、syscall_32.tbl、64ビット用カーネルであればsyscall_32.tblsyscall_64.tbl

からそれぞれプロセッサーの種類にあわせてシステムコールを定義したヘッダーが生成されます。



sys_call_tableの生成は少し複雑です。現在までにパソコン用のプロセッサーは

いろいろな種類のプロセッサーが開発されてきました。Linuxはプロセッサーが新しくなるたびに

対応してきました。プロセッサーの種類や機能が異なるとシステムコール呼び出しの規約(ルール)

も違うことがあります。システムコール呼び出しのルールのことをABI(Application Binary

Interface)と言います。パソコン用のプロセッサーには次のようなABIがあります。

パソコン用プロセッサーのABI
ABI 説明
i386 x86アーキテクチャーのABIとなります。IA-64が発表された以降はIA-32とも呼ばれます。また、x86-64と比較してx86-32とも言います。
64 x86アーキテクチャーを64ビットに拡張したABIとなります。AMD株式会社が仕様を先に発表し、開発したためAMD64とも呼ばれます。またx86の64ビット拡張であることからx86-64とも言います。(IA-64とはアーキテクチャーが異なることに注意してください。IA-64とは互換性がありません。)
IA-32エミュレーション IA-64アーキテクチャーではIA-32アーキテクチャーをソフトウェアでエミュレーションすることができます。カーネルでエミュレーション機能を対応できるため、x86-64で動作するカーネルでもIA-32エミュレーションのABIに対応しています。
x32 x86-64で32ビットアドレスを扱うアプリケーション(32ビットメモリー空間を扱うプログラムでサイズを小さくしたいときなどに使われます)用のABIとなります。x32は32ビットのメモリー空間を扱いますが、i386と違い、レジスターやALUはx86-64を使用しています。


これらのABIごとにシステムコールが準備されます。



ABIごとのシステムコールを定義するために使われるのがsyscall_32.tblsyscall_64.tblとなります。

それでは、このテーブルの中身がどのようになっているのかを見ていきましょう。

syscall_32.tblは次のようなフォーマットになっています。

				
<number> <abi> <name> <entry point> <compat entry point>



syscall_64.tblのフォーマットは次のようになっています。compat entry pointはありません。

				
<number> <abi> <name> <entry point>



syscall_32.tblとyscall_64.tblのフォーマット
項目 説明
number システムコールの番号
abi Application Binary Interfaceの略です。アーキテクチャーごとに呼び出し規約が異なりますので、システムコールのABIを記述します。

[syscall_32.tbl]

i386:常に固定でi386となります。

[syscall_64.tbl]

common:32ビット用と共通の関数を使用します。
64:64ビットの関数を使用します。
x32:32ビットアドレス空間を扱うアプリケーション用です。
name システムコールの名前です。ユーザープログラムがシステムコールを発行するときに使用する__NR_の定義に使われます。
entry point システムコールの関数アドレスです。
compat entry point syscall_32.tbl専用の項目です。IA-32エミュレーションのシステムコールが呼び出されたときにこのエントリーポイントが呼び出されます。


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

[syscall_32.tbl]

[number][abi]   [name]                  [entry point]
3       i386    read                    sys_read
4       i386    write                   sys_write



readwriteにはI32エミュレーション専用のシステムコールはありませんので、

compat entry pointは空けておきます。

openシステムコールなどI32エミュレーション専用のシステムコールが必要な場合は次の

ようにcompat entry pointに記述します。

[syscall_32.tbl]

[number][abi]   [name]                  [entry point]         [compat entry point]
5       i386    open                    sys_open              compat_sys_open





syscall_64.tblではsyscall_32.tblと共通の場合ABIはcommonとなります。

また、64ビットABIには64と記述し、x32 ABIにはx32と記述します。

[syscall_64.tbl]

[number][abi]   [name]                  [entry point]
0       common  read                    sys_read
1       common  write                   sys_write
.
.
.
19      64      readv                   sys_readv
20      64      writev                  sys_writev
.
.
.

#
# x32-specific system call numbers start at 512 to avoid cache impact
# for native 64-bit operation.
#
512     x32     rt_sigaction            compat_sys_rt_sigaction
513     x32     rt_sigreturn            stub_x32_rt_sigreturn



となっています。



そして、カーネルをビルドしたときにMakefileからsyscalltdr.shsyscalltbl.shが実行

されます。これによりsyscall_32.tblsyscall_32.tblからsys_call_tableに登録するための、

関数定義とシステムコール番号の定義がarch/x86/include/generated/asm

生成されます。また、ユーザー空間で使用するシステムコール番号の定義が

arch/x86/include/generated/uapi/asmに生成されます。



例としてx86-64では次のようにヘッダーファイルが作られます。

[arch/x86/include/generated/asm]

clkdev.h
syscalls_32.h
syscalls_64.h
unistd_32_ia32.h
unistd_64_x32.h



syscalls_32.hの中身を少し見てみます。

[syscalls_32.h]

__SYSCALL_I386(0, sys_restart_syscall, sys_restart_syscall)
__SYSCALL_I386(1, sys_exit, sys_exit)
__SYSCALL_I386(2, sys_fork, stub32_fork)
__SYSCALL_I386(3, sys_read, sys_read)
__SYSCALL_I386(4, sys_write, sys_write)
__SYSCALL_I386(5, sys_open, compat_sys_open)



ここで__SYSCALL_I386arch/x86/ia32/syscall_ia32.cに定義されています。

[arch/x86/ia32/syscall_ia32.c]

#define __SYSCALL_I386(nr, sym, compat) extern asmlinkage void compat(void) ;
#include <asm/syscalls_32.h>
#undef __SYSCALL_I386

#define __SYSCALL_I386(nr, sym, compat) [nr] = compat,

typedef void (*sys_call_ptr_t)(void);

extern void compat_ni_syscall(void);

const sys_call_ptr_t ia32_sys_call_table[__NR_ia32_syscall_max+1] = {
        /*
         * Smells like a compiler bug -- it doesn't work
         * when the & below is removed.
         */
        [0 ... __NR_ia32_syscall_max] = &compat_ni_syscall,
#include <asm/syscalls_32.h>
};



少々難解ですが、少しずつ見ていきます。


#define __SYSCALL_I386(nr, sym, compat) extern asmlinkage void compat(void) ;
#include <asm/syscalls_32.h>



ここでは、システムコールのプロトタイプ宣言を行っています。ここでvoid compat(void)

なっています。各システムコールで引数の個数に違いがありますが、ここでは関数アドレスのみ

ia32_sys_call_tableに登録したいです。とりあえず、ここではシステムコールをvoid compat(void)

でプロトタイプ宣言しておくと、全てのシステムコールがsys_call_ptr_t型の関数

アドレスとして登録できます。後はリンク時にリンカーがアドレスを解決してくれます。そして、

ia32_sys_call_tableはアセンブラーから呼び出しを行いますので、sys_call_ptr_t型の

関数で特にコンパイルに支障はありません。

__SYSCALL_I386の定義の後asm/syscalls_32.hをインクルードしてプロトタイプ宣言が

完了します。


#undef __SYSCALL_I386

#define __SYSCALL_I386(nr, sym, compat) [nr] = compat,



プロトタイプ宣言の後いったん__SYSCALL_I386の定義を解除して再定義します。

ここでは、ia32_sys_call_tableの1つ1つの要素となる定義を行っています。行の最後が゛,”に

なっていることに注意してください。また、ここではIA-32エミュレーションABIとなりますので

compat entry pointの方を登録しています。



そして、ia32_sys_call_tableを定義していきます。


typedef void (*sys_call_ptr_t)(void);

extern void compat_ni_syscall(void);

const sys_call_ptr_t ia32_sys_call_table[__NR_ia32_syscall_max+1] = {
        /*
         * Smells like a compiler bug -- it doesn't work
         * when the & below is removed.
         */
        [0 ... __NR_ia32_syscall_max] = &compat_ni_syscall,
#include <asm/syscalls_32.h>
};



最後にsys_call_ptr_t型の関数アドレスを定義して、ia32_sys_call_table配列を

作成しています。ia32_sys_call_table配列には最初にcompat_ni_syscallのアドレスを全要素と

しています。これは、定義の無いシステムコールが呼び出されたときにcompat_ni_syscall

とりあえず呼び出されるようにするためとなっています。ここで[0 ... __NR_ia32_syscall_max]

配列の全要素であることを意味しています。



compat_ni_syscallでは次のように-ENOSYSエラーを返すだけの関数となっています。

[arch/x86/ia32/nosyscall.c]

long compat_ni_syscall(void)
{
        return -ENOSYS;
}



そして、ia32_sys_call_tableの途中で先ほど再定義した__SYSCALL_I386asm/syscalls_32.h

インクルードして配列の要素を上書きしていきます。



32ビットLinuxではIA-32エミュレーションABIと同じ方法でi386のABIが作成されます。この場合

compat entry pointではなくentry pointの関数アドレスでsys_call_tableが作られます。

今回は64ビットLinuxのみの説明となりますので、IA-32エミュレーションの方の説明でした。



同様にcommon、x32と64のABIも同じようなやり方でsys_call_tableが作られます。

syscalls_64.hの中身を少し見てみます。

[syscalls_64.h]
[arch/x86/include/generated/uapi/asm]

unistd_32.h
unistd_64.h
unistd_x32.h



unistd_32.hunistd_64.hsyscalls_32.hsyscalls_64.hが生成されます。

ここに、それぞれ32ビット用と64ビット用のカーネルにあわせたsys_call_tableが定義されることになります。



簡単なシステムコールの実装

システムコールを実装するには以下のファイルを編集します。

この例では、Linuxのソースが置いてあるディレクトリにmy_syscallディレクトリを作って

fsディレクトリと同じ階層のディレクトリです)そこに今回実装する簡単なシステムコールの中身を

置きます。また開発環境は64ビットのLinuxです。

64ビットLinuxでシステムコールを実装するときに編集するファイル
ファイルパス 説明
my_syscall/my_syscall.c 今回実装するシステムコールの処理実体です。
my_syscall/Makefile 今回実装するシステムコールのmakeファイルです。
arch/x86/syscalls/syscall_64.tbl システムコールテーブルが定義さされているファイルです。
include/linux/syscalls.h システムコール処理関数のプロトタイプ宣言を追加するファイルです。
Makefile my_syscallディレクトリを追加します。


それではそれぞれのファイルについて実装または編集してきます。

my_syscall/my_syscall.cの実装

まずはLinuxソースルートディレクトリに移動してmy_syscallディレクトリを作成します。


$ cd /usr/src/linux-source-3.17.1/linux-source-3.17.1
$ sudo -s
[パスワード入力]
# mkdir my_syscall
# cd my_syscall



推奨
ここでは/usr/src配下のLinuxソースディレクトリを作業ディレクトリにして

していますが、先にバックアップをとっておくか別の作業ディレクトリにコピーしてから

一般ユーザーで作業することをおすすめします。



それでは、my_syscallディレクトリに今回作成するシンプルなシステムコール処理を

作っていきます。ここではファイル名をmy_syscall.cとしました。

[my_syscall.c]
				
#include <linux/kernel.h>
#include <linux/syscalls.h>

SYSCALL_DEFINE2( my_syscall, char __user *, buf, int, count )
{
	long err;
    char text[ ] = "my syscall!";

    printk( "<MY_SYSCALL>%s\n", text );

    if( count < sizeof( text ) )
    {
        return( -ENOMEM );
    }

    /* copy untill null terminator */
    err = copy_to_user( buf, text, sizeof( text ) );
    	
    return( err );
}



関数宣言の引数に注意してください。SYSCALL_DEFINEを使用する場合には、型と変数名の

間に゛,”を入れます。処理内容は非常にシンプルです。printkでデバッグ情報をログに書き出して、

ユーザーが指定したバッファーにテキストを書き込むだけの処理となります。ユーザーが指定した

バッファーの大きさが十分でない場合にエラーを返します。

my_syscall/Makefileの実装

次に同じディレクトにMakefileを作っていきます。次の一行だけです。

[Makefile]
				
obj-y := my_syscall.o



arch/x86/syscalls/syscall_64.tbl

sys_call_tablesys_my_syscallシステムコールを追加します。

この例では、arch/x86/syscalls/に移動してファイルを編集します。

				
# cd ../arch/x86/syscalls/



次のようにsys_my_syscallsyscall_64.tblに追加します。

[syscall_64.tbl]
				
310     64      process_vm_readv        sys_process_vm_readv
311     64      process_vm_writev       sys_process_vm_writev
312     common  kcmp                    sys_kcmp
313     common  finit_module            sys_finit_module
314     common  sys_my_syscall          sys_my_syscall



今回はテーブルの最後に追加しました。この例ではmy_syscallシステムコールの番号は

314となります。

include/linux/syscalls.hの編集

syscalls.hヘッダーファイルに追加したシステムコールのプロトタイプ宣言を追加します。

この例ではinclude/linux/に移動して作業を行います。

				
# cd ../../../include/linux



次のようにsys_my_syscallのプロトタイプ宣言を追加します。

[syscalls.h]
			
asmlinkage long sys_kcmp(pid_t pid1, pid_t pid2, int type,
                         unsigned long idx1, unsigned long idx2);
asmlinkage long sys_finit_module(int fd, const char __user *uargs, int flags);

asmlinkage long sys_my_syscall( char __user *buf, int count );



ここではSYSCALL_DEFINEで展開された後の記述を行います。

asmlinkageを付けただけの普通の関数としてプロトタイプ宣言します。

Makefileの編集

最後にLinuxソースのルートディレクトリに移動してMakefileを編集します。

最初に作業ディレクトリを移動します。

				
# cd ../../



そして、Makefileを開きcore-yで検索します。下記のように最後にmy_syscall

ディレクトリを追記します。これにより、カーネルのビルドを行うとmy_syscallディレクトリで

makeコマンドを実行してくれます。

[Makefile]

ifeq ($(KBUILD_EXTMOD),)
core-y          += kernel/ mm/ fs/ ipc/ security/ crypto/ block/ my_syscall/



Tipsのカーネルの再構築を参照しながらカーネルを

ビルド・インストール・再起動してください。



結構簡単だったかと思います。では、次にmy_syscallシステムコールを呼び出すユーザー

プログラムを作っていきます。

追加したシステムコールを呼び出すユーザープログラムの実装

追加実装したシステムコールは、たったいま自分で作ったので、もちろんライブラリーには

APIがありません。ですので、 ファイルシステムへのアクセス で見てきましたsyscallを使って呼び出します。

この例では適当なディレクトリにファイルcall_my_syscall.cを作成しました。

[call_my_syscall.c]
				
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>

#define SYS_my_syscall  314

int main( int argc, char *argv[ ] )
{
        char    buf[ 256 ];
        long    ret;

        ret = syscall( SYS_my_syscall, buf, sizeof( buf ) );

        if( ret < 0 )
        {
                fprintf( stderr, "erro : %ld\n", ret );
        }
        else
        {
                printf( "buf = %s\n", buf );
        }

        return( 0 );
}



syscall関数で314番目のシステムコールを呼び出しを行っています。



コンパイルします。

				
$ gcc -o call_my_syscall call_my_syscall.c
$ ./call_my_syscall
my syscall!



実行すると、my_syscallシステムコールがバッファーを書き換えます。

そしてその内容が表示されるかと思います。



次のようにdemsgコマンドを実行すると、システムコールの呼び出しがログに記録されていることを

確認できます。

				
$ dmesg
<MY_SYSCALL>my syscall!





このようにして、システムコールの処理が呼び出されます。

次にどのように操作ができるのか、ファイルシステム関連のシステムコールについて

見ていきましょう。

PR 説明

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

inserted by FC2 system