本記事の構成および論理分析にはAI(人工知能)を使用しています。情報の正確性は、システム管理者(UNIXユーザー)による手動検証済みです。
C言語で作るLC-3仮想マシン 第4回: LC-3アセンブリでHello Worldを書く | UNIX Cafe

第3回までの作業で、LC-3 VMは.objファイルを読み込み、命令を実行し、PUTSやHALTなどのTRAPも扱えるようになりました。
今回は、自分で作ったVMで “Hello World” を表示します。ただし入口は2つ用意します。
A. すぐ試すコース
printfで .obj を直接作る
外部アセンブラは不要
B. アセンブリを書くコース
hello.asmを書く
lc3asで .obj に変換するどちらのコースでも、最後に自作VMで同じ出力を確認します。
Hello, World!
HALTVMとアセンブラの役割は違う
lc3.cはアセンブラではありません。担当するのは、すでに機械語になっている.objファイルを読み込んで実行する部分です。
hello.obj --> lc3.c --> 実行一方、アセンブラが担当するのは、人間が読めるアセンブリをLC-3の16bit word列に変換する部分です。
hello.asm --> lc3as --> hello.objこの記事では、まず.objの正体を見るためにprintfで直接ファイルを作ります。そのあとで、同じ.objをlc3asで作ります。
作業ディレクトリ
今回の検証用にlc3as-labというディレクトリを使います。
lc3as-lab/
├── src/
│ └── hello.asm
├── build/
├── tools/
└── Makefileprintfコースだけ試す場合、必要なのはbuild/とVM本体だけです。lc3asコースまで試す場合は、tools/に既存のlc3asを用意します。
A: まずはprintfでobjを直接作る
外部ツールを入れたくない場合は、.objに入るbyte列を直接書き出せばOKです。
cd lc3as-lab
mkdir -p build
printf '\x30\x00\xe0\x02\xf0\x22\xf0\x25\x00\x48\x00\x65\x00\x6c\x00\x6c\x00\x6f\x00\x2c\x00\x20\x00\x57\x00\x6f\x00\x72\x00\x6c\x00\x64\x00\x21\x00\x0a\x00\x00' > build/hello-printf.objこれはアセンブラを使っているわけではなくて、.objに入るbyte列を、そのままファイルへ書き出しているだけです。
自作VMで実行します。
../main build/hello-printf.obj出力結果は次のようになります。
Hello, World!
HALTMakefileを使う場合は、次のコマンドだけで実行できます。
make run-printfprintfで作ったobjを見る
xxdで中身を見ます。
xxd build/hello-printf.obj出力結果は次のようになります。
00000000: 3000 e002 f022 f025 0048 0065 006c 006c 0....".%.H.e.l.l
00000010: 006f 002c 0020 0057 006f 0072 006c 0064 .o.,. .W.o.r.l.d
00000020: 0021 000a 0000 .!....先頭の4wordに注目します。
3000 e002 f022 f025意味は次のとおりです。
3000 origin
e002 LEA R0, HELLO
f022 PUTS
f025 HALT3000は命令ではなく、このプログラムをメモリのどこに読み込むかを示す開始アドレスです。
その後ろに、Hello, World!\nが1文字1wordで続きます。最後の0000が文字列の終端です。
.objファイルの中では、LC-3の16bit wordが上位byte、下位byteの順に並びます。そのためxxdでは30 00 e0 02のように見えますが、LC-3のwordとして読むと3000 e002になります。
ASCII コード表を見てみる
次に、先ほどの出力結果の中から、最初の行の後半4wordの部分を見てみます。
0048 0065 006c 006c H.e.l.lターミナルから次のように打つと、ASCII文字の一覧が見られます。
man ascii- Oct(Octal):8進数(0〜7の数字を使って数える方法)
- Hex(Hexadecimal):16進数(0〜9とA〜Fを使って数える方法)
- Dec(Decimal):10進数(私たちが普段使っている0〜9の数字)
いくつかの表が出てきますが、今回見るのは、The hexadecimal set です。
対応表の中から後ろの4wordを探してみます。
The hexadecimal set:
00 nul 01 soh 02 stx 03 etx 04 eot 05 enq 06 ack 07 bel
08 bs 09 ht 0a nl 0b vt 0c np 0d cr 0e so 0f si
10 dle 11 dc1 12 dc2 13 dc3 14 dc4 15 nak 16 syn 17 etb
18 can 19 em 1a sub 1b esc 1c fs 1d gs 1e rs 1f us
20 sp 21 ! 22 " 23 # 24 $ 25 % 26 & 27 '
28 ( 29 ) 2a * 2b + 2c , 2d - 2e . 2f /
30 0 31 1 32 2 33 3 34 4 35 5 36 6 37 7
38 8 39 9 3a : 3b ; 3c < 3d = 3e > 3f ?
40 @ 41 A 42 B 43 C 44 D 45 E 46 F 47 G
48 H 49 I 4a J 4b K 4c L 4d M 4e N 4f O
50 P 51 Q 52 R 53 S 54 T 55 U 56 V 57 W
58 X 59 Y 5a Z 5b [ 5c \ 5d ] 5e ^ 5f _
60 ` 61 a 62 b 63 c 64 d 65 e 66 f 67 g
68 h 69 i 6a j 6b k 6c l 6d m 6e n 6f o
70 p 71 q 72 r 73 s 74 t 75 u 76 v 77 w
78 x 79 y 7a z 7b { 7c | 7d } 7e ~ 7f del48 が ‘H‘、65 が ‘e‘、6c が ‘l‘ であることがわかります。
同じ内容をアセンブリとして書く
次に、同じプログラムをLC-3アセンブリとして書きます。
.ORIG x3000
LEA R0, HELLO
PUTS
HALT
HELLO .STRINGZ "Hello, World!\n"
.ENDLEA R0, HELLOは、HELLOラベルのアドレスをR0に入れます。PUTSは、R0が指すLC-3メモリ上の文字列を表示します。HALTはVMを停止します。
B: lc3asでアセンブルする
ここから先は、既存のLC-3アセンブラlc3asを使うコースです。
lc3asはlc3toolsに含まれるコマンドラインアセンブラです。環境によって導入方法が異なるため、この記事では作業ディレクトリ内にlc3toolsを置き、lc3asだけをビルドして使います。
mkdir -p lc3as-lab/tools
cd lc3as-lab/tools
curl -L -o lc3tools_v12.zip https://highered.mheducation.com/sites/dl/free/0072467509/104652/lc3tools_v12.zip
unzip lc3tools_v12.zip
cd lc3tools
./configure --installdir "$(pwd)/../bin"
make lc3asこのlc3toolsは古い教材用ツールなので、環境によっては警告が出ます。今回必要なのはlc3asだけなので、GUIシミュレータやlc3sim-tkまでは使いません。
lc3asが用意できたら、次のように実行します。
cd ../../
tools/lc3tools/lc3as src/hello.asm成功すると、次のような表示になります。
STARTING PASS 1
0 errors found in first pass.
STARTING PASS 2
0 errors found in second pass.src/hello.objとsrc/hello.symが生成されます。
lc3as版を自作VMで実行する
生成された.objをbuild/に移して実行します。
mv src/hello.obj build/hello.obj
mv src/hello.sym build/hello.sym
../main build/hello.obj出力結果は次のようになります。
Hello, World!
HALTMakefileを使う場合は、次のコマンドだけで実行できます。
make runlc3as 版を最初に作った printf 版と比較する
ここでは、次の2つの.objを比較します。
build/hello-printf.obj # printfでbyte列を直接書いたもの
build/hello.obj # lc3asがsrc/hello.asmから生成したもの
つまり、前半で手で作ったbyte列と、lc3asがアセンブリから生成したbyte列が完全に同じかを確認します。
cmp -s build/hello-printf.obj build/hello.objcmp -sは、2つのファイルが同じなら何も表示せずに終了します。何も出なければ、printf版とlc3as版は完全に一致しています。
Makefileでは次で確認できます。
make compare-printf内容が一致すれば、xxdでprintf版の中身が表示されます。一致しない場合はcmpの時点でmakeが止まります。
00000000: 3000 e002 f022 f025 0048 0065 006c 006c 0....".%.H.e.l.l
00000010: 006f 002c 0020 0057 006f 0072 006c 0064 .o.,. .W.o.r.l.d
00000020: 0021 000a 0000 ここで重要なのは、printf版とlc3as版が同じbyte列になることです。つまり、アセンブラは特別な魔法ではなく、hello.asmを読んでこのbyte列を自動生成しているだけです。
LEA R0, HELLOがe002になる理由
LEA R0, HELLOは、「HELLOのアドレスをR0という箱(レジスタ)に入れる」命令です。
このプログラムでは、各word(命令やデータ)は次のように並んでいます。
x3000: LEA R0, HELLO // いま実行している命令
x3001: PUTS // LEA実行時の PC の位置
x3002: HALT
x3003: HELLO の文字列 // 文字データ HELLO の先頭目的地であるHELLOはx3003にあります。
ただし、LC-3では命令を実行するとき、PC(プログラムカウンタ)はすでに次の命令を指して待機しています。つまり、x3000にあるLEAを実行するときのPCのスタート地点はx3001になります。
HELLO の位置 = x3003
LEA実行時のPC = x3001
offset = x3003 - x3001
= 2このように、「現在地からあと2歩進めばHELLOに着く」ということがわかるため、LEA R0, HELLOのPCoffset9(距離)は2になります。
ビットフィールド図(命令フォーマット)の見方
次に、この「2歩」という情報を、コンピュータが読める16ビットの設計図(ビットフィールド図)に当てはめてみます。
15 12 11 9 8 0
+--------+------+-------------------------+
| 1110 | DR | PCoffset9 |
+--------+------+-------------------------+
(4ビット) (3ビット) (9ビット)この16個のマス目(15〜0ビット)は、次のような役割でチーム分けされています。
- 左側(4ビット): 「
LEA命令を実行する」という世界共通の合言葉。今回は1110が入ります。 - 真ん中(3ビット):
DR(Destination Register)。結果を入れる8つの箱(R0〜R7)の指定です。今回はR0を指定するので、000になります。 - 右側(9ビット):
PCoffset9。先ほど計算した目的地までの距離です。「2」なので、9マスの2進数で000000010になります。
2進数を16進数に変換して「e002」の完成
これらすべてを16ビットで順番につなげると、次の値になります。
1110 000 000000010最後に、これを4桁ずつ区切って16進数に変換(翻訳)します。
1110= 16進数のe(10進数だと14)0000= 16進数の00000= 16進数の00010= 16進数の2
左から順番に並べると e002 になります。
今回できたこと
今回は、Hello Worldを2つの方法で.objにしました。
printfで.objのbyte列を直接書き出したlc3asでhello.asmをhello.objに変換した- どちらの
.objも自作VMで実行できた - どちらの
.objも同じbyte列になった LEA R0, HELLOがe002になる理由を確認した
外部ツールを入れたくない場合は、printfコースだけでも十分に試せます。
lc3asコースまで進めると、次回の準備ができます。次回は、今回lc3asがやってくれた変換処理を、Hello Worldが通る最小構成で自作します。
