VBAの勉強を始めてみた

色々試しています。

CPUよもやま話(アセンブリ言語で命令のイメージを掴もう)

今回は、CPUに対しての命令がどのように行われるのか見てみましょう。

 

アセンブリ言語とは?

皆さんご存知のとおりコンピューターの中で情報は、0と1で表されます。0と1で表現されたプログラムは機械語と呼ばれます。しかし、命令の内容が0と1の羅列では人間にはかなり分かりにくいものです。

そんなとき、アセンブリ言語を用いて説明すると比較的分かりやすくすることができます。何故かというと、アセンブリ言語ソースコードの1行は機械語の1命令に相当し、かつ機械語の内容を(人間が理解・記述しやすいように簡略化した英単語や記号の組み合わせで)そのまま表現しているからです。

例えば、こんなのがあります。

MOV	A,B

これは、レジスタBのデータをレジスタAに転送せよ という命令です。

f:id:kouten0430:20190218142651p:plain

オペランドにはレジスタだけではなく、メモリ→レジスタレジスタ→メモリ、即値→レジスタ・・・・・・等とすることもできます。(CPUがそのような命令を実装していることが前提)
※即値とはコード中に直接書かれた値のことです。イミディエイトデータとも呼びます。

他にも、こんなのがあります。

ADD	A,B	レジスタAとレジスタBの値を加算して、レジスタAに転送
NOT	A	レジスタAの値を論理否定して、レジスタAに転送
JMP	[番地]	指定番地へ無条件でジャンプ
IN	A	I/Oの入力ポートのデータをAレジスタに転送
OUT	A	Aレジスタの値をI/Oの出力ポートに転送

等々。ただし、繰り返しになりますがCPUがそのような命令を実装していれば、です(例えば、MOVでメモリからメモリへ転送したくても、それに該当する機械語〔回路〕が存在しなければ実行することはできない)。次回以降、機械語の正体を説明する際に、ポイントになるので軽く覚えておいて下さい。

アセンブリ言語で書かれたソースコードは、アセンブラというソフトウェアで機械語に変換して実行します。(今回はイメージを掴むためにアセンブリ言語を持ち出しているだけなので、変換うんぬんは気にしなくてもよいです)

さて、命令がどのようにCPUで実行されているか、高水準言語とアセンブリ言語を比較してイメージを掴んでみましょう。今回は扱う命令やデータがすべて1バイト(8bit)であるものとします。

 

順次進行の仕組み(プログラムカウンタとは?)

基本的にプログラムはメモリの小さい番地から大きい番地へ順に実行されます。これをプログラムの基本三構造のうち順次進行と呼びます。プログラムカウンタには、次に実行される命令が入ったメモリ番地が格納されています。まず、プログラムの開始位置(メモリ番地)がセットされ、順次、カウントアップしていきます。命令は、プログラムカウンタの値を参照してメモリから命令レジスタに取り出されるという寸法です。
試しに算術演算を順次進行で行ってみましょう。

 

(高水準言語で表現した場合)

  a = 10
  b = 6
  c = a + b

 

アセンブリ言語で表現した場合)

MOV	[0],10	即値の10をメモリの0番地に転送
MOV	[1],6	即値の6をメモリの1番地に転送
MOV	A,[0]	メモリの0番地の値をAレジスタに転送
MOV	B,[1]	メモリの1番地の値をBレジスタに転送
ADD	A,B	レジスタAとレジスタBの値を加算して、レジスタAに転送
MOV	[n],A	Aレジスタの値をメモリのn番地に転送
END		ソースコードの終わり(OSに戻る)

※大カッコで囲んだ数値はメモリの番地として扱われます。(大カッコがなければ即値として扱われます)

f:id:kouten0430:20190218145804p:plain

※メモリの番地は説明のために適当に割り当てたものです。

高水準言語であればコンパクトに書けることを、アセンブリ言語では長々とした命令になってしまいます。アセンブリ言語(≒機械語)で複数の命令に渡る処理を、人間に分かりやすく・抽象的に記述できるようにしたものが高水準言語です。って、説明不要ですね(@_@;)

 

繰り返しの仕組み

次に、基本三構造のうちの繰り返しをやってみましょう。アセンブリ言語でJMPのオペコードを使うと、プログラムカウンタの値を強制的に変更し、プログラムの流れを変更することができます。試しに先の順次進行のコードを無限ループさせてみましょう(処理の内容に意味はありません)。

 

(高水準言語で表現した場合)

jp:
  a = 10
  b = 6
  c = a + b
  GoTo jp

 

アセンブリ言語で表現した場合)

MOV	[0],10	即値の10をメモリの0番地に転送
MOV	[1],6	即値の6をメモリの1番地に転送
MOV	A,[0]	メモリの0番地の値をAレジスタに転送
MOV	B,[1]	メモリの1番地の値をBレジスタに転送
ADD	A,B	レジスタAとレジスタBの値を加算して、レジスタAに転送
MOV	[n],A	Aレジスタの値をメモリのn番地に転送
JMP	[2]	指定アドレスをプログラムカウンタにセット
END		ソースコードの終わり(OSに戻る)

f:id:kouten0430:20190218170707p:plain


条件分岐の仕組み

最後に、基本三構造のうちの条件分岐をやってみましょう。レジスタAとBの加算結果が15を超えるのなら、何もせずに処理を終了します。

 

(高水準言語で表現した場合)

  a = 10
  b = 6
  If a + b <= 15 Then
    c = a + b
  End If

 

アセンブリ言語で表現した場合)

MOV	[0],10	即値の10をメモリの0番地に転送
MOV	[1],6	即値の6をメモリの1番地に転送
MOV	A,[0]	メモリの0番地の値をAレジスタに転送
MOV	B,[1]	メモリの1番地の値をBレジスタに転送
ADD	A,B	レジスタAとレジスタBの値を加算して、レジスタAに転送
JC	[9]	キャリーフラグが1なら指定アドレスをプログラムカウンタにセット
MOV	[n],A	Aレジスタの値をメモリのn番地に転送
END		ソースコードの終わり(OSに戻る)

f:id:kouten0430:20190218170823p:plain

前々回の加算器を思い出して下さい。ADD A,Bは、10(1010)+6(0110)=16(10000)で、4ビット目が桁上がりするので、これをキャリーフラグにセットしています。

f:id:kouten0430:20190218150446p:plain

これを応用して、例えばレジスタAがn以上なら条件分岐 という処理を次のようにするができます。

ADD	A,9	レジスタAの値に(16-n)の値を加算する(この場合、7以上でキャリーが発生する)
JC	[番地]	キャリーフラグが1なら指定アドレスをプログラムカウンタにセット

JCは(Jump if Carryの略)はキャリーフラグが1ならジャンプします。逆にキャリーフラグが0ならジャンプするJNC(Jump if Not Carry)というのもあります。
参照するフラグレジスタの種類によって、他にもいろいろな条件付ジャンプがあります。

回数指定のループも条件付ジャンプで作ることができます。やり方は簡単なので、ぜひ各自で考えてみて下さいね!(←手抜き)

 

記事を書くにあたって、以下の書籍を参考にしています。 

CPUの創りかた

CPUの創りかた

 

 

プログラムはなぜ動くのか 第2版 知っておきたいプログラムの基礎知識

プログラムはなぜ動くのか 第2版 知っておきたいプログラムの基礎知識

 

 

なお、今回使用したアセンブリ言語は、あくまで説明用の架空のものです。