/ «2010-01-01 (Fri) ^ 2010-01-05 (Tue)» ?
   西田 亙の本:GNU 開発ツール -- hello.c から a.out が誕生するまで --

Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール


2010-01-04 (Mon)

[Thoughts][Hardware] 新春 機械語入門講座・その2

前回、機械語習得のためには、まずは目的とするプロセッサのプログラマーズ・マニュアルとハードウェア・マニュアルを手元に用意することが肝要とお話しました。何度も繰り返している通り、データシートというものは「分かる人が分かる」ようにしか書かれていません。ですから、初心者が挑戦する際には挫折がつきものなのですが、かと言って敬遠していては、一生データシートを読めるようにはなりません。

大切なことは、膨大なマニュアルの一箇所でも良いから「ここは理解できた」という経験を地道に重ねていくことです。この鍛錬を1年、3年と気長に続けていけば、必ずやデータシートから必要な情報を自在に抽出できるようになります。しかし・・しかし、現実は厳しい。学校や教科書は「データシートの読み方」を教えてはくれないからです。そのくせ、社会人として現場に放り出されると、どっさりと分厚いマニュアルを手渡されるという、この理不尽さ。

是非とも、この "理不尽さ" に一矢報いてやりましょう。

自分が「分かる」ことは何か?

難攻不落に見える山にも、必ずや攻略の糸口はあるはずです。自分は何が分かり、何が分からないのか。分かることがひとつでもあれば、そこを足がかりにして登頂はスタートできます。ただし、ここが大切なところですが、その足がかりは間違いのない堅牢なポイントでなければなりません。自分は分かっているつもりでも、実はあやふやな理解のために、力をいれた途端に足場が崩れて滑落・・ということがしばしば起こるからです。

加えて懸念すべきは、この "滑落" に数々の教科書や専門書が関与しているという事実です。例えば、機械語の解説書であれば必ず登場する技術用語のひとつに "即値" という言葉があります。"immediate" の邦訳ですが、考えれば考えるほどおかしな言葉です。「すなわちの値」って一体何なのでしょうか?日本語として全く意味不明、訳が分かりません。しかし、教科書はこのことには一切触れず、ページは進み、授業も進むのであります。

”即値" という記述を目にして、思考停止に陥っても何ら恥ずかしいことはありません。「分からない」ことが正しい。しかし、「分からない」を放っておくだけでは事態は改善しません。ジャイロ・ツェペリも言っています。

 おまえは これから 「できるわけがない」というセリフを……

 4回だけ言っていい

「分かるわけがない」が許されるのは、4回だけなのです。ジョニィ・ジョースターの如く、冷静な観察力と透徹した思考によって、「分からない」を乗り越えなければなりません。

Tiny Virtual Machine

"新春 機械語入門講座・その2" をはじめるにあたり、登山の出発点をどこに置くのか、はたと困りました。色々考えた末に、以前 GCCプログラミング工房で教材として使用した octopus を思いだし、2010年版の超小型仮想機械 Tiny Virtual Machine (TVM) を作成してみました。ソースリストはC言語で記述していますが、苦手な方はコードの内容を理解する必要はありません。お好きなスクリプト言語で書き換えて頂いても結構。TVM がどのような構造に基づいて動作するのか、この点が理解できれば "データシート攻略" への扉は開きます。

まず最初に、TVM 理解のための前提条件を掲げておきます。

  • バイトとビットの意味を理解している
  • 2進数・10進数・16進数の相互変換ができる
  • レジスタ・プログラムカウンタ・メモリという言葉を聞いたことがある

いかがでしょうか、ハードルはかなり低いと思います。途中で「看板に嘘偽りあり」と思われた方は、気軽にご指摘ください。

次に TVM の仕様です(今はこの内容が理解できる必要はありません)。

  • 命令語長は1バイト
  • 1バイト長のレジスタ4つを搭載 (R0, R1, R2, R3)
  • プログラムカウンタは1バイト長 (PC)
  • メモリは32バイトを搭載 (ユーザの努力に応じて無料で拡張可能)
  • 実装している命令は6種類 (NOP, SLEEP, LOADI, STORE, DEC, JMP)
  • 算術演算として減算(DEC)を搭載
  • ゼロフラグを搭載 (DECの実行結果に応じて変化)
  • 条件分岐可能 (JNZ)
  • メモリの最終番地に疑似 LED 発光機能を搭載 (すげぇ!)
  • 動作周波数は1Hzから100Hz程度まで可変

泣く子も黙るほどの、豪華仕様であります(この2日間、仕様決定に悩みましたです、ハイ)。ちなみに実装している6命令の根拠ですが、「秋月電子のH8ボードで LED 点滅プログラムを実行するため」ただこれだけ。逆に言えば TVM さえ理解できれば、全く未知のマイコンボードであっても、リファレンスマニュアルを読むだけで LED ピカピカできる訳です(もちろん開発環境は No, thank you!)。

最初にメモリありき

それでは、TVM の中で最も簡単な構造を持つ、メモリから実装していきましょう。実際のソース tvm1.c を示します。

    1	//
    2	// t v m 1 . c
    3	//
    4	//   Tiny Virtual Machine 1, public domain
    5	//
    6	
    7	#include <stdint.h>	    // uint*_t
    8	#include <stdio.h>	    // sscanf(), fprintf(), stderr
    9	
   10	#define MAXMEM	32
   11	
   12	uint8_t mem[ MAXMEM ];
   13	
   14	int setup_memory(int argc, char** argv) {
   15	    int i = 0, val, ret;
   16	
   17	    while (argc > 1) {
   18		ret = sscanf(argv[ i+1 ], "%x", &val);
   19		if (ret == 0) {
   20		    fprintf(stderr, "Illegal hexadecimal data [%s].\n", argv[ i+1 ]);
   21		    return 1;
   22		}
   23		if (i >= MAXMEM) {
   24		    fprintf(stderr, "Memory overrun.\n");
   25		    return 1;
   26		}
   27		mem[ i++ ] = val & 0xFF;
   28		argc--;
   29	    }
   30	    return 0;
   31	}
   32	
   33	void dump_memory(void) {
   34	    int i;
   35	
   36	    fprintf(stderr, "\n");
   37	    fprintf(stderr, "+-------------------------------------------------+\n");
   38	    fprintf(stderr, "|  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F |\n");
   39	    fprintf(stderr, "|-------------------------------------------------|\n");
   40	    fprintf(stderr, "|");
   41	    for (i = 0; i < 16; i++)
   42		fprintf(stderr, " %02X", mem[ i ]);
   43	    fprintf(stderr, " |\n");
   44	    fprintf(stderr, "+-------------------------------------------------+\n");
   45	    fprintf(stderr, "| 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F |\n");
   46	    fprintf(stderr, "|-------------------------------------------------|\n");
   47	    fprintf(stderr, "|");
   48	    for (i = 16; i < MAXMEM; i++)
   49		fprintf(stderr, " %02X", mem[ i ]);
   50	    fprintf(stderr, " |\n");
   51	    fprintf(stderr, "+-------------------------------------------------+\n");
   52	    fprintf(stderr, "\n");
   53	}
   54	
   55	int main(int argc, char** argv) {
   56	    int ret;
   57	
   58	    ret = setup_memory(argc, argv);
   59	    if (ret)
   60		return ret;
   61	    dump_memory();
   62	    return 0;
   63	}
   64	

このプログラムの肝は12行目、ただ一行です。

 uint8_t mem[ MAXMEM ];

uint8_t というのは "符号無し8ビット長整数型" の宣言であり、標準ヘッダーファイルの <stdint.h> に由来しています。ホスト環境の違いを超えて、決められたバイト長の整数型を保証するための仕組みです。この型宣言により、符号無し1バイトを要素として格納する配列 mem が定義されます(配列長は MAXMEM, すなわち32)。

tvm1.c 内部ではふたつの関数、setup_memory(), dump_memory() が定義されており、それぞれメモリ初期化とメモリ内容出力を担当しています。

setup_memory() は main() から argc, argv を引き継ぎ、コマンド引数に指定された16進数文字列を sscanf() 標準ライブラリ関数を用いてバイナリ値に変換し(val に格納)、これを順次 mem 配列に格納していきます。16進数として不適切な文字列が指定された場合、もしくは指定されたデータが合計32バイトを超えた場合は、エラー値を返します。

dump_memory() は、mem 配列の内容を16バイトずつ、上下二段で表示します。わざわざ、出力先に fprintf() を用いて stderr が指定されている理由は、次回明らかになります。

以下、実行例です。

 $ gcc -Wall -o tvm1 tvm1.c
 $ ./tvm1
 
 +-------------------------------------------------+
 |  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F |
 |-------------------------------------------------|
 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
 +-------------------------------------------------+
 | 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F |
 |-------------------------------------------------|
 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
 +-------------------------------------------------+
 
 $ ./tvm1 1 2 3 4 5 6 7 8 9 a b c d e f 10
 
 +-------------------------------------------------+
 |  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F |
 |-------------------------------------------------|
 | 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 |
 +-------------------------------------------------+
 | 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F |
 |-------------------------------------------------|
 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
 +-------------------------------------------------+
 
 $ ./tvm1 1 2 3 4 5 x
 Illegal hexadecimal data [x].
 $

最初の実行例はコマンド引数にデータを指定しなかった例ですが、この場合は全てゼロで初期化されたメモリが用意されます(グローバルスコープの未初期化データは、プロセス起動時にゼロに初期化される:参考 .bss セクション)。2番目の実行例は、引数に1から16までを指定した場合ですが、その通りにメモリが初期化されています(後半はゼロに初期化されたまま)。最後の例は、データとして "x" を指定した場合ですが、エラーメッセージを表示し、異常終了しています。

次回は、このメモリ・モジュールに「疑似 LED 発光機能」を追加して、ピカピカさせてみましょう。