4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cでメモリ管理、free忘れてリーク祭り Part3

Last updated at Posted at 2025-12-12

シリーズ一覧

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はもっと複雑:

  1. ページアロケータ: 4KB単位で物理メモリを管理
  2. スラブアロケータ: 小さいオブジェクト用(kmalloc)
  3. バディシステム: 連続メモリ確保の効率化
  4. 仮想メモリ: ページテーブルによるアドレス変換

今回作ったのは一番シンプルなフリーリスト方式。実用には程遠いけど、原理は同じ。

まとめ

メモリ管理を実装した:

  • バンプアロケータ: 確保のみ(解放不可)
  • フリーリストアロケータ: malloc/free対応
  • コアレッシング: 隣接空きブロックの結合

そして学んだバグ:

  • free忘れ → リーク
  • 二重解放 → クラッシュ
  • Use-After-Free → 未定義動作
  • バッファオーバーフロー → ヒープ破壊

Cでメモリ管理、こわいですねぇ。だからRustが流行るわけだ。

次回Part4では、printf を自作する。%d の実装が意外と難しいんだよね。


次回: printfを自作する、%dの実装が意外と難しい Part4

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?