Linux
kernel
driver
LinuxDay 22

Linuxのunlocked_ioctlとcompat_ioctlの違い

More than 3 years have passed since last update.


はじめに

どうもです。@akachochinです。Advent calendarの記事になります。

この間Linuxのデバドラのコードにかかわる仕事をしていて、include/linux/fs.hを見ました。で、file_operations構造体を見たら、ioctlを呼び出す関数ポインタが


include/linux/fs.h

        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

というメンバになっていました。Linux Device Drivers 3rd editionを見る限り、このようなメンバは載っていません。(ioctlに関するメンバはioctlのみ掲載されていました)

せっかくの機会なので、この場を借りて、unlocked_ioctlとcompat_ioctlの違いを見ましょう。


まずDocumentationを見る

Documentationディレクトリの下を検索してみましょう。

すると、以下の記述を見つけることができます。


Documentation/filesystems/vfs.txt


unlocked_ioctl: called by the ioctl(2) system call.

compat_ioctl: called by the ioctl(2) system call when 32 bit system calls
are used on 64 bit kernels.


うーん、これだと今一つイメージが湧きません。では、ソースを見ることにしましょう。


ソースを見る

compat_ioctlが呼ばれる最も一般的なルートは以下の箇所になりそうです。


fs/compat_ioctl.c

COMPAT_SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, 

compat_ulong_t, arg32)
{
/* 略 */
/*
* To allow the compat_ioctl handlers to be self contained
* we need to check the common ioctls here first.
* Just handle them with the standard handlers below.
*/

switch (cmd) {
/* 略 */
default:
if (f.file->f_op->compat_ioctl) {
error = f.file->f_op->compat_ioctl(f.file, cmd, arg);
if (error != -ENOIOCTLCMD)
goto out_fput;
}

if (!f.file->f_op->unlocked_ioctl)
goto do_ioctl;
break;
}

if (compat_ioctl_check_table(XFORM(cmd)))
goto found_handler;

error = do_ioctl_trans(fd, cmd, arg, f.file);
if (error == -ENOIOCTLCMD)
error = -ENOTTY;

goto out_fput;

found_handler:
arg = (unsigned long)compat_ptr(arg);
do_ioctl:
error = do_vfs_ioctl(f.file, fd, cmd, arg);
out_fput:
fdput(f);
out:
return error;
}


ここで、COMPAT_SYSCALL_DEFINE3(ioctl)は、以下の動作をします。


  • compat_ioctlが定義されていたら、それを実行し、-ENOIOCTLCMD以外の戻り値であれば終了。

  • そうでない場合、compat_ioctl_check_table()を呼び、渡されたcmdが特殊な扱いをするものか調べます。具体的には、fs/compat_ioctl.cに定義されているioctl_pointer[]内に該当cmdがあるか調べます。特殊な扱いをするものであれば、do_vfs_ioctl()が呼ばれます。

  • do_vfs_ioctl()の呼出しを経由して、unlocked_ioctl()が呼ばれるケースがあります。

また、unlocked_ioctlが呼ばれる最も一般的なルートは以下の箇所になりそうです。


fs/ioctl.c

static long vfs_ioctl(struct file *filp, unsigned int cmd,

unsigned long arg)
{
int error = -ENOTTY;

if (!filp->f_op->unlocked_ioctl)
goto out;

error = filp->f_op->unlocked_ioctl(filp, cmd, arg);
if (error == -ENOIOCTLCMD)
error = -ENOTTY;
out:
return error;
}

/* 略 */

/*
* When you add any new common ioctls to the switches above and below
* please update compat_sys_ioctl() too.
*
* do_vfs_ioctl() is not for drivers and not intended to be EXPORT_SYMBOL()'d.
* It's just a simple helper for sys_ioctl and compat_sys_ioctl.
*/

int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd,
unsigned long arg)
{
/* 略 */
default:
if (S_ISREG(inode->i_mode))
error = file_ioctl(filp, cmd, arg);
else
error = vfs_ioctl(filp, cmd, arg);
break;
}
return error;
}

/* 略 */

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{
int error;
struct fd f = fdget(fd);

if (!f.file)
return -EBADF;
error = security_file_ioctl(f.file, cmd, arg);
if (!error)
error = do_vfs_ioctl(f.file, fd, cmd, arg);
fdput(f);
return error;
}


次に、SYSCALL_DEFINE3のioctlとCOMPAT_SYSCALL_DEFINE3のioctlの差を調べてみましょう。


SYSCALL_DEFINEとCOMPAT_SYSCALL_DEFINE

まず、SYSCALL_DEFINEのほうから見ます。


include/linux/syscalls.h

#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##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_DEFINE3の場合、以下の通りになります。

* 実装されている関数は、SYSC##nameというstatic関数となる。例えば、今回のioctlの場合、SYSCioctlという関数名になる。

* 実装されている関数を直呼び出しするのでなく、SyS##nameで定義されるasmlinkage属性の関数経由で呼ばれる。今回の場合、SySioctlとなる。

一方で、COMPAT_SYSCALL_DEFINEは以下の通りとなります。


include/linux/compat.h

/* 略 */

#define COMPAT_SYSCALL_DEFINE3(name, ...) \
COMPAT_SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

#define COMPAT_SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long compat_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))\
__attribute__((alias(__stringify(compat_SyS##name)))); \
static inline long C_SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
asmlinkage long compat_SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__));\
asmlinkage long compat_SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))\
{ \
return C_SYSC##name(__MAP(x,__SC_DELOUSE,__VA_ARGS__)); \
} \
static inline long C_SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))


  • 実装されている関数は、C_SYSC##nameというstatic関数となる。例えば、今回のioctlの場合、C_SYSCioctlという関数名になる。

  • 実装されている関数を直呼び出しするのでなく、compat_SyS##nameで定義されるasmlinkage属性の関数経由で呼ばれる。今回の場合、compat_SySioctlとなる。

ふたつはあまり変わらないように思われますが、第二引数に着目してください。


include/linux/syscalls.h

// asmlinkageの関数(ガワ)

asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
// staticで定義される実際の実装側の呼出
long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__)); \
// 実際の実装側の関数
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))


include/linux/compat.h

// asmlinkageの関数(ガワ)

asmlinkage long compat_SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__));\
// staticで定義される実際の実装側の呼出
return C_SYSC##name(__MAP(x,__SC_DELOUSE,__VA_ARGS__)); \
// 実際の実装側の関数
static inline long C_SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))

システムコール本体に渡している第二引数に指定している__SC_CASTと__SC_DELOUSEの違いを見れば良さげですね。


include/linux/syscalls.h

#define __TYPE_IS_LL(t) (__same_type((t)0, 0LL) || __same_type((t)0, 0ULL))

#define __SC_DECL(t, a) t a
#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a
#define __SC_CAST(t, a) (t) a

__builtin_choose_exprはGCCのビルドイン関数で、第一引数が非ゼロなら第二引数を返しそうでないなら第三引数を返すというものです。

さて、__TYPE_IS_LLは、引数に渡された型がunsigned long longもしくはlong longのいずれか(要するに64bitかどうか)と等しいかを見ているようです。

そして、__TYPE_IS_LLの結果が(unsigned) long longであれば、__SC_LONGマクロはその第二引数に渡された変数をlong longで宣言します。そうでなければlongで宣言します。

参考までに__same_typeを載せます。


include/linux/compiler.h

#ifndef__same_type                                                       # define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))

#endif


include/linux/compat.h

#ifndef __SC_DELOUSE

#define __SC_DELOUSE(t,v) ((t)(unsigned long)(v))
#endif

一方で、__SC_DELOUSEマクロは第二引数で渡された変数を強制的にunsigned longでキャストします。つまり強制的に32bit値にするということです。

また、上で紹介したGCCビルドイン関数はここに載っていますので参考にしてください。

つまり、以下のことが言えそうです。

* 普通のシステムコールは渡された引数が32bit値か64bit値かを見て適切に宣言してくれる。(64bit値対応版といえそう)

* compatなシステムコールは、いかなる場合も強制的に引数が32bitに切り捨てられた後、指定の型にキャストされる。

参考までに、__MAP(x,__SC_LONG,__VA_ARGS__)は以下のとおりです。


include/linux/syscalls.h

#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 __MAP(n,...) __MAP##n(__VA_ARGS__)

よって、マクロの展開は、こんな感じになります。

-> __MAP(3,__SC_LONG,systype1,sysarg1,systype2,sysarg2,systype3,sysarg3)

※__VA_ARGS__はマクロに渡されたシステムコールの引数

-> __MAP3(__SC_LONG,systype1,sysarg1,systype2,sysarg2,systype3,sysarg3)

-> __SC_LONG(systype1,sysarg1) , __MAP2(systype2,sysarg2,systype3,sysarg3)

-> __SC_LONG(systype1,sysarg1) ,__SC_LONG(systype2,sysarg2) ,__MAP1(systype3,sysarg3)

->__SC_LONG(systype1,sysarg1) ,__SC_LONG(systype2,sysarg2),__SC_LONG(systype3,sysarg3)


では、誰がcompatとそうでないシステムコールの呼び分けをするのか

結論から言うと、libcのようです。

x86の場合、システムコールテーブルは以下のソース内にあります。


arch/x86/kernel/syscall_64.c

/* 略 */

#ifdef CONFIG_X86_X32_ABI
# define __SYSCALL_X32(nr, sym, compat) __SYSCALL_64(nr, sym, compat)
#else
# define __SYSCALL_X32(nr, sym, compat)
/* nothing */
#endif

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

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

extern void sys_ni_syscall(void);

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
/*
* Smells like a compiler bug -- it doesn't work
* when the & below is removed.
*/

[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};



arch/x86/kernel/syscall_32.c

/* 略 */

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

#define __SYSCALL_I386(nr, sym, compat) [nr] = sym,
/* 略 */
__visible const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
/*
* Smells like a compiler bug -- it doesn't work
* when the & below is removed.
*/

[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_32.h>
};

sys_call_tableの各要素の初期値は「sys_ni_syscall」です。これは-ENOSYSだけを返す関数で「存在しないシステムコール番号を指定した」事を意味します。

それでは、誰が存在するシステムコールのエントリをsys_call_tableに設定しているのでしょうか。

その鍵は上で引用したソース内のasm/syscalls_64.hもしくはasm/syscalls_32.hにありそうです。しかし、arch/x86/include/asmの下にはありません。

実は、カーネルをビルドすることによって、arch/x86/include/generatedが生成され、その下にsyscalls_64.hもしくはsyscalls_32.hが生成されます。

(メカニズムを知りたい方はarch/x86/syscallsの下をみると良さそうです。)

さて、ioctlに限定して、syscalls_64.hとsyscalls_32.hを見ると以下のようになっています。


arch/x86/include/generated/asm/syscalls_64.h

/* 略 */

__SYSCALL_64(16, sys_ioctl, sys_ioctl)
/* 略 */
__SYSCALL_X32(514, compat_sys_ioctl, compat_sys_ioctl)
/* 略 */

64bit版では、CONFIG_X86_X32_ABIが有効な場合、_SYSCALL_X32で指定されたcompatが有効になります。これまでの話から、sysioctlとcompat_sys_ioctlは別物の独立したシステムコールです。


arch/x86/include/generated/asm/syscalls_32.h

/* 略 */

__SYSCALL_I386(54, sys_ioctl, compat_sys_ioctl)
/* 略 */

一方、32bit版では、ioctlは常にsys_ioctlが使われます。compatは使われません。

ちなみに、sys_call_tableは以下のようにしてアセンブラのコードから扱うため、該当コードからシステムコールで定義された関数が呼ばれることになります。


arch/x86/kernel/entry_32.S

sysenter_do_call:

cmpl $(NR_syscalls), %eax
jae sysenter_badsys
call *sys_call_table(,%eax,4)
movl %eax,PT_EAX(%esp)

(entry_64.Sもあります。興味のある方はぜひ。)


まとめ

ここまで述べたことから、先に引用した


unlocked_ioctl: called by the ioctl(2) system call.

compat_ioctl: called by the ioctl(2) system call when 32 bit system calls


are used on 64 bit kernels.


も具体的にイメージできるようになりました。

unlocked_ioctlは、通常のioctl実装で、compat_ioctlはCONFIG_X86_X32_ABIを有効にしてビルドした64bitカーネルに対して32bit版のioctlを呼び出したとき(例えば32bit版のアプリからioctlを叩いたとき)に呼ばれるioctlということになります。

それでは。