Advanced 3: Writing Your Own Heap#
Sample code#
Directory:
docs/os-on-arduino/code/adv3_heap
uv run pio run -d adv3_heap -t upload
uv run pio device monitor -b 115200
Output starts when you send the single character S.
Source code#
The sketch for this chapter. Click a file name to expand. The code is mirrored automatically from the manuscript repository, so what you see here is always current.
src/main.cpp — 75 lines
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 lines
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 lines
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}
On the board#
Sending S prints all the dumps at once. The last part shows that an allocation fails when there is no contiguous block even though the total free space is enough, and the detection of a double free.#
Pitfalls#
(Collected from reader questions — ask via GitHub Issues.)