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

日々勉強中。。。

Tips
リンク

システムコール >

0から作るLinuxプログラム システムコールその2 カーネル内のシステムコール

システムコール

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


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

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


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

  1. ユーザープログラムからのシステムコールの呼び出し


  2. (現在のページ) カーネル内のシステムコール


  3. 簡単なシステムコールの作成




カーネル内のシステムコールの説明について

この説明は前半がExt2ファイルシステムその3 カーネルサイドから見たシステムコール

同じ説明となっています。

カーネルのシステムコール定義

これまで見てきましたようにシステムコールを呼び出すとカーネルの処理に移り、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はテールコール問題を解消するために定義されています。

テールコール問題とは、システムコールを例にとりますと、システムコール処理では

do_systemcallまたはvfs_systemcallsystemcallにはシステムコールの名前が入ります。

例えば、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が定義されることになります。



PR 説明

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

inserted by FC2 system