機械語
機械語(きかいご、machine languageあるいはmachine codeあるいは binary machine language)は、コンピュータの中央処理装置(CPU)が直接理解し実行することができる命令からなる言語。マシン語(マシンご)とも。
機械語は0や1を並べた形、ビットの組み合わせパターン(ビット列)で表されるものであり、人間が日常使う言葉とはかけ離れていて読み書きしやすい形式ではない。
そのため、コンピュータのプログラムの開発に機械語が直接使われることはほとんどない。通常は人間が読みやすい形式(高級言語)でプログラムを書いてそれをコンパイラで機械語に翻訳してその機械語でコンピュータを動かしている。(ただし組込システムのエンジニアなどハードウェア寄りのエンジニアは機械語モニタを用いて機械語を直接操作することもある。)
機械語は中央処理装置(CPUやMPU)の動作をひとつひとつ指示するものであり、指示の内容はたとえば次のようなものである。
演算装置内部での演算(論理演算や2進数と2進数の加法、減法、乗法、除法など)
入出力チャネル(※)に対する指令(※ 入出力チャネルとはCPUが入出力バスとデータ信号をやりとりするためのチャネル(通路、流路)のこと。I/Oチャネルとも)
各プロセッサは、その設計段階で命令セットアーキテクチャ(ISA:Instruction Set Architecture)が定められ、そのISAの中で各命令の識別番号(オペコード)を定められる。機械語のプログラムは、基本的には、その識別番号(と操作対象のデータ)を実行順に並べた形式になっている。
機械語の体系は、CPU(MPU)のアーキテクチャごとに大きく異なっている。あるアーキテクチャのCPUのための機械語は、別のアーキテクチャのCPUには全く使えない。→#機械語と互換性
現在ではコンピュータのプログラムは基本的には高級言語で書くということはすでに説明したが、ハードウェア寄りのエンジニアなどがあえて機械語レベルのプログラムを作成する場合でも、大抵は機械語そのものを直接書くのではなく、機械語と1対1で対応するアセンブリ言語を使っている。→#機械語とアセンブリ言語、エンジニアが機械語を使用する状況
次に機械語の命令群が書かれた具体例を示す。
CASLのペーパーマシンCOMETについて、情報処理技術者試験の出題範囲を示す資料に処理系作成者に対する便宜として、定義の後に付されている参考資料(定義ではない)にもとづく機械語の例を以下に挙げる。なお、この機械語コードは16進数表現(※)であり、2バイト単位で区切って(※)ある。
(※)CPU自体が実行している生(なま)の機械語は、物理的な実態としては(4個から64個ほどの)電気的スイッチ群のOn/Off、正確に言うと各電圧の高・低(High/Low)の組み合わせであり集積回路では片方を 0 ボルト にしてしまうと回路の動作が不安定になってしまうので、実際には、厳密に言うとHigh/Lowの2つの電圧で実現している。デジタルコンピュータでは、要は、2値方式であることが重要で、Hi/Lowでも2値論理の論理回路としてはOn/Offと等価な動作をするので、それを土台としてCPUは設計されている。、 それを直接的に文字(数字)にするには1/0を多数連ねた2進数(8桁、16桁、32桁、64桁など)で表現するが、人間が読みやすいように、その2進数を16進数に変換して表示することも一般的である。そして16進表示でも、その文字列をただ長々と何十文字や何百文字も連ねてしまうと、人間の認識能力では誰でも誤読を連発するので、 2バイトつまり16進数の4文字ごとに、あるいは1バイトつまり16進2文字ごとに区切って間に空白を入れて可読性を高めるということが画面上や紙面上では一般的である。だから情報処理技術者試験の文面でも、受験者に分かりやすくなるように16進表示にしてあり、4文字ごとに空白をはさんでいる。
一般的な機械語プログラムは以下のような構成となっている。
命令部(オペコード)
CPUに処理をさせるための命令の番号。
アドレス部(オペランド)
情報として利用するデータが格納されている、あるいは結果の記録先のレジスタやメモリアドレス、ジャンプ先などを示す、後述のイミディエイト値もこれに含めることもある。命令によって、個数や長さが異なる。オペランドの数について、0アドレス方式、1アドレス方式、2アドレス方式、3アドレス方式がある。0アドレス方式はオペレーションコードだけで、オペランドは存在しない。
オペランドの一部に含めることもある。演算に使用する整数値などのデータが命令に引き続いて置かれているもの。即値とも。
データ部は実行されない部分である。プログラムで使用するデータのうち前述のイミディエイト値に収まらないもの、文字列リテラルなどのような定数データ、グローバル変数(機械語やアセンブリ言語プログラミングの用語ではワークエリア)のためのヒープ領域やローカル変数のためのスタック領域など。通常ある程度まとめて置かれる。通例、命令として解釈することはできないが、強制的にデータ部にジャンプさせ、命令部として解釈させることで、仮にデータ部に機械語相当の命令データが配置されていた場合はそれらを実行することができる環境も存在する。これを悪用して、リターンアドレスの書き換えなどにより不正なコードを実行させてしまうような悪意のあるソフトウェアからコンピュータを保護するため、データ実行防止(DEP)と呼ばれる機能を備えている環境もある。
以上の各部分に具体的に何ビットずつ割り振って、どういう順番に並べるか、という形式(フォーマット)のことを機械語フォーマットなどと言う。アーキテクチャにより機械語フォーマットはまちまちだが、1命令を構成するデータ長が固定の「固定長」式と、命令やオペランドの種類により変化する「可変長」式に大別される。可変長の場合、機械語命令の種類によってアドレス部やデータ部、そして中には命令部までも長さが変わる。このため、読み込み位置が1バイトずれれば機械語の命令はそれ以降のすべての命令が正しく読み込まれず意味を失うため、そういった機械語フォーマットのバイナリを対象とする逆アセンブラは工夫を要する。またメモリが限られるシステムでは本来の命令の途中に飛び込み別の意味に使うというトリック的な手法もある。
基本的に、あるアーキテクチャの中央処理装置のための機械語は、別のアーキテクチャの中央処理装置のためには全く使えない。
たとえば、Pentium系列とPowerPC系列の双方で動く機械語プログラムが存在しないのは、命令セットに互換性が無いからである。
たとえ同じメーカー(や同系列企業)の中央処理装置でも、ある世代の中央処理装置のための機械語が、そのアーキテクチャの「世代」が代替わりし、アーキテクチャが変化すると、全く動かない。機械語プログラムがそのまま動くか否か、という互換性を「バイナリ互換性」と言う。
この常識を変えてみようと試みられたことがあり、1994年にIBMからコンセプトが公表されたプロセッサPowerPC 615はx86とPowerPC命令の両立を目指し、32-bit PowerPC コード、64-bit PowerPC コード、32-bit x86 コードの実行が可能になる、という構想のものだったが、結局量産には至らなかった。
なお、まったく役に立たない雑学だが、注意深く機械語命令を選ぶことによって、異なるアーキテクチャでもたまたま動作する、ごくごく小さなプログラムを、シャレで(お遊び的に)書くことは可能な場合がある。
CPUの仕様が異なれば、機械語もそれぞれのCPUごとに異なる。上記類似点の範囲でのCPUごとの機械語の仕様の差異には、以下のようなものが挙げられる。
CPUが理解できる命令の種類や数が異なる(CISC、RISC、VLIW)
命令の長さが異なる(CISCとRISCとでは長さが異なることがある。また、同じアーキテクチャでも、命令のビット数の違いも影響する)
演算方法が異なる(レジスタ - レジスタ間演算やメモリ - レジスタ間演算の違い。RISCでは後者の演算ができない)
データの記録方法が異なる(エンディアンやアラインメントの相違)
実行形式のバイナリファイルの記録形式が異なる(PE、COFF、ELFなど)
機械語で書かれたコードには頻出するパターンが存在する。
機械語に関数構文は存在しないが、関数に相当するパターンが存在する。
関数は引数を受け取り、ローカル変数を確保し、ボディの命令を実行し、戻り値を返すルーチンである。このルーチンを呼び出す場合、制御が戻るポイントを控え、引数を用意し、ルーチンへ制御を移して実行し、戻り値を記録し、戻りポイントへ制御を移す。
これらの処理によりサブルーチンへ制御が移り、関数ボディの命令列が実行される。関数命令の最終行が実行されたのち、次の処理が必要となる。
これらの処理によりメインルーチンへ制御が復帰する。
このパターンは関数の処理内容に関わらず普遍的である。前半の関数呼び出しに相当するパターンを関数プロローグ(function prologue)という。後半の関数からの復帰に相当するパターンを関数エピローグ(function prologue)という。
関数プロローグ・エピローグは同じ結果が得られるいくつかのパターンが存在する(呼出規約)。例えば引数をスタックに積むパターンとレジスタに置くパターンがある。
機械語を扱いたい場合でも機械語は0/1の組合せや16進表記であり扱いづらいので、代わりにアセンブリ言語を使うことが一般的である。アセンブリ言語は、プログラミング言語の中では機械語に一番近く、機械語とほぼ1対1に対応し、なおかつ人間に理解しやすいニーモニックで書ける。
アセンブリ言語で書かれたプログラムを機械語に変換すれば、その機械語でコンピュータは動く。(アセンブリ言語で書かれたプログラムを機械語に変換することをアセンブル(する)と言う。)アセンブラというソフトウェアを使えばアセンブリ言語から機械語への変換を自動的に行うことができる。
一方、逆アセンブラというものもあり、これはアセンブラと逆向きの作業、つまり機械語のプログラム(機械語コード)をアセンブリ言語に変換するソフトウェアである。アセンブラと逆アセンブラを使うことで、機械語←→アセンブリ言語 の間の変換を自在に行うことができる。
なお人が自分の眼と頭脳と手を使ってアセンブルの作業を行うことをハンドアセンブルという。コンピュータの黎明期にはしばしばハンドアセンブルが行われていたが、近年ではほぼ全く行われていない。プロセッサが4ビットや8ビット(2進数で4桁や8桁)の時代ならば、やろうとすればハンドアセンブルもできたが、その後CPUは16ビット、32ビット、64ビットとなり、16ビット以降は人間の頭脳や手の能力では扱いきれなくなった。
1970年代後半のマイクロコンピュータや1980年代前半のパーソナルコンピュータの登場で個人ユーザがコンピュータを使うようになったが、当時はアセンブラも高額で個人では購入しづらく、コンピュータがまだ8bitだったので、個人ユーザはハンドアセンブルを行うことがよくあった。
なお基本的にはアセンブリ言語は機械語と1対1に対応するが、若干の例外はあり、簡単なマクロなどを備えているものは多く、遅延スロットを利用するコードに自動的に変形するなどといった機能を持つものもある。また特殊な短縮形など(x86でAXがオペランドの場合など)について、機械語では違いがある場合をアセンブリ言語では明示的に指定できない場合もある。
システム開発の際の文書(仕様書)が失われることはよくある事だが、文書が残っていないシステムを分析する場合、プログラムを解析するしか方法が無いことがあり、特にソースコードも残っていないような場合には、機械語コードを解析するしかなくなる。そのような場合は、逆アセンブラで逆アセンブルつまり機械語からアセンブリ言語に変換することで、最低限ではあるが、機械語コードを解析できるようになる。さらにヒューリスティックな機能を備えたソフトウェアも使えれば、ソースコードのサブルーチン名や変数名などもある程度推測してくれる。ただしプログラムの"意味"を解析するのは人が行なう必要がある。
ここでは、プログラム内蔵方式を前提とする。一般に電源投入ないし、いわゆるコールドリセットの直後にCPUが実行するコードはROMに置いておくか、CPUの動作に依らない方法でRAMに書き込まれている必要がある(ブート)。
オペレーティングシステム(OS)がブートされた後の、OS運用下では、が存在するシステムの場合、補助記憶装置中のファイルシステムに、いわゆる「実行可能バイナリ」などと呼ばれる実行ファイルとして機械語プログラムが存在しており、それがファイルシステムから主記憶にロードされて実行される、というような形態が一般的である。なお、実行時に共有ライブラリを動的リンクするなど、近年はこの「ロードして実行する」という手続きが複雑になる傾向もあり、実行時コンパイル等が一般的になると主流の形態も変化するかもしれない。
現在でも全てのプログラムは、たとえ高級言語で書かれていようが、ユーザには見えていなかろうが、結局は全て機械語に変換されて実行されている。コンピュータの中央処理装置は常に機械語で動いている。コンピュータはどの瞬間も、機械語無しでは全く動かない。
コンピュータサイエンティストやエンジニアたちの数十年以上の努力の積み重ねのおかげでアセンブラが作られ、コンパイラも作られ、高級言語も作られ、便利なアプリケーション・ソフトウェアもあるので、現在では、一般ユーザも、パソコンしか触ったことがないような巷の若いソフトウェアエンジニアも、機械語は直接書いたり読んだりしなくても、コンピュータをそれなりに操れる。
だが、一般ユーザや巷のソフトウェアエンジニアが機械語を直接書いたり読んだりしていなくても、それは「彼らは直接書いたり読んだりしていない」というだけのことでしかなく、実際にはコンピュータの核心部分の中央処理装置は常に機械語で動いている。また、現在でもいわゆる「組み込み系のエンジニア」や「ハードウェア系のエンジニア」などは、しばしばアセンブラや逆アセンブラを扱う必要があり、アセンブラや逆アセンブラを使えばその画面には機械語そのものが表示されているのでそれを目にすることになる。こうしたエンジニアは、時には機械語を直接自分の目で読んだり書いたりする場合もある。また忘れてならないが、CPUやMPUの開発企業(たとえばインテルやAMDなど)で新たなCPUやMPUのアーキテクチャを設計するエンジニアたちは、しばしば機械語についてかなり深いレベルで検討しており、新たなアーキテクチャを創造する場合は新たな機械語も作り出す。
人間がわざわざ直接機械語を書いたり読んだりする場合は、以前は次のような理由であった。
アセンブラが存在しないか高価なため購入できない、クロスアセンブラであるため別のコンピュータが必要、など
きわめて単純な処理の繰り返しで高速に処理させたいのにもかかわらず、コンピュータの性能が低いうえに、プログラミング言語もBASICインタプリタで望みの処理速度が得られず、機械語で書いた別ルーチンを呼び出す形にすれば可能な場合(たとえば1970年代~1980年代など、たとえば2Dシューティングゲームソフトの上下左右の画面スクロール処理など)
今日では、GNU Binutilsないし同様なライブラリがあることも多く、そういったユーティリティやライブラリを使うことで、アセンブラ・逆アセンブラを書いたりリバースエンジニアリングなどですら、機械語に直接触れずできることも多い。そのため、巷のエンジニアたちが機械語を直接読んだり書いたりするのは、そのようなユーティリティやライブラリが(まだ)無い新しいプロセッサの場合や対応していない新機能などを使う場合、プログラミング言語には馴染まない特殊な命令を扱う場合、trampolineのようなテクニックが必要な場合、プロセッサのバグに当たった(等の可能性が疑われる)場合、何らかの理由でコアダンプを直接解析しなければならない場合、などに限られてきている。