Closed29

forkシステムコールを作る

junyaUjunyaU

x86アーキテクチャだと、フラグレジスタの値を取り出す場合の専用の命令があるんだ。知らなかった。

pushfq : フラグレジスタの内容をスタックにプッシュする

junyaUjunyaU

今現在のコンテキストを取る必要がある気がするので、取得しておく。
でも引数使うと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
junyaUjunyaU

このスタックポインタの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
junyaUjunyaU

あ、ただローカル変数開放してるだけか

001DF929: 48 81 C4 00 03 00 00       add    rsp, 0x300

何でここでおかしくなるんや

junyaUjunyaU

子プロセスのスタックをうまく設定できてないな

junyaUjunyaU

childはparentのカーネルスタックをコピーしている。
parentの現在のrspをそのままchildのrspに設定すると、parentのスタックを指してしまうはず。なので、parentのスタックの底からrspを引いて距離を求めて、
child_stack_bottom - (parent_stack_bottom - current_rsp)
をchildのrspに設定すればよいはず。

junyaUjunyaU

下記を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;
junyaUjunyaU
    pop r11 ; restore RFLAGS
    pop rcx ; restore RIP
    pop rbp
    o64 sysret

復帰するフラグレジスタとripの値がおかしくなってるから起きてる模様

junyaUjunyaU

childのページテーブルがうまく設定されてないのかな

junyaUjunyaU

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
junyaUjunyaU

ところどころスタックの値が一致してるから余計意味わからん

junyaUjunyaU

読み取り専用のメモリに書き込もうとしたとき、本来のCoWではPFのハンドラを使って物理フレームを遅延作成するはずだが、なぜかPFのFaultHandlerすら呼ばれずにクラッシュしている状態

junyaUjunyaU
    mov rsp, rbp ; restore stack pointer

    push 1
    add rsp, 8

ユーザーのスタックに切り替えたところで、適当な値をスタックにpush(つまり書き込み操作)すると、
PFは出ずに、0x0000FFF2というよくわからん場所に飛ばされる

junyaUjunyaU

TLBが残ってて前のページに設定されてた物理フレームに飛ばされてるのかな
仮想アドレスは同一なわけだしありえそう

junyaUjunyaU

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.
junyaUjunyaU

書き込みができなかったのは、cr0のwpフラグが0になっていなかったからか

junyaUjunyaU

CRO

システムの基本的なフラグの設定をするレジスタ。ページングの有効/無効、メモリアクセスのキャッシュの有効/無効、WPの有効/無効などの設定を行う。
WP(Write Protect)が有効の場合、カーネルモードは書き込み不可のユーザープロセスのメモリに書き込みを行うことはできないが、無効の場合は書き込み無効であってもカーネルモードから書き込みができるようになる。

CR2

ページフォルトしたアドレスを格納するレジスタ。PF時にはこの値をもとにいろいろな操作を行う。

CR3

ページテーブルの先頭アドレスを格納するレジスタ。これを切り替えることによってプロセスごとに独立したメモリ空間を実現できる。
ちなみに、CR3を書き換えたら自動的にすべてのTLBがフラッシュされるから、すべてのTLBをflushしたいときは以下のような操作をすればよさそう

mov rax, cr3
mov cr3, rax
junyaUjunyaU

WPフラグが悪さしてたことは分かったけど、sysretで起こるエラーはまだ治ってない

junyaUjunyaU

まだ試してないから仮説でしかないけど、現状の挙動は以下のようになってると推測できる

childはparentからfork実行後の状態のメモリをコピーする。(CoW)

parentは処理を進める。進めることによってparentのスタックの値やそのほかのメモリに入っているデータいろいろ変わる。

その後childはfork直後から再開しようとするが、parentのメモリ(childが参照しているデータ)はfork実行から進んだ状態になっているためデータの不整合が起き意図しない挙動になる

junyaUjunyaU

これでいけるのではなかろうか

理想
parentプロセスの現在の状態のメモリをどこかに読み取り専用でコピーしておく。
childは読み取り専用のデータを参照するようにする

parentは処理を進める。進めることによってparentのスタックの値やそのほかのメモリに入っているデータいろいろ変わるが読み取り専用のデータはfork実行直後のままになっている。

その後childはfork直後から再開しようとするとPFが起きる。PFが起きれば読み取りデータからコピーすればok

junyaUjunyaU

やっぱそうだったわ。

int main(void)
{
	int pid = sys_fork();

	if (pid == 0) {
		printf("Child process");
	} else {
		printf("Parent process : child pid = %d", pid);
	}

	exit(0);
}

実行結果

junyaUjunyaU

ちょっとブラッシュアップしたら久しぶりに記事にでもまとめるかー

このスクラップは29日前にクローズされました