第4章: プリエンプティブスケジューラ#

SysTick を心臓にして、タスクを強制的に切り替えるスケジューラを作ります。

この章で分かること: 重い処理があっても、ほかのアプリが止まらないのはなぜ?

PC で重い計算を回しっぱなしにしても、ほかのアプリは動き続けます。 OS がタイマ割り込みのたびに、走っているプログラムを強制的に切り替えているからです。 この章では、それと同じ仕組みを自分で作ります。

PC と違うのは、UNO R4 の CPU コアが 1 つしかないことです。 PC ならコアが複数あり、重い処理が 1 つのコアを使い切っても、ほかのコアは空いています。 コアが 1 つだと、切り替えなければ本当にすべてが止まります。 実際に止まるところは、この章の最後の 壊してみる で見られます。

サンプルコード#

  • ディレクトリ: docs/os-on-arduino/code/04_scheduler

  • ビルドと書き込み:

uv run pio run -d 04_scheduler -t upload
uv run pio device monitor -b 115200

コードを見る: docs/os-on-arduino/code/04_scheduler

ソースコード#

この章のスケッチです。ファイル名をクリックすると開きます。コードは原稿リポジトリから自動で同期されているので、ここに出ているものが常に最新です。

src/main.cpp — 135 行

GitHub で開く

  1/**
  2 * Chapter 4: Preemptive Scheduler Demo
  3 *
  4 * Tasks are automatically switched by SysTick timer interrupt.
  5 * No need to call os_yield() - the scheduler runs every TIME_SLICE_MS.
  6 */
  7
  8#include <Arduino.h>
  9#include "os_kernel.h"
 10#include "Arduino_LED_Matrix.h"
 11
 12// Arduino UNO R4 WiFi は基板上に "L" と印字された LED を持っています。
 13// 追加部品なしで動かせるよう、これを主 LED として使います。
 14#define LED1_PIN LED_BUILTIN
 15
 16// 2個目以降の LED は外付けせず、基板内蔵の 12×8 LED マトリクスで代用します。
 17// 左半分を LED2、右半分を LED3 として丸ごと点灯・消灯します。
 18// マトリクス制御の仕組みは第7章で学びます。ここではライブラリに任せます。
 19static ArduinoLEDMatrix matrix;
 20static uint8_t matrix_frame[8][12];
 21
 22static void matrix_led(int led, bool on)
 23{
 24    int col_start = (led == 3) ? 6 : 0;
 25    for (int row = 0; row < 8; row++) {
 26        for (int col = col_start; col < col_start + 6; col++) {
 27            matrix_frame[row][col] = on ? 1 : 0;
 28        }
 29    }
 30    matrix.renderBitmap(matrix_frame, 8, 12);
 31}
 32
 33// Task 1: Fast LED blink
 34void task_led1(void)
 35{
 36    pinMode(LED1_PIN, OUTPUT);
 37    while (1) {
 38        digitalWrite(LED1_PIN, HIGH);
 39        os_sleep(100);  // Sleep 100ms
 40        digitalWrite(LED1_PIN, LOW);
 41        os_sleep(100);
 42    }
 43}
 44
 45// Task 2: Slow blink on LED2 (matrix left half)
 46void task_led2(void)
 47{
 48    while (1) {
 49        matrix_led(2, true);
 50        os_sleep(500);  // Sleep 500ms
 51        matrix_led(2, false);
 52        os_sleep(500);
 53    }
 54}
 55
 56// Task 3: Serial output with system info
 57void task_serial(void)
 58{
 59    int count = 0;
 60    while (1) {
 61        Serial.print("[");
 62        Serial.print(os_get_tick());
 63        Serial.print("ms] Count: ");
 64        Serial.print(count++);
 65        Serial.print(" | Current task: ");
 66        Serial.println(os_get_current_task());
 67
 68        os_sleep(1000);  // Sleep 1 second
 69    }
 70}
 71
 72// Task 4: Heavy computation (demonstrates preemption)
 73void task_heavy(void)
 74{
 75    volatile uint32_t sum = 0;
 76    while (1) {
 77        // Heavy computation - will be preempted
 78        for (volatile int i = 0; i < 1000000; i++) {
 79            sum += i;
 80        }
 81        Serial.print("Heavy task completed iteration, sum=");
 82        Serial.println(sum);
 83        sum = 0;
 84
 85        // 計算完了をマトリクス右半分(LED3 相当)のトグルで示す
 86        static bool led3_on = false;
 87        led3_on = !led3_on;
 88        matrix_led(3, led3_on);
 89    }
 90}
 91
 92void setup()
 93{
 94    Serial.begin(115200);
 95    while (!Serial) { }
 96
 97    // LEDマトリクスを初期化(LED2 / LED3 の代用)
 98    matrix.begin();
 99
100    Serial.println("=== Preemptive Scheduler Demo ===");
101    Serial.println();
102    Serial.println("Tasks are automatically switched every 10ms by SysTick.");
103    Serial.println("The heavy_task demonstrates that even long-running");
104    Serial.println("computations don't block other tasks.");
105    Serial.println();
106
107    // Initialize OS
108    os_init();
109
110    // Create tasks
111    os_create_task(task_led1, "LED1", 512);
112    os_create_task(task_led2, "LED2", 512);
113    os_create_task(task_serial, "Serial", 1024);
114    os_create_task(task_heavy, "Heavy", 512);
115
116    Serial.println("Tasks created:");
117    for (int i = 0; i < os_get_task_count(); i++) {
118        Serial.print("  ID ");
119        Serial.print(i);
120        Serial.print(": ");
121        Serial.println(os_get_task_name(i));
122    }
123    Serial.println();
124    Serial.println("Starting preemptive scheduler...");
125    Serial.flush();
126    delay(100);
127
128    // Start OS
129    os_start();
130}
131
132void loop()
133{
134    // Never reached
135}
include/os_kernel.h — 79 行

GitHub で開く

 1/**
 2 * os_kernel.h - Mini OS Kernel Header
 3 * Chapter 4: Preemptive Scheduler with SysTick
 4 */
 5
 6#ifndef OS_KERNEL_H
 7#define OS_KERNEL_H
 8
 9#include <stdint.h>
10
11#ifdef __cplusplus
12extern "C" {
13#endif
14
15// Task states
16typedef enum {
17    TASK_READY,
18    TASK_RUNNING,
19    TASK_BLOCKED,
20    TASK_SUSPENDED,
21    TASK_TERMINATED
22} TaskState;
23
24// Task Control Block (TCB)
25typedef struct {
26    uint32_t *sp;
27    uint32_t *stack_base;
28    uint32_t stack_size;
29    TaskState state;
30    uint8_t id;
31    uint8_t priority;
32    char name[16];
33    void (*entry)(void);
34    uint32_t wake_tick;     // For os_sleep()
35    uint32_t cpu_ticks;     // CPU usage counter
36} TCB;
37
38// System configuration
39#define MAX_TASKS 8
40#define DEFAULT_STACK_SIZE 1024
41// タスク用スタックをまとめて置く領域。os_create_task() は
42// ここから要求されたぶんだけ切り出す。
43// 既定より大きなスタックを要求するタスク(シェルなど)があるため、
44// MAX_TASKS * DEFAULT_STACK_SIZE では足りない。余裕を持たせる。
45#define STACK_POOL_SIZE (MAX_TASKS * DEFAULT_STACK_SIZE + 2048)
46#define IDLE_STACK_SIZE 256
47#define TIME_SLICE_MS 10
48
49// Kernel API
50void os_init(void);
51int os_create_task(void (*entry)(void), const char *name, uint32_t stack_size);
52void os_start(void);
53void os_yield(void);
54void os_sleep(uint32_t ms);
55
56// Task info API
57int os_get_task_count(void);
58const char* os_get_task_name(int id);
59TaskState os_get_task_state(int id);
60int os_get_current_task(void);
61int os_get_cpu_usage(int task_id);
62
63// Internal
64void os_schedule(void);
65uint32_t os_get_tick(void);
66
67// Global variables
68extern TCB g_tasks[MAX_TASKS];
69extern volatile int g_task_count;
70extern volatile int g_current_task;
71extern volatile int g_next_task;
72extern volatile uint32_t g_system_ticks;
73extern const uint32_t g_tcb_size;
74
75#ifdef __cplusplus
76}
77#endif
78
79#endif // OS_KERNEL_H
src/os_kernel.cpp — 287 行

GitHub で開く

  1/**
  2 * os_kernel.cpp - Mini OS Kernel Implementation
  3 * Chapter 4: Preemptive Scheduler with SysTick
  4 */
  5
  6#include "os_kernel.h"
  7#include <Arduino.h>
  8#include <string.h>
  9
 10// Global variables
 11TCB g_tasks[MAX_TASKS];
 12volatile int g_task_count = 0;
 13volatile int g_current_task = -1;
 14volatile int g_next_task = -1;
 15volatile uint32_t g_system_ticks = 0;
 16static volatile uint32_t g_tick_count = 0;
 17
 18const uint32_t g_tcb_size = sizeof(TCB);
 19
 20// Stack areas
 21static uint32_t g_stack_pool[STACK_POOL_SIZE / 4];
 22static uint32_t g_stack_used;   // 切り出し済みのワード数
 23
 24// Idle task
 25static void idle_task(void)
 26{
 27    while (1) {
 28        __WFI();
 29    }
 30}
 31
 32// Initialize stack for a task
 33// タスク関数が return したときの受け皿。例外フレームの LR スロットは
 34// ここを指す (第3章 3.4)。EXC_RETURN を置く場所ではない。
 35static void task_exit(void)
 36{
 37    g_tasks[g_current_task].state = TASK_TERMINATED;
 38    os_yield();
 39    while (1) { }  // ここには到達しない
 40}
 41
 42static uint32_t* init_task_stack(uint32_t *stack_top, void (*entry)(void))
 43{
 44    uint32_t *sp = stack_top;
 45
 46    // Hardware auto-saved registers
 47    *(--sp) = 0x01000000;        // xPSR: Thumb bit
 48    *(--sp) = (uint32_t)entry;   // PC
 49    *(--sp) = (uint32_t)task_exit;  // LR: タスクが return したときの戻り先
 50    *(--sp) = 0;                 // R12
 51    *(--sp) = 0;                 // R3
 52    *(--sp) = 0;                 // R2
 53    *(--sp) = 0;                 // R1
 54    *(--sp) = 0;                 // R0
 55
 56    // Software saved registers
 57    *(--sp) = 0;  // R11
 58    *(--sp) = 0;  // R10
 59    *(--sp) = 0;  // R9
 60    *(--sp) = 0;  // R8
 61    *(--sp) = 0;  // R7
 62    *(--sp) = 0;  // R6
 63    *(--sp) = 0;  // R5
 64    *(--sp) = 0;  // R4
 65
 66    return sp;
 67}
 68
 69void os_init(void)
 70{
 71    memset(g_tasks, 0, sizeof(g_tasks));
 72    g_task_count = 0;
 73    g_current_task = -1;
 74    g_next_task = -1;
 75    g_system_ticks = 0;
 76
 77    // Create idle task (ID=0)
 78    os_create_task(idle_task, "idle", IDLE_STACK_SIZE);
 79}
 80
 81int os_create_task(void (*entry)(void), const char *name, uint32_t stack_size)
 82{
 83    if (g_task_count >= MAX_TASKS) {
 84        return -1;
 85    }
 86
 87    int id = g_task_count;
 88    TCB *task = &g_tasks[id];
 89
 90    task->id = id;
 91    task->state = TASK_READY;
 92    task->priority = id;
 93    task->entry = entry;
 94    task->wake_tick = 0;
 95    task->cpu_ticks = 0;
 96    strncpy(task->name, name, sizeof(task->name) - 1);
 97
 98    if (stack_size == 0) {
 99        stack_size = DEFAULT_STACK_SIZE;
100    }
101    uint32_t words = (stack_size + 3) / 4;
102    if (g_stack_used + words > STACK_POOL_SIZE / 4) {
103        return -1;          // プールが尽きた。黙って縮めず、失敗として返す
104    }
105    g_stack_used += words;
106    task->stack_size = stack_size;
107    task->stack_base = &g_stack_pool[g_stack_used];   // スタックは上端から下へ伸びる
108    task->sp = init_task_stack(task->stack_base, entry);
109
110    g_task_count++;
111    return id;
112}
113
114void os_schedule(void)
115{
116    // Guard: don't schedule before OS is initialized
117    if (g_task_count == 0) return;
118
119    // Wake up blocked tasks
120    for (int i = 0; i < g_task_count; i++) {
121        if (g_tasks[i].state == TASK_BLOCKED) {
122            if (g_system_ticks >= g_tasks[i].wake_tick) {
123                g_tasks[i].state = TASK_READY;
124            }
125        }
126    }
127
128    // Round-robin scheduling
129    int next = g_current_task;
130    do {
131        next = (next + 1) % g_task_count;
132        if (g_tasks[next].state == TASK_READY ||
133            g_tasks[next].state == TASK_RUNNING) {
134            break;
135        }
136    } while (next != g_current_task);
137
138    if (next != g_current_task || g_current_task < 0) {
139        if (g_current_task >= 0) {
140            if (g_tasks[g_current_task].state == TASK_RUNNING) {
141                g_tasks[g_current_task].state = TASK_READY;
142            }
143        }
144        g_tasks[next].state = TASK_RUNNING;
145        g_next_task = next;
146
147        // Trigger PendSV
148        SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
149    }
150}
151
152void os_yield(void)
153{
154    os_schedule();
155}
156
157void os_sleep(uint32_t ms)
158{
159    if (g_current_task > 0) {  // Don't block idle task
160        g_tasks[g_current_task].state = TASK_BLOCKED;
161        g_tasks[g_current_task].wake_tick = g_system_ticks + ms;
162        os_schedule();
163    }
164}
165
166uint32_t os_get_tick(void)
167{
168    return g_system_ticks;
169}
170
171int os_get_task_count(void)
172{
173    return g_task_count;
174}
175
176const char* os_get_task_name(int id)
177{
178    if (id < 0 || id >= g_task_count) return "";
179    return g_tasks[id].name;
180}
181
182TaskState os_get_task_state(int id)
183{
184    if (id < 0 || id >= g_task_count) return TASK_TERMINATED;
185    return g_tasks[id].state;
186}
187
188int os_get_current_task(void)
189{
190    return g_current_task;
191}
192
193void os_start(void)
194{
195    // Set PendSV to lowest priority
196    NVIC_SetPriority(PendSV_IRQn, 0xFF);
197
198    // Configure SysTick for TIME_SLICE_MS interval
199    SysTick_Config(SystemCoreClock / 1000 * TIME_SLICE_MS);
200
201    g_current_task = -1;
202    g_next_task = 0;
203
204    // Do NOT switch CONTROL to PSP here — PSP is not yet initialized.
205    // PendSV returns with EXC_RETURN=0xFFFFFFFD, which automatically
206    // switches thread mode to PSP after loading the first task's context.
207    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
208
209    while (1) { }
210}
211
212// SysTick Handler - called every TIME_SLICE_MS
213extern "C" void SysTick_Handler(void)
214{
215    // 実際に経過した時間を、いま走っていたタスクの取り分として計上する。
216    // os_schedule() 側で数えると os_yield() の回数まで混ざってしまう。
217    if (g_current_task >= 0) {
218        g_tasks[g_current_task].cpu_ticks += TIME_SLICE_MS;
219    }
220    g_system_ticks += TIME_SLICE_MS;
221    os_schedule();
222}
223
224// PendSV Handler - Context Switch
225extern "C" __attribute__((naked)) void PendSV_Handler(void)
226{
227    __asm volatile (
228        "   MRS     R0, PSP              \n"
229        "   LDR     R1, =g_current_task  \n"
230        "   LDR     R2, [R1]             \n"
231        "   CMP     R2, #0               \n"
232        "   BLT     _skip_save           \n"
233        // Save R4-R11 only when there is a valid current task
234        "   STMDB   R0!, {R4-R11}        \n"
235        "   LDR     R3, =g_tasks         \n"
236        "   LDR     R4, =g_tcb_size      \n"
237        "   LDR     R4, [R4]             \n"
238        "   MUL     R2, R2, R4           \n"
239        "   ADD     R3, R3, R2           \n"
240        "   STR     R0, [R3]             \n"
241        "_skip_save:                     \n"
242        "   LDR     R1, =g_next_task     \n"
243        "   LDR     R2, [R1]             \n"
244        "   LDR     R1, =g_current_task  \n"
245        "   STR     R2, [R1]             \n"
246        "                                \n"
247        "   LDR     R3, =g_tasks         \n"
248        "   LDR     R4, =g_tcb_size      \n"
249        "   LDR     R4, [R4]             \n"
250        "   MUL     R2, R2, R4           \n"
251        "   ADD     R3, R3, R2           \n"
252        "   LDR     R0, [R3]             \n"
253        "                                \n"
254        "   LDMIA   R0!, {R4-R11}        \n"
255        "   MSR     PSP, R0              \n"
256        "                                \n"
257        "   LDR     LR, =0xFFFFFFFD      \n"
258        "   BX      LR                   \n"
259    );
260}
261
262// CPU使用率を取得(パーセント)
263// 直前にこの関数を呼んだ時点からの差分で計算する。
264// ps のようにタスクごとにループで呼ぶため、基準時刻もタスクごとに持つ。
265int os_get_cpu_usage(int task_id)
266{
267    if (task_id < 0 || task_id >= g_task_count) {
268        return -1;
269    }
270
271    static uint32_t last_total[MAX_TASKS] = {0};
272    static uint32_t last_task_ticks[MAX_TASKS] = {0};
273
274    uint32_t total = g_system_ticks;
275    uint32_t delta_total = total - last_total[task_id];
276
277    if (delta_total == 0) {
278        return 0;
279    }
280
281    uint32_t delta_task = g_tasks[task_id].cpu_ticks - last_task_ticks[task_id];
282
283    last_total[task_id] = total;
284    last_task_ticks[task_id] = g_tasks[task_id].cpu_ticks;
285
286    return (delta_task * 100) / delta_total;
287}

実機での出力#

Arduino UNO R4 WiFi の実機で実際に取得した出力です(macOS / Windows の 両方で同じ結果を確認しています)。手元の出力と見比べてください。

Heavy が重い計算を回している最中も、1 秒ごとに Count を印字する Serial タスク(ID 3)の 周期が乱れません。これがプリエンプションの効果です。Current task は印字している Serial タスク自身の ID なので、いつも 3 です。

../_images/ch04_preemption.gif

Count がきっちり 1 秒ごとに出続け、その合間に Heavy が割り込まれている ところ。実機の Arduino UNO R4 WiFi で録ったものです#

Heavy task completed iteration, sum=1783293664
[3010ms] Count: 3 | Current task: 3
Heavy task completed iteration, sum=1783293664
[4010ms] Count: 4 | Current task: 3
Heavy task completed iteration, sum=1783293664
Heavy task completed iteration, sum=1783293664
[5010ms] Count: 5 | Current task: 3
Heavy task completed iteration, sum=1783293664
[6010ms] Count: 6 | Current task: 3
Heavy task completed iteration, sum=1783293664
Heavy task completed iteration, sum=1783293664
[7010ms] Count: 7 | Current task: 3
...

実機ではこう光ります。

L LED(LED1、0.1 秒ごと)とマトリクス左半分(LED2、0.5 秒ごと)は、重い計算を回し続ける Heavy タスクがいても止まらずに点滅します。右半分は Heavy が計算を 1 回終えるたびに切り替わります。(UNO R4 WiFi を Web カメラで 30 fps で撮影)

シミュレータで見る#

実機が無くても、同じファームウェアを シミュレータ で動かすとこう見えます。

../_images/sim_ch04_blink.gif

L(LED1)が 100ms ごと、マトリクス左半分(LED2)が 500ms ごとに点滅し、右半分は重い計算の Heavy タスクが 1 周するたびに切り替わります。Heavy が CPU を回し続けている最中も、L と左半分の点滅は乱れません。(シミュレータで撮影)#

壊してみる#

動いているコードを 1 か所だけ変えると、何が起きるでしょう。 変える前に結果を予想してから、答えを開いてください。

改造したコードは、ビルドしてできた firmware.elf を ブラウザ版にドロップ すれば、基板が無くても試せます。 書き込みは要りません。元に戻すときは git checkout -- 04_scheduler です。

uv run pio run -d 04_scheduler
# できた 04_scheduler/.pio/build/uno_r4_wifi/firmware.elf をブラウザ版にドロップ

注釈

答えに載せた出力は、ブラウザ版と同じ QEMU で実際に動かして取ったものです。 実機ではまだ確かめていません。止まる場所やダンプの数字は、実機では違うことがあります。

1. タイムスライスを 1 秒にする#

include/os_kernel.h の 1 行を変えます。タスクを切り替える間隔が 10 ms から 1 秒になります。

-#define TIME_SLICE_MS 10
+#define TIME_SLICE_MS 1000

予想: L LED の点滅と、1 秒ごとの Count の印字はどうなるでしょう。

答え

Starting preemptive scheduler... のあと、何も出なくなります。 L LED も点滅しません。

Tasks created:
  ID 0: idle
  ID 1: LED1
  ID 2: LED2
  ID 3: Serial
  ID 4: Heavy

Starting preemptive scheduler...

SysTick のカウンタは 24 ビットで、1 回に数えられるのは 16,777,216 クロックまでです。 48 MHz で 1 秒ぶんは 48,000,000 クロックなので、入りきりません。 SysTick_Config() は、入りきらない値を渡されるとタイマを動かさずに 1 を返します (CMSIS core_cm4.h の SysTick_Config()、 上限は SysTick_LOAD_RELOAD_Msk = 0xFFFFFF)。 os_start() はこの戻り値を見ていないので、割り込みが一度も来ないまま、 最初に選ばれた idle タスクが __WFI() で眠り続けます。

QEMU で確かめたところ、349 ms までは動き、350 ms(16,800,000 クロック)で止まりました。 100 ms にすると動き続けますが、Count の時刻が [3100ms] のように 100 ms 刻みになります。

直すなら: SysTick_Config() の戻り値が 0 でなければ、エラーを出して止まるようにします。

2. SysTick から切り替えを外す#

src/os_kernel.cpp の SysTick_Handler() で、os_schedule() の呼び出しをコメントにします。 時間は進みますが、タイマ割り込みでタスクを切り替えなくなります。

     g_system_ticks += TIME_SLICE_MS;
-    os_schedule();
+    // os_schedule();

予想: os_sleep() を呼ぶタスクはそこで切り替わります。どのタスクが動き続けるでしょう。

答え

これも Starting preemptive scheduler... のあと、何も出ません。

最初に走るのは ID 0 の idle タスクです。idle は os_sleep() を呼ばず、 __WFI() で割り込みを待つだけのループなので、自分から CPU を手放しません。 切り替えのきっかけが os_sleep() だけになると、idle が CPU を持ったまま、 ほかのタスクは一度も走りません。

第3章の協調的な切り替え(第3章: コンテキストスイッチの実装)では、タスクが自分で os_yield() を呼ぶ 約束でした。約束を守らないタスクが 1 つあるだけで全体が止まる、というのが プリエンプションで解いている問題です。

3. レジスタ R4〜R11 を保存しない#

PendSV_Handler() の、R4〜R11 を積む命令と戻す命令を、スタックポインタを 動かすだけの命令に置き換えます。スタックの形は変わらないので、中身だけが 保存も復元もされなくなります。

-        "   STMDB   R0!, {R4-R11}        \n"
+        "   SUB     R0, R0, #32          \n"
 ...
-        "   LDMIA   R0!, {R4-R11}        \n"
+        "   ADD     R0, R0, #32          \n"

予想: R0〜R3 や PC などは、例外に入るときに CPU が自動で積んでいます。 残りの 8 本を保存しないと、何が起きるでしょう。

答え

最初の Count を 1 行出したところで、フォールトで止まります。 下のダンプは Arduino コアに入っているフォールトハンドラ(CmBacktrace)が出したものです。

Starting preemptive scheduler...
[10ms] Count: 0 | Current task: 3


Firmware name: PROJECT_NAME, compiled on: Sep 28 2026
Fault on interrupt or bare metal(no OS) environment
(スタックのダンプは省略)
=================== Registers information ====================
  R0 : 000000f0  R1 : e000ed00  R2 : e000ed00  R3 : 10000800
  R12: 00000001  LR : 00006981  PC : 00004860  PSR: 41000000
==============================================================
Usage fault is caused by attempts to switch to an invalid state (e.g., ARM)

R4〜R11 は、関数を呼んでも中身が変わらない約束のレジスタです。コンパイラは、 ループの途中の値やアドレスをここに置いたまま、ほかの関数を呼びます。 保存しないと、切り替わった先のタスクが前のタスクの値をそのまま使ってしまいます。 今回は、化けた値を飛び先のアドレスとして使い、不正な状態に切り替えようとして止まりました (最後の行)。どこで止まるかは、どの値がどう化けるかで変わります。

つまずきやすいポイント#

(読者からの質問に応じて随時追記します。質問は GitHub Issues へどうぞ)