シリーズ一覧
| Part | タイトル | 内容 |
|---|---|---|
| Part1 | 令和にCでOS書く狂人の記録 | VGA出力 |
| Part2 | アセンブリとCの橋渡し | リンカスクリプト |
| Part3 | メモリ管理とmalloc | 本記事 |
| Part4 | printfを自作する | フォーマット出力 |
| Part5 | Rustで書き直したくなった | 移行検討 |
はじめに
自作OSで一番やらかしやすいところ、それがメモリ管理。
malloc して free 忘れる。ダングリングポインタを踏む。二重解放でクラッシュ。Cあるあるだよね。
今回は自作 malloc / free を実装する。そして見事にメモリリークを起こす(予定)。
なぜメモリ管理が必要?
今までの実装:
- グローバル変数(固定サイズ)
- スタック変数(関数終了で消える)
でも、こういうことしたくない?
- 「実行時に配列サイズを決めたい」
- 「関数をまたいでデータを保持したい」
- 「可変長の文字列を扱いたい」
そこでヒープメモリの出番。
ヒープの基本構造
メモリレイアウト:
┌─────────────────┐ 低アドレス
│ カーネルコード │ .text
├─────────────────┤
│ 読み取り専用 │ .rodata
├─────────────────┤
│ 初期化済みデータ │ .data
├─────────────────┤
│ BSS │ .bss
├─────────────────┤
│ │
│ ヒープ ↓ │ ← malloc で確保
│ │
│ ...空き... │
│ │
│ スタック ↑ │ ← 関数呼び出しで使用
│ │
└─────────────────┘ 高アドレス
最もシンプルなアロケータ:バンプアロケータ
まずは「確保はできるけど解放できない」やつから:
src/memory.c
#include "memory.h"
// ヒープ領域(とりあえず64KB)
#define HEAP_SIZE (64 * 1024)
static char heap[HEAP_SIZE];
static size_t heap_index = 0;
// バンプアロケータ(解放不可)
void* kmalloc_bump(size_t size) {
// アライメント(4バイト境界)
size = (size + 3) & ~3;
if (heap_index + size > HEAP_SIZE) {
return NULL; // メモリ不足
}
void* ptr = &heap[heap_index];
heap_index += size;
return ptr;
}
// 使用量を表示
void print_heap_usage(void) {
print("Heap usage: ");
print_int(heap_index);
print(" / ");
print_int(HEAP_SIZE);
print(" bytes\n");
}
シンプルだけど、解放できないから実用的じゃない。でもカーネル初期化時の一時的な確保にはこれで十分なこともある。
フリーリストアロケータ
解放もできるやつ:
src/memory.c
// メモリブロックのヘッダ
typedef struct block_header {
size_t size; // ブロックサイズ(ヘッダ含まず)
int is_free; // 空きフラグ
struct block_header* next; // 次のブロック
} block_header_t;
#define HEAP_SIZE (64 * 1024)
static char heap[HEAP_SIZE];
static block_header_t* free_list = NULL;
// 初期化
void memory_init(void) {
free_list = (block_header_t*)heap;
free_list->size = HEAP_SIZE - sizeof(block_header_t);
free_list->is_free = 1;
free_list->next = NULL;
}
malloc の実装
void* kmalloc(size_t size) {
// アライメント
size = (size + 7) & ~7;
block_header_t* current = free_list;
// First-fit: 最初に見つかった十分なブロックを使う
while (current != NULL) {
if (current->is_free && current->size >= size) {
// ブロックを分割できる?
if (current->size >= size + sizeof(block_header_t) + 8) {
// 分割
block_header_t* new_block = (block_header_t*)
((char*)current + sizeof(block_header_t) + size);
new_block->size = current->size - size - sizeof(block_header_t);
new_block->is_free = 1;
new_block->next = current->next;
current->size = size;
current->next = new_block;
}
current->is_free = 0;
return (char*)current + sizeof(block_header_t);
}
current = current->next;
}
return NULL; // メモリ不足
}
free の実装
void kfree(void* ptr) {
if (ptr == NULL) return;
block_header_t* header = (block_header_t*)
((char*)ptr - sizeof(block_header_t));
header->is_free = 1;
// 隣接する空きブロックをマージ(コアレッシング)
block_header_t* current = free_list;
while (current != NULL && current->next != NULL) {
if (current->is_free && current->next->is_free) {
// 次のブロックを吸収
current->size += sizeof(block_header_t) + current->next->size;
current->next = current->next->next;
continue; // さらにマージできるかチェック
}
current = current->next;
}
}
よくあるバグ集
1. free忘れ(メモリリーク)
void process_data(void) {
char* buffer = kmalloc(1024);
// 何か処理...
if (error) {
return; // ← free忘れてる!
}
kfree(buffer);
}
症状: 動くけどだんだんメモリが減っていく
2. 二重解放
char* ptr = kmalloc(100);
kfree(ptr);
kfree(ptr); // ← クラッシュ or ヒープ破壊
症状: 運が良ければクラッシュ、悪ければデータ破壊
3. 解放後アクセス(Use-After-Free)
char* ptr = kmalloc(100);
kfree(ptr);
ptr[0] = 'A'; // ← 解放済みメモリにアクセス!
症状: 何が起きるかわからない(最悪)
4. バッファオーバーフロー
char* ptr = kmalloc(10);
strcpy(ptr, "This is way too long!"); // ← 10バイトに収まらない
症状: 隣のブロックのヘッダを破壊 → 次のmallocでクラッシュ
デバッグ用機能
メモリダンプ
void memory_dump(void) {
print("=== Memory Dump ===\n");
block_header_t* current = free_list;
int block_num = 0;
while (current != NULL) {
print("Block ");
print_int(block_num);
print(": addr=0x");
print_hex((unsigned int)current);
print(" size=");
print_int(current->size);
print(" ");
print(current->is_free ? "[FREE]" : "[USED]");
print("\n");
current = current->next;
block_num++;
}
}
出力例:
=== Memory Dump ===
Block 0: addr=0x00104000 size=1024 [USED]
Block 1: addr=0x00104410 size=512 [FREE]
Block 2: addr=0x00104618 size=2048 [USED]
Block 3: addr=0x00104E20 size=61440 [FREE]
リーク検出
static int alloc_count = 0;
static int free_count = 0;
void* kmalloc_debug(size_t size, const char* file, int line) {
void* ptr = kmalloc(size);
if (ptr) {
alloc_count++;
print("[ALLOC] ");
print(file);
print(":");
print_int(line);
print(" size=");
print_int(size);
print("\n");
}
return ptr;
}
void kfree_debug(void* ptr, const char* file, int line) {
if (ptr) {
free_count++;
print("[FREE] ");
print(file);
print(":");
print_int(line);
print("\n");
}
kfree(ptr);
}
#define kmalloc(size) kmalloc_debug(size, __FILE__, __LINE__)
#define kfree(ptr) kfree_debug(ptr, __FILE__, __LINE__)
void check_leaks(void) {
print("Allocs: ");
print_int(alloc_count);
print(", Frees: ");
print_int(free_count);
if (alloc_count != free_count) {
print(" *** LEAK DETECTED ***");
}
print("\n");
}
テスト
void test_memory(void) {
print("=== Memory Test ===\n");
memory_init();
// 基本的な確保と解放
print("Test 1: Basic alloc/free\n");
char* p1 = kmalloc(100);
char* p2 = kmalloc(200);
char* p3 = kmalloc(300);
memory_dump();
kfree(p2);
print("After freeing p2:\n");
memory_dump();
// 解放した場所に再確保(フラグメンテーション)
print("Test 2: Reuse freed block\n");
char* p4 = kmalloc(150);
memory_dump();
// クリーンアップ
kfree(p1);
kfree(p3);
kfree(p4);
check_leaks();
}
実際のOS でのメモリ管理
本物のOSはもっと複雑:
- ページアロケータ: 4KB単位で物理メモリを管理
- スラブアロケータ: 小さいオブジェクト用(kmalloc)
- バディシステム: 連続メモリ確保の効率化
- 仮想メモリ: ページテーブルによるアドレス変換
今回作ったのは一番シンプルなフリーリスト方式。実用には程遠いけど、原理は同じ。
まとめ
メモリ管理を実装した:
- バンプアロケータ: 確保のみ(解放不可)
- フリーリストアロケータ: malloc/free対応
- コアレッシング: 隣接空きブロックの結合
そして学んだバグ:
- free忘れ → リーク
- 二重解放 → クラッシュ
- Use-After-Free → 未定義動作
- バッファオーバーフロー → ヒープ破壊
Cでメモリ管理、こわいですねぇ。だからRustが流行るわけだ。
次回Part4では、printf を自作する。%d の実装が意外と難しいんだよね。