調査対象版数
linux-4.2(mainline)
システムコールエントリ関数の定義
表題が「システムコールエントリ関数の定義」としてしまったんですが、実際には、システムコールエントリ関数を定義するマクロのことを書いています。
(システムコールエントリ関数の中身については書いていません)
前に投稿した記事(システムコールエントリについて見てみた。(linuxソース解析))で書いた__SYSCALL_DEFINExマクロの定義がどのように展開されるかを調べてみた。
このマクロは、システムコールエントリ関数を定義するもので、結構難解だった。
ただ、C言語のスキルの底上げになるような実装が結構あったと思う。
まず、実際にマクロが使用されているサンプルとしてsetgidシステムコールのコードを見てみる。
kernel\sys.c(386): SYSCALL_DEFINE1(setgid, gid_t, gid)
/*
* setgid() is implemented like SysV w/ SAVED_IDS
*
* SMP: Same implicit races as above.
*/
SYSCALL_DEFINE1(setgid, gid_t, gid)
{
struct user_namespace *ns = current_user_ns();
const struct cred *old;
struct cred *new;
int retval;
kgid_t kgid;
kgid = make_kgid(ns, gid);
if (!gid_valid(kgid))
return -EINVAL;
new = prepare_creds();
if (!new)
return -ENOMEM;
old = current_cred();
retval = -EPERM;
if (ns_capable(old->user_ns, CAP_SETGID))
new->gid = new->egid = new->sgid = new->fsgid = kgid;
else if (gid_eq(kgid, old->gid) || gid_eq(kgid, old->sgid))
new->egid = new->fsgid = kgid;
else
goto error;
return commit_creds(new);
error:
abort_creds(new);
return retval;
}
SYSCALL_DEFINE1(setgid, gid_t, gid)
の行を見ると、SYSCALL_DEFINE1マクロの引数は3つあるが、
実際には、setgidの引数はgid_t gid
の1つである。
マクロの定義を見ると分かるが、1つ以上の引数についても基本的なマクロの実装は同じ。
関連マクロ定義
関連するマクロ定義は、次の通り。
(これらのマクロはinclude\linux\syscalls.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__)
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
#define __PROTECT(...) asmlinkage_protect(__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__))
SYSCALL_METADATAは補助的な(?)意味合いのように思われたので、ここでは見ないこととした。
ここでの記事ではシステムコールのエントリ関数がどのように展開されるか絞って見ていくことにする。
SYSCALL_METADATAは、システムコールエントリ関数の中にメタデータを埋め込むためのマクロ。
(CONFIG_FTRACE_SYSCALLSマクロがdefineされている時のみメタデータを埋め込むコードが有効になる(configの設定))
ここでの記事ではシステムコールのエントリ関数がどのように展開されるか絞って見ていくため、SYSCALL_METADATAについては除外する。
__SYSCALL_DEFINExの頭から見ていくと、
更に、__MAP、__SC_DECL、と__stringifyのマクロがある。これらの定義は、それぞれ次の通り。
#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__)
#define __SC_DECL(t, a) t a
#define __TYPE_IS_L(t) (__same_type((t)0, 0L))
#define __TYPE_IS_UL(t) (__same_type((t)0, 0UL))
#define __TYPE_IS_LL(t) (__same_type((t)0, 0LL) || __same_type((t)0, 0ULL))
#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a
#define __SC_CAST(t, a) (t) a
#define __SC_ARGS(t, a) a
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))
include\linux\stringify.h(9): #define __stringify_1(x...) #x
include\linux\stringify.h(10): #define __stringify(x...) __stringify_1(x)
具体的にマクロを展開するために、setgidの場合で見ていく。
setgidは、SYSCALL_DEFINE1(setgid, gid_t, gid)
のように書かれている。
この例で、マクロがどのように展開されるかを見ていく。
SYSCALL_DEFINE1マクロを展開すると、SYSCALL_DEFINEx(1, _setgid, gid_t, gid)
になり、
SYSCALL_DEFINExマクロを展開して、SYSCALL_METADATAマクロの部分を除外すると、__SYSCALL_DEFINEx(1, _setgid, gid_t, gid)
となる。
更に、__SYSCALL_DEFINExマクロを展開すると、次のようになる。
/*①*/
asmlinkage long sys_setgid(__MAP(1,__SC_DECL,gid_t,gid))
__attribute__((alias(__stringify(SyS_setgid))));
/*②*/
static inline long SYSC_setgid(__MAP(1,__SC_DECL,gid_t,gid));
/*③*/
asmlinkage long SyS_setgid(__MAP(1,__SC_LONG,gid_t,gid));
/*④*/
asmlinkage long SyS_setgid(__MAP(1,__SC_LONG,gid_t,gid))
{
/*⑤*/
long ret = SYSC_setgid(__MAP(1,__SC_CAST,gid_t,gid));
/*⑥*/
__MAP(1,__SC_TEST,gid_t,gid);
/*⑦*/
__PROTECT(1, ret,__MAP(1,__SC_ARGS,gid_t,gid));
return ret;
}
static inline long SYSC_setgid(__MAP(1,__SC_DECL,gid_t,gid)) /*⑧*/
上記の内容を上から見ていく(コメントで書いた丸付き数字とリンクして見ていく)。
①の定義内容
__MAP(1,__SC_DECL,gid_t,gid
の部分だけ展開していくと、
__MAP1(__SC_DECL,gid_t,gid)
となり、更に__SC_DECL(gid_t,gid)
になり、
最終的にはgid_t gid
になる。
これを①に当て嵌めると、
asmlinkage long sys_setgid(gid_t gid))
__attribute__((alias("SyS_setgid")));
に展開される。
ここで__MAPマクロについて考えてみる。
__MAPマクロは、型と変数名に対して第2引数のマクロで展開して、カンマ区切りで、展開した内容を第1引数の組み合わせ数分
繋げている。
setgidの場合だと、組み合わせの数が1なので例としては分かり難いが、
第2引数のマクロ__SC_DECLが型と変数名を空白区切り繋げるので、(組み合わせ数が1より大きければ)それをカンマ区切りで繋げる。
結果的に、これは関数の変数の定義部分に展開される。
(第2引数のマクロにより展開される列挙されるパターンが変わる)
②のマクロの展開
次の②の行は、上で見てきた内容から、
static inline long SYSC_setgid(gid_t gid);
に展開される。
③のマクロの展開
③は、__MAPマクロの第2引数のマクロが__SC_LONGとなっている。
この__SC_LONGマクロの内容は、以下のようになっている。
__SC_LONGマクロの定義内容
#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a
#define __TYPE_IS_LL(t) (__same_type((t)0, 0LL) || __same_type((t)0, 0ULL))
# define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))
__same_typeマクロは、引数a、bの型が一致しているかを返すマクロで、
__TYPE_IS_LLマクロは、0を引数tでcastしたものと、0LLの型が一致するか、あるいは
0を引数tでcastしたものと、0ULLの型が一致するかを返すマクロ。
LLとULLは、それぞれlong long
、unsigned long long
を表す整数表現。
なので、__TYPE_IS_LLは、引数tの型が、long long
、unsigned long long
のどちらかであれば真をそうでなければ偽を返す。
ここまで見て、やっと__SC_LONGの定義。
__builtin_choose_exprは、gccの組み込み関数で、第1引数の評価値が真なら第2引数を、偽なら第3引数の値を返す。
(3項演算子との違いは、多分、ビルド時に評価されるか、実行時に評価されるかだと思う)
(__builtin_choose_exprも実行時に評価されるようだ。3項演算子との違いは不明)
(__builtin_choose_exprはプリプロセッサを通過したところまでは残るが、真のコード(第二引数)のみがコンパイルされ、偽のコード(第三引数)はコンパイルされない⇒第一引数の条件はコンパイラが判定できるconst値のみが指定できる)
これらのことから、__SC_LONGマクロは、引数tがlong long
、unsigned long long
のどちらかの場合は、0LLの型(long long
)、そうでない場合は、0Lの型(long
)で引数aを定義する。
上記を踏まえて、③の定義を見ると、gid_tは、unsigned int
なので、__SC_LONG(gid_t,gid)
は、
long gid
に展開される。ここまで来ると、単にマクロを追いかけていると言う感じではなくなってくる。
最終的に③のマクロを展開していくと、
asmlinkage long SyS_setgid(long gid);
のようになる。
④のマクロの展開
これは、③の展開と同様で、
asmlinkage long SyS_setgid(long gid)
のように展開される。
⑤のマクロの展開
⑤は、__MAPマクロの第2引数のマクロが__SC_CASTとなっている。
この__SC_CASTマクロの内容は、次の通り。
#define __SC_CAST(t, a) (t) a
__SC_CASTマクロは、第1引数tで引数aをcastするような構文に展開される。これが__MAPの第2引数になっているので、
⑤は、次のように展開される。
long ret = SYSC_setgid((gid_t)gid);
SyS_setgid関数では、gidをlong型で受け取って、SYSC_setgidの呼び出しでgid_tにcastしている。
⑥のマクロの展開
⑥は、__MAPマクロの第2引数のマクロが__SC_TESTとなっている。
この__SC_TESTマクロの内容は、以下のようになっている。
__SC_TESTマクロの定義内容
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))
__TYPE_IS_LLは、上の方で書いた通り。
(「__TYPE_IS_LLマクロは、0を引数tでcastしたものと、0LLの型が一致するか、あるいは
0を引数tでcastしたものと、0ULLの型が一致するかを返すマクロ。」)
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))
BUILD_BUG_ON_ZEROマクロはもの凄く凝っています。これは中々書けないです。
まず、!!(e)
の部分は、eの値の1回目の否定で、eの値が0以外だと、全て1なるので、
2回の否定で、eの値が0以外の場合は1、0の場合は0になる。
struct { int:-!!(e); }
は、structでビットフィールドを定義している。
eが0の場合はビットフィールドのサイズが0となるが、eが0以外の場合はビットフィールドのサイズが-1となり、
コンパイルエラーとなる。
BUILD_BUG_ON_ZEROは、このようにコンパイル時のエラーチェックをするもの。
実際に構造体を定義する訳ではないので、sizeofで括っている。
⑥のマクロを強引に展開(簡略した形で展開)すると、
(sizeof(struct { int:-!!(0); }))
。
展開した内容に、__builtin_types_compatible_pとかを書こうとすると、訳が分からないものになりそうなので、御容赦ください。
結局、__SC_TESTマクロが何をするものかを書くと、
引数tの型が、long long
、unsigned long long
以外で、大きさがsizeof(long)より大きい場合に
コンパイルエラーにする。
システムコールエントリの変更/追加をコンパイル時にチェックするような感じ。
⑦のマクロの展開
__MAPマクロの第2引数に__SC_ARGSが指定されている。
__SC_ARGSマクロの定義は、次の通り。
#define __SC_ARGS(t, a) a
引数名だけを展開している。
__PROTECTマクロの定義は、次の通り。
#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
#define asmlinkage_protect(n, ret, args...) \
__asmlinkage_protect##n(ret, ##args)
#define __asmlinkage_protect_n(ret, args...) \
__asm__ __volatile__ ("" : "=r" (ret) : "0" (ret), ##args)
#define __asmlinkage_protect0(ret) \
__asmlinkage_protect_n(ret)
#define __asmlinkage_protect1(ret, arg1) \
__asmlinkage_protect_n(ret, "m" (arg1))
#define __asmlinkage_protect2(ret, arg1, arg2) \
__asmlinkage_protect_n(ret, "m" (arg1), "m" (arg2))
#define __asmlinkage_protect3(ret, arg1, arg2, arg3) \
__asmlinkage_protect_n(ret, "m" (arg1), "m" (arg2), "m" (arg3))
#define __asmlinkage_protect4(ret, arg1, arg2, arg3, arg4) \
__asmlinkage_protect_n(ret, "m" (arg1), "m" (arg2), "m" (arg3), \
"m" (arg4))
#define __asmlinkage_protect5(ret, arg1, arg2, arg3, arg4, arg5) \
__asmlinkage_protect_n(ret, "m" (arg1), "m" (arg2), "m" (arg3), \
"m" (arg4), "m" (arg5))
#define __asmlinkage_protect6(ret, arg1, arg2, arg3, arg4, arg5, arg6) \
__asmlinkage_protect_n(ret, "m" (arg1), "m" (arg2), "m" (arg3), \
"m" (arg4), "m" (arg5), "m" (arg6))
setgidの場合で展開を進めると、
__PROTECT(1, ret,__MAP(1,__SC_ARGS,gid_t,gid));
⇒
__PROTECT(1, ret,gid);
⇒
asmlinkage_protect(1,ret,gid);
⇒
asmlinkage_protect_1(ret,gid);
⇒
asmlinkage_protect_n(ret, "m" (gid));
⇒
__asm__ __volatile__ ("" : "=r" (ret) : "0" (ret), "m" (gid));
展開はできたけど、これが何を意味するかはさっぱり分からない。
ネットで検索してみたところ、見つけたのが、次のサイト。
この記事によると、上記のようなasm文の書き方を拡張アセンブリ構文と言うらしい。
記述は、次の通りとのこと。
__asm__ ( アセンブリテンプレート
: 出力オペランド /* オプション /
: 入力オペランド / オプション /
: 破壊されるレジスタのリスト / オプション */
)
アセンブリテンプレートは、アセンブラ命令が書かれますが、拡張アセンブリ構文の場合レジスタなどの書き方が、基本的なアセンブラ命令の書き方とは微妙に異なります。各オペランドは、オペランド制約文字と括弧で括られた、C言語の式を記述できます。
次は、同上サイトに書かれているサンプル。
int in1=10,in2=5,out1;
__asm__ ("movl %1, %%eax; /* アセンブリテンプレート /
addl %2, %%eax;
movl %%eax, %0;"
:"=r"(out1) / 出力 /
:"r"(in1),"r"(in2) / 入力 /
:"%eax" / 破壊されるレジスタ */
);
上記の内容を踏まえて、__asm__ __volatile__ ("" : "=r" (ret) : "0" (ret), "m" (gid));
を見ると、
まず、"アセンブリテンプレート"の部分が空。
"出力"がretで、"入力"がretとgid。
あと、"=r"とか"0"とか"m"は、何だろう?
これについても先ほどの参考サイトの中に説明があった。これらは制約文字と言い。
・"r"は(x86の場合)、「eax,ebx,ecx,edx,esi,ediの中から、使用レジスタが自動割り当てされる。」。
・"0"は、「"0"は0番目の出力オペランド制約であることを示しています。
例えば、出力オペランドが下記の場合、"=a"(in_out)が"0" で "=b"(in2)が"1"となります。
:"=a"(in_out),"=b"(in2)」。
⇒なるほど、出力オペランドのretの制約だと言うことなのだろう。
・"m"は、「メモリ制約」、「メモリ制約は、直接メモリ上にストアされます。」。
⇒gidはSyS_setgidの引数で、スタックに積まれているはず。これが"メモリ制約"になる?
ここまで見ても、まだ良くわからない。
"アセンブリテンプレート"の部分が空なので、実行されるasm文はないはず。
なので、変数を"出力"、"入力"オペランドに指定していることに意味があるはず。
また、「volatileを書くことによって、 asm命令が除去されたり、 大きく移動されたり、 1つにまとめられたりする
ことを防ぐことができます。」ともあるので、gccの最適化に絡んだ実装のような気がする。
もう少し、ネットで情報を集めてみると、次のサイトが見つかった。
ちょうど調べたかった情報が書かれていた。が、まだ完全に理解できていない感じ。
今まで追いかけてきた例の場合だと、SYSC_setgid関数の呼び出しで、引数gidの内容が確実に参照できるように最適化の
抑止を掛けている感じなのだろう。
もう少し調べて、追加事項があったら、この記事に追記しよう。
やっと一通りマクロの展開を見ることができたけど、消化不良気味。
次はforkの本体の内容を見ようと思うけど、少し時間を空けるかも。
・・・。
もう少し調べていたら、次のサイトを見つけた。
このサイトの中に、
通常vfs_systemcall関数は
通常の関数と同じように引数の受け渡しにスッタクが用いられますが、システムコールでは
レジスターで渡すことになります。レジスターで渡すので、通常の関数のようにスタックに引数が
積まれているように動作します。システムコールの場合スタックには引数が積まれていませんので、
通常の関数呼び出しすると不定義な動作となってしまいます。これをテールコール問題と言い、
この問題を防ぎvfs_systemcallに確実に引数をレジスター渡しできるようにasmlinkage_protect
しておきます。
前のサイトでも書いてあったのかも知れないけど、この記述を見て理解できた。
SyS_setgidを通さないで、直接SYSC_setgidを呼び出すと、引数が渡らない。
確かに、forkのように引数0の場合は、asmlinkage_protectしてなかった。