forkシステムコールを作る
UNIX系OSの仲間入りを果たしたい
x86アーキテクチャだと、フラグレジスタの値を取り出す場合の専用の命令があるんだ。知らなかった。
pushfq : フラグレジスタの内容をスタックにプッシュする
今現在のコンテキストを取る必要がある気がするので、取得しておく。
でも引数使うとrdiレジスタの値を取得できないから困ったな。(いったん保留にして後で治す)
global get_current_context
get_current_context:
pushfq
pop qword [rdi + 0x10] ; save RFLAGS
mov [rdi + 0x40], rax
mov [rdi + 0x48], rbx
mov [rdi + 0x50], rcx
mov [rdi + 0x58], rdx
; mov [rdi + 0x60], rdi ; rdi is the first argument
mov [rdi + 0x68], rsi
mov [rdi + 0x70], rsp
mov [rdi + 0x78], rbp
mov [rdi + 0x80], r8
mov [rdi + 0x88], r9
mov [rdi + 0x90], r10
mov [rdi + 0x98], r11
mov [rdi + 0xa0], r12
mov [rdi + 0xa8], r13
mov [rdi + 0xb0], r14
mov [rdi + 0xb8], r15
fxrstor [rdi + 0xC0]
mov rax, cr3
mov [rdi + 0x00], rax
mov rax, [rsp] ; save RIP
mov [rdi + 0x08], rax
lea rax, [rsp + 8] ; save stack pointer
mov [rdi + 0x70], rax
mov ax, cs
mov [rdi + 0x20], rax
mov ax, ss
mov [rdi + 0x28], rax
mov ax, fs
mov [rdi + 0x30], rax
mov ax, gs
mov [rdi + 0x38], rax
ret
このスタックポインタの0x300の調整のせいか思うようにchildプロセスにjumpできない
なんだろこれ
001DF790: 55 push rbp
001DF791: 48 89 E5 mov rbp, rsp
001DF794: 48 81 EC 00 03 00 00 sub rsp, 0x300
.
.
.
001DF929: 48 81 C4 00 03 00 00 add rsp, 0x300
001DF930: 5D pop rbp
001DF931: C3 ret
あ、ただローカル変数開放してるだけか
001DF929: 48 81 C4 00 03 00 00 add rsp, 0x300
何でここでおかしくなるんや
子プロセスのスタックをうまく設定できてないな
childはparentのカーネルスタックをコピーしている。
parentの現在のrspをそのままchildのrspに設定すると、parentのスタックを指してしまうはず。なので、parentのスタックの底からrspを引いて距離を求めて、
child_stack_bottom - (parent_stack_bottom - current_rsp)
をchildのrspに設定すればよいはず。
下記をrspに設定したらちゃんとsys_forkからreturnできた!
size_t stack_size = t->stack.size();
child->stack.resize(stack_size);
memcpy(child->stack.data(), t->stack.data(), stack_size * sizeof(uint64_t));
uint64_t stack_end = reinterpret_cast<uint64_t>(&t->stack[stack_size]);
uint64_t rsp_elapsed = stack_end - current_ctx.rsp;
uint64_t rbp_elapsed = stack_end - current_ctx.rbp;
child->ctx.rsp = reinterpret_cast<uint64_t>(&child->stack[stack_size]) - rsp_elapsed;
child->ctx.rbp = reinterpret_cast<uint64_t>(&child->stack[stack_size]) - rbp_elapsed;
今度はsysretでエラー
pop r11 ; restore RFLAGS
pop rcx ; restore RIP
pop rbp
o64 sysret
復帰するフラグレジスタとripの値がおかしくなってるから起きてる模様
childのページテーブルがうまく設定されてないのかな
cr3の値はちゃんと切り替わっている
parent
memory read --size 4 --format x --count 20 $rsp
0xffffffffffffefb8: 0x00000039 0x00000000 0x00000282 0x00000000
0xffffffffffffefc8: 0x00002457 0xffff8000 0xffffeff0 0xffffffff
0xffffffffffffefd8: 0x0000201b 0xffff8000 0x00000000 0x00000000
0xffffffffffffefe8: 0x00000000 0x00000000 0x018e3420 0x00000000
0xffffffffffffeff8: 0x00000000 0x00000000 0xfffff100 0xffffffff
child
memory read --size 4 --format x --count 20 $rsp
0xffffffffffffefb8: 0x00000000 0x00000000 0xffffeff0 0xffffffff
0xffffffffffffefc8: 0x000024f6 0xffff8000 0x00000000 0x00000000
0xffffffffffffefd8: 0x00002049 0xffff8000 0x00000000 0x00000000
0xffffffffffffefe8: 0x00000005 0x00000000 0x018e3420 0x00000000
0xffffffffffffeff8: 0x00000000 0x00000000 0xfffff100 0xffffffff
ところどころスタックの値が一致してるから余計意味わからん
CoWの設定はできていそうなんだよな
読み取り専用のメモリに書き込もうとしたとき、本来のCoWではPFのハンドラを使って物理フレームを遅延作成するはずだが、なぜかPFのFaultHandlerすら呼ばれずにクラッシュしている状態
mov rsp, rbp ; restore stack pointer
push 1
add rsp, 8
ユーザーのスタックに切り替えたところで、適当な値をスタックにpush(つまり書き込み操作)すると、
PFは出ずに、0x0000FFF2というよくわからん場所に飛ばされる
TLBが残ってて前のページに設定されてた物理フレームに飛ばされてるのかな
仮想アドレスは同一なわけだしありえそう
Intel® 64 and IA-32 Architectures Software Developer’s Manualから参照
Invalidates any translation lookaside buffer (TLB) entries specified with the source operand. The source operand is
a memory address. The processor determines the page that contains that address and flushes all TLB entries for
that page.1
The INVLPG instruction is a privileged instruction. When the processor is running in protected mode, the CPL must
be 0 to execute this instruction.
The INVLPG instruction normally flushes TLB entries only for the specified page; however, in some cases, it may
flush more entries, even the entire TLB. The instruction invalidates TLB entries associated with the current PCID
and may or may not do so for TLB entries associated with other PCIDs. (If PCIDs are disabled — CR4.PCIDE = 0 —
the current PCID is 000H.) The instruction also invalidates any global TLB entries for the specified page, regardless
of PCID.
For more details on operations that flush the TLB, see “MOV—Move to/from Control Registers” in the Intel® 64 and
IA-32 Architectures Software Developer’s Manual, Volume 2B, and Section 4.10.4.1, “Operations that Invalidate
TLBs and Paging-Structure Caches,” in the Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume
3A.
This instruction’s operation is the same in all non-64-bit modes. It also operates the same in 64-bit mode, except
if the memory address is in non-canonical form. In this case, INVLPG is the same as a NOP.
INVLPGしても効果なかった
書き込みができなかったのは、cr0のwpフラグが0になっていなかったからか
CRO
システムの基本的なフラグの設定をするレジスタ。ページングの有効/無効、メモリアクセスのキャッシュの有効/無効、WPの有効/無効などの設定を行う。
WP(Write Protect)が有効の場合、カーネルモードは書き込み不可のユーザープロセスのメモリに書き込みを行うことはできないが、無効の場合は書き込み無効であってもカーネルモードから書き込みができるようになる。
CR2
ページフォルトしたアドレスを格納するレジスタ。PF時にはこの値をもとにいろいろな操作を行う。
CR3
ページテーブルの先頭アドレスを格納するレジスタ。これを切り替えることによってプロセスごとに独立したメモリ空間を実現できる。
ちなみに、CR3を書き換えたら自動的にすべてのTLBがフラッシュされるから、すべてのTLBをflushしたいときは以下のような操作をすればよさそう
mov rax, cr3
mov cr3, rax
WPフラグが悪さしてたことは分かったけど、sysretで起こるエラーはまだ治ってない
あー分かったかもしれない
まだ試してないから仮説でしかないけど、現状の挙動は以下のようになってると推測できる
childはparentからfork実行後の状態のメモリをコピーする。(CoW)
↓
parentは処理を進める。進めることによってparentのスタックの値やそのほかのメモリに入っているデータいろいろ変わる。
↓
その後childはfork直後から再開しようとするが、parentのメモリ(childが参照しているデータ)はfork実行から進んだ状態になっているためデータの不整合が起き意図しない挙動になる
これでいけるのではなかろうか
理想
parentプロセスの現在の状態のメモリをどこかに読み取り専用でコピーしておく。
childは読み取り専用のデータを参照するようにする
↓
parentは処理を進める。進めることによってparentのスタックの値やそのほかのメモリに入っているデータいろいろ変わるが読み取り専用のデータはfork実行直後のままになっている。
↓
その後childはfork直後から再開しようとするとPFが起きる。PFが起きれば読み取りデータからコピーすればok
やっぱそうだったわ。
int main(void)
{
int pid = sys_fork();
if (pid == 0) {
printf("Child process");
} else {
printf("Parent process : child pid = %d", pid);
}
exit(0);
}
実行結果
荒削りだけどとりあえずできた
ちょっとブラッシュアップしたら久しぶりに記事にでもまとめるかー