応用編 第3章: ヒープ自作 — malloc / free を書く#
malloc / free を自分で書き、断片化と戦います。
サンプルコード#
ディレクトリ:
docs/os-on-arduino/code/adv3_heapビルドと書き込み:
uv run pio run -d adv3_heap -t upload
uv run pio device monitor -b 115200
S を 1 文字送ると出力が始まります。
ソースコード#
この章のスケッチです。ファイル名をクリックすると開きます。コードは原稿リポジトリから自動で同期されているので、ここに出ているものが常に最新です。
src/main.cpp — 75 行
1/**
2 * 応用編 第3章: ヒープ自作 — malloc / free を書く
3 *
4 * 本章の課題 3.9.1 をそのまま実行する。
5 *
6 * heap_init() → a=malloc(100) → b=malloc(100) → c=malloc(100)
7 * → free(b) → free(a)
8 *
9 * 各ステップで heap_dump() を呼び、free(a) のあとに a と b が
10 * ひとつの空きブロックへ結合されることを確かめる。
11 * 最後に外部断片化(合計は足りるのに連続では取れない)を再現する。
12 */
13
14#include <Arduino.h>
15#include "os_heap.h"
16
17static void step(const char *label)
18{
19 Serial.println();
20 Serial.print(F("### ")); Serial.println(label);
21 heap_dump();
22}
23
24void setup()
25{
26 Serial.begin(115200);
27 while (!Serial) { }
28 while (Serial.read() != 'S') { delay(1); }
29
30 Serial.println(F("=== Advanced 3: my_malloc / my_free ==="));
31
32 heap_init();
33 step("heap_init()");
34
35 void *a = my_malloc(100); step("a = my_malloc(100)");
36 void *b = my_malloc(100); step("b = my_malloc(100)");
37 void *c = my_malloc(100); step("c = my_malloc(100)");
38
39 my_free(b); step("my_free(b)");
40 my_free(a); step("my_free(a) <- a と b が結合されるはず");
41 my_free(c); step("my_free(c) <- ヒープが 1 つの空きブロックに戻る");
42
43 // 課題: ヒープを満杯にしてから 1 つおきに解放すると、
44 // 空き合計は足りるのに連続では取れない(外部断片化)
45 Serial.println();
46 Serial.println(F("### 外部断片化の確認"));
47
48 void *p[8];
49 for (int i = 0; i < 8; i++) {
50 p[i] = my_malloc(496); // (ヘッダ 16 + 496) × 8 = 4096 で満杯
51 }
52 my_free(p[1]);
53 my_free(p[3]);
54 my_free(p[5]); // 496 バイトの穴が 3 つ
55 Serial.print(F("空き合計 = ")); Serial.println((unsigned)heap_free_total());
56
57 void *d = my_malloc(600);
58 Serial.print(F("my_malloc(600) -> ")); Serial.println(d ? "OK" : "NULL");
59 Serial.println(F("空きの合計は足りていても、連続した領域が無ければ取れない"));
60
61 my_free(p[2]); // p[1]〜p[3] が 1520 バイトの穴に結合される
62 void *e = my_malloc(600);
63 Serial.print(F("my_free(p[2]) 後の my_malloc(600) -> ")); Serial.println(e ? "OK" : "NULL");
64
65 // 3.7 デバッグ機能: 二重 free を検出する
66 Serial.println();
67 Serial.println(F("### 二重 free の検出"));
68 my_free(p[0]);
69 my_free(p[0]); // ここで [heap] double free detected が出る
70
71 Serial.println();
72 Serial.println(F("done."));
73}
74
75void loop() { }
include/os_heap.h — 22 行
1// os_heap.h
2//
3// 本書 応用編 第3章「ヒープ自作」で作る freelist 型アロケータ。
4
5#pragma once
6
7#include <stddef.h>
8#include <stdint.h>
9
10#ifdef __cplusplus
11extern "C" {
12#endif
13
14void heap_init(void);
15void *my_malloc(size_t n);
16void my_free(void *p);
17void heap_dump(void); // 3.7 デバッグ機能
18size_t heap_free_total(void); // 空き合計(断片化の観察用)
19
20#ifdef __cplusplus
21}
22#endif
src/os_heap.cpp — 128 行
1// os_heap.cpp
2//
3// 本書 応用編 第3章の実装。ブロックヘッダ + 単方向フリーリスト + first-fit +
4// 隣接結合。ヒープは明示的な配列として持つ(3.2.2 の「スタックとヒープの衝突」
5// を避けるため)。
6
7#include <Arduino.h>
8#include <string.h>
9#include "os_heap.h"
10
11typedef struct block_header {
12 size_t size; // データ部のバイト数 (ヘッダは含まない)
13 int free; // 1 なら空き、0 なら使用中
14 struct block_header *next; // 次のブロックヘッダ
15 uint32_t magic; // 二重 free / 破壊検出用
16} block_header_t;
17
18#define HEAP_MAGIC 0xA110CA7E // "ALLOCATE" の語呂合わせ
19#define ALIGN8(x) (((x) + 7u) & ~7u)
20#define HEAP_SIZE 4096
21
22static uint8_t heap_area[HEAP_SIZE] __attribute__((aligned(8)));
23static block_header_t *heap_head = NULL;
24
25void heap_init(void)
26{
27 heap_head = (block_header_t *)heap_area;
28 heap_head->size = HEAP_SIZE - sizeof(block_header_t);
29 heap_head->free = 1;
30 heap_head->next = NULL;
31 heap_head->magic = HEAP_MAGIC;
32}
33
34void *my_malloc(size_t n)
35{
36 if (n == 0) return NULL;
37 n = ALIGN8(n); // 8 の倍数に切り上げ
38
39 block_header_t *b = heap_head;
40 while (b != NULL) {
41 if (b->magic != HEAP_MAGIC) {
42 // ヘッダが破壊されている (バッファオーバーラン等)
43 return NULL;
44 }
45 if (b->free && b->size >= n) {
46 // 余りが十分なら分割 (split)
47 if (b->size >= n + sizeof(block_header_t) + 8) {
48 block_header_t *rest =
49 (block_header_t *)((uint8_t *)b + sizeof(block_header_t) + n);
50 rest->size = b->size - n - sizeof(block_header_t);
51 rest->free = 1;
52 rest->next = b->next;
53 rest->magic = HEAP_MAGIC;
54 b->size = n;
55 b->next = rest;
56 }
57 b->free = 0;
58 return (uint8_t *)b + sizeof(block_header_t);
59 }
60 b = b->next;
61 }
62 return NULL; // 空きなし
63}
64
65void my_free(void *p)
66{
67 if (p == NULL) return;
68
69 block_header_t *b =
70 (block_header_t *)((uint8_t *)p - sizeof(block_header_t));
71
72 if (b->magic != HEAP_MAGIC) {
73 // 不正なポインタ、もしくはヘッダ破壊
74 Serial.println(F("[heap] bad pointer or corrupted header"));
75 return;
76 }
77 if (b->free) {
78 // すでに free 済み → 二重 free 検出
79 Serial.println(F("[heap] double free detected"));
80 return;
81 }
82 b->free = 1;
83
84 // 後続ブロックと結合
85 if (b->next != NULL && b->next->free) {
86 b->size += sizeof(block_header_t) + b->next->size;
87 b->next = b->next->next;
88 }
89
90 // 前のブロックと結合 (単方向リストなので先頭から探す)
91 block_header_t *prev = heap_head;
92 while (prev != NULL && prev->next != b) prev = prev->next;
93 if (prev != NULL && prev->free) {
94 prev->size += sizeof(block_header_t) + b->size;
95 prev->next = b->next;
96 }
97}
98
99void heap_dump(void)
100{
101 block_header_t *b = heap_head;
102 int i = 0;
103 size_t total_free = 0, total_used = 0;
104
105 Serial.println(F("---- heap dump ----"));
106 while (b != NULL) {
107 char line[72];
108 snprintf(line, sizeof(line), "[%02d] +%04u size=%5u %s",
109 i, (unsigned)((uint8_t *)b - heap_area),
110 (unsigned)b->size, b->free ? "FREE" : "USED");
111 Serial.println(line);
112 if (b->free) total_free += b->size;
113 else total_used += b->size;
114 b = b->next;
115 i++;
116 }
117 Serial.print(F("total free=")); Serial.print((unsigned)total_free);
118 Serial.print(F(" used=")); Serial.println((unsigned)total_used);
119}
120
121size_t heap_free_total(void)
122{
123 size_t total = 0;
124 for (block_header_t *b = heap_head; b != NULL; b = b->next) {
125 if (b->free) total += b->size;
126 }
127 return total;
128}
実機での出力#
Arduino UNO R4 WiFi の実機で実際に取得した出力です(macOS / Windows の 両方で同じ結果を確認しています)。手元の出力と見比べてください。
ヒープの状態を段階ごとにダンプしています。
S を送ると、すべてのダンプがまとめて出ます。最後は「空きの合計は
足りていても連続領域が無ければ取れない」ところと、二重 free の検出です#
=== Advanced 3: my_malloc / my_free ===
### heap_init()
---- heap dump ----
[00] +0000 size= 4080 FREE
total free=4080 used=0
### a = my_malloc(100)
---- heap dump ----
[00] +0000 size= 104 USED
[01] +0120 size= 3960 FREE
total free=3960 used=104
### b = my_malloc(100)
---- heap dump ----
[00] +0000 size= 104 USED
[01] +0120 size= 104 USED
[02] +0240 size= 3840 FREE
total free=3840 used=208
...
つまずきやすいポイント#
(読者からの質問に応じて随時追記します。質問は GitHub Issues へどうぞ)