C言語で作るLC-3仮想マシン 第4回: LC-3アセンブリでHello Worldを書く | UNIX Cafe

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

System Note $ cat /proc/ai-disclosure

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

C言語で作るLC-3仮想マシン 第4回: LC-3アセンブリでHello Worldを書く | UNIX Cafe

第3回までの作業で、LC-3 VMは.objファイルを読み込み、命令を実行し、PUTSHALTなどのTRAPも扱えるようになりました。

今回は、自分で作ったVMで “Hello World” を表示します。ただし入口は2つ用意します。

A. すぐ試すコース
   printfで .obj を直接作る
   外部アセンブラは不要

B. アセンブリを書くコース
   hello.asmを書く
   lc3asで .obj に変換する

どちらのコースでも、最後に自作VMで同じ出力を確認します。

Hello, World!
HALT
目次

VMとアセンブラの役割は違う

lc3.cはアセンブラではありません。担当するのは、すでに機械語になっている.objファイルを読み込んで実行する部分です。

hello.obj  --> lc3.c -->  実行

一方、アセンブラが担当するのは、人間が読めるアセンブリをLC-3の16bit word列に変換する部分です。

hello.asm  --> lc3as -->  hello.obj

この記事では、まず.objの正体を見るためにprintfで直接ファイルを作ります。そのあとで、同じ.objlc3asで作ります。

作業ディレクトリ

今回の検証用にlc3as-labというディレクトリを使います。

lc3as-lab/
├── src/
│   └── hello.asm
├── build/
├── tools/
└── Makefile

printfコースだけ試す場合、必要なのは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!
HALT

Makefileを使う場合は、次のコマンドだけで実行できます。

make run-printf

printfで作った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  HALT

3000は命令ではなく、このプログラムをメモリのどこに読み込むかを示す開始アドレスです。

その後ろに、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 del

48 が ‘H‘、65 が ‘e‘、6c が ‘l‘ であることがわかります。

同じ内容をアセンブリとして書く

次に、同じプログラムをLC-3アセンブリとして書きます。

.ORIG x3000

        LEA R0, HELLO
        PUTS
        HALT

HELLO   .STRINGZ "Hello, World!\n"

.END

LEA R0, HELLOは、HELLOラベルのアドレスをR0に入れます。PUTSは、R0が指すLC-3メモリ上の文字列を表示します。HALTはVMを停止します。

B: lc3asでアセンブルする

ここから先は、既存のLC-3アセンブラlc3asを使うコースです。

lc3aslc3toolsに含まれるコマンドラインアセンブラです。環境によって導入方法が異なるため、この記事では作業ディレクトリ内に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.objsrc/hello.symが生成されます。

lc3as版を自作VMで実行する

生成された.objbuild/に移して実行します。

mv src/hello.obj build/hello.obj
mv src/hello.sym build/hello.sym
../main build/hello.obj

出力結果は次のようになります。

Hello, World!
HALT

Makefileを使う場合は、次のコマンドだけで実行できます。

make run

lc3as 版を最初に作った 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.obj

cmp -sは、2つのファイルが同じなら何も表示せずに終了します。何も出なければ、printf版とlc3as版は完全に一致しています。

Makefileでは次で確認できます。

make compare-printf

内容が一致すれば、xxdprintf版の中身が表示されます。一致しない場合は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 の先頭

目的地であるHELLOx3003にあります。

ただし、LC-3では命令を実行するとき、PC(プログラムカウンタ)はすでに次の命令を指して待機しています。つまり、x3000にあるLEAを実行するときのPCのスタート地点はx3001になります。

HELLO の位置 = x3003
LEA実行時のPC = x3001

offset = x3003 - x3001
       = 2

このように、「現在地からあと2歩進めばHELLOに着く」ということがわかるため、LEA R0, HELLOPCoffset9(距離)は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進数の 0
  • 0000 = 16進数の 0
  • 0010 = 16進数の 2

左から順番に並べると e002 になります。

今回できたこと

今回は、Hello Worldを2つの方法で.objにしました。

  • printf.objのbyte列を直接書き出した
  • lc3ashello.asmhello.objに変換した
  • どちらの.objも自作VMで実行できた
  • どちらの.objも同じbyte列になった
  • LEA R0, HELLOe002になる理由を確認した

外部ツールを入れたくない場合は、printfコースだけでも十分に試せます。

lc3asコースまで進めると、次回の準備ができます。次回は、今回lc3asがやってくれた変換処理を、Hello Worldが通る最小構成で自作します。

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

この記事を書いた人

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

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

目次