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

命令実行をトレースしてVMの動きを見る
前回は、Hello Worldが通るだけの小さいLC-3アセンブラをCで作り、hello.asmからhello-minias.objを生成しました。これで、VMに渡す.objを自分で作る流れまで確認できました。
第6回からは、VMの動きを観察する機能を追加していきます。まずは、VMが.objをどの番地から読み、どの16bit命令としてdecodeし、レジスタや条件フラグをどう変えているのかを1命令ずつ見えるようにします。
今回は、VMに--traceオプションを追加します。命令を1つ実行するたびにPC、命令word、opcode、TRAP名、レジスタ、条件フラグを表示し、前回作ったHello Worldの.objがどの順番で実行されているのかを観察します。
GitHubでは lc3-vm-c という名前で公開しています。
この記事のゴール
第4回では、LC-3アセンブリでHello Worldを書き、lc3asを使って.objファイルを作成しました。
第5回では、その変換処理を自分で確かめるために、hello.asmを.objへ変換できる小さいアセンブラを作りました。
今回は、できあがった.objをVMがどの番地から読み、どの命令として実行しているのかを見えるようにします。
VMに--traceオプションを追加し、命令を1つ実行するたびに、次の情報を表示できるようにします。
PC
命令word
opcode名
TRAP名
レジスタ
条件フラグ最後に、Hello Worldが次のような流れで実行されていることを確認します。
PC=3000 INSTR=E002 OP=LEA
PC=3001 INSTR=F022 OP=TRAP PUTS
PC=3002 INSTR=F025 OP=TRAP HALT今回の作業の進め方
今回の作業は、VM本体のlc3.cに、デバッグ用の表示機能を追加する形で進めます。
lc3.c
↓
--trace オプションを追加する
↓
fetchしたPCと命令wordを表示する
↓
opcode名を表示する
↓
TRAP命令を少し詳しく表示する
↓
レジスタと条件フラグを表示する
↓
Hello Worldをトレースする通常実行では、今までどおり余計な情報は表示しません。
この記事では、コードブロックの直前に役割を書きます。確認するコードは読むだけの抜粋、追加するコードは書き足す部分、変更後のコードは既存コードを置き換える部分です。
今回は、各パートで少しずつコードを追加していきます。ただし、途中では実行確認を挟みません。トレース機能は複数の部品がそろってから動かすほうが分かりやすいので、必要な変更を最後まで入れてから、--traceを付けてまとめて確認します。途中に出てくる出力例は、実際にその場で実行するものではなく、完成後のトレース出力から注目する部分だけを抜き出した説明用の例です。
実行例: 通常実行では、.objだけを指定します。
./lc3 programs/hello.obj--traceを付けたときだけ、命令実行の様子を表示します。
実行例: トレース実行では、先頭に--traceを付けます。
./lc3 --trace programs/hello.obj1. トレースとは何か
トレースとは、プログラムの実行中に「いま何を実行しているか」を記録したり、画面に表示したりすることです。
LC-3 VMの場合は、命令を1つfetchするたびに、その命令の情報を表示できれば、実行の流れを追いやすくなります。
最初は、次の2つだけでも役に立ちます。
確認する出力: 完成後のトレース出力から、PCと命令wordだけを抜き出した例です。
PC=3000 INSTR=E002PCは、命令が置かれていたアドレスです。
INSTRは、メモリから読み出した16bitの命令wordです。
この2つが見えるだけで、VMがどの番地からどの命令を読んでいるかを確認できます。
2. fetch時のPCを残す
LC-3 VMでは、命令をfetchするときにPCを進めます。ここをそのまま表示すると、「命令が置かれていた番地」と「次に読む番地」が混ざって見えます。
まず、現在のfetch処理を確認します。ここは説明用の抜粋です。
確認するコード: 既存のfetch処理は、命令を読むと同時にPCを進めています。
// 現在のPCから命令を読み、PCを次の番地へ進める
uint16_t instr = mem_read(reg[R_PC]++);
// 上位4bitをopcodeとして取り出す
uint16_t op = instr >> 12;この書き方では、mem_read()が終わった時点で、reg[R_PC]は次の命令を指しています。
トレースで表示したいのは「命令が置かれていた番地」です。そのため、fetch前のPCを別の変数に残します。
変更後のコード: 元のfetch周辺を、次のように置き換えます。
// fetch前のPCを残しておく
uint16_t pc_before = reg[R_PC];
// 命令を読み、PCを次の命令へ進める
uint16_t instr = mem_read(reg[R_PC]++);
// opcodeは命令wordの上位4bit
uint16_t op = instr >> 12;以降のトレース表示では、このpc_beforeを使います。
3. まずはPCと命令wordを表示する
まずは、VMがどの番地からどの命令wordを読んでいるかだけを表示します。
追加するコード: 命令をfetchした直後に、次の表示処理を追加します。
if (trace_enabled) {
// trace時だけ、命令があった番地と命令wordを表示する
printf("PC=%04X INSTR=%04X\n", pc_before, instr);
}完成後のトレース出力では、最初の数命令から次の部分を確認できます。
確認する出力: 完成後の出力から、PCと命令wordに注目した例です。
PC=3000 INSTR=E002
PC=3001 INSTR=F022
PC=3002 INSTR=F025これで、VMがx3000から順番に命令を読んでいることが分かります。まだ命令名は出していませんが、読み込み位置の確認には十分です。
4. opcode名を表示する
次に、命令wordを命令名として読めるようにします。LC-3では、16bit命令の上位4bitがopcodeです。
VM本体では、すでに次のように取り出しています。ここは既存コードの確認です。
確認するコード: opcodeは命令wordの上位4bitです。
// LC-3命令の上位4bitがopcode
uint16_t op = instr >> 12;ただし、このままだと14や15のような数値でしか見えません。
そこで、opcode番号を文字列に変換する関数を用意します。これで、14ではなくLEAのように表示できます。
追加するコード: opcode定義の近く、またはmain()より前に次の関数を追加します。
static const char *opcode_name(uint16_t op)
{
// opcode番号を、人間が読みやすい命令名に変換する
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";
}
}表示は次のようにします。
変更後のコード: 先ほど追加したPCと命令wordの表示を、opcode名も出す形に置き換えます。
if (trace_enabled) {
// 数値のopcodeではなく、命令名として表示する
printf("PC=%04X INSTR=%04X OP=%s\n", pc_before, instr, opcode_name(op));
}完成後のトレース出力では、opcode名も確認できます。
確認する出力: 完成後の出力から、opcode名までに注目した例です。
PC=3000 INSTR=E002 OP=LEA
PC=3001 INSTR=F022 OP=TRAP
PC=3002 INSTR=F025 OP=TRAP5. TRAP命令を少し詳しく表示する
Hello Worldでは、PUTSとHALTがどちらもTRAP命令として実行されます。
確認する対応: アセンブリ命令とTRAP命令の対応です。
PUTS -> TRAP x22 -> F022
HALT -> TRAP x25 -> F025opcode名だけだと、どちらもOP=TRAPに見えます。
そこで、TRAPコードも文字列にします。
追加するコード: opcode_name()の近くに、次の関数を追加します。
static const char *trap_name(uint16_t trapvect)
{
// TRAPベクタ番号を、LC-3のサービス名に変換する
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";
}
}TRAP命令の場合だけ、下位8bitを取り出して表示します。
変更後のコード: opcode名を表示していた部分を、次のように置き換えます。
if (trace_enabled) {
// まず共通部分としてPC、命令word、opcode名を表示する
printf("PC=%04X INSTR=%04X OP=%s", pc_before, instr, opcode_name(op));
if (op == OP_TRAP) {
// TRAP命令だけは下位8bitにサービス番号が入っている
uint16_t trapvect = instr & 0xFF;
printf(" %s", trap_name(trapvect));
}
// 1命令分のトレース行をここで閉じる
printf("\n");
}完成コードでは、この表示処理をtrace_instruction()という関数にまとめています。処理内容は同じです。
完成後のトレース出力では、TRAP名まで確認できます。
確認する出力: 完成後の出力から、TRAP名までに注目した例です。
PC=3000 INSTR=E002 OP=LEA
PC=3001 INSTR=F022 OP=TRAP PUTS
PC=3002 INSTR=F025 OP=TRAP HALT6. レジスタの状態を表示する
命令名だけでなく、レジスタの変化も見えるようにします。命令の結果がレジスタに反映されているかを確認するためです。
Hello Worldで特に見たいのは、LEA R0, HELLOのあとにR0へ文字列のアドレスが入るところです。
確認する出力: 完成後の出力で、LEA実行後に見たいレジスタの値です。
R0=3003まずは、R0からR7、PC、CONDを表示する関数を作ります。
追加するコード: トレース用関数として、main()より前に追加します。
static void trace_registers(void)
{
// 汎用レジスタR0からR7をまとめて表示する
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]);
// まずはPCと条件フラグの数値を同じ行に出す
printf("PC=%04X COND=%04X\n", reg[R_PC], reg[R_COND]);
}この時点では、条件フラグをCOND=0002のような数値で表示しています。次の章で、これをP、Z、Nの文字表示に変更します。
この関数は、命令を実行したあとに呼びます。そうすると、その命令によってレジスタがどう変わったかを確認しやすくなります。
追加するコード: 各命令のswitch処理が終わったあとに、次の呼び出しを追加します。
if (trace_enabled) {
// 命令実行後のレジスタ状態を表示する
trace_registers();
printf("\n");
}7. 条件フラグを読みやすくする
COND=0002のような表示でも間違いではありません。
ただし、LC-3の条件フラグはP、Z、Nのどれかなので、文字で表示したほうが読みやすくなります。
追加するコード: 条件フラグの値を文字へ変換する関数を追加します。
static const char *cond_name(uint16_t cond)
{
// 条件フラグのbit値を、P/Z/Nの文字に変換する
switch (cond) {
case FL_POS: return "P";
case FL_ZRO: return "Z";
case FL_NEG: return "N";
default: return "?";
}
}最終的な表示は、次のように変えます。
変更後の出力: 条件フラグは数値ではなく、次のように表示します。
COND=Z変更後のコード: trace_registers()のCOND表示を、cond_name()を使う形に変更します。
static void trace_registers(void)
{
// 汎用レジスタR0からR7をまとめて表示する
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]);
// 条件フラグは数値ではなくP/Z/Nで表示する
printf("PC=%04X COND=%s\n", reg[R_PC], cond_name(reg[R_COND]));
}8. –trace オプションを追加する
普段の実行では、トレース表示は邪魔になります。通常実行では今までどおりプログラムの出力だけを見たいからです。
そこで、--traceを付けたときだけ有効にします。
実行例: 目標にするコマンド形です。
./lc3 --trace hello.obj引数の扱いは、次のようにします。
元のVMを複数の.objファイルを順番に読み込める形にしている場合もあります。ただ、今回は--traceの追加を分かりやすくするため、読み込む.objは1つだけにします。
変更後のコード: main()の引数処理を、通常実行とトレース実行を分ける形に変更します。
int trace_enabled = 0;
const char *image_path = NULL;
if (argc == 2) {
// 通常実行: 引数は.objファイルだけ
image_path = argv[1];
} else if (argc == 3 && strcmp(argv[1], "--trace") == 0) {
// トレース実行: --traceの次を.objファイルとして読む
trace_enabled = 1;
image_path = argv[2];
} else {
// 想定外の引数なら使い方を表示して終了する
fprintf(stderr, "usage: %s [--trace] image.obj\n", argv[0]);
return 2;
}これで、通常実行とトレース実行を切り替えられます。
9. 今回の完成コード
ここまでの変更をまとめると、次の形になります。
完成コード: lc3.cの全文です。今回追加・変更した場所には、// 追加:や// 変更:のコメントを付けています。実行確認は、このコードを入れ終わってから行います。
#include <stdio.h>
#include <stdint.h>
#include <signal.h>
// 追加: strcmp()を使うために必要
#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)
uint16_t memory[MEMORY_MAX]; /* 65536 locations */
uint16_t reg[R_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];
}
// 追加: opcode番号を、人間が読みやすい命令名に変換する
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";
}
}
// 追加: TRAPベクタ番号を、LC-3のサービス名に変換する
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";
}
}
// 追加: 条件フラグのbit値を、P/Z/Nの文字に変換する
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 "?";
}
}
// 追加: 1命令分のトレース行を表示する
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]));
}
int main(int argc, const char* argv[])
{
// 変更: --traceあり/なしで引数を分ける
int trace_enabled = 0;
const char *image_path = NULL;
if (argc == 2) {
// 通常実行: 引数は.objファイルだけ
image_path = argv[1];
} else if (argc == 3 && strcmp(argv[1], "--trace") == 0) {
// トレース実行: --traceの次を.objファイルとして読む
trace_enabled = 1;
image_path = argv[2];
} else {
// 想定外の引数なら使い方を表示して終了する
fprintf(stderr, "usage: %s [--trace] image.obj\n", argv[0]);
return 2;
}
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 */
// 変更: fetch前のPCを保存してから命令を読む
uint16_t pc_before = reg[R_PC];
// 命令を読み、PCを次の命令へ進める
uint16_t instr = mem_read(reg[R_PC]++);
// opcodeは命令wordの上位4bit
uint16_t op = instr >> 12;
// 追加: 命令実行前にトレース行を表示する
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();
printf("\n");
}
}
restore_input_buffering();
return 0;
}10. Hello Worldをトレースして確認する
完成コードを入れ終わったら、ここでまとめて確認します。使うのは、第5回で作ったhello-minias.objです。
実行して確認: まず、VMをビルドします。
cd LC-3VM
cc -o lc3 lc3.c先に、通常実行が今までどおり動くことを確認します。
./lc3 lc3as-lab/build/hello-minias.obj確認する出力: 通常実行では、トレース情報は表示されません。Hello WorldとHALTだけが出ればOKです。
Hello, World!
HALT次に、--traceを付けて実行します。ここで、命令ごとの情報が表示されることを確認します。
./lc3 --trace lc3as-lab/build/hello-minias.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
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
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=P11. トレース出力を読む
ここから、トレース出力を1命令ずつ読んでいきます。まず、最初の命令を見ます。
確認する出力: ここからは、トレース結果の読み方です。
PC=3000 INSTR=E002 OP=LEAx3000にあるLEA R0, HELLOを実行しています。
実行後、R0にはHELLOの番地であるx3003が入ります。
次の命令を見ます。
確認する出力: 2つ目の命令はTRAP PUTSです。
PC=3001 INSTR=F022 OP=TRAP PUTSPUTSは、R0が指すLC-3メモリ上の文字列を表示します。
なお、TRAP命令を実行すると、VMは戻り番地として次のPCをR7に保存します。そのため、PUTSのあとにはR7=3002、HALTのあとにはR7=3003になります。
最後の命令を見ます。
確認する出力: 最後の命令はTRAP HALTです。
PC=3002 INSTR=F025 OP=TRAP HALTHALTはVMの実行ループを止めます。
これで、Hello WorldがVM内部でどのように実行されているかを、1命令ずつ追えるようになりました。
今回できたこと
今回は、LC-3 VMにトレース機能を追加しました。
--traceオプションでトレース表示をON/OFFできるようにしました- fetchしたPCと命令wordを表示しました
- opcode名を表示しました
TRAP PUTSとTRAP HALTを区別して表示しました- レジスタと条件フラグを表示しました
- Hello Worldが
LEA、PUTS、HALTの順に実行されることを確認しました
トレース機能があると、VMが正しく命令を読んでいるか、PCがどこを指しているか、レジスタがどう変わるかを確認しやすくなります。VMが思ったとおりに動かないときも、まずこの出力を見ることで、どこまで正しく進んでいるかを切り分けやすくなります。
次に試せること
次は、トレース機能を土台にして、命令実行数を数える簡易プロファイル機能を作ってみます。
たとえば、次のような情報を集計できるプログラムです。
total instructions: 12345
ADD: 1200
LD: 830
ST: 412
TRAP: 9命令ごとの実行回数が分かると、2048.objやrogue.objのような大きめのLC-3プログラムが、どの命令を多く使っているかを眺められます。
