本記事の構成および論理分析にはAI(人工知能)を使用しています。情報の正確性は、システム管理者(UNIXユーザー)による手動検証済みです。
C言語で作るLC-3仮想マシン 第8回: ステップ実行で1命令ずつ進める | UNIX Cafe

ステップ実行で1命令ずつ進める
前回は、VMに--profileオプションを追加し、実行した命令数の合計とopcodeごとの実行回数を数えられるようにしました。
プロファイル機能では、プログラム全体でどの命令が多く実行されたかを見られます。一方で、「次の命令へ進む前にいったん止めて、PCやレジスタを落ち着いて確認する」ことはまだできません。
第8回では、VMに--stepオプションを追加します。1命令実行するたびに状態を表示し、step>プロンプトでEnter入力を待つようにします。
GitHubではlc3-vm-cという名前で公開しています。
この記事時点のコードは、GitHubのlessons/08-stepに完成版として置いています。リポジトリ直下のlc3.cは、連載の進行に合わせて最新版へ更新していきます。
今回作るステップ実行の全体像
第6回の--traceでは、VMが命令を実行するたびにPC、命令word、opcode、レジスタを表示しました。
第8回の--stepでは、そのtrace表示を使いながら、1命令ごとにEnter入力を待ちます。
fetch
PCが指す命令wordを読む
↓
trace
命令情報を表示する
↓
execute
命令を実行する
↓
trace
レジスタと条件フラグを表示する
↓
step
Enter入力を待つ今回追加する主な機能は、以下の通りです。
step_enabled: ステップ実行を有効にするフラグwait_for_step():step>を表示してEnter入力を待つ関数--stepオプション: ステップ実行を有効にする引数- step時のtrace自動ON:
--stepだけでも命令情報を表示する
第8回で作業する場所は、VM本体の中ではかなり限られています。
lc3.c
├─ 補助関数
│ └─ wait_for_step() を追加
│
└─ main()
├─ step_enabled を追加
├─ --step オプションを追加
├─ step時にtrace_enabledを自動でONにする
└─ 1命令実行後にwait_for_step()を呼ぶステップ実行とは何か
ステップ実行とは、プログラムを一気に最後まで実行せず、1命令ずつ止めながら進める実行方法です。
通常実行では、Hello Worldはすぐに終わります。
./lc3 examples/hello.obj実行結果は次のようになります。
Hello, World!
HALTこれだけでは、LEAでR0にどの番地が入ったのか、PUTSの前後でR7がどう変わったのかが分かりません。
そこで、--traceを使って1命令ごとの情報を表示します。さらに--stepを使うと、表示を読んでからEnterで次へ進められます。
wait_for_step()を追加する
まず、ステップ実行用の入力待ち関数を追加します。
追加するコード: print_profile()の後、main()より前に追加します。(補助関数)
// 追加: step実行時に、次の命令へ進むまで待つ
static void wait_for_step(void)
{
int c;
printf("step> ");
fflush(stdout);
do {
c = getchar();
} while (c != '\n' && c != EOF);
printf("\n");
}step>を表示したあと、Enterが押されるまでgetchar()で待ちます。Enter入力を受け取った後は、次のtrace表示との間に空行を入れて読みやすくしています。
第3回で端末を非カノニカルモードにしているため、入力はEnterまでまとめて待つ通常の端末動作とは少し違います。ただ、ここでは「Enterを押したら次へ進む」という用途に絞り、改行文字まで読み捨てる形にしています。
–stepオプションを受け取る
次に、main()の引数処理へ--stepを追加します。
第7回の時点では、--traceと--profileを受け取れるようになっていました。
変更後のコード: main()の引数処理を、次の形に変更します。(main関数)
int trace_enabled = 0;
int profile_enabled = 0;
int step_enabled = 0; // 追加するコード
const char *image_path = NULL;
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--trace") == 0) {
trace_enabled = 1;
} else if (strcmp(argv[i], "--profile") == 0) {
profile_enabled = 1;
// –stepオプションを受け取る:追加するコード
} else if (strcmp(argv[i], "--step") == 0) {
step_enabled = 1;
} else if (argv[i][0] == '-') {
fprintf(stderr, "unknown option: %s\n", argv[i]);
fprintf(stderr, "usage: %s [--trace] [--profile] [--step] image.obj\n", argv[0]);
return 2;
} else if (image_path == NULL) {
image_path = argv[i];
} else {
fprintf(stderr, "usage: %s [--trace] [--profile] [--step] image.obj\n", argv[0]);
return 2;
}
}--stepを見つけたら、step_enabledを1にします。
usage表示にも[--step]を追加します。
step時はtraceも自動で有効にする
ステップ実行では、止まるだけではあまり意味がありません。どの命令で止まっているのかを一緒に見たいので、--stepが指定された場合はtrace_enabledも自動で有効にします。
追加するコード: 引数処理が終わり、image_path == NULLのチェックを済ませた後に追加(main関数)
if (image_path == NULL) {
fprintf(stderr, "usage: %s [--trace] [--profile] [--step] image.obj\n", argv[0]);
return 2;
}
// step実行では、毎回止まる前に命令情報も見たいのでtraceを自動で有効にする
if (step_enabled) {
trace_enabled = 1;
}これで、次のように--stepだけを指定しても、命令情報とレジスタ情報が表示されます。
./lc3 --step examples/hello.obj1命令ごとにEnterを待つ
最後に、実行ループの末尾でwait_for_step()を呼びます。
第7回の実行ループ末尾には、trace有効時にレジスタを表示する処理がありました。
if (trace_enabled) {
trace_registers();
if (running) {
printf("\n");
}
}この直後に、step有効時だけ入力待ちを追加します。
追加するコード: trace_registers()の呼び出し後に追加します。(main関数)
if (trace_enabled) {
trace_registers();
if (running) {
printf("\n");
}
}
// step有効時は、1命令実行するたびにEnter入力を待つ
if (step_enabled && running) {
wait_for_step();
}runningが0になった後は、もう次の命令へ進む必要がありません。そのため、TRAP HALTで停止した後にはstep>を出さないようにしています。
Hello Worldをステップ実行する
ビルドして、Hello Worldをステップ実行します。
cd lessons/08-step
make
./lc3 --step examples/hello.obj最初にLEA命令が表示されます。
PC=3000 INSTR=E002 OP=LEA
R0=3003 R1=0000 R2=0000 R3=0000 R4=0000 R5=0000 R6=0000 R7=0000 PC=3001 COND=P
step>ここでEnterを押すと、次の命令へ進みます。
PC=3001 INSTR=F022 OP=TRAP PUTS
Hello, World!
R0=3003 R1=0000 R2=0000 R3=0000 R4=0000 R5=0000 R6=0000 R7=3002 PC=3002 COND=P
step>もう一度Enterを押すと、TRAP HALTまで進みます。
PC=3002 INSTR=F025 OP=TRAP HALT
HALT
R0=3003 R1=0000 R2=0000 R3=0000 R4=0000 R5=0000 R6=0000 R7=3003 PC=3003 COND=Pこの出力を見ると、Hello Worldは次の順番で動いていることが分かります。
x3000のLEAで、文字列の先頭番地x3003をR0へ入れるx3001のTRAP PUTSで、R0が指す文字列を表示するx3002のTRAP HALTで、VMを停止する
–trace、–profile、–stepを組み合わせる
第7回の引数処理を拡張しているので、--trace、--profile、--stepは組み合わせて使えます。
./lc3 --step examples/hello.obj
./lc3 --trace --step examples/hello.obj
./lc3 --profile --step examples/hello.obj--stepを指定した場合は、--traceを書かなくてもtrace表示が自動で有効になります。
--profileも同時に指定すると、ステップ実行で1命令ずつ確認したあと、最後に命令数の集計も表示されます。
./lc3 --profile --step examples/hello.obj
PC=3000 INSTR=E002 OP=LEA
R0=3003 R1=0000 R2=0000 R3=0000 R4=0000 R5=0000 R6=0000 R7=0000 PC=3001 COND=P
step>
PC=3001 INSTR=F022 OP=TRAP PUTS
Hello, World!
R0=3003 R1=0000 R2=0000 R3=0000 R4=0000 R5=0000 R6=0000 R7=3002 PC=3002 COND=P
step>
PC=3002 INSTR=F025 OP=TRAP HALT
HALT
R0=3003 R1=0000 R2=0000 R3=0000 R4=0000 R5=0000 R6=0000 R7=3003 PC=3003 COND=P
profile:
total instructions: 3
LEA 1
TRAP 22048やRogueで使うときの注意
今回の--stepは、Hello Worldのような短いLC-3プログラムを、数命令ずつ目で追うための機能です。2048.objやrogue.objのようなゲームを普通に遊ぶためのモードではありません。
理由は2つあります。1つ目は、入力の扱いが分かりにくくなることです。ゲーム本体は、移動や操作のためにキーボード入力を読みます。一方、--stepもVM側でstep>を表示し、次の命令へ進むためにEnter入力を待ちます。
たとえば2048でwを押して盤面を動かしたい場面でも、その前後でVM側のstep>に対して何度もEnterを押す必要があります。同じ端末に「VMを進める入力」と「ゲームへ渡す入力」が並ぶため、今どちらの入力をしているのかが分かりにくくなります。
2つ目は、命令数が多いことです。ゲームは1画面を表示したり、1手分の処理を進めたりするだけでも、多くのLC-3命令を実行します。--stepでは1命令ごとに止まるため、ゲームの動きを少し見るだけでも大量のEnterが必要になります。
そのため、第8回ではHello Worldのような小さいプログラムを使って--stepを確認します。2048.objやrogue.objでは、実行全体の傾向を見るなら--profile、流れをざっと追うなら--traceのほうが扱いやすいです。特定の場所だけで止めたい場合は、次回作るブレークポイント機能のほうが向いています。
今回の完成コード
ここまでの変更をまとめると、次の形になります。
完成コード: lc3.c の全文です。今回追加・変更した場所には、// 追加:や// 変更:のコメントを付けています。実行確認は、このコードを入れ終わってから行います。
#include <stdio.h>
#include <stdint.h>
#include <signal.h>
#include <string.h>
/* unix only */
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/termios.h>
#include <sys/mman.h>
enum
{
R_R0 = 0,
R_R1,
R_R2,
R_R3,
R_R4,
R_R5,
R_R6,
R_R7,
R_PC, /* program counter */
R_COND,
R_COUNT
};
enum
{
FL_POS = 1 << 0, /* P */
FL_ZRO = 1 << 1, /* Z */
FL_NEG = 1 << 2, /* N */
};
enum
{
OP_BR = 0, /* branch */
OP_ADD, /* add */
OP_LD, /* load */
OP_ST, /* store */
OP_JSR, /* jump register */
OP_AND, /* bitwise and */
OP_LDR, /* load registar */
OP_STR, /* store registar */
OP_RTI, /* unused */
OP_NOT, /* bitwise not */
OP_LDI, /* load indirect */
OP_STI, /* store indirect */
OP_JMP, /* jump */
OP_RES, /* reserved (unused) */
OP_LEA, /* load effective address */
OP_TRAP /* execute trap */
};
enum
{
MR_KBSR = 0xFE00, /* keyboard status */
MR_KBDR = 0xFE02 /* keyboard data */
};
enum
{
TRAP_GETC = 0x20, /* get character from keyboard, not echoed onto the terminal */
TRAP_OUT = 0x21, /* output a character */
TRAP_PUTS = 0x22, /* output a word string */
TRAP_IN = 0x23, /* get character from keyboard, echoed onto the terminal */
TRAP_PUTSP = 0x24, /* output a byte string */
TRAP_HALT = 0x25 /* halt the program */
};
#define MEMORY_MAX (1 << 16)
#define OPCODE_COUNT 16
uint16_t memory[MEMORY_MAX]; /* 65536 locations */
uint16_t reg[R_COUNT];
static uint64_t total_instructions;
static uint64_t op_counts[OPCODE_COUNT];
struct termios original_tio;
void disable_input_buffering(void)
{
tcgetattr(STDIN_FILENO, &original_tio);
struct termios new_tio = original_tio;
new_tio.c_lflag &= ~ICANON & ~ECHO;
tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
}
void restore_input_buffering(void)
{
tcsetattr(STDIN_FILENO, TCSANOW, &original_tio);
}
uint16_t check_key(void)
{
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 0;
return select(1, &readfds, NULL, NULL, &timeout) != 0;
}
void handle_interrupt(int signal)
{
(void)signal;
restore_input_buffering();
printf("\n");
exit(-2);
}
uint16_t sign_extend(uint16_t x, int bit_count)
{
if ((x >> (bit_count - 1)) & 1) {
x |= (0xFFFF << bit_count);
}
return x;
}
uint16_t swap16(uint16_t x)
{
return (x << 8) | (x >> 8);
}
void update_flags(uint16_t r)
{
if (reg[r] == 0)
{
reg[R_COND] = FL_ZRO;
}
else if (reg[r] >> 15) /* a 1 in the left-most bit indicates negative */
{
reg[R_COND] = FL_NEG;
}
else
{
reg[R_COND] = FL_POS;
}
}
void read_image_file(FILE* file)
{
/* the origin tells us where in memory to place the image */
uint16_t origin;
fread(&origin, sizeof(origin), 1, file);
origin = swap16(origin);
/* we know the maximum file size so we only need one fread */
uint16_t max_read = MEMORY_MAX - origin;
uint16_t* p = memory + origin;
size_t read = fread(p, sizeof(uint16_t), max_read, file);
/* swap to little endian */
while (read-- > 0)
{
*p = swap16(*p);
++p;
}
}
int read_image(const char* image_path)
{
FILE* file = fopen(image_path, "rb");
if (!file) { return 0; }
read_image_file(file);
fclose(file);
return 1;
}
void mem_write(uint16_t address, uint16_t val)
{
memory[address] = val;
}
uint16_t mem_read(uint16_t address)
{
if (address == MR_KBSR)
{
if (check_key())
{
memory[MR_KBSR] = (1 << 15);
memory[MR_KBDR] = getchar();
}
else
{
memory[MR_KBSR] = 0;
}
}
return memory[address];
}
static const char *opcode_name(uint16_t op)
{
switch (op) {
case OP_BR: return "BR";
case OP_ADD: return "ADD";
case OP_LD: return "LD";
case OP_ST: return "ST";
case OP_JSR: return "JSR";
case OP_AND: return "AND";
case OP_LDR: return "LDR";
case OP_STR: return "STR";
case OP_RTI: return "RTI";
case OP_NOT: return "NOT";
case OP_LDI: return "LDI";
case OP_STI: return "STI";
case OP_JMP: return "JMP";
case OP_RES: return "RES";
case OP_LEA: return "LEA";
case OP_TRAP: return "TRAP";
default: return "UNKNOWN";
}
}
static const char *trap_name(uint16_t trapvect)
{
switch (trapvect) {
case TRAP_GETC: return "GETC";
case TRAP_OUT: return "OUT";
case TRAP_PUTS: return "PUTS";
case TRAP_IN: return "IN";
case TRAP_PUTSP: return "PUTSP";
case TRAP_HALT: return "HALT";
default: return "UNKNOWN";
}
}
static const char *cond_name(uint16_t cond)
{
switch (cond) {
case FL_POS: return "P";
case FL_ZRO: return "Z";
case FL_NEG: return "N";
default: return "?";
}
}
static void trace_instruction(uint16_t pc_before, uint16_t instr, uint16_t op)
{
printf("PC=%04X INSTR=%04X OP=%s", pc_before, instr, opcode_name(op));
if (op == OP_TRAP) {
uint16_t trapvect = instr & 0xFF;
printf(" %s", trap_name(trapvect));
}
printf("\n");
}
static void trace_registers(void)
{
printf("R0=%04X R1=%04X R2=%04X R3=%04X ", reg[R_R0], reg[R_R1], reg[R_R2], reg[R_R3]);
printf("R4=%04X R5=%04X R6=%04X R7=%04X ", reg[R_R4], reg[R_R5], reg[R_R6], reg[R_R7]);
printf("PC=%04X COND=%s\n", reg[R_PC], cond_name(reg[R_COND]));
}
static void print_profile(void)
{
printf("\nprofile:\n");
printf("total instructions: %llu\n\n",
(unsigned long long)total_instructions);
for (uint16_t op = 0; op < OPCODE_COUNT; ++op) {
if (op_counts[op] == 0) {
continue;
}
printf("%-6s %10llu\n",
opcode_name(op),
(unsigned long long)op_counts[op]);
}
}
// step実行時に、次の命令へ進むまで待つ:追加するコード
static void wait_for_step(void)
{
int c;
printf("step> ");
fflush(stdout);
do {
c = getchar();
} while (c != '\n' && c != EOF);
printf("\n");
}
int main(int argc, const char* argv[])
{
int trace_enabled = 0;
int profile_enabled = 0;
int step_enabled = 0; // 追加するコード
const char *image_path = NULL;
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--trace") == 0) {
trace_enabled = 1;
} else if (strcmp(argv[i], "--profile") == 0) {
profile_enabled = 1;
// –stepオプションを受け取る:追加するコード
} else if (strcmp(argv[i], "--step") == 0) {
step_enabled = 1;
} else if (argv[i][0] == '-') {
fprintf(stderr, "unknown option: %s\n", argv[i]);
fprintf(stderr, "usage: %s [--trace] [--profile] [--step] image.obj\n", argv[0]);
return 2;
} else if (image_path == NULL) {
image_path = argv[i];
} else {
fprintf(stderr, "usage: %s [--trace] [--profile] [--step] image.obj\n", argv[0]);
return 2;
}
}
if (image_path == NULL) {
fprintf(stderr, "usage: %s [--trace] [--profile] [--step] image.obj\n", argv[0]);
return 2;
}
// step実行では、毎回止まる前に命令情報も見たいのでtraceを自動で有効にする:追加するコード
if (step_enabled) {
trace_enabled = 1;
}
if (!read_image(image_path))
{
printf("failed to load image: %s\n", image_path);
exit(1);
}
signal(SIGINT, handle_interrupt);
disable_input_buffering();
/* since exactly one condition flag should be set at any given time, set the Z flag */
reg[R_COND] = FL_ZRO;
/* set the PC to starting position */
/* 0x3000 is the default */
enum { PC_START = 0x3000 };
reg[R_PC] = PC_START;
int running = 1;
while (running)
{
/* FETCH */
uint16_t pc_before = reg[R_PC];
uint16_t instr = mem_read(reg[R_PC]++);
uint16_t op = instr >> 12;
if (profile_enabled) {
++total_instructions;
++op_counts[op];
}
if (trace_enabled) {
trace_instruction(pc_before, instr, op);
}
switch (op)
{
case OP_ADD:
{
/* destination register (DR) */
uint16_t r0 = (instr >> 9) & 0x7;
/* first operand (SR1) */
uint16_t r1 = (instr >> 6) & 0x7;
/* whether we are in immediate mode */
uint16_t imm_flag = (instr >> 5) & 0x1;
if (imm_flag)
{
uint16_t imm5 = sign_extend(instr & 0x1F, 5);
reg[r0] = reg[r1] + imm5;
}
else
{
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] + reg[r2];
}
update_flags(r0);
}
break;
case OP_AND:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t imm_flag = (instr >> 5) & 0x1;
if (imm_flag)
{
uint16_t imm5 = sign_extend(instr & 0x1F, 5);
reg[r0] = reg[r1] & imm5;
}
else
{
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] & reg[r2];
}
update_flags(r0);
}
break;
case OP_NOT:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
reg[r0] = ~reg[r1];
update_flags(r0);
}
break;
case OP_BR:
{
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
uint16_t cond_flag = (instr >> 9) & 0x7;
if (cond_flag & reg[R_COND])
{
reg[R_PC] += pc_offset;
}
}
break;
case OP_JMP:
{
/* Also handles RET */
uint16_t r1 = (instr >> 6) & 0x7;
reg[R_PC] = reg[r1];
}
break;
case OP_JSR:
{
uint16_t long_flag = (instr >> 11) & 1;
reg[R_R7] = reg[R_PC];
if (long_flag)
{
uint16_t long_pc_offset = sign_extend(instr & 0x7FF, 11);
reg[R_PC] += long_pc_offset; /* JSR */
}
else
{
uint16_t r1 = (instr >> 6) & 0x7;
reg[R_PC] = reg[r1]; /* JSRR */
}
}
break;
case OP_LD:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
reg[r0] = mem_read(reg[R_PC] + pc_offset);
update_flags(r0);
}
break;
case OP_LDI:
{
/* destination registar (DR) */
uint16_t r0 = (instr >> 9) & 0x7;
/* PCoffset 9 */
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
/* add pc_offset to the current PC, look at that memory location to get the final address */
reg[r0] = mem_read(mem_read(reg[R_PC] + pc_offset));
update_flags(r0);
}
break;
case OP_LDR:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
reg[r0] = mem_read(reg[r1] + offset);
update_flags(r0);
}
break;
case OP_LEA:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
reg[r0] = reg[R_PC] + pc_offset;
update_flags(r0);
}
break;
case OP_ST:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
mem_write(reg[R_PC] + pc_offset, reg[r0]);
}
break;
case OP_STI:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
mem_write(mem_read(reg[R_PC] + pc_offset), reg[r0]);
}
break;
case OP_STR:
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
mem_write(reg[r1] + offset, reg[r0]);
}
break;
case OP_TRAP:
reg[R_R7] = reg[R_PC];
switch (instr & 0xFF)
{
case TRAP_GETC:
/* read a single ASCII char */
reg[R_R0] = (uint16_t)getchar();
update_flags(R_R0);
break;
case TRAP_OUT:
putc((char)reg[R_R0], stdout);
fflush(stdout);
break;
case TRAP_PUTS:
{
/* one char per word */
uint16_t* c = memory + reg[R_R0];
while (*c)
{
putc((char)*c, stdout);
++c;
}
fflush(stdout);
}
break;
case TRAP_IN:
{
printf("Enter a character: ");
char c = getchar();
putc(c, stdout);
fflush(stdout);
reg[R_R0] = (uint16_t)c;
update_flags(R_R0);
}
break;
case TRAP_PUTSP:
{
/* one char per byte (two bytes per word)
here we need to swap back to
big endian format */
uint16_t* c = memory + reg[R_R0];
while (*c)
{
char char1 = (*c) & 0xFF;
putc(char1, stdout);
char char2 = (*c) >> 8;
if (char2) putc(char2, stdout);
++c;
}
fflush(stdout);
}
break;
case TRAP_HALT:
puts("HALT");
fflush(stdout);
running = 0;
break;
}
break;
case OP_RES:
case OP_RTI:
default:
abort();
break;
}
if (trace_enabled) {
trace_registers();
if (running) {
printf("\n");
}
}
// 追加:step有効時は、1命令実行するたびにEnter入力を待つ:追加するコード
if (step_enabled && running) {
wait_for_step();
}
}
restore_input_buffering();
if (profile_enabled) {
print_profile();
}
return 0;
}
今回できたこと
今回は、LC-3 VMにステップ実行機能を追加しました。
--stepオプションを追加しましたstep_enabledでステップ実行の有効/無効を切り替えましたwait_for_step()でstep>を表示し、Enter入力を待つようにしました--step指定時はtrace表示も自動で出すようにしました- Hello Worldを1命令ずつ進め、PC、命令word、レジスタ、条件フラグを確認しました
ステップ実行があると、VMが命令をfetchし、decodeし、executeする流れを、自分のタイミングで止めながら確認できます。
次回はブレークポイントで指定番地で止める
ステップ実行では、すべての命令で止まります。小さいプログラムでは便利ですが、命令数が多いプログラムでは毎回Enterを押すのが大変です。
次回は、指定した番地に来たときだけ止まるブレークポイント機能を追加します。
