1000行で作るOS - C標準ライブラリ

  1. はじめに
  2. 開発環境
  3. RISC-V入門
  4. OSの全体像
  5. ブート
  6. Hello World!
  7. C標準ライブラリ
  8. カーネルパニック
  9. 例外処理
  10. メモリ割り当て
  11. プロセス
  12. ページテーブル
  13. アプリケーション
  14. ユーザーモード
  15. システムコール
  16. ディスク読み書き
  17. ファイルシステム
  18. おわりに

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, ...);

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

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

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

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

メモリ操作

メモリ操作関数を実装しましょう。

memcpy関数はsrcからnバイト分をdstにコピーします。

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

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

common.c
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");