2.5. 移植の土台作り
型定義・コンソール出力・時刻と足回りが揃ったので、自作プロトコルスタックのコードを受け入れるための土台を作ります。
ここでの大方針は「プロトコルスタック本体のコードはできる限り書き換えない」です。本体を書き換える代わりに、xv6の側に「本体が期待している環境」を用意して差を吸収します。ディレクトリ構成にもその方針がそのまま現れます。
kernel/net/ ... プロトコルスタック本体(コードをそのまま配置)
kernel/net/platform/xv6-riscv/ ... プラットフォーム依存コード(xv6向けの差分はすべてここへ)
kernel/net/platform/xv6-riscv/libc/ ... 簡易libc(標準ライブラリの肩代わり)
この章では、簡易libcと便利ライブラリ(util.c、util.h)まで移植し、ログ出力が動くところまで確認します。
2.5.1. ディレクトリ構成とMakefile
ディレクトリの作成
上記のディレクトリを一括で作成します(libcの下にはシステムヘッダ用のsysサブディレクトリも必要です)。
$ mkdir -p kernel/net/platform/xv6-riscv/libc/sys
Makefileの修正
新しいディレクトリをMakefileに組み込みます。パスを表す変数(N、P、L)を定義し、インクルードパス・依存関係ファイルの取り込み・make cleanの対象に追加します。
📝 Makefile
K=kernel
U=user
+N=$K/net
+P=$N/platform/xv6-riscv
+L=$P/libc
...
CFLAGS += -fno-builtin-printf -fno-builtin-fprintf -fno-builtin-vprintf
-CFLAGS += -I.
+CFLAGS += -I. -I $K -I $N -I $P -I $L
CFLAGS += $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)
...
--include kernel/*.d user/*.d
+-include $K/*.d $U/*.d $N/*.d $P/*.d $L/*.d
clean:
rm -f *.tex *.dvi *.idx *.aux *.log *.ind *.ilg \
*/*.o */*.d */*.asm */*.sym \
+ $N/*.o $N/*.d $P/*.o $P/*.d $L/*.o $L/*.d \
$K/kernel fs.img \
mkfs/mkfs .gdbinit \
$U/usys.S \
$(UPROGS)
Important
-Iで指定したディレクトリは、#include "..."だけでなく#include <...>(山括弧)の検索でも、標準のシステムディレクトリより先に探索されます。「-I $Lを指定しておくと、<stdio.h>のような標準ヘッダの参照がlibcディレクトリのファイルで解決される」というこの性質が、次の簡易libcの仕掛けの核心です。あわせて追加した-I $Kにより、<types.h>や<time.h>がxv6のkernel/types.hとkernel/time.hに解決されるようになります。
Note
-includeは、先頭の-が付いていない通常のincludeと違い「指定したファイルが存在しなくてもエラーにせず読み飛ばす」ディレクティブです。ここで取り込んでいる*.dは、コンパイル時にGCCが自動生成するヘッダ依存関係ファイルで、「どの.oがどのヘッダに依存しているか」が書かれています。これを取り込むことで、ヘッダを変更したときに関連する.oだけが再ビルドされるようになります。ただし一度もビルドしていない初回は.dがまだ存在しないため、通常のincludeだとエラーで止まってしまいます。-includeにしておくことで、初回は黙ってスキップし、生成された.dが2回目以降のビルドで効くようになります。今回はディレクトリを追加したので、その配下の.dも取り込み対象に加えています。
2.5.2. 簡易libcの実装
自作プロトコルスタックのコードは、通常のC言語プログラムとして<stdio.h>や<string.h>といった標準ヘッダをインクルードしています。しかし、xv6のカーネル空間に標準ライブラリはありません。
そこで、標準ヘッダと同じ名前のファイルをlibcディレクトリに配置します。前述のインクルードパスの性質により、本体のコードが#include <stdio.h>と書いたままで簡易libcのヘッダが読み込まれるため、プロトコルスタック側のコードを書き換えずにビルドが通るようになります。
なお、ここで作成するファイルの中身は「移植対象のプロトコルスタックが実際に使っている機能」だけを最小限にまとめます。
| ファイル | 内容 |
|---|---|
stdio.h | FILE型とfprintf()などの宣言(実装は後述のstdio.c) |
stdint.h | ビット幅指定の整数型の定数(型そのものは2.1でkernel/types.hに追加済み) |
string.h | xv6のkernel/string.cにある関数のプロトタイプ宣言 |
ctype.h | isascii() / isprint() |
errno.h | errnoとEINTR |
limits.h | INT_MAXなどの定数 |
stddef.h / stdlib.h / unistd.h | ほぼ空の受け皿(インクルードされてもエラーにしないため) |
sys/types.h | kernel/types.hへの橋渡し |
sys/param.h | MAX() / MIN() |
sys/time.h | kernel/time.hへの橋渡し |
ポイントは「橋渡し」のヘッダです。たとえばsys/types.hは#include <types.h>と書いてあるだけで、これが-I $Kによってxv6のkernel/types.hに解決されます。つまり、2.1で追加した型定義がそのままプロトコルスタック側から見えるようになります。同様にsys/time.hは2.3で作ったkernel/time.hへつながります。ここまでの作業が、簡易libcを通してプロトコルスタック本体へ供給されるという構図です。
ヘッダファイルの配置
まず橋渡しと定数だけの小さなヘッダファイル群を作成します。
📝 kernel/net/platform/xv6-riscv/libc/sys/types.h
#ifndef SYS_TYPES_H
#define SYS_TYPES_H
#include <types.h>
#endif
📝 kernel/net/platform/xv6-riscv/libc/sys/param.h
#ifndef SYS_PARAM_H
#define SYS_PARAM_H
#ifndef MAX
#define MAX(x, y) ((x) > (y) ? (x) : (y))
#endif
#ifndef MIN
#define MIN(x, y) ((x) < (y) ? (x) : (y))
#endif
#endif
📝 kernel/net/platform/xv6-riscv/libc/sys/time.h
#ifndef SYS_TIME_H
#define SYS_TIME_H
#include <time.h>
#endif
📝 kernel/net/platform/xv6-riscv/libc/stddef.h
#ifndef STDDEF_H
#define STDDEF_H
#include <sys/types.h>
#endif
📝 kernel/net/platform/xv6-riscv/libc/stdint.h
#ifndef STDINT_H
#define STDINT_H
#include <sys/types.h>
#define INT8_MAX 127
#define INT8_MIN (-128)
#define UINT8_MAX 255
#define INT16_MAX 32767
#define INT16_MIN (-32768)
#define UINT16_MAX 65535
#define INT32_MAX 2147483647
#define INT32_MIN (-2147483648)
#define UINT32_MAX 4294967295U
#endif
📝 kernel/net/platform/xv6-riscv/libc/limits.h
#ifndef LIMITS_H
#define LIMITS_H
#define CHAR_BIT 8
#define INT_MAX 2147483647
#define INT_MIN (-2147483648)
#define UINT_MAX 4294967295U
#define LONG_MAX 9223372036854775807L
#define LONG_MIN (-LONG_MAX - 1L)
#endif
📝 kernel/net/platform/xv6-riscv/libc/ctype.h
#ifndef CTYPE_H
#define CTYPE_H
#define isascii(x) ((x >= 0x00) && (x <= 0x7f))
#define isprint(x) ((x >= 0x20) && (x <= 0x7e))
#endif
📝 kernel/net/platform/xv6-riscv/libc/errno.h
#ifndef ERRNO_H
#define ERRNO_H
#define EINTR 4
extern int errno;
#endif
📝 kernel/net/platform/xv6-riscv/libc/stdlib.h
#ifndef STDLIB_H
#define STDLIB_H
#include <sys/types.h>
#endif
📝 kernel/net/platform/xv6-riscv/libc/unistd.h
#ifndef UNISTD_H
#define UNISTD_H
#include <sys/types.h>
#endif
string.hにはxv6が既に持っている文字列関数(kernel/string.c)のプロトタイプ宣言を書き並べます。実装はxv6のものをそのまま利用するので、宣言だけで済みます。
📝 kernel/net/platform/xv6-riscv/libc/string.h
#ifndef STRING_H
#define STRING_H
#include "types.h"
extern int
memcmp(const void *v1, const void *v2, uint n);
extern void *
memmove(void *dst, const void *src, uint n);
extern void *
memcpy(void *dst, const void *src, uint n);
extern void *
memset(void *dst, int c, uint n);
extern int
strlen(const char *s);
extern int
strncmp(const char *p, const char *q, uint n);
extern char *
strncpy(char *s, const char *t, int n);
#endif
stdio.hではFILE型と標準入出力関数を宣言します。snprintf()とvsnprintf()の実体は2.2でprintk.cに実装済みなので、ここでは宣言だけです。
📝 kernel/net/platform/xv6-riscv/libc/stdio.h
#ifndef STDIO_H
#define STDIO_H
#include <stdarg.h>
#include <sys/types.h>
typedef struct _FILE FILE;
extern FILE *stderr;
extern int
fprintf(FILE *fp, const char *fmt, ...);
extern int
vfprintf(FILE *fp, const char *fmt, va_list ap);
extern int
snprintf(char *buf, size_t n, const char *fmt, ...);
extern int
vsnprintf(char *buf, size_t n, const char *fmt, va_list ap);
extern void
flockfile(FILE *fp);
extern void
funlockfile(FILE *fp);
#endif
標準入出力の実装
stdio.hで宣言した残りの関数(fprintf()、vfprintf()、flockfile()、funlockfile())を実装します。
FILEの実体はスピンロックと再帰カウントを持つ構造体で、出力先としてはstderrだけを用意しますvfprintf()は、2.2で作ったvsnprintf()で文字列バッファに整形してからprintk()で出力しますflockfile()/funlockfile()はロックの獲得と解放です。複数行にわたる出力が他のログと混ざらないように、本体のログ出力関数がこれで囲んでいます
Important
flockfile()は再帰ロックとして実装しています。POSIXのflockfile()は同じスレッドからの再ロックを許容する仕様で、自作プロトコルスタックもこれを前提にしています。各プロトコルの*_print()(ether_print()・ip_print()など)はflockfile()で囲んだ中から、パケットの16進ダンプのためにhexdump()を呼ぶことがあり、hexdump()もまた内部でflockfile()します。ここで単純なスピンロックを使うと同じロックの二重取得によりpanic: acquireのエラーが発生してしまうため、holding()で「すでに自分が保持しているか」を判定し、再帰時はカウントを増やすだけにしています。funlockfile()はカウントが0になったときにだけ解放します。
📝 kernel/net/platform/xv6-riscv/libc/stdio.c
#include <stdio.h>
#include <stdarg.h>
#include "param.h"
#include "riscv.h"
#include "spinlock.h"
#include "defs.h"
struct _FILE {
struct spinlock lock;
int depth; /* recursion count (POSIX flockfile allows recursive locking) */
};
static FILE _stderr;
FILE *stderr = &_stderr;
void
flockfile(FILE *fp)
{
if (holding(&fp->lock)) { /* already locked by this context: just recurse */
fp->depth++;
return;
}
acquire(&fp->lock);
fp->depth = 1;
}
void
funlockfile(FILE *fp)
{
if (--fp->depth == 0)
release(&fp->lock);
}
int
vfprintf(FILE *fp, const char *fmt, va_list ap)
{
char buf[256];
int n;
n = vsnprintf(buf, sizeof(buf), fmt, ap);
printk("%s", buf);
return n;
}
int
fprintf(FILE *fp, const char *fmt, ...)
{
va_list ap;
int n;
va_start(ap, fmt);
n = vfprintf(fp, fmt, ap);
va_end(ap);
return n;
}
Makefileの修正
stdio.cをビルド対象に追加します。
📝 Makefile
OBJS = \
...
$K/plic.o \
$K/rtc.o \
$K/time.o \
- $K/virtio_disk.o
+ $K/virtio_disk.o \
+ $L/stdio.o
2.5.3. プラットフォームヘッダの用意
プロトコルスタック本体のコードは「プラットフォーム固有の機能はplatform.hをインクルードすれば使える」という約束で書かれています。xv6向けのplatform.hを用意します。
いまの時点では、xv6の基本ヘッダを取りまとめてインクルードするだけの薄いヘッダです。メモリ操作・割り込み・スケジューラなどのプラットフォーム依存コードは、本体の移植を進める中で必要になったタイミングで追加していきます。
📝 kernel/net/platform/xv6-riscv/platform.h
#ifndef PLATFORM_H
#define PLATFORM_H
#include "types.h"
#include "riscv.h"
#include "defs.h"
#endif
2.5.4. 便利ライブラリの移植
準備が整ったので、自作プロトコルスタックの便利ライブラリ(util.c、util.h)を移植します。ログ出力・16進ダンプ・キュー・バイトオーダー変換・チェックサム計算といった、本体の全モジュールが使う道具箱です。
strftimeの追加
便利ライブラリのログ出力関数lprintf()は、タイムスタンプの整形にstrftime()を使います。2.3で作った時刻関連コードの延長として、kernel/time.cに簡易版(書式は"%T"のみ対応)を追加します。
📝 kernel/time.c
struct tm *
localtime_r(const time_t *timep, struct tm *result)
{
...
result->tm_isdst = 0;
return result;
}
+
+size_t
+strftime(char *s, size_t max, const char *format, const struct tm *tm)
+{
+ (void)format; /* only supports "%T" */
+ return snprintf(s, max, "%02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec);
+}
あわせてkernel/time.hも整備します。簡易libcのsys/time.hから<time.h>としてインクルードされ、プロトコルスタック本体からも参照されるヘッダになるので、インクルードガードを付けて時刻関連関数のプロトタイプ宣言をここに集約します。
📝 kernel/time.h
+#ifndef TIME_H
+#define TIME_H
+
+#include "types.h"
+
struct timeval {
long tv_sec;
long tv_usec;
};
...
int tm_yday; // 0-365
int tm_isdst; // zero
};
+
+extern time_t time(time_t*);
+extern int gettimeofday(struct timeval*, void*);
+extern time_t mktime(struct tm*);
+extern struct tm* localtime_r(const time_t*, struct tm*);
+extern size_t strftime(char*, size_t, const char*, const struct tm*);
+
+#endif
便利ライブラリの配置
ここからは、事前学習で開発したmicropsのコードをコピーして利用していきます。以降、手元のmicropsのディレクトリを$MICROPSと表記します。
$ export MICROPS=/path/to/microps
便利ライブラリのファイルをkernel/netディレクトリの直下にコピーします。
$ cp $MICROPS/util.{h,c} kernel/net/
コピーしたソースコードはそのまま利用できます。util.hの冒頭のインクルードが<stdio.h>のような標準ヘッダのままである点に注目してください。ここまでに用意した簡易libcのおかげで、コピーしたファイルがそのままビルドできます。
Makefileの修正
util.cをビルド対象に追加します。
📝 Makefile
OBJS = \
...
$K/rtc.o \
$K/time.o \
$K/virtio_disk.o \
+ $N/util.o \
$L/stdio.o
2.5.5. 動作確認
便利ライブラリの動作確認のために、自作プロトコルスタックのメインモジュールに相当するファイルを追加して、ログ出力と16進ダンプをテストします。
Warning
このファイルは一時的なもので後ほど正式なものに差し替えます。
📝 kernel/net/net.c
#include "platform.h"
#include "util.h"
int
net_init(void)
{
char msg[] = "Hello, SecCamp2026!";
debugf("%s", msg);
debugdump(msg, sizeof(msg));
return 0;
}
netinit()はxv6のmain()関数から呼び出すことになるので、defs.hにプロトタイプ宣言を追加しておきます。
📝 kernel/defs.h
...
// virtio_disk.c
void virtio_disk_init(void);
void virtio_disk_rw(struct buf *, int);
void virtio_disk_intr(void);
+// net/net.c
+int net_init(void);
+
// number of elements in fixed-size array
#define NELEM(x) (sizeof(x)/sizeof((x)[0]))
...
MakefileのOBJSにnet.oを追加します。
Note
16進ダンプのための
debugdump()マクロを有効にするためにCFLAGSに-DHEXDUMPも追加しておきます。
📝 Makefile
OBJS = \
...
$K/virtio_disk.o \
$N/util.o \
+ $N/net.o \
$L/stdio.o
...
-CFLAGS += -I. -I $K -I $N -I $P -I $L
+CFLAGS += -I. -I $K -I $N -I $P -I $L -DHEXDUMP
xv6のmain()関数からnet_init()を呼び出すようにします。
📝 kernel/main.c
...
virtio_disk_init(); // emulated hard disk
printdate();
+ net_init(); // network stack
userinit(); // first user process
...
再ビルドした後、make qemuを実行してxv6を起動させます。
xv6 kernel is booting
2026/08/11 10:00:00
10:00:00.123 [D] net_init: Hello, SecCamp2026! (kernel/net/net.c:10)
+------+-------------------------------------------------+------------------+
| 0000 | 48 65 6c 6c 6f 2c 20 53 65 63 43 61 6d 70 32 30 | Hello, SecCamp20 |
| 0010 | 32 36 21 00 | 26!. |
+------+-------------------------------------------------+------------------+
hart 1 starting
hart 2 starting
init: starting sh
$
Note
見覚えのある書式のログメッセージと16進ダンプが出力されるはずです。プロトコルスタック本体のコードが、簡易libc越しにxv6のカーネル内で動いた瞬間です。
お疲れさまでした、これでプロトコルスタックを移植する準備が整いました!