C言語で作るLC-3仮想マシン 第10回: レジスタとメモリをダンプして状態を見る | UNIX Cafe

* 当サイトでは、コンテンツの一部に広告を掲載しています。

System Note $ cat /proc/ai-disclosure

本記事の構成および論理分析にはAI(人工知能)を使用しています。情報の正確性は、システム管理者(UNIXユーザー)による手動検証済みです。

C言語で作るLC-3仮想マシン 第10回: レジスタとメモリをダンプして状態を見る | UNIX Cafe
目次

レジスタとメモリをダンプして状態を見る

前回は、VMに--break x3002オプションを追加し、指定番地に到達したときだけ止められるようにしました。

ブレークポイントで止まれるようになると、見たい場所まで一気に進められます。ただ、この時点では、止まったあとやプログラム終了後にVM内部の状態をまとめて確認する専用のオプションはありません。

たとえば「最後にR0には何が残ったのか」「x3000付近のメモリには何が入っているのか」を見るには、毎回trace出力を追う必要があります。短いプログラムなら追えますが、少し長くなると確認しづらくなります。

第10回では、プログラム終了後にレジスタ一覧とメモリ範囲を表示する--dump-registers--dump-memoryを追加します。実行中の動きはそのままにして、最後に必要な状態だけを取り出せるようにします。

GitHubではlc3-vm-cという名前で公開しています。

この記事の完成コードは、GitHubのlessons/10-dumpに置いています。リポジトリ直下のlc3.cは、連載の進行に合わせて最新版へ更新していきます。

Static Badge Static Badge Static Badge Static Badge

今回作るdump機能の全体像

今回追加するコマンドは次の2つです。どちらもLC-3プログラムを実行したあと、最後に状態を表示します。

./lc3 --dump-registers examples/hello.obj
./lc3 --dump-memory x3000 16 examples/hello.obj

--dump-registersは、プログラム終了後にレジスタの最終状態を表示します。R0からR7PC、条件フラグのCONDをまとめて確認できます。

Hello, World!
HALT

registers:
R0=3003 R1=0000 R2=0000 R3=0000 R4=0000 R5=0000 R6=0000 R7=3003 PC=3003 COND=P

--dump-memory x3000 16は、x3000から16 word分のメモリ内容を表示します。ここでの16はバイト数ではなく、LC-3のメモリ1個分であるword数です。

Hello, World!
HALT

memory x3000..x300F:
x3000: E002 F022 F025 0048 0065 006C 006C 006F
x3008: 002C 0020 0057 006F 0072 006C 0064 0021

第10回で追加する主な機能は、以下の通りです。

  • dump_registers(): レジスタ一覧を表示する関数
  • dump_memory(start, count): 指定範囲のメモリを表示する関数
  • parse_count(): --dump-memoryの件数を数値へ変換する関数
  • --dump-registersオプション: レジスタダンプを有効にする引数
  • --dump-memory xADDR COUNTオプション: メモリダンプを有効にする引数

作業する場所は、VM本体の中では次の範囲です。補助関数を追加し、main()ではコマンドライン引数を読んで、VM停止後に必要な表示を呼び出します。

lc3.c
├─ 補助関数
│  ├─ parse_count() を追加
│  ├─ dump_registers() を追加
│  └─ dump_memory() を追加
│
└─ main()
   ├─ dump用フラグを追加
   ├─ --dump-registers を読む
   ├─ --dump-memory xADDR COUNT を読む
   └─ VM停止後にdump関数を呼ぶ

ダンプは実行後に表示する

今回は、複雑な対話式デバッガにはしません。実行中にコマンドを入力して、メモリ表示やレジスタ表示を切り替えるような本格的な仕組みは、この連載では扱いません。

--step--breakの停止中に毎回メモリを表示するのではなく、まずはプログラムがHALTしたあとに最終状態を表示します。つまり、VMを最後まで動かしてから「結果を見る」機能です。

この形にすると、通常実行、trace、profile、breakと組み合わせやすくなります。dumpは実行後に追加で表示するだけなので、既存の実行ループを大きく変えずに済みます。

imageを読む
  ↓
VMを実行する
  ↓
HALTで停止する
  ↓
必要ならregister dumpを表示する
  ↓
必要ならmemory dumpを表示する
  ↓
必要ならprofileを表示する

今回の目的は、VM内部の状態を読みやすく取り出すことです。停止中にコマンドを入力する対話式デバッガではなく、実行結果を確認するための小さな道具としてdumpを追加します。

COUNTを数値として読む

--dump-memoryでは、開始番地と表示するword数を指定します。

./lc3 --dump-memory x3000 16 examples/hello.obj

この例では、開始番地がx3000、表示するword数が16です。

開始番地は、第9回で追加したparse_address()で読みます。これはx3000のようなLC-3の番地を読むための関数でした。

一方、COUNTは番地ではなく「何word表示するか」という個数です。役割が違う値なので、番地用の関数を使い回さず、個数用のparse_count()を追加します。

基本は16のような10進表記で指定します。ただ、LC-3では番地や値を16進数で見ることが多いので、x10のような16進表記も受け付けられるようにします。この場合のx10は10進数の16を意味します。

追加するコード: parse_address()の後、表示用の補助関数より前に追加します。(補助関数

// 追加: --dump-memory の COUNT を10進数または x 始まりの16進数として読む
static int parse_count(const char *s, unsigned int *out)
{
    char *end;
    unsigned long value;
    int base = 10;

    if (s == NULL || *s == '\0') {
        return 0;
    }

    if (s[0] == 'x' || s[0] == 'X') {
        ++s;
        base = 16;
    }

    if (*s == '\0') {
        return 0;
    }

    value = strtoul(s, &end, base);
    if (*end != '\0' || value == 0 || value > MEMORY_MAX) {
        return 0;
    }

    *out = (unsigned int)value;
    return 1;
}

baseは、strtoul()に渡す基数です。最初は10にしておき、文字列がxまたはXで始まっていたら、先頭の1文字を飛ばしてから16進数として読みます。

たとえば16なら10進数として16、x10なら16進数として16になります。どちらも最終的には同じdump_memory_countへ入ります。

value == 0もエラーにしています。0 word分のメモリダンプは何も表示できないため、ここでは指定ミスとして扱います。

件数はuint16_tではなくunsigned intで受け取ります。LC-3のメモリは65536 wordありますが、uint16_tで表せる最大値は65535です。メモリ全体を指定できる余地を残すため、ここでは少し大きい整数型を使います。

usage表示を関数にまとめる

オプションが増えてきたので、エラー時に表示するusage文字列を関数にまとめます。

usageは「このプログラムはどんな引数を受け取るか」を示す案内です。未知のオプションを指定したとき、必要な引数が足りないとき、画像ファイルが指定されていないときなど、複数の場所から表示します。

第9回までは、それぞれのエラー箇所でusage文字列を書いていました。第10回では--dump-registers--dump-memoryが加わるため、同じ文字列を複数箇所に置くと、あとで直す場所が増えて更新漏れが起きやすくなります。

追加するコード: 先ほど追加したparse_count()の後に追加します。(補助関数

// 追加: オプションが増えたので usage 表示を1か所にまとめる
static void print_usage(const char *program)
{
    fprintf(stderr,
            "usage: %s [--trace] [--profile] [--step] [--break xADDR] "
            "[--dump-registers] [--dump-memory xADDR COUNT] image.obj\n",
            program);
}

これ以降は、引数のエラー時にprint_usage(argv[0]);を呼ぶだけで、同じusageを表示できます。argv[0]には実行されたプログラム名が入っているので、./lc3として実行した場合はusageにも./lc3が表示されます。

レジスタを表示する

レジスタ表示は、すでに第6回以降で使っているtrace_registers()を再利用します。新しい表示処理を最初から作るのではなく、既存の「レジスタを1行で表示する関数」に見出しだけを足します。

trace_registers()は、R0からR7PCCONDを1行で表示できます。プログラム終了後の最終状態も、trace中の表示と同じ形式で見られるようにします。

今回追加するdump_registers()は、見出しを付けてからtrace_registers()を呼ぶだけです。

追加するコード: trace_registers()の後に追加します。(補助関数

// 追加: プログラム終了後にレジスタの最終状態を表示する
static void dump_registers(void)
{
    printf("\nregisters:\n");
    trace_registers();
}

ここでは、ダンプ専用の別形式を増やさないようにします。traceとdumpで同じレジスタ表示を使うことで、読者は同じ見方で状態を確認できます。

メモリを範囲指定で表示する

次に、指定範囲のメモリを表示するdump_memory(start, count)を追加します。startは表示を始める番地、countはそこから何word表示するかを表します。

表示形式は次のようにします。

memory x3000..x300F:
x3000: E002 F022 F025 0048 0065 006C 006C 006F
x3008: 002C 0020 0057 006F 0072 006C 0064 0021

1行に8 wordずつ表示します。左端には、その行の先頭番地を表示します。

1 wordずつ縦に並べると長くなりやすく、逆に全部を1行にすると読みにくくなります。8 word単位にすると、Hello Worldのような短いプログラムでは命令部分と文字列部分を見やすく確認できます。

追加するコード: 先ほど追加したdump_registers()の後に追加します。(補助関数

// 追加: 指定範囲のメモリを8 wordずつ表示する
static void dump_memory(uint16_t start, unsigned int count)
{
    unsigned int offset = 0;

    printf("\nmemory x%04X..x%04X:\n", start, (uint16_t)(start + count - 1));

    while (offset < count) {
        uint16_t address = start + offset;
        unsigned int words_on_line = count - offset;

        if (words_on_line > 8) {
            words_on_line = 8;
        }

        printf("x%04X:", address);
        for (unsigned int i = 0; i < words_on_line; ++i) {
            printf(" %04X", mem_read(address + i));
        }
        printf("\n");

        offset += words_on_line;
    }
}

offsetは、開始番地から何word分進んだかを表します。最初は0なのでstartそのものを表示し、1行表示するたびに表示済みのword数だけ増やします。

words_on_lineには、その行で表示するword数を入れます。基本は8 wordですが、最後の行だけは残りが8 word未満になることがあります。そのため、残りword数を見てから最大8 wordに丸めています。

メモリの読み取りにはmem_read()を使います。通常のメモリならmemory[address]を返すだけですが、LC-3 VMではキーボード用のメモリマップドレジスタもmem_read()側で扱っています。

今回の確認ではx3000付近だけを見るので、実際には通常のメモリを読んでいます。それでも、VM内でメモリを読むときはmem_read()を通す、という形に揃えておくと、あとで読む場所によって処理が変わってしまう心配を減らせます。

dump用の変数を追加する

main()の先頭に、dump用のフラグと値を追加します。(main関数

ここで追加する変数は、コマンドライン引数を読んだ結果を保存しておくためのものです。引数を読んだ時点ではまだVMは実行していないので、「あとで表示するかどうか」と「どの範囲を表示するか」だけを覚えておきます。

変更前:

int trace_enabled = 0;
int profile_enabled = 0;
int step_enabled = 0;
int break_enabled = 0;
uint16_t breakpoint = 0;
const char *image_path = NULL;

変更後:

int trace_enabled = 0;
int profile_enabled = 0;
int step_enabled = 0;
int break_enabled = 0;
// 追加: dumpオプションの指定状態とメモリ範囲を保存する
int dump_registers_enabled = 0;
int dump_memory_enabled = 0;
uint16_t breakpoint = 0;
uint16_t dump_memory_start = 0;
unsigned int dump_memory_count = 0;
const char *image_path = NULL;

dump_registers_enabledは、--dump-registersが指定されたかどうかを保存します。0なら表示しない、1ならVM停止後に表示する、というフラグです。

dump_memory_enabledは、--dump-memoryが指定されたかどうかを保存します。開始番地はdump_memory_start、表示件数はdump_memory_countに入れます。この3つを分けておくと、VMの実行後にそのままdump_memory(dump_memory_start, dump_memory_count)を呼べます。

–dump-registersを受け取る

次に、コマンドライン引数の処理へ--dump-registersを追加します。これは「レジスタを表示するかどうか」を切り替えるだけのオプションです。

追加するコード: --stepの分岐の後に追加します。(main関数

// 追加: レジスタダンプを有効にする
} else if (strcmp(argv[i], "--dump-registers") == 0) {
    dump_registers_enabled = 1;

このオプションは追加の引数を取りません。指定されたらdump_registers_enabledを1にするだけです。実際の表示は、VMが停止したあとに行います。

–dump-memoryを受け取る

--dump-memoryは、開始番地と件数を続けて受け取ります。つまり、このオプションだけは追加で2つの引数が必要です。

./lc3 --dump-memory x3000 16 examples/hello.obj

追加するコード: --dump-registersの分岐の後に追加します。(main関数

// 追加: メモリダンプの開始番地と件数を読む
} else if (strcmp(argv[i], "--dump-memory") == 0) {
    unsigned int last_address;

    if (i + 2 >= argc ||
        !parse_address(argv[i + 1], &dump_memory_start) ||
        !parse_count(argv[i + 2], &dump_memory_count)) {
        print_usage(argv[0]);
        return 2;
    }

    last_address = (unsigned int)dump_memory_start + dump_memory_count - 1;
    if (last_address >= MEMORY_MAX) {
        fprintf(stderr, "dump range exceeds LC-3 memory: x%04X + %u\n",
                dump_memory_start,
                (unsigned int)dump_memory_count);
        return 2;
    }

    dump_memory_enabled = 1;
    i += 2;

i + 2 >= argcで、開始番地と件数の両方があるか確認します。argv[i]--dump-memoryなので、argv[i + 1]が開始番地、argv[i + 2]が件数になります。

次に、parse_address()で開始番地を読み、parse_count()で件数を読みます。どちらかが失敗した場合は、指定が間違っているのでusageを表示して終了します。

最後に、指定範囲がLC-3メモリを超えないか確認します。たとえば--dump-memory xFFFE 4のように指定すると、xFFFExFFFFの次まで読もうとしてしまいます。

そこで、開始番地と件数から最後の番地を計算します。LC-3のメモリはx0000からxFFFFまでなので、最後のアドレスがMEMORY_MAX以上になったらエラーにします。

--dump-memoryは2つの追加引数を消費するため、最後にi += 2;で読み終えた分を進めます。これを忘れると、次のループでx300016を別の引数として処理してしまいます。

usageの呼び出しを置き換える

変更するコード: print_usage()を追加したので、既存のusage表示を置き換えます。

ここは新しい機能を増やすというより、同じusage文字列を何度も書かないようにする整理です。エラー時の表示内容は変えず、呼び出し方だけをprint_usage(argv[0]);に揃えます。

たとえば、--breakの引数が不正な場合は次のようにします。(main関数

} else if (strcmp(argv[i], "--break") == 0) {
    if (i + 1 >= argc || !parse_address(argv[i + 1], &breakpoint)) {
        print_usage(argv[0]);
        return 2;
    }
    break_enabled = 1;
    ++i;

未知のオプションや、画像ファイルが指定されていない場合も、同じprint_usage()を呼びます。これで、引数エラーの表示を1つの関数で管理できます。

VM停止後にdumpを出す

実行ループが終わったあと、端末設定を戻してからdumpを表示します。ここでいう「実行ループが終わる」とは、LC-3プログラムがHALTしてrunningが0になった状態です。

変更するコード: restore_input_buffering();の後に追加します。(main関数

restore_input_buffering();

// 追加: VM停止後に必要なdumpを表示する
if (dump_registers_enabled) {
    dump_registers();
}

if (dump_memory_enabled) {
    dump_memory(dump_memory_start, dump_memory_count);
}

if (profile_enabled) {
    print_profile();
}

端末入力の設定は、VM実行中に変更しています。そのため、表示処理の前にrestore_input_buffering()を呼んでおきます。dump表示そのものは普通のprintf()なので、端末を通常の状態に戻してから出すほうが扱いやすくなります。

--profileと組み合わせた場合は、dumpのあとにprofileを表示します。出力順は、レジスタ、メモリ、profileです。これにより、先にVMの最終状態を見てから、最後に実行命令数を確認できます。

実行して確認する

まずビルドします。

cd lessons/10-dump
make

レジスタダンプを確認します。

./lc3 --dump-registers examples/hello.obj

出力例です。

Hello, World!
HALT

registers:
R0=3003 R1=0000 R2=0000 R3=0000 R4=0000 R5=0000 R6=0000 R7=3003 PC=3003 COND=P

R0にはx3003が残っています。Hello Worldプログラムでは、LEA命令で文字列の先頭番地をR0に入れ、その後PUTSR0の指す文字列を表示します。

メモリダンプも確認します。

./lc3 --dump-memory x3000 16 examples/hello.obj

出力例です。

Hello, World!
HALT

memory x3000..x300F:
x3000: E002 F022 F025 0048 0065 006C 006C 006F
x3008: 002C 0020 0057 006F 0072 006C 0064 0021

x3000にはLEA命令、x3001にはPUTSx3002にはHALTが入っています。x3003以降には、Hello, World!の文字列が1文字ずつ格納されています。

このようにメモリダンプを見ると、命令として置かれているwordと、文字列データとして置かれているwordを同じ画面で確認できます。

レジスタとメモリの両方を同時に指定することもできます。この場合は、レジスタダンプを表示してから、続けてメモリダンプを表示します。

./lc3 --dump-registers --dump-memory x3000 16 examples/hello.obj

profileやbreakと組み合わせる

今回のdump機能は、これまで追加してきたデバッグ用オプションと組み合わせられます。dumpはプログラム終了後に表示するだけなので、traceやbreakの動きを邪魔しません。

たとえば、profileとメモリダンプを同時に使うと、最終的なメモリ内容と実行命令数を一緒に確認できます。

./lc3 --profile --dump-memory x3000 16 examples/hello.obj

出力例です。

Hello, World!
HALT

memory x3000..x300F:
x3000: E002 F022 F025 0048 0065 006C 006C 006F
x3008: 002C 0020 0057 006F 0072 006C 0064 0021

profile:
total instructions: 3

LEA             1
TRAP            2

--breakと組み合わせると、指定番地でいったん停止し、Enterで続行したあとに最終レジスタを確認できます。breakで途中状態を見て、dumpで終了後の状態を見る、という使い分けです。

./lc3 --break x3002 --dump-registers examples/hello.obj

この連載では、停止中にコマンドを入力して状態表示を切り替える本格的な対話式デバッガまでは扱いません。第10回では、プログラム終了後に状態を取り出せるようにしておきます。

完成コード

#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 int parse_address(const char *s, uint16_t *out)
{
    char *end;
    unsigned long value;

    if (s == NULL || *s == '\0') {
        return 0;
    }

    if (s[0] == 'x' || s[0] == 'X') {
        ++s;
    }

    if (*s == '\0') {
        return 0;
    }

    value = strtoul(s, &end, 16);
    if (*end != '\0' || value > 0xFFFF) {
        return 0;
    }

    *out = (uint16_t)value;
    return 1;
}

// 追加: --dump-memory の COUNT を10進数または x 始まりの16進数として読む
static int parse_count(const char *s, unsigned int *out)
{
    char *end;
    unsigned long value;
    int base = 10;

    if (s == NULL || *s == '\0') {
        return 0;
    }

    if (s[0] == 'x' || s[0] == 'X') {
        ++s;
        base = 16;
    }

    if (*s == '\0') {
        return 0;
    }

    value = strtoul(s, &end, base);
    if (*end != '\0' || value == 0 || value > MEMORY_MAX) {
        return 0;
    }

    *out = (unsigned int)value;
    return 1;
}

// 追加: オプションが増えたので usage 表示を1か所にまとめる
static void print_usage(const char *program)
{
    fprintf(stderr,
            "usage: %s [--trace] [--profile] [--step] [--break xADDR] "
            "[--dump-registers] [--dump-memory xADDR COUNT] image.obj\n",
            program);
}

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 dump_registers(void)
{
    printf("\nregisters:\n");
    trace_registers();
}

// 追加: 指定範囲のメモリを8 wordずつ表示する
static void dump_memory(uint16_t start, unsigned int count)
{
    unsigned int offset = 0;

    printf("\nmemory x%04X..x%04X:\n", start, (uint16_t)(start + count - 1));

    while (offset < count) {
        uint16_t address = start + offset;
        unsigned int words_on_line = count - offset;

        if (words_on_line > 8) {
            words_on_line = 8;
        }

        printf("x%04X:", address);
        for (unsigned int i = 0; i < words_on_line; ++i) {
            printf(" %04X", mem_read(address + i));
        }
        printf("\n");

        offset += words_on_line;
    }
}

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]);
    }
}

static void wait_for_step(void)
{
    int c;

    printf("step> ");
    fflush(stdout);

    do {
        c = getchar();
    } while (c != '\n' && c != EOF);

    printf("\n");
}

static void wait_for_break(void)
{
    int c;

    printf("break> ");
    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;
    int break_enabled = 0;
    // 追加: dumpオプションの指定状態とメモリ範囲を保存する
    int dump_registers_enabled = 0;
    int dump_memory_enabled = 0;
    uint16_t breakpoint = 0;
    uint16_t dump_memory_start = 0;
    unsigned int dump_memory_count = 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;
        } else if (strcmp(argv[i], "--step") == 0) {
            step_enabled = 1;
        // 追加: レジスタダンプを有効にする
        } else if (strcmp(argv[i], "--dump-registers") == 0) {
            dump_registers_enabled = 1;
        // 追加: メモリダンプの開始番地と件数を読む
        } else if (strcmp(argv[i], "--dump-memory") == 0) {
            unsigned int last_address;

            if (i + 2 >= argc ||
                !parse_address(argv[i + 1], &dump_memory_start) ||
                !parse_count(argv[i + 2], &dump_memory_count)) {
                print_usage(argv[0]);
                return 2;
            }

            last_address = (unsigned int)dump_memory_start + dump_memory_count - 1;
            if (last_address >= MEMORY_MAX) {
                fprintf(stderr, "dump range exceeds LC-3 memory: x%04X + %u\n",
                        dump_memory_start,
                        (unsigned int)dump_memory_count);
                return 2;
            }

            dump_memory_enabled = 1;
            i += 2;
        } else if (strcmp(argv[i], "--break") == 0) {
            if (i + 1 >= argc || !parse_address(argv[i + 1], &breakpoint)) {
                print_usage(argv[0]);
                return 2;
            }
            break_enabled = 1;
            ++i;
        } else if (argv[i][0] == '-') {
            fprintf(stderr, "unknown option: %s\n", argv[i]);
            print_usage(argv[0]);
            return 2;
        } else if (image_path == NULL) {
            image_path = argv[i];
        } else {
            print_usage(argv[0]);
            return 2;
        }
    }

    if (image_path == NULL) {
        print_usage(argv[0]);
        return 2;
    }

    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 */
        if (break_enabled && reg[R_PC] == breakpoint) {
            uint16_t pc = reg[R_PC];
            uint16_t instr = mem_read(pc);
            uint16_t op = instr >> 12;

            printf("breakpoint hit at x%04X\n", pc);
            trace_instruction(pc, instr, op);
            trace_registers();
            printf("\n");
            wait_for_break();
        }

        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");
            }
        }

        if (step_enabled && running) {
            wait_for_step();
        }
    }
    restore_input_buffering();

    // 追加: VM停止後に必要なdumpを表示する
    if (dump_registers_enabled) {
        dump_registers();
    }

    if (dump_memory_enabled) {
        dump_memory(dump_memory_start, dump_memory_count);
    }

    if (profile_enabled) {
        print_profile();
    }

    return 0;
}

まとめ

第10回では、VMの状態を後から確認するためのdump機能を追加しました。

  • --dump-registersでレジスタの最終状態を表示できるようになりました
  • --dump-memory xADDR COUNTで指定範囲のメモリ内容を表示できるようになりました
  • trace、profile、step、breakと組み合わせて、VM内部を観察しやすくなりました

状態を見られる道具が揃ってきました。

次回は、小さいLC-3プログラムを用意し、trace、profile、step、break、dumpを組み合わせて実際にデバッグしていきます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

のいのアバター のい UNIX Cafe マスター

Macintosh Color Classicから始まった旅は、長いWindows時代を経て、Windows10のサポート終了をきっかけにUNIXの世界へ戻ってきました。UNIX Cafeでは、UNIX・Linux・そしてMacな世界を、むずかしい言葉を使わず、物語のように書いています。プログラミングは、アイデアをコンピューターに伝えるための言葉です。簡単な単語と文法を覚えれば、誰でもコマンドを使えます。ぜひ一度、やさしいプログラミングの世界をのぞいてみてください。

目次