0. はじめに
C言語を学んだあとに、もっとパソコンの仕組みを理解したいと思ったので、評判の良かった『はじめて読むマシン語』を読みました。そこで学んだ内容のうち基本的な部分を、以下の3つの理由からまとめたいと思います。
- インプットした知識を整理し、あとで見返したい
- 自分のような知識ゼロの人間は環境構築でハマると思った(自分はハマった)
- 初学者に優しい機械語やZ80エミュレータの説明がネット上にあまり無い
対象読者
- CPUやメモリなど基本事項は知っているが、機械語がどう動くのか知らない人
- C言語を学び、CPUやメモリの挙動などパソコンについて知りたくなった人
- パソコンそのものの仕組みに少しでも興味がある人
- 『はじめて読むマシン語』をこれから読もうとしている初学者
できるだけ特別な予備知識を必要としないような説明を心がけますが、読者によっては多少わからない部分があるかと思います。その場合は各自ググってください。逆に基本事項を知っている人には不要な説明がしばらく続くので、2章の基礎知識など不要な箇所は適宜飛ばしてください。
ちなみに実際に機械語を書かなくても理解できるように書くつもりです。つまり、実際のコードとその結果を記すことで、実際にエミュレータを導入して手を動かさなくても理解できるように書いたつもりです。ベッドで横になってケツを掻きむしりながら読み進めても理解できるくらいのライトな記事を目指します。もちろん実際に手を動かしてみるほうが身につくし、なにより楽しいと思います。書かれたことをボーッと読むより自分で調べて手を動かした知識のほうが血肉になるってそれ一番言われてるから。
達成目標
読み終わった後に読者がこういう状態になってたら良いなと思っています。
- プログラムの基本的な挙動がどのように実行されているのかを理解している
- ニーモニックの解説と対応表があれば簡単な機械語が読めるようになっている
環境
- Ubuntu 18.04 LTS
- YAZE-AG V2.40.5 (エミュレータ)
- CPU: Z80 / OS: CP/M 2.2 (エミュレートする環境)
今回使用するエミュレータの都合上、Windowsの人はcygwinは必要です。(もしかしたら、Windows10の新機能WSLでも起動するかもしれません)
1. 環境設定
実際に手を動かさない人は飛ばしてください。
8bビットのパソコンを使う良さ
『はじめて読むマシン語』(以下本書と呼ぶ)の良さは、実際に当時(1983年頃)のパソコンを触って機械語を打ち込むことで、メモリの書き換えを体験できる点です。当時のパソコンのCPUは8ビット= 2^8=256通りの情報しか一度に処理できませんでした(ビットについての説明は後で行いますのでご安心を)。それに対し今のパソコンでは64ビット、つまり256倍の数(2^64=1,844京6,744兆737億955万 1,615通り)の情報を一度に処理できます。そこまで性能が上がるとCPUが複雑になってきて、初学者が勉強する題材としては不向きです。シンプルな構造だった1980年代当時のパソコンを使うのが丁度良いのです。本書ではPC-8801というパソコンをメインとする、Z80というCPUを搭載した機種を想定して書かれています。
しかし、今時1980年代のCPU環境をエミュレートする人は結構な知識を持った方が多いらしく、初学者向けの丁寧な解説サイトがあまりなくて困りました。これが今回まとめようと思った要因の1つです。以下その環境設定について書いていきます。
Z80環境を用意する
Z80の主なエミュレータとして、Z80packとYAZE-AG(Yet Another Z80 Emulator by AG)の2つがありました。今回は日本語の解説ページが存在した後者を使用します。
YAZEのインストール
DownloadページからYAZEをダウンロードします。(使用したのはYAZE-AG-2.40.5)
tarファイルを解凍し、解凍したディレクトリに移り、インストールを行います。
Makefile_○○○○
という名前のMakefileがたくさんあるので、自分の環境に近いものを選び、Makefileとしてコピーします。(コピーは必須では無いが、後にオプションをいじる可能性を考えて一応)大体の人はMakefile_linux_64_intel_corei7
で問題ないと思います。自分はCore i5-8250Uでしたが、問題ありませんでした。
$ tar -xf yaze-ag-2.40.5_with_keytrans.tar.gz
$ cd yaze-ag-2.40.5_with_keytrans
$ ls | grep Makefile
Makefile_cygwin_32
Makefile_cygwin_64_core2duo
Makefile_cygwin_64_corei7
Makefile_first
Makefile_freebsd
Makefile_linux_32_i586
Makefile_linux_64_amd_athlon64
Makefile_linux_64_intel_corei7
Makefile_linux_64_intel_nocona
Makefile_Orange_Pi_Plus2_ARMv7
Makefile_radxa_Rock_RockPro_RockLite
Makefile_Raspberry_Pi_1
Makefile_Raspberry_Pi_2_Model_B
Makefile_Raspberry_Pi_3_Model_B_ARMv7_32Bit
Makefile_Raspberry_Pi_3_Model_B_ARMv8_64Bit
Makefile_solaris_cc
Makefile_solaris_gcc
$ cp Makefile_linux_64_intel_corei7 Makefile
$ make
$ sudo make install
YAZEはyaze
コマンドで実行できます。
$ yaze
Copyright 1995,1998 Frank D. Cringle. Pagetables Copyright by Michael Haardt.
MMU and CP/M 3.1 extensions Copyright (c) 2000,2018 by Andreas Gerlich.
Keytranslation Copyright (c) 2010,2015 by Jon Saxton
yaze-ag comes with ABSOLUTELY NO WARRANTY; for details
see the file "COPYING" in the distribution directory.
RAM: 1024 KByte, 4 KByte YAZEPAGESIZE, 256 PAGES
MMU: 16 TABLES, 16 PAGEPOINTERS per TABLE, selected MMU-PAGETABLE: T00
Running '/usr/local/lib/yaze/yaze-cpm3.boot'
BOOTSYS - CPM3.SYS, V 1.30 16.01.2015 (c) 2000,2015 by A. Gerlich/Jon Saxton
No CP/M vectors found; try to boot CP/M 3.1 using yaze-ag ...
Loading CP/M 3.1 ...
62K TPA
DRIVES: A B C/ D . F G H I J K L M N . P
CP/M 3.1 BIOS for yaze-ag, V 1.10.1 02.03.2015, Copyright (c) by A.Gerlich
A>pause
PAUSE Press any Key ...
すると上記のような画面が出てくるので、Press any key
がなくなるまでキー入力を続けてください。7回くらい押したらA>; END of profile.sub
と出てきて終わるはずです。
起動確認が終わったら、sys
コマンドでシステムに戻り、quit
コマンドで終了します。
A>; END of profile.sub
A>sys
$>quit
YAZEを一度起動すると、ホームディレクトリにcpm
というディレクトリが作成されているはずです。確認してみてください。
YAZEの環境設定
次に、YAZEの環境を整えていきます。細かい説明はしませんが、いくつか設定をしておく必要があります。CP/MやZ80等の知識がないので、何となくこれで動いたよっていう設定です。
必要なもの
- CP/M 2.2 BINARY (from The Unofficial CP/M Web site)
- MON80.zip (from 古典電脳物語-80系プログラムの開発の 80 系プログラム開発)
上記の2つのZipファイルを展開しておいてください。
CP/M2.2を快適に使えるようにする
CP/MというのはWindowsやLinuxと同じくOSの一つです。当時はCP/M2.2が主流だったようですが、YAZEはデフォルトのままだとCP/M3.1で起動するみたいです。しかも、そのままだとCP/M2.2をまともに使えないそうなので、まずはCP/M2.2を快適に使用できる環境を整えます。
LOAD, PIPコマンドの導入
先程展開したcpm22-b.zipの中から、LOAD.COM
とPIP.COM
をYAZEのCドライブに当たる部分にコピーします。
$ cp LOAD.COM ~/cpm/disksort
$ cp PIP.COM ~/cpm/disksort
次にYAZEを起動し、CP/M3.1環境に入り、以下のコマンドを入力してください。2つ目のコマンドだけ上書きをするかと聞いてくるので、Y
を入力します。
A> PIP A:LOAD.COM=C:LOAD.COM
A> PIP A:PIP.COM=C:PIP.COM
DESTINATION IS R/O, DELETE (Y/N)? Y
A>
PIPコマンドは元々組み込まれているはずのファイルコピーコマンドなのですが、なぜかそのままだとCP/M2.2環境で正常に動作しないそうです。LOADコマンドはアセンブラが生成したオブジェクトファイルを実行ファイルに変換するもので、これまたアセンブリ言語を使う場合必須のはずの機能ですがデフォルトだと動きません。なので、外からこれらのコマンドのプログラムを移植してきました。
以上が正常に終われば、一旦YAZEを終了してください。(sys
→quit
)
モニタープログラムの導入
コンピューターの入出力を監視し、制御するプログラム。キーボードからの入力やディスプレイへの出力などを制御する。さまざまな機能が加わり、BIOSに発展した。MS-DOS以降のOSの環境では、あまり使われなくなった。
次に、モニタープログラムを導入していきます。モニタープログラムを使ってメモリのアドレスに直接アクセスし、メモリの数値の変化を確認します。(この辺の話はあとで説明します)とにかくパソコンの状態を知るためのプログラムと理解しておいてください。
本書では独自のモニタープログラムをBASICという言語で実装しており、それを操作することでパソコンの挙動を確認しています。ただ、BASICにも色々種類があり調べるのが面倒だったこと、コードを打ち込むときにバックスペースが使えず入力で心が折れたことから、自前で用意することにしました。環境を整えるのに時間がかかったら萎えて放り投げてしまう性分なので。
今回使用するのが先ほどダウンロードしたMON80というプログラムです。今度はソースファイルにあたるMON80.ASM
というファイルを、先ほどと同様にYAZEのCドライブにコピーします。
$ cp MON80.ASM ~/cpm/disksort
次に、オプションコマンドを利用し、CP/M2.2環境でYAZEを起動します(今後YAZEを起動するときはいつもこの方法です)。すると下のような画面になり、入力待ちの状態になると思います。
$ yaze -l -1 -b yaze.boot
pwd=/home/tk/cpm
starting yaze_bin -l -1 -b yaze.boot
Yet Another Z80 Emulator by AG, final release 2.40.5 (MMU, KeyTr)
Copyright 1995,1998 Frank D. Cringle. Pagetables Copyright by Michael Haardt.
MMU and CP/M 3.1 extensions Copyright (c) 2000,2018 by Andreas Gerlich.
Keytranslation Copyright (c) 2010,2015 by Jon Saxton
yaze-ag comes with ABSOLUTELY NO WARRANTY; for details
see the file "COPYING" in the distribution directory.
RAM: 1024 KByte, 4 KByte YAZEPAGESIZE, 256 PAGES
MMU: 16 TABLES, 16 PAGEPOINTERS per TABLE, selected MMU-PAGETABLE: T00
Running '/usr/local/lib/yaze/yaze.boot'
A>
起動が完了したら、モニタープログラムをインストールしていきます。
A>PIP A:MON80.ASM=C:MON80.ASM
A>B:MAC MON80
CP/M MACRO ASSEM 2.0
2946
01BH USE FACTOR
END OF ASSEMBLY
A>LOAD MON80
FIRST ADDRESS 0100
LAST ADDRESS 2945
BYTES READ 2846
RECORDS WRITTEN 51
A>
PIP
コマンドでCドライブからAドライブにソースファイルを移動させ、MAC
コマンドでアセンブルし、LOAD
コマンドで実行しています。何をしてるのかわからなくてもいいです。とりあえずこれでモニタープログラムが使えるようになりました。ちなみに、デフォルトだとMAC
コマンドが使えずハマりました。なぜか知らないけどBドライブにあるらしく、B:MAC
として対処しています。
MON80の説明書はこちらのリンクから見れます。(見なくても後で説明するので大丈夫)
以上で環境設定が終わりました。いよいよ本題の説明に入っていきます。
2. 基礎知識
何も知らない人のために1からざっくりと説明していきます。各項目の見出しを見て話の内容の想像がつく人はこの章を丸々飛ばしてもらって構いません。
機械語とは
プログラムを書いて実行するとき、そのままの言語だと実行されません。基本的にコンピューターは、機械語と呼ばれる数字の列でしか物事を認識できないからです。例えばC言語でプログラムを書いても、実行するためにはコンパイルという翻訳作業が必要になります。その翻訳された言語がアセンブリ言語というものです。アセンブリ言語は機械語と1対1対応の英単語が割り当てられています。これをニーモニックと呼びます。流れとしては、
人間のアイデア→C言語→アセンブリ→機械語→CPUが理解、実行する
という感じですね。本稿ではこの機械語を通じてパソコンの動く仕組みを理解しようという試みを行っているわけです。ちなみにPythonなど一部の言語ではコンパイル(翻訳)なしにプログラムが作動します。実はこれは勝手にリアルタイムで命令をコンパイルして実行してくれているからで、このような言語はインタプリタ言語とかインタプリタ型言語と呼ばれたりします。
プログラムが動く仕組み
プログラムに含まれる命令はCPUで解釈され、演算が実行されます。CPUのスペックによって、パソコンのスペックがだいたい決まります。CPUが脳みそに例えられるのはそのためです。ただし、基本的にCPUは命令を解釈し演算を実行する場所であって、記憶をする場所ではありません。CPUが行った演算結果はメモリという場所に保管されます。CPUについては後で詳しく説明しますが、とりあえず今回扱うのは8ビットのパソコンのCPUだということだけ覚えておいてください。
メモリについて
RAMやROMは本稿では触れなくてもいいので、説明を割愛しました。知らない人はまた他の文献等で出てきたときに調べてみてください。
ビット( bit )とバイト( byte )
さて、次に一度は聞いたことのある「ビット」と「バイト」の説明です。
ビット(bit)とはコンピューターが扱う情報を構成する最小単位で、「0」または「1」のどちらかを取ります。「0」か「1」かという形を取る理由は、パソコンの制御に電流が使われているからです。例えば、「電気が流れれば1、流れなければ0」のように、電流のオン/オフという2つの状態に数字を割り当てています。
次にバイトですが、ビットを1つのまとまりにした単位のことで、1バイト=8ビットです。次のアドレスの項目で説明するように、8ビットを1バイトとした方が都合が良かったからそうなってるそうです。詳しく知りたい人はこちらの記事を読んでみてください。
アドレス
先程話したとおり、CPUが行った演算の結果など、様々なデータが保管される場所をメモリと呼びます。メモリにはアドレスという場所を表す情報があり、アドレスを指定してデータの入出力を行います。モノを入れるためのいくつもの箱がズラリと並んでいて、それぞれの箱に位置情報が紐づけされているのを想像するとわかりやすいです。大量に並べられた箱(メモリ)にそれぞれ個別の箱を識別する位置情報(アドレス)がなければ,モノの出し入れを依頼する人(CPU)は思うようにモノの出し入れができず困ってしまいます。アドレスは重要です。
さて、今回扱うZ80のような8ビットのCPUですが、その名の通り一度に8ビットの情報を処理できます。この単位が1バイトと定められました。8ビットのパソコンに搭載されているメモリには、16進法で0000〜FFFFまでの65,536個ものアドレスが存在し、各アドレスには1バイト(=8ビット)の情報が入出力されます。
ちなみに65,536個と聞くとかなり多いように思いますが、
65,536 = 64 * 1024 = 64 * 2^10
なので、64キロバイト(64Kbyte)ということになります。当時のパソコンのメモリは64Kbyteしかなかったということになります。ちなみに今のメモリはだいたい8GB〜16GBが主流です。Windows2000の電卓アプリの容量が約90Kbyteらしいですので、それと比べてもいかに64Kbyteが小さいかが見て取れます。
2進法と16進法
先程出てきた16進法について話をしておきます。
まず、上述したとおりパソコンは「0」か「1」かで情報を扱っています。一般的にこれを2進法で表された数として解釈します。2進法ついて知らない方はこちらをご覧ください。2進法は英語でbinaryと言います。「バイナリ」とか「バイナリエディタ」という言葉を聞いたことがある人がいると思いますが、それは2進法の形でデータを扱うことに関係する文脈だったはずです。
16進法では0〜Fまでの16個の英数字を使って数値を表します。
2進法 | 10進法 | 16進法 |
---|---|---|
0 | 0 | 0 |
1 | 1 | 1 |
10 | 2 | 2 |
11 | 3 | 3 |
100 | 4 | 4 |
101 | 5 | 5 |
110 | 6 | 6 |
111 | 7 | 7 |
1000 | 8 | 8 |
1001 | 9 | 9 |
1010 | 10 | A |
1011 | 11 | B |
1100 | 12 | C |
1101 | 13 | D |
1110 | 14 | E |
1111 | 15 | F |
10000 | 16 | 10 |
10001 | 17 | 11 |
10010 | 18 | 12 |
10011 | 19 | 13 |
例えば8ビットの情報を扱うということは、「0 or 1」のどちらかを取る情報(ビット)が8つあるということで、その組み合わせは2^8=256通り存在します。つまり、8ビットは00000000〜11111111最大で256個の情報を伝えることができます。この8ビットの情報を一々2進数で表していると、8桁も記載する必要があり面倒です。しかし16進法を使うとスッキリと表せます。256は16^2なので、16進法を使うと2桁で8ビットの情報のすべての組み合わせを表すことができます。
先ほど紹介したアドレスの総数も65,536 = 16^4なので、0000〜FFFFの4桁の16進数ですべて表すことができます。
16進数を表す際には末尾にHをつけて16進数であることを明示することがあり、今回扱うアセンブリでもHを末尾につけます。
CPUについて
CPUの部品とはたらき
今回扱う8ビットのパソコンに搭載されているCPU(Central Processing Unit)は、概ね以下の部品で構成されています。今のCPUでも自作など専門的なことをしない限り、だいたい以下で挙げる部品を覚えておけばなんとかなりそうです。
レジスタ
8ビットか16ビットの情報を一時的に保持する場所で、CPU内にはいくつかのレジスタが存在します。レジスタの数など、構成はパソコンによって違います。今回扱うZ80というCPUには、A, F, B, C, D, E, H, L, SP, PCレジスタがあります。そんなものは覚えなくてもいいです。重要なのはAレジスタは特別なレジスタで、アキュームレジスタと呼ばれるということです。Aレジスタには、唯一演算機能が搭載されており、加減演算、論理演算、比較などが可能です。今回使うのはこのAレジスタだけです。Fレジスタはフラグ・レジスタと呼ばれ、様々な状態(正か負かなど)を保存する場所だとか、SP(スタック・ポインタ)レジスタに一時的な演算結果を保持するだとか、PC(プログラムカウンタ)レジスタにアドレスを保持して様々な条件処理が可能になるだとか、知っておいたほうが良いことはたくさんあります。どれも重要な役割がありますが、今回は使用しないので割愛します。CPUについての説明は探せばいくらでもあるので、知らない人は各自調べてみてください。
バス
CPUとメモリなど、CPUと他の部分とをつなぐ線のことをバス(Bus)と呼びます。ファイルの場所を指すパス(Path)ではなくバス(Bus)です。データを載せる乗り物をイメージすれば良いと思います。バスにはアドレスバスとデータバスが存在します。アドレスバスでは操作するメモリのアドレスの送信を、データバスでは入出力するデータの送受信を行います。
以下の図を使ってCPUがメモリの値を書き換えるまでの過程を見ていきましょう。
- CPUからメモリに、アドレスバスを通じて操作したいメモリのアドレスが送信される
- 指定されたアドレスのメモリが選択される
- CPUからメモリに、データバスを通じて書き換えたい値が送信される
- 指定されたアドレスのメモリの値が書き換えられる
ざっくりとこんな手順でメモリの操作がされています。(簡単のためコントロール信号など詳しい説明は省いています)アドレスバスはCPU→メモリの一方通行ですが、データバスはCPUとメモリを双方向で繋いでいます。なので書き込みだけではなく読み込みも可能なわけです。
ここで復習ですが、1ビットは「1か0か」という2値で表されましたよね。また、メモリのアドレスは65,536個存在しましたよね。65,536=2^16であることから、メモリのアドレスは16ビットの情報と言えます。16ビットの情報ということは「0か1か」の情報を16個渡せば良いわけです。アドレスバスは、A0〜A15までの16本の線にそれぞれ電流を流すことによって、16ビットであるアドレスの情報をメモリに伝えています。
上図を例に説明すると、伝えたいアドレス情報2FA3は16進数なので、まずは2進数に変換してあげます。すると0010111110100011という値が得られるので、これをもとにアドレスバスに電気信号を送ります。右端の値がA0で左端の値がA15に対応するとしたら、A0〜A1, A5, A7〜11, A13に電気信号を流すことで、2FA3というアドレス情報をメモリに伝えることができます。
同様にして、データバスでもデータのやり取りを行っています。今回扱っているCPUは8ビットのパソコンのものだということを思い出してください。一度に8ビットの情報まで扱うことができるCPUなので、データのやり取りに必要なバスの線の数も8本です。上図の例では78=01001110なので、それに対応するデータバスの線に電気信号が流されデータの送信が行われます。
I/Oポートについて
モニターやキーボードなど、すべての入出力装置はI/Oポートと呼ばれる場所で制御されています。I/Oポートにある入出力装置にはそれぞれアドレスが振り分けられており、メモリと同じようにアドレスを指定して指示を出すことで様々な挙動を実現しています。ここでは深く触れませんが、上で説明したメモリ操作と同じ具合に、アドレスバスでアドレスを指定し、データバスでデータのやり取りを行っています。Z80ではI/Oポートのアドレスを8ビットで表し、00〜FFまでの値を振り分けています。
長々と説明してきましたが、以上が基礎知識の説明となります。CPUの挙動をイメージできれば、達成目標の1つ目「プログラムの基本的な挙動がどのように実行されているのかを理解している」はほぼクリアです。以下では実際にZ80のエミュレータを操作して、その挙動を追っていきます。
3. Z80で直接メモリを操作する
基礎知識の説明が長かったので忘れたかもしれませんが、以下のコマンドでCP/M2.2環境のYAZEを起動します。
$ yaze -l -1 -b yaze.boot
pwd=/home/tk/cpm
starting yaze_bin -l -1 -b yaze.boot
Yet Another Z80 Emulator by AG, final release 2.40.5 (MMU, KeyTr)
Copyright 1995,1998 Frank D. Cringle. Pagetables Copyright by Michael Haardt.
MMU and CP/M 3.1 extensions Copyright (c) 2000,2018 by Andreas Gerlich.
Keytranslation Copyright (c) 2010,2015 by Jon Saxton
yaze-ag comes with ABSOLUTELY NO WARRANTY; for details
see the file "COPYING" in the distribution directory.
RAM: 1024 KByte, 4 KByte YAZEPAGESIZE, 256 PAGES
MMU: 16 TABLES, 16 PAGEPOINTERS per TABLE, selected MMU-PAGETABLE: T00
Running '/usr/local/lib/yaze/yaze.boot'
A>
上記のような画面になり、入力待ちの状態になれば起動完了です。
MON80(モニター)の使い方
はじめにモニタープログラムの基本動作の紹介です。モニタープログラムはMON80
と入力すれば起動してくれます。
A>MON80
MON80 Version 2.1 CP/M Edition
Intel8080 Monitor Program
(C)1996-2006 Office TETSU
[8000]
上のように[8000]という数値が出てきて入力待ちの画面になればMON80の起動完了です。この[8000]というのはメモリのアドレスを示していて、今は8000というアドレスを参照している状態です。
本稿で使うコマンドはたったの4つです。最低太字のとこだけ理解しましょう。
-
XXXX
参照するアドレスを変更する場合は参照したいアドレスを直接入力します。
-
DUMP XXXX, YYYY
XXXXからYYYYまでの値を表示します。(アドレスを指定しなければ現在参照しているアドレスから16バイトの情報を表示します。DUMP後エンターを押すと続けて16バイトの情報を表示します)
-
DEFINE XX, YY, ZZ, ....
現在参照しているアドレスに引数を入力します。引数が複数ある場合は、連続してアドレスに値を代入してくれます。
-
GOSUB XXXX
指定したアドレスのプログラムを実行します。プログラム(正確にはサブルーチンという)は末尾がRETかそれに相当する処理で終わっている必要があります。
アセンブリと機械語について
以下では「この動作はアセンブリではこのニーモニック(命令)で、機械語ではこのように書きます」と、アセンブリと機械語の文法事項に関して軽く流しています。詳しく知りたい方は「z80 アセンブラ」や「Z80 機械語」などで検索をかけてみてください。参考になりそうなサイトを挙げておきます。
- アセンブリ:Z80アセンブラプログラミング
- 機械語:Z80 教室
メモリのデータの入出力
データの直接入力
では、メモリを直接操作してみましょう。まずはA000〜A004に値を入力してみます。
まずはアドレスA000
移動し、DUMP
コマンドで初期値がすべて0A
であることを確認します。DUMP
後はその次のアドレスに参照先が移動しています(A010
)。しかし値を入力するDEFINE
コマンドは、そのときに参照しているアドレスから入力を開始します。なので、まずはA000
に戻る必要があります。A000
に移動した後DEFINE
コマンドで値を入力し、DUMP
でA000
の数値を見て数値が変わっていることを確認します。
ちなみにコマンドを打ち間違えてもバックスペースで文字の消去ができません。以下の2行目のようにコマンドを間違えてしまったらエラーが返ってくるので、もう一度正しいコマンドを入力しましょう。入力する値を間違えた場合は、間違えた箇所にアドレスを戻し、再度DEFINE
します。
[8000]A000
[A000]DUMO
ERROR-DUMO
[A000]DUMP
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ASCII
A000 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A ................
[A010]A000
[A000]DEFINE 11, 22, 33, 44, 55
[A005]DUMP A000
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ASCII
A000 11 22 33 44 55 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A ."3DU...........
A000
〜A004
の値が変わっていることが確認できました。このようにメモリの値を変化させることでプログラムの内容をメモリに保存し、それを読み込むことでプログラムを実行しているのです。
機械語を使ったデータ入力
ではいよいよ機械語を使ってCPUとメモリの操作を行っていきます。
大きな流れは以下の通り。
- アセンブリで操作内容を表す
- そのアセンブリを機械語に変換する
- 機械語をメモリに直接入力する(上で行った操作)
- 機械語を入力したアドレスの先頭を指定してプログラムを実行する
1. アセンブリで操作内容を表す
まずは、アセンブリで以下のようなプログラムを書いてみます。わかりやすいようにアセンブリのニーモニックとその内容を書いています。
アセンブリでは16進数の末尾にHをつけ、先頭の値がアルファベットのときは0をつけます。(例:32EB→32EBH, FFFF→0FFFFH)
- ORG 0B000H ←このプログラムの書きはじめのアドレス
- LD A, 5FH ←レジスタAに5Fを入力
- LD (0A005H), A ←アドレスA005にレジスタAの値を入力
- RET ←プログラムの終わりを示す
LD X, Y
でY
をX
に入力するという意味です。16進数にカッコがついていたらその値によって表されるアドレスを示し、カッコがついていなかったらその値をそのものを表します。2行目では5F
という数値を扱い、3行目ではA005
というアドレスを扱っています。
2. アセンブリを機械語に変換する
ではこのプログラムを機械語で書いてみましょう。機械語を書くときには、対応表というモノを参考にするとわかりやすいです。こちらの対応表を見てみてください。
先程の2行目の操作は「Aに入力する操作」、つまりLD A, x
です。入力する値が8ビットの数値なので、x=n
に当たります。なので、一番右上の3E n
という機械語を使います。
また、3行目の「操作は任意のアドレス(nn)
に入力する操作」、つまりLD (nn), x
です。入力元がA
なので、左から3つ目、下から3つ目の32 nn
という機械語を使います。
以上のようにして対応表から機械語を探し出すことができます。
機械語を書く前に注意すべきことは、16ビットの情報を書く順が逆だということです。例えばAB12
という16ビットの情報があったとき、機械語では12AB
と書きます。つまり、16ビットの情報を8ビット(1バイト)ずつに分解し、後ろの8ビットから記入するのです。それに注意しながら、先程のアセンブリを機械語に直してみます。
- 機械語なし ← ORG 0B000H ←このプログラムの下記はじめのアドレス
- 3E 5F ← LD A, 5FH ←レジスタAに5Fを入力
- 32 05 A0 ← LD (0A005H), A ←アドレスA005にレジスタAの値を入力
- C9 ← RET ←プログラムの終わりを示す
これで無事機械語を書くことができました。それでは、この機械語をメモリに入力していきましょう。
ようになる
3. 機械語をメモリに直接入力する
先程と同じ要領でメモリに直接値を入力していきましょう。先程操作した状態のままであれば、現在の参照アドレスがA010
のはずです。なのでまずはアドレスB000
に移動し、そこから上の機械語を入力していきます。入力を間違えてもバックスペースで消せないので、間違えたアドレスに移動し再度入力し直してください。必ず入力後は入力値が正しいかDUMP
コマンドで確認しましょう。
[A010]B000
[B000]DEFINE 3E, 5F, 32, 05, A0, C9
[B006]DUMP B000
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ASCII
B000 3E 5F 32 05 A0 C9 0B 0B 0B 0B 0B 0B 0B 0B 0B 0B >_2.............
[B010]
4. プログラムを実行する
無事入力が終わったら、入力したプログラムを実行しましょう。今回指定したアドレスA005
の実行前の値を確認し、GOSUB
コマンドでプログラムを実行します。その後、アドレスA005
の値が変化していることを確認します。
[B010]DUMP A000
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ASCII
A000 11 22 33 44 55 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A ."3DU...........
[A010]GOSUB B000
[A010]DUMP A000
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F ASCII
A000 11 22 33 44 55 5F 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A ."3DU_..........
[A010]
きちんと値が5F
に変化したことが確認できました。
4. おわりに
パソコンの基礎知識から、Z80エミュレータの導入、モニタープログラムでのメモリ操作に、対応表を見ながらの機械語入力まで、一通り説明してきました。各レジスタの説明や、コール・リターン命令、スタックの概念など、パソコンの仕組みを理解する上で必要な知識はまだまだあります。しかし、これまで説明してきたことを駆使すれば、本やネットを通じて情報を仕入れ、実際にエミュレータを動かして確認することができると思います。こちらに示されている記事や書籍を当たってみてください。
本稿を書くきっかけになった『はじめて読むマシン語』は本当にわかりやすい良本だったので、中古の在庫があるうちに購入して読んでみるのをおすすめします。
誤字や説明の誤り等がありましたらコメントで知らせていただけると幸いです。