C標準ライブラリ

Hello Worldを済ませたところで、基本的な型やメモリ操作、文字列操作関数を実装しましょう。一般的にはC言語の標準ライブラリ (例: stdint.hstring.h) を利用しますが、今回は勉強のためにゼロから作ります。

本章で紹介するものはC言語でごく一般的なものなので、ChatGPTに聞くとしっかりと答えてくれる領域です。実装や理解に手こずる部分があった時には試してみてください。便利な時代になりましたね。

基本的な型

まずは基本的な型といくつかのマクロを定義します。

common.h
typedef int bool;
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;
typedef uint32_t size_t;
typedef uint32_t paddr_t;
typedef uint32_t vaddr_t;

#define true  1
#define false 0
#define NULL  ((void *) 0)
#define align_up(value, align)   __builtin_align_up(value, align)
#define is_aligned(value, align) __builtin_is_aligned(value, align)
#define offsetof(type, member)   __builtin_offsetof(type, member)
#define va_list  __builtin_va_list
#define va_start __builtin_va_start
#define va_end   __builtin_va_end
#define va_arg   __builtin_va_arg

void *memset(void *buf, char c, size_t n);
void *memcpy(void *dst, const void *src, size_t n);
char *strcpy(char *dst, const char *src);
int strcmp(const char *s1, const char *s2);
void printf(const char *fmt, ...);

ほとんどは標準ライブラリにあるものですが、いくつか便利なものを追加しています。

  • paddr_t: 物理メモリアドレスを表す型。
  • vaddr_t: 仮想メモリアドレスを表す型。標準ライブラリでいうuintptr_t
  • align_up: valuealignの倍数に切り上げる。alignは2のべき乗である必要がある。
  • is_aligned: valuealignの倍数かどうかを判定する。alignは2のべき乗である必要がある。
  • offsetof: 構造体のメンバのオフセット (メンバが構造体の先頭から何バイト目にあるか) を返す。

align_upis_alignedは、メモリアラインメントを気にする際に便利です。例えば、align_up(0x1234, 0x1000)0x2000を返します。また、is_aligned(0x2000, 0x1000)は真となります。

各マクロで使われている__builtin_から始まる関数はClangの独自拡張 (ビルトイン関数) です。これらの他にも、さまざまなビルトイン関数・マクロ があります。

なお、これらのマクロはビルトイン関数を使わなくても標準的なCのコードで実装することもできます。特にoffsetofの実装手法は面白いので、興味のある方は検索してみてください。

メモリ操作

次のメモリ操作関数を実装します。memcpy関数はsrcからnバイト分をdstにコピーします。

memset関数はbufの先頭からnバイト分をcで埋めます。この関数は、bssセクションの初期化のために5章で実装済みです。kernel.cからcommon.cに移動させましょう。

common.c
void *memset(void *buf, char c, size_t n) {
    uint8_t *p = (uint8_t *) buf;
    while (n--)
        *p++ = c;
    return buf;
}

void *memcpy(void *dst, const void *src, size_t n) {
    uint8_t *d = (uint8_t *) dst;
    const uint8_t *s = (const uint8_t *) src;
    while (n--)
        *d++ = *s++;
    return dst;
}

*p++ = c;のように、ポインタの間接参照とポインタの操作を一度にしている箇所がいくつかあります。わかりやすく分解すると次のようになります。C言語ではよく使われる表現です。

*p = c;    //ポインタの間接参照を行う
p = p + 1; // 代入を済ませた後にポインタを進める

文字列操作

まずは、strcpy関数です。この関数はsrcの文字列をdstにコピーします。

common.c
char *strcpy(char *dst, const char *src) {
    char *d = dst;
    while (*src)
        *d++ = *src++;
    *d = '\0';
    return dst;
}

strcpy関数はdstのメモリ領域よりsrcの方が長い時でも、dstのメモリ領域を越えてコピーを行います。バグや脆弱性に繋がりやすいため、一般的にはstrcpyではなく代替の関数を使うことが推奨されています。

本書では簡単のためstrcpyを使いますが、余力があれば代替の関数 (strcpy_s) を実装して代わりに使ってみてください。

次にstrcmp関数です。s1s2を比較します。s1s2が等しい場合は0を、s1の方が大きい場合は正の値を、s2の方が大きい場合は負の値を返します。

common.c
int strcmp(const char *s1, const char *s2) {
    while (*s1 && *s2) {
        if (*s1 != *s2)
            break;
        s1++;
        s2++;
    }

    return *(unsigned char *)s1 - *(unsigned char *)s2;
}

比較する際に unsigned char * にキャストしているのは、比較する際は符号なし整数を使うというPOSIXの仕様に合わせるためです。

strcmp関数はよく文字列が同一であるかを判定したい時に使います。若干ややこしいですが、!strcmp(s1, s2) の場合 (ゼロが返ってきた場合に) に文字列が同一になります。

if (!strcmp(s1, s2))
    printf("s1 == s2\n");
else
    printf("s1 != s2\n");