見出し画像

アセンブラ:機械語を知ろう

【はじめに】

コンピュータの根本的な動きを知るのに機械語の理解はとても役に立ちます。


【機械語とアセンブラの違い】

アセンブラはアセンブリ言語とも呼ばれます。

機械語はコンピューターの命令コードそのもののバイナリコードです。

バイナリコードをそのままバイナリエディタで書いてもプログラムを記述できますが、そのままだと意味がよくわからないので、意味がわかるように書いたのがアセンブラです。

なのでアセンブラと命令コードの対応は1対1です。


【命令セット】

CPUがサポートしている命令群のことを命令セットと言います。

命令セットそのものはCPUによって違います。
しかし、命令の種類そのものが大きくかけ離れているわけではありません。

CPUの役目はとにもかくにも演算です。
演算を素早く行うための競争が日々繰り広げられています。

命令が変わってしまうとソフトウェアの互換性が無くなってしまうので、過去との互換性を保つために拡張されることがほとんどです。

しかし速度競争の結果、設計思想を変えざるを得ない時には命令セットを複数系統持ったり、エミュレータを使ってサポートしたりします。


【命令】

命令セットは沢山の種類の命令がありますが、大まかに分類するとだいたい3種類しかありません。

演算、コピー、分岐の3種類です。

ほとんどの命令はこの3種類のどれか、もしくは組み合わせで出来ています。


〈演算命令〉

演算はCPUの基本です。

足し算、引き算などの算術演算のほか、AND、ORなどの論理演算をサポートしています。

CPUによりますが、基本的にはレジスタに対してしか演算は行えません。

不動小数点演算などの複雑な計算はCPUの基本的な演算回路ではなく、浮動小数点演算回路が行います。

以前のCPUでは浮動小数点を行うためのコプロセッサがCPUと別に装備されていましたが、今は内蔵するケースが多いと思います。

コプロセッサがない場合はソフトウェアで実装するしかなく、とても処理時間のかかる重い処理でした。


〈コピー命令〉

メモリとレジスタの情報のコピーを行ったり、レジスタとレジスタの情報のコピーを行います。

前述のように基本的には演算はレジスタに対して行うので、例えば足し算をするときには以下のようになります。

①メモリから値を読み取りAレジスタに値を入れる。
②メモリから足したい値を読み取りBレジスタに入れる。
③A,Bレジスタの値を足して、Aレジスタの値を更新する。
④Aレジスタの値をメモリに再度書き戻す。
といった操作を行います。

C言語で言うa++;のようなただの足し算でもアトミック性が保障されないのは、このように単純そうに見える処理でも命令が数ステップに別れるためです。


〈分岐命令〉

実行するアドレスを変えたり、条件によって飛び先を変えたり、サブルーチンに飛んだりする命令です。

条件によって飛び先を変える命令の場合、ステータスレジスタの内容によって動作を変えます。

ステータスレジスタは演算の状態によって変化するレジスタなので、直前に状態変化をするための演算を行います。

具体的に言うと

①演算
②分岐命令

という順番に実行します。

例えば引き算の命令をすれば、引いた結果が負なのか、ゼロなのかが分かると、C言語であれば比較演算子==、<、>、<=、>=が判定できます。


【アドレスとは?】

アドレスはその名の通り、住所のことです。

なんの住所なのかと言うと、データ入れ物がある住所のことです。

実際の住所とは違い、国名・県名・市町村名などはなく、ただの数字しかありません。

番地とも呼ばれる事があります。

コンピュータのバスシステムで使われます。

バスシステムにはRAM(主記憶)が接続される他、外部ハードウェアが接続されます。

基本的に1バイト=8bitずつ情報が格納されており、1バイトにつき1番地が割り当てられています。

レジスタ長と同じ長さのアドレスを持っていることがほとんどで、64bitのレジスタ長では64bit長のアドレスを持っていて、0〜2^64-1番地のアドレスを持っています。

プログラムもアドレスを持ったメモリー上に配置され、分岐命令の分岐先はアドレスを指定することで行われます。


【バスシステムとは】

CPUのバスシステムにはアドレスバスとデータバスという線があります。

他にもバスクロックと読み出しや書き込みを表す線やその他の制御線があります。
バスクロックに同期して情報をやり取りします。

アドレスバスに乗っている値がアドレス値、データバスに載っている値がそのアドレスのデータ値になります。

例えばメモリから値を読む動作では以下のようになります。

①CPUが読み出しモードに設定し、アドレスバスに読みたいアドレスを乗せる
②メモリ装置がデータバスにデータを乗せて、読み出し可能に設定
③CPUが読み出し可能なのを確認し、データバスからデータを読みだす


【レジスタ】

一般的にコンピュータの主記憶としてメモリー=RAMがあり、外部記憶としてHDDのようなストレージがあるということは、コンピュータを触っている人にとっては常識ですが、さらにローレベルの記憶装置としてレジスタがあります。

このレジスタがコンピュータの動作と密接に関わってきます。

RAMは基本的にCPUのバスに繋がっており、バスシステムを介して情報のやり取りをします。

CPUのバスシステムを使ってアクセスする場合は、アドレスを持っています。

ストレージはCPUのバスには直接繋がっておらず、バスシステムとは別の通信で情報のやり取りをします。

レジスタの場合はアドレスを持っておらず、命令を実行する回路が直接操作します。

レジスタはストレージはおろかメモリよりも速く、コンピュータシステムの中では最も速い記憶装置です。

レジスタにはいくつかの種類があります。
以降に代表的なレジスタを紹介します。


〈プログラムカウンタ:PC〉

プログラムが実行されているアドレスを入れておくレジスタです。

そのアドレスに入っているのは命令です。

命令を実行すると次に実行するアドレスに書き換えられます。


〈ステータスレジスタ:SR〉

命令を実行した時に、実行した内容に応じた副作用の結果に書き換えられます。

例えば引き算をした結果が正なのか負なのかが入るようになっています。

ステータスレジスタを見て実行結果を変える命令と組み合わせて分岐を実現します。


〈スタックポインタ:SP〉

スタックという記憶方式を実現するためのレジスタです。

スタックはコンピュータプログラムを動作させるために便利な記憶方式で、LIFO(Last In First Out=後入れ先出し)という方式です。

これは名前の通り後に入れた物が先に出てくる方式で、井戸の中に直径程の大きなものをぶち込んでいき、一番上のものから取り出すイメージです。

この記憶方式のおかげでサブルーチンをコールした時の内容の退避や変数の受け渡しも簡単に間違いなく出来るし、割り込みで別のプログラムが不意に実行されたとしても、お互いに領域を干渉されずにプログラムが実行できます。


□スタックポインタの動作

スタックポインタはスタック領域のメモリのあるアドレスを指しています。

例えばサブルーチンを実行したい場合、メモリに様々な値を入れておきたくなります。

主に以下の値です。
・サブルーチンから戻ってくるアドレス
・サブルーチンに渡す値

これらを入れてからサブルーチンを実行し、サブルーチンの最後にはスタックを見て書いてあるアドレスに戻ります。

サブルーチンが何重に実行されても、一番直近に実行されたサブルーチンの戻り先がスタックのすぐそばにあるので、順繰りに引き出して戻って行くことが出来ます。

間に割り込みが入っても、割り込み先で戻り先のアドレスをスタックに詰めて行き、割り込みから通常プログラムに戻る際にスタックを見て戻ります。


〈汎用レジスタ〉

上記以外のレジスタで、様々な使われ方をします。

演算に使う、相対アドレッシングに使うなど様々です。

CPUによってはアドレスレジスタとデータレジスタに役割が別れているものもあります。


【アドレッシングモード】

例えばメモリの内容を読み出すときに、そのメモリのアドレスを知らないとアクセス出来ませんが、命令の横にアドレスを書く場合、64bit CPUの場合素直に書くと64bit+αの命令長になってとても無駄が多いです。

そんな時はベースとなるアドレスをレジスタに格納しておき、そのレジスタの値からどれだけずれたところにあるデータを拾ってくる、ということをやると命令長を短くできます。

アドレスを解決する方法はいくつかあります。

このいくつかの方法のことをアドレッシングモードと言います。

ほとんどの場合、レジスタ相対アドレッシングかPC相対アドレッシングで解決します。

全て相対アドレッシングで記述することが出来ると、メモリのどのアドレスにプログラムを置いても実行できるようになるので、色々なプログラムを取っ替え引っ替え配置し、実行するシステムに向いています。
(リロケータブルプログラムと呼ばれます)


【パイプライン】

プログラムもメモリに入っているので、プログラムを実行するだけで上記のような動作が休みなしにひっきりなしにガンガン行われます。

最近はメモリの速度とCPUの速度に差があり、メモリが相対的に遅いので、プログラムを1命令読んで、実行するということをやるとCPUの動作が遅くなってしまいます。

それを防ぐために読み出しと実行を並列に実行するということをやります。
それをパイプラインと呼びます。

有限のパイプラインに命令を先読みしておき、命令を実行する際にはパイプラインから命令を引き出して実行します。

ただし、分岐命令等でパイプラインに入っている命令とは違う命令を実行する必要がある場合は、パイプラインをクリアしておく必要があります。
コーディング的にはnopをいくつか実行し、パイプラインを初期化するということを書いたりします。


【メモリキャッシュ】

前述の通り、メモリとCPUの速度に違いあるので、少しの容量ですが速いメモリを用意して間にかませることで、アクセスの速度差を吸収します。

基本的にはCPUはキャッシュにあるデータを読みます。

キャッシュは一度読んだアドレスにヒットするとメモリアクセスをしないのでバスが空いて動作が速くなります。

読み出しだけなら何の矛盾もないですが、書き込み部分で矛盾が生じる可能性があります。

キャッシュへの書き込みがすぐさまメモリに書き込まれると矛盾は生じませんが、条件がそろわないと書き込まれない方式だと、書き込まれる前にキャッシュを通さずにデータが読まれるとメモリの内容とCPUが認識している内容に矛盾が生じます。

DMA(ダイレクト・メモリ・アクセス)等、CPUとは別のハードウェアがメモリアクセスをするとこの矛盾が問題を起こしやすくなります。

キャッシュは速度差のあるシステムにはよく使われる手法でHDD等のストレージやプリンタ等の出力に時間のかかるハードウェアに装備されることが多いです。


【リトルエンディアン/ビッグエンディアン】

メモリに格納する方式です。

レジスタは今のCPUだと64bit長のレジスタを持っていますが、このレジスタの値を0〜7番地に格納する際、上位桁の8bitから順に格納するか、下位桁の8bitから順に格納するかの違いです。

リトルエンディアンが下位から、ビッグエンディアンが上位からです。

エンディアンの違いは互換性と拡張性から来るものがほとんどですが、それぞれ得手不得手があります。

エンディアンという名前ははもともとガリバー旅行記から由来するもので、卵をお尻のほうから割る種族と頭の方から割る種族にちなんでいます。
そこからもわかるように違いは思想の違いのようなもので些細なものです。

今時のCPUだと格納方式を選ぶことが出来るようになっています。

エンディアンはシステム依存なので、プログラムを別のシステムに移植すると互換性が無かったりして失敗します。


【あとがき】

アセンブラに触れる機会は普通は無いかと思いますが、例えばLINUXカーネルなんか見ると、ちょくちょく混ざってるので、ネットにある情報だけでも触れることは出来ます。

16ビットのレジスタが4本しか無いCPUでRAMの検査プログラムを書いた時には、パズルのようでとても楽しかったです。

RAMの検査なのでRAMが使えず、ステータスレジスタを書き換えない命令を駆使して組み上げました。

アセンブラでプログラムを組むと作るのに時間はかかるしたいしたことは出来ませんが、コンピュータってどんなもの?という疑問を解決するのにうってつけです。

この記事が気に入ったらサポートをしてみませんか?