2.4. カーネルスタックの拡張
自作プロトコルスタックを移植する前に、カーネルスタックのサイズを拡張します。
xv6は1プロセスあたり「1ページ(4KB)」のカーネルスタックを割り当てています。しかし自作プロトコルスタックは、送受信のたびに「ソケット層 → UDP/TCP → IP → デバイスドライバ」と深い関数呼び出しを重ね、各層がパケット組み立て用のバッファをローカル変数(スタック)に確保します。さらにログ出力(debugf等)も整形用のバッファをスタックに積みます。これらを合わせると 4KB を容易に超え、スタックが溢れてカーネルがクラッシュしてしまいます。
そこで、あらかじめカーネルスタックのサイズを「4ページ(16KB)」に拡張しておきます。
Important
xv6には2種類のカーネルスタックがあり、両方を拡張する必要があります。
- プロセスごとのカーネルスタック: プロセスがシステムコールを実行するときに使われます
- CPUごとのスタック(
stack0): スケジューラや、CPUがアイドル状態のときに発生した割り込みの処理に使われますパケットの送受信は、システムコール(例:
sendto)からの送信と、割り込み駆動の受信・応答の両方で発生します。前者はプロセスのカーネルスタック、後者はCPUがアイドルならCPUごとのスタック上で走るため、これら両方を拡張する必要があります。
プロセスごとのカーネルスタックの拡張
プロセスのカーネルスタックは、kernel/memlayout.hのKSTACKマクロで配置が決められています。スタックのページ数をKSTACK_PAGESとして定義し、スロットの間隔を「スタックNページ+ガードページ1枚」に変更します。
📝 kernel/memlayout.h
...
// map kernel stacks beneath the trampoline,
// each surrounded by invalid guard pages.
-#define KSTACK(p) (TRAMPOLINE - ((p) + 1) * 2 * PGSIZE)
+#define KSTACK_PAGES 4
+#define KSTACK(p) (TRAMPOLINE - ((p) + 1) * (KSTACK_PAGES + 1) * PGSIZE)
...
Note
ガードページ(未マップの1ページ)はスタックの直下に残るので、スタックがあふれて下限を突き抜けるとページフォルトで検出できる、というxv6の安全機構はそのまま維持されます。
kernel/proc.cのproc_mapstacks()で、スタック領域をKSTACK_PAGES枚分マッピングします。kalloc()は4KBページを1枚ずつ返しますが、物理的には非連続でも、連続した仮想アドレスへマップすればスタックとして機能します。
📝 kernel/proc.c
...
void
proc_mapstacks(pagetable_t kpgtbl)
{
struct proc *p;
for (p = proc; p < &proc[NPROC]; p++) {
- char *pa = kalloc();
- if (pa == 0)
- panic("kalloc");
- uint64 va = KSTACK((int)(p - proc));
- kvmmap(kpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
+ for (int j = 0; j < KSTACK_PAGES; j++) {
+ char *pa = kalloc();
+ if (pa == 0)
+ panic("kalloc");
+ uint64 va = KSTACK((int)(p - proc)) + j * PGSIZE;
+ kvmmap(kpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
+ }
}
}
...
スタックポインタの初期値も、スタック頂上(KSTACK_PAGES枚上)を指すように変更します。ここで注意が必要なのは、プロセスのカーネルスタックポインタが「2ヶ所」で設定されている点です。
1つ目はkernel/proc.cのallocproc()で、これはカーネルスレッド(forkret)に切り替わるときに使われます。
📝 kernel/proc.c
...
static struct proc *
allocproc(void)
{
...
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
- p->context.sp = p->kstack + PGSIZE;
+ p->context.sp = p->kstack + KSTACK_PAGES * PGSIZE;
return p;
}
...
2つ目はkernel/trap.cのprepare_return()で、こちらは「ユーザプロセスがシステムコールなどでカーネルに入るとき」に使われます。ここを直し忘れると、sendto()のようなシステムコール経由の送信だけが1ページのスタックで走ってしまい、溢れます。
📝 kernel/trap.c
void
prepare_return(void)
{
...
p->trapframe->kernel_satp = r_satp(); // kernel page table
- p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
+ p->trapframe->kernel_sp = p->kstack + KSTACK_PAGES * PGSIZE; // process's kernel stack
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()
...
}
CPUごとのスタックの拡張
CPUごとのスタックstack0はkernel/start.cで宣言されています。1CPUあたりのサイズをKSTACK_PAGES倍(4倍)にします。
📝 kernel/start.c
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "defs.h"
void main();
void timerinit();
// entry.S needs one stack per CPU.
-__attribute__((aligned(16))) char stack0[4096 * NCPU];
+__attribute__((aligned(16))) char stack0[4096 * KSTACK_PAGES * NCPU];
...
stack0の配列を大きくするだけでは足りません。各CPUのスタックポインタはkernel/entry.Sで「stack0 + (hartid + 1) × 4096」と計算されており、1CPUあたりの間隔が4096バイト固定になっています。ここも合わせます。
📝 kernel/entry.S
# set up a stack for C.
# stack0 is declared in start.c,
# with a 4096-byte stack per CPU.
# sp = stack0 + ((hartid + 1) * 4096)
la sp, stack0
- li a0, 1024*4
+ li a0, 1024*4*4
csrr a1, mhartid
addi a1, a1, 1
mul a0, a0, a1
add sp, sp, a0
動作確認
再ビルドして、これまでどおりxv6が起動することを確認します。
$ make qemu
見た目の動作は変わりませんが、これでカーネルスタックに 16KB の余裕ができ、この先の自作プロトコルスタックが深い呼び出しやパケットバッファをスタック上に確保しても溢れなくなります。