panda's tech note

Advent Calendar 2018: advos

Day 13: ユーティリティ関数

そろそろデバッグが大変になってきたのでmemcmp()memcpy()snprintf()のカーネル版を作ります。これらはヘッダファイルsrc/kernel/kernel.hに定義します。

Calling Convention

いくつかの関数はアセンブリに定義します。C言語からアセンブリのコードを呼ぶとき,引数や返り値などの呼び出し規則が存在します。その呼び出し規則を Calling Convention と呼びます。これまでもhlt()などいくつかのアセンブリで書かれた関数を読んでいましたが,引数がなかったためそれほど問題になりませんでしたが,これ以降は引数や返り値を扱うため,ここで説明します。

Calling ConventionはCPUアーキテクチャとOSにより異なります。今回は,CPUはx86-64,コンパイラとしてgccを使っているため,x86-64のBSD,Linux,OSXなどで使われているSystem V AMD ABIのCalling Conventionを説明します。

整数の場合,

  • 引数(左から第一引数):%rdi, %rsi, %rdx, %rcx, %r8, %r9
  • 返り値:%rax, %rdx のレジスタを使います。7個以上の引数がある場合は,最終引数から順にスタックに積まれます。また,スタックポインタは16バイトでアライメントを取ります。これはMOVDQA命令のようにアライメントを必要とする命令に対応するためです。また,スタックの128バイト分は red zone と呼ばれており,この領域は%rspを変更することなく局所変数などで使われる可能性があります。そのため,例外ハンドラや割り込みハンドラなどでこの領域が書き換わらないようにする必要があります。

関数内で値の変わる可能性のあるレジスタは,%rax, %rdi, %rsi, %rdx, %rcx, %r8, %r9, %r10, %r11の9つです。これらのレジスタは関数呼び出し後に値が変わる可能性があるので,アセンブリからC言語の関数を呼ぶ場合には,呼び出し側でこれらのレジスタを保持するか,使わないようにする必要があります。逆に関数呼び出しで値が保持されるレジスタは%rbx, %rsp, %rbp, %r12, %r13, %r14, %r15の7つです。C言語から呼ばれるアセンブリでの関数はこれらの値をスタックなどに退避して保持する必要があります。

kmemset

kmemsetmemset(3)と同一の機能を提供します。実は11日目に実装していましたが説明しませんでした。実体はsrc/kernel/x86_64/asm.Sに定義しています。x86-64のCalling Conventionに従って,第一引数(%rdi)のメモリ領域を第二引数(%rsi)の値で第三引数(%rdx)バイト分初期化します。rep stosb%alの値を(%rdi)から%rcxバイト分繰り返し書き込む命令です。

_kmemset:
    pushq	%rdi
    movl	%esi,%eax	/* c */
    movq	%rdx,%rcx	/* len */
    cld			/* Ensure the DF cleared */
    rep	stosb		/* Set %al to (%rdi)-(%rdi+%rcx) */
    popq	%rdi
    movq	%rdi,%rax	/* Restore for the return value */
    ret

kmemcmp

kmemcmpmemcmp(3)と同一の機能を提供します。こちらもsrc/kernel/x86_64/asm.Sに以下のように定義しています。

_kmemcmp:
    xorq	%rax,%rax
    movq	%rdx,%rcx	/* n */
    cld			/* Ensure the DF cleared */
    repe	cmpsb		/* Compare byte at (%rsi) with byte at (%rdi) */
    jz	1f
    decq	%rdi		/* rollback one */
    decq	%rsi		/* rollback one */
    movb	(%rdi),%al	/* *s1 */
    subb	(%rsi),%al	/* *s1 - *s2 */
1:
    ret

kmemcpy

kmemcpymemcpy(3)と同一の機能を提供します。こちらもsrc/kernel/x86_64/asm.Sに以下のように定義しています。memcpy(3)なので,第一,第二引数で示す領域がオーバーラップしている場合に正しく対応していません。

_kmemcpy:
    movq	%rdi,%rax	/* Return value */
    movq	%rdx,%rcx	/* n */
    cld			/* Ensure the DF cleared */
    rep	movsb		/* Copy byte at (%rsi) to (%rdi) */
    ret

ksnprintf/kvsnprintf

ksnprintfkvsnprintfを実装します。こちらはsnprintf(3)vsnprintf(3)と完全に互換ではありません。実装はsrc/kernel/strfmt.cを参照してください。

ksnprintfでは可変長引数を使います。また,kvsnprintfは,可変長引数変数va_listを引数として取ります。上述した通り,System V AMD ABI (x86-64)のCalling Conventionでは,引数はレジスタおよびスタックを使って表現されます。つまり,可変長の引数から値を取り出すva_arg()などは標準ライブラリの関数ではなく,コンパイラがこの引数の保存されているレジスタ・スタックからの値の取り出す処理をアセンブリで出力するためのマクロです。このマクロはビルトインマクロとして定義されているため,src/kernel/kernel.hで以下の通り定義します。これにより,通常のC言語のプログラムと同様に,可変長引数を扱うことができるようになります。

typedef __builtin_va_list va_list;
#define va_start(ap, last)      __builtin_va_start((ap), (last))
#define va_arg                  __builtin_va_arg
#define va_end(ap)              __builtin_va_end(ap)
#define va_copy(dest, src)      __builtin_va_copy((dest), (src))
#define alloca(size)            __builtin_alloca((size))

まとめと明日の予定

今日はメモリ操作関数であるmemsetmemcmpmemcpyのカーネル用関数とsnprintfvsnprintfの簡易版カーネル用関数をユーティリティ関数を実装しました。明日はマルチコアに移っていこうと思いますが予定は未定です。