FreeBSDカーネル開発シリーズ
| Part1 ビルド | Part2 モジュール | Part3 ドライバ | Part4 システムコール | Part5 DTrace |
|---|---|---|---|---|
| ✅ Done | ✅ Done | ✅ Done | 👈 Now | - |
はじめに
システムコールはカーネルとユーザー空間の境界。
open(), read(), write(), fork()...全部システムコール。
今回は自分だけのシステムコールを作ってみる。
システムコールの仕組み
┌─────────────────────────────────────────────────────────────┐
│ User Space │
│ │
│ printf("hello") │
│ ↓ │
│ write(1, "hello", 5) ← libc関数 │
│ ↓ │
│ syscall(SYS_write, 1, "hello", 5) ← システムコール呼び出し │
├─────────────────────────────────────────────────────────────┤
│ Kernel Space │
│ │
│ sys_write(td, uap) ← カーネル内の実装 │
│ ↓ │
│ vn_write() → ファイルシステム → デバイスドライバ │
└─────────────────────────────────────────────────────────────┘
x86_64ではsyscall命令でカーネルモードに移行する。
FreeBSDのシステムコール一覧
# システムコール番号を確認
cat /usr/src/sys/kern/syscalls.master | head -50
; ...
0 AUE_NULL STD {
int syscall(
int number,
...
);
}
1 AUE_EXIT STD {
void sys_exit(
int rval
);
}
2 AUE_FORK STD {
int fork(void);
}
3 AUE_READ STD {
ssize_t read(
int fd,
void *buf,
size_t nbyte
);
}
; ...
方法1: カーネルモジュールでシステムコールを追加
カーネルを再ビルドせずにシステムコールを追加できる。
ソースコード
mkdir -p ~/kmod/mysyscall
cd ~/kmod/mysyscall
vi mysyscall.c
#include <sys/param.h>
#include <sys/module.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/sysent.h>
#include <sys/sysproto.h>
#include <sys/proc.h>
/* システムコールの引数 */
struct mysyscall_args {
int value;
};
/* システムコールの実装 */
static int
mysyscall(struct thread *td, struct mysyscall_args *uap)
{
int input = uap->value;
int result;
printf("mysyscall called with value=%d\n", input);
/* 何か処理(ここでは2倍にするだけ) */
result = input * 2;
/* 戻り値を設定 */
td->td_retval[0] = result;
return (0); /* エラーなし */
}
/* システムコール番号(未使用の番号を使う) */
/* 210番は nosys (未実装) なので使える */
#define MYSYSCALL_NUM 210
/* 元のシステムコールエントリ(復元用) */
static struct sysent old_sysent;
static int syscall_num = MYSYSCALL_NUM;
/* 新しいシステムコールエントリ */
static struct sysent mysyscall_sysent = {
.sy_narg = 1, /* 引数の数 */
.sy_call = (sy_call_t *)mysyscall,
.sy_auevent = AUE_NULL,
.sy_flags = SYF_CAPENABLED,
.sy_thrcnt = SY_THR_STATIC,
};
static int
mysyscall_modevent(module_t mod, int event, void *arg)
{
int error = 0;
switch (event) {
case MOD_LOAD:
/* 元のエントリを保存 */
old_sysent = sysent[syscall_num];
/* 新しいエントリを設定 */
sysent[syscall_num] = mysyscall_sysent;
printf("mysyscall loaded at syscall number %d\n", syscall_num);
break;
case MOD_UNLOAD:
/* 使用中かチェック */
if (old_sysent.sy_call != (sy_call_t *)nosys) {
/* 元が nosys じゃないなら復元 */
printf("Warning: overwriting existing syscall\n");
}
/* 元に戻す */
sysent[syscall_num] = old_sysent;
printf("mysyscall unloaded\n");
break;
default:
error = EOPNOTSUPP;
break;
}
return (error);
}
static moduledata_t mysyscall_mod = {
"mysyscall",
mysyscall_modevent,
NULL
};
DECLARE_MODULE(mysyscall, mysyscall_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
Makefile
KMOD= mysyscall
SRCS= mysyscall.c
.include <bsd.kmod.mk>
ビルド&ロード
make
kldload ./mysyscall.ko
mysyscall loaded at syscall number 210
ユーザー空間から呼び出す
vi test_mysyscall.c
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>
#define MYSYSCALL_NUM 210
int main(void)
{
int input = 42;
int result;
printf("Calling mysyscall(%d)\n", input);
/* システムコール呼び出し */
result = syscall(MYSYSCALL_NUM, input);
printf("Result: %d\n", result);
return 0;
}
cc -o test_mysyscall test_mysyscall.c
./test_mysyscall
Calling mysyscall(42)
Result: 84
dmesgで確認:
dmesg | tail -1
# mysyscall called with value=42
方法2: カーネルソースに直接追加
本格的にシステムコールを追加するなら、カーネルソースを編集する。
syscalls.masterを編集
vi /usr/src/sys/kern/syscalls.master
ファイル末尾に追加:
; カスタムシステムコール
600 AUE_NULL STD {
int my_custom_syscall(
int value,
char *message
);
}
実装ファイルを作成
vi /usr/src/sys/kern/sys_mycustom.c
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/sysproto.h>
#include <sys/kernel.h>
#include <sys/proc.h>
#ifndef _SYS_SYSPROTO_H_
struct my_custom_syscall_args {
int value;
char *message;
};
#endif
int
sys_my_custom_syscall(struct thread *td, struct my_custom_syscall_args *uap)
{
char msg[256];
int error;
/* ユーザー空間からメッセージをコピー */
error = copyinstr(uap->message, msg, sizeof(msg), NULL);
if (error) {
return (error);
}
printf("my_custom_syscall: value=%d, message='%s'\n",
uap->value, msg);
/* 処理結果を戻り値に設定 */
td->td_retval[0] = uap->value * 3;
return (0);
}
Makefileに追加
vi /usr/src/sys/conf/files
末尾に追加:
kern/sys_mycustom.c standard
カーネルを再ビルド
cd /usr/src
# syscalls.masterから自動生成
make -C sys/kern sysent
# カーネルビルド
make buildkernel KERNCONF=GENERIC
make installkernel KERNCONF=GENERIC
reboot
呼び出しテスト
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>
#define MY_CUSTOM_SYSCALL 600
int main(void)
{
int result;
result = syscall(MY_CUSTOM_SYSCALL, 10, "Hello from userspace!");
printf("Result: %d\n", result);
return 0;
}
エラーハンドリング
システムコールでエラーを返す方法。
#include <sys/errno.h>
int
sys_my_syscall(struct thread *td, struct my_syscall_args *uap)
{
/* 引数チェック */
if (uap->value < 0) {
return (EINVAL); /* Invalid argument */
}
/* 権限チェック */
if (priv_check(td, PRIV_ROOT)) {
return (EPERM); /* Operation not permitted */
}
/* メモリ不足 */
void *ptr = malloc(1024, M_TEMP, M_NOWAIT);
if (ptr == NULL) {
return (ENOMEM); /* Cannot allocate memory */
}
/* ... */
return (0); /* 成功 */
}
ユーザー空間とのデータ転送
copyin/copyout
/* ユーザー空間 → カーネル */
char kernel_buf[256];
error = copyin(uap->user_ptr, kernel_buf, len);
if (error) {
return (error);
}
/* カーネル → ユーザー空間 */
error = copyout(kernel_buf, uap->user_ptr, len);
if (error) {
return (error);
}
copyinstr(文字列)
char str[256];
size_t done;
error = copyinstr(uap->str, str, sizeof(str), &done);
if (error) {
return (error);
}
printf("String: %s (len=%zu)\n", str, done);
fubyte/subyte(1バイト)
int byte = fubyte(uap->ptr);
if (byte == -1) {
return (EFAULT);
}
error = subyte(uap->ptr, 0x42);
if (error) {
return (EFAULT);
}
複数の戻り値
FreeBSDではtd_retval[0]とtd_retval[1]で2つの値を返せる。
int
sys_my_syscall(struct thread *td, struct my_syscall_args *uap)
{
/* 2つの値を返す */
td->td_retval[0] = 100; /* 1つ目 */
td->td_retval[1] = 200; /* 2つ目 */
return (0);
}
ユーザー空間では:
#include <sys/types.h>
/* pipe(2)のような2値を返すシステムコール */
int fds[2];
syscall(SYS_pipe, fds);
/* fds[0] = td_retval[0], fds[1] = td_retval[1] */
セキュリティ考慮事項
権限チェック
#include <sys/priv.h>
/* rootかチェック */
if (priv_check(td, PRIV_ROOT)) {
return (EPERM);
}
/* 特定の権限をチェック */
if (priv_check(td, PRIV_NET_RAW)) {
return (EPERM);
}
Capsicum対応
/* Capability Modeでも動作するか */
static struct sysent mysyscall_sysent = {
.sy_flags = SYF_CAPENABLED, /* Capabilityモードで許可 */
/* ... */
};
デバッグ
ktrace/kdump
# システムコールをトレース
ktrace -t c ./test_mysyscall
kdump
2345 test_mysyscall CALL mysyscall(0x2a)
2345 test_mysyscall RET mysyscall 84
dtraceで追跡
dtrace -n 'syscall::mysyscall:entry { printf("pid=%d, arg=%d", pid, arg0); }'
まとめ
FreeBSDでカスタムシステムコールを作る方法:
- モジュール方式: 既存のsysentを上書き(開発・テスト向け)
- カーネル直接編集: syscalls.masterに追加(本番向け)
重要なポイント:
-
copyin/copyoutでユーザー空間とデータ転送 -
td->td_retvalで戻り値を設定 - エラーは
EINVAL等のerrnoを返す - 権限チェックを忘れずに
次回予告
Part5: DTraceでカーネルを丸裸にする
DTraceはSun(現Oracle)が開発した最強のトレースツール。カーネル内部で何が起きているか、リアルタイムで可視化できる。パフォーマンス分析やデバッグに必須のツールを使いこなそう。
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!