「アセンブラ言語」ではなく「アセンブリ言語」

日本語では、アセンブラ言語という言い方も広く通じる。

ただし、用語を分けるなら次のようになる。

assembly language
        = 人間が書く、機械語に近い記号言語
        
        assembler
        = assembly languageを機械語へ変換するプログラム
        
        assemble
        = その変換を行うこと

したがって、言語の名称としてはアセンブリ言語、変換器の名称としてはアセンブラと呼ぶのが分かりやすい。

IBMの説明でも、assembler languageは機械語の代わりに命令を記述できる記号的なプログラミング言語であり、assemblerがそれを機械語へ翻訳するとされている。

アセンブリ言語は、機械語に人間が扱える名前を付けた層である。

機械語は数字である

CPUが直接実行するのは、文字としてのMOVADDではない。

命令を表すビット列である。

人間が読む表現
        MOV
        ADD
        JMP
        CMP
        
        CPUが読む表現
        命令コード
        レジスタ番号
        アドレス指定方式
        即値

たとえば、あるCPUで「値を移す」「二つの値を足す」「条件に応じて別の場所へ進む」という操作には、それぞれ決められた数値の形式がある。

しかし、数値だけを並べてプログラムを書くのは困難である。

そこで、命令へ短い記号名――mnemonic――を与える。

machine codeの数値
                ↓ 名前を付ける
        mnemonic
                ↓ 人間が記述する
        assembly source

アセンブリ言語は、高水準言語を単純化したものではない。

機械語を、人間が読めるように記号化したものから始まった。

mnemonic――命令を思い出すための名前

mnemonicは、記憶を助けるものを意味する。

アセンブリ言語では、CPUの命令に短い名前を付ける。

代表的な形は次のようなものになる。

MOV   値を移す
        ADD   加える
        SUB   引く
        CMP   比較する
        JMP   別の場所へ進む
        CALL  手続きを呼び出す
        RET   呼び出し元へ戻る

ただし、同じ綴りの命令が、どのCPUにも同じ意味で存在するわけではない。

命令名、使えるレジスタ、オペランドの順序、アドレス指定、命令の長さは、CPUアーキテクチャとアセンブラによって異なる。

アセンブリ言語は、コンピュータ全体の共通語ではない。それぞれの機械へ近づくための方言である。

小さな例

x86系のIntel記法では、次のようなコードを書ける。

mov eax, 2
        add eax, 3

意味だけを読めば、

EAXというレジスタへ2を入れる
        EAXへ3を加える
        結果としてEAXは5になる

という流れである。

ここではeaxがレジスタ名、23が即値、movaddが命令のmnemonicである。

この二行をそのまま別のCPUへ持っていっても、同じようには動かない。

ARMのA64、RISC-V、IBM Z、古いZ80や68000では、レジスタ名も命令形式も異なる。

同じ目的を実現できても、機械が提供する言葉が違う。

レジスタ――CPU内部の作業場所

アセンブリ言語を読むと、レジスタ名が頻繁に現れる。

レジスタは、CPU内部にある高速な記憶場所である。

メモリ
        = 広いが、CPUから見ると遠い
        
        レジスタ
        = 数は少ないが、演算のすぐそばにある

CPUは、多くの場合、

1. メモリから値を読む2. レジスタへ置く3. レジスタ上で計算する4. 結果をメモリへ戻す

という動きをする。

高水準言語では、変数名の背後に隠れているこの移動が、アセンブリ言語では表面へ出る。

そのため、アセンブリ言語は「何を計算するか」だけでなく、値が機械の中をどう移動するかを書く言語でもある。

ラベル――数値のアドレスへ名前を付ける

機械語の分岐命令は、本来、次に実行する位置を数値で指定する。

しかし、人間が命令の位置をすべて数値で管理するのは難しい。

そこでラベルを使う。

loop_start:
            add eax, 1
            cmp eax, 10
            jne loop_start

loop_startはCPUが直接理解する名前ではない。

アセンブラが、最終的な命令位置や相対距離へ置き換える。

人間
        loop_startへ戻る
        
        assembler
        現在位置から何バイト戻るか計算する
        
        CPU
        数値として指定された位置へ分岐する

ラベルは、アドレスに意味を与える。

アセンブリ言語は命令だけでなく、数字だけだった場所へ、人間のための地名を付ける。

命令だけではない

アセンブリ・ソースには、CPUが実行する命令以外の記述も含まれる。

directive

アセンブラへ指示を与える。

  • どのセクションへ置くか
  • データを何バイト確保するか
  • 外部シンボルを参照するか
  • 文字列や定数を配置するか
  • アラインメントをどうするか

GNU assemblerでは、.から始まる記述の多くがdirectiveとして扱われる。

comment

人間向けの説明を書く。

CPUへは渡らない。

macro

複数の記述をまとめ、短い名前で展開する。

pseudo-instruction

CPUにそのまま一命令として存在しない表現を、アセンブラが一つまたは複数の実命令へ変換することがある。

したがって、

アセンブリ言語の一行が、必ず機械語の一命令へ一対一で対応するとは限らない。

命令文は一命令へ変換されることが多いが、directiveは命令ではなく、pseudo-instructionやmacroは複数命令へ展開される場合がある。

assemblerは何を組み立てるのか

アセンブラは、文字を単純に命令コードへ置き換えるだけではない。

mnemonicを命令形式へ変換する
        レジスタ番号を符号化する
        即値が収まるか確認する
        ラベルの位置を計算する
        シンボル表を作る
        再配置情報を残す
        データやセクションを配置する

GNU assembler asは、ソース内のシンボル、定数、式、directive、機種依存機能を扱い、通常はobject fileを出力する。

完成した実行ファイルになる前には、さらにlinkerが必要になることが多い。

assembly source
              ↓ assembler
        object file
              ↓ linker
        executable / library
              ↓ loader
        memoryへ配置
              ↓
        CPUが実行

assembleという語には、断片へ位置を与え、参照関係を結び、機械が実行できる形へ組み立てる感覚がある。

compilerとの違い

高水準言語のcompilerも、最終的には機械語へつながる。

ただし、入力される抽象度が違う。

高水準言語
        何をしたいかを比較的抽象的に書く
                ↓ compiler
        多くの機械命令へ変換する
        
        アセンブリ言語
        どの命令・レジスタ・分岐を使うかを近い層で書く
                ↓ assembler
        機械語へ符号化する

Cなどのcompilerは、途中でアセンブリ・コードを生成することもある。

しかし、現在のcompilerが必ず人間向けのアセンブリ・テキストを経由するとは限らない。内部表現から直接object codeを生成する場合もある。

重要なのは、compilerとassemblerをファイル拡張子で分けることではなく、扱う言語の抽象度で分けることである。

一つのCPUにも複数の書き方がある

同じx86の命令でも、Intel syntaxとAT&T syntaxでは表記が異なる。

Intel syntax
        宛先, 元
        
        AT&T syntax
        元, 宛先

レジスタ名への記号、即値の書き方、命令サイズの表現なども異なる。

さらに、assemblerごとのdirectiveやmacro構文もある。

つまり、アセンブリ言語は、

CPUアーキテクチャ
                ×
        命令セット
                ×
        assemblerの文法
                ×
        object file形式
                ×
        OSの呼出規約

の組合せとして現れる。

同じCPU向けのコードでも、どの環境で書かれたかによって見た目が変わる。

machine-dependent――機械に近い代わりに、機械へ縛られる

IBMは、assembler languageを機械語に形式と内容が最も近い記号言語と説明している。

この近さは長所であり、制約でもある。

長所

  • CPUの命令を直接選べる
  • レジスタやメモリ配置を細かく扱える
  • 特殊命令やハードウェア機能を使える
  • 実行結果を命令単位で追いやすい
  • compilerが生成したコードを確認できる

制約

  • CPUが変わると書き直しが必要になる
  • OSやABI、呼出規約にも依存する
  • 大きなプログラムでは管理が難しい
  • 人間が細部を引き受ける範囲が広い
  • 安全性や可搬性を自動的には得られない

高水準言語は機械との差を隠す。

アセンブリ言語は、その差を隠さずに引き受ける。

いつ使われるのか

現在、一般的なアプリケーション全体をアセンブリ言語だけで書くことは多くない。

それでも、次の領域では重要である。

  • OSの起動部分
  • interruptやcontext switch
  • 組込み機器の初期化
  • CPU固有命令の利用
  • 暗号・画像・数値処理の最適化
  • compiler、debugger、emulatorの開発
  • firmware解析
  • malware解析
  • crash dumpの読解
  • reverse engineering
  • 古い機種やゲーム機の保存・再現

アセンブリ言語は消えた古い言語ではない。

人間が普段いる高水準の世界と、CPUが実行する世界の継ぎ目で、いまも使われている。

reverse engineeringでは、元の文章は戻らない

実行ファイルをdisassemblerへ渡すと、機械語からアセンブリ風の命令列を得られる。

しかし、それは元のassembly sourceそのものではない。

通常、次の情報は失われている。

  • 元のラベル名
  • 変数名
  • コメント
  • macroの使い方
  • source fileの構成
  • 開発者が考えていた設計意図
  • compilerが変換する前の型や式
元のソース
        分かりやすい名前と説明がある
                ↓ assemble / compile
        機械語
        多くの人間向け情報が消える
                ↓ disassemble
        命令列は戻る
        元の意味までは戻らない

disassemblerは、数値を命令として読み直す。

reverse engineerは、その命令列へ、再び意味と名前を与える。

アセンブリ言語は、機械語へ名前を与える言語である。逆解析では、その名前が失われた機械語へ、もう一度仮の名前を付け直す。

codeとdataの境界

実行ファイルの中にあるバイト列が、命令なのかデータなのかは、単独では決まらない場合がある。

同じ数値列でも、CPUが命令位置として実行すればcodeになり、表や文字列として参照すればdataになる。

バイト列
          ├─ 命令として読む
          ├─ 数値として読む
          ├─ 文字列として読む
          └─ アドレス表として読む

正しい逆アセンブルには、実行開始位置、分岐先、セクション情報、呼出規約、周囲の参照関係が必要になる。

これは当サイトの中心にある問いへつながる。

物の意味は、その中の数値だけで決まるのか。それとも、システムがどう扱うかによって決まるのか。

高水準言語より「本物」に近いのか

アセンブリ言語は機械語に近い。

だが、それが機械の真実そのものというわけではない。

一つのassembly instructionが、CPU内部ではさらに細かなmicro-operationへ分解されることがある。

命令の実行順序も、現代のCPU内部では並べ替え、予測、並列化される。

assembly source
        人間が見る命令
                ↓
        machine instruction
        ISAが定義する命令
                ↓
        microarchitecture
        内部で分解・並列実行される処理
                ↓
        回路上の信号変化

アセンブリ言語は、回路そのものではない。

CPUが外部へ約束しているInstruction Set Architecture――ISA――を、人間が扱うための表現である。

機械に最も近い言語でさえ、機械と人間のあいだに置かれた一枚の仕様書である。

WebAssemblyは同じものか

WebAssemblyにもassemblyという語が入っている。

しかし、特定の物理CPU向けアセンブリ言語そのものではない。

W3Cの仕様は、WebAssemblyを安全で可搬な低水準コード形式であり、中心にはvirtual instruction set architectureがあると説明している。

x86 assembly / ARM assembly
        = 特定の物理的な命令セットへ近い
        
        WebAssembly
        = 実装先から独立した仮想命令セット

WebAssemblyは低水準で命令形式を持つが、最終的には実行環境が物理CPUの命令へ変換する。

名前は近くても、機械への結び付き方が異なる。

初期コンピュータでは、機械語とアセンブリ言語が仕事場だった

IBM ResearchによるFortran史では、1954年以前、ほとんどのプログラミングは機械語またはアセンブリ言語で行われていたと回想されている。

初期のsymbolic programmingは、数値アドレスへ名前を付け、十進数を扱い、命令の位置計算を人間から少しずつ引き受けた。

高水準言語が登場する前、アセンブリ言語は低水準の専門言語というより、コンピュータへ仕事をさせるための主要な入口だった。

数値の命令を直接書く
                ↓
        命令とアドレスへ名前を付ける
                ↓
        assemblerが数値へ戻す
                ↓
        さらに高い抽象度の言語が生まれる

アセンブリ言語は、機械語から高水準言語へ移る途中で捨てられた段階ではない。

機械と記号の間へ、最初に本格的な橋を架けた言語だった。

当サイトにおける位置

当サイトの多くの標本は、最終的にアセンブリ言語の層へ接続する。

  • software crackingは、保護判定を行う命令列を読む
  • malware解析は、実行形式から挙動を復元する
  • modchipは、本体がどの信号や処理を信用するかへ介入する
  • reverse engineeringは、命令列から構造と設計を復元する
  • 危険文書には、逆アセンブル結果やpatch情報が再流通した
  • バッ活は、機械語、ダンプリスト、コピー・プロテクト解析を紙面へ持ち出した

ただし、アセンブリ言語そのものは攻撃の言語ではない。

OS、compiler、device driver、ゲーム、組込み機器、科学計算、防御分析にも使われる。

それを危険にするかどうかは、低水準であることではなく、対象、権限、目的、変更、配布の仕方で決まる。

アセンブリ言語は、機械の秘密を書く言語ではない。機械が実行する約束を、人間が読めるぎりぎりの場所へ置いた言語である。

この標本が示す境界

  • 数値へ名前を付けると、機械はどこまで理解可能になるか
  • CPUの命令セットは言語なのか、仕様なのか
  • 人間向けのラベルやコメントが消えた後、意味を復元できるか
  • codeとdataは数値そのものではなく、扱われ方によって分かれるのか
  • 高水準言語が隠した機械の動きを、誰が引き受けるのか
  • 特定機種へ深く結び付くことと、機械を深く理解することは同じか
  • アセンブリ命令の下にmicroarchitectureがあるなら、どの層を機械の本体と呼ぶのか

アセンブリ言語は、上から見れば細かすぎる。

下から見れば、まだ名前と記号に包まれている。

人間の言葉が機械の数値へ変わる、その最後の見える層である。

関連標本

資料・出典