axx: 命令型アセンブリ言語の一般化 — 自由構文パターン言語と三層構造による一般アセンブラの設計と実装
前川 田井介 (fygar256) の axx.py / caxx.c に関する紹介論文(2026 年 10 月版)
要旨
axx (Arbitrary eXtended X assembler) は、個々のプロセッサ向けに専用アセンブラを実装するという従来の枠組みを離れ、「すべての命令型アセンブリ言語は instruction :: error_patterns :: binary_list という単一のパターン形式に還元できる」という洞察に基づいて構築された一般アセンブラである。本稿では、axx の中心的発明である自由構文パターン言語、トークナイザを持たない文字単位マッチング、特異度スコアによる順序非依存パターン照合、そして計算能力を宣言的な核から切り離すという設計判断を紹介する。現行版の axx は、「読み込み前に計算するマクロ層」「宣言的なパターン層」「符号化時に呼び出されるミニ言語」という三層構造をとる。パターン層は照合の停止性を言語仕様として保証したまま、書き手が .call と明示的に書いたときにだけチューリング完全なミニ言語へ制御を渡す。
本版では、前版以降に現行仕様へ加わった二つの大きな到達点を新たに論じる。第一は ELF の一般化である。ELF を書き出すコードから機種番号による分岐が取り除かれ、リロケーション型、ELF クラス、RELA/REL、命令語内のビット欄、対になるリロケーション、r_info の並び、加数の単位、セクショングループ、CFI に至るまでを、パターンファイルの宣言だけで記述できるようになった。組み込みの 11 機種の表も、宣言をあらかじめ書いておいたものにすぎない。第二は テキスト出力によるソース間変換である。binary_list に文字列テンプレートを書けるようになったことで、パターンファイルはバイナリ生成器であると同時にアセンブリ言語間の翻訳器にもなり、作者が axx2 構想として掲げていた方向の一部が、現行の宣言的な核のまま実現された。あわせて、集合と集合代数、シンボル捕捉子 !Y などシンボル体系の拡充、AArch64 (SVE/SME2)・PowerPC64 (POWER10)・RISC-V RV64 全体・MIPS (MIPS I〜MIPS64 Release 6 と各拡張) に及ぶ実用パターンファイル、二実装を 186 組の比較で突き合わせる検証体制を報告し、歴史的なメタアセンブラの系譜における axx の位置づけと、AI がパターンファイルを生成する時代における意義を論じる。
1. はじめに
アセンブラは通常、特定の命令セットアーキテクチャ (ISA) に一対一で対応する形で実装されてきた。GNU as のようなマルチターゲットアセンブラであっても、各ターゲットはコード中にハードコードされたバックエンドとして存在し、新しいプロセッサへの対応はアセンブラ本体の改修を意味する。
axx はこの構図を逆転させる。アセンブラ本体はいかなる ISA の知識も持たない最小の照合エンジンであり、個々のプロセッサの知識はすべて外部の宣言的データ、すなわちパターンファイル(プロセッサ記述ファイル)に置かれる。ユーザーは実在プロセッサの仕様書をパターンファイルに書き写すだけで、そのプロセッサのアセンブラを得る。axx が対象とするのは仮想 CPU だけではなく「抽象化された実在 CPU」であり、実プロセッサの仕様をパターンファイルに変換すればそのまま直接アセンブルできる。
現行版では、この「本体は機種を知らない」という原則が、命令の符号化だけでなくオブジェクトファイルの生成にまで貫かれている。どの CPU 向けにリンク可能な ELF を出すかもまた、パターンファイルに書かれた宣言だけで決まる(第 7 節)。
着想は 1986 年、作者が大学時代のアルバイト先である東京電子設計において得たものであり、名称 AXX と C による原型もこの時点で存在した。動作するコードの公開は 2024 年、38 年を経て当時のプログラムリストを再発見し、Python で書き直したことによる。ここで重要なのは、この 38 年が単なる空白ではなく、着想がハードウェアの多様化(VLIW、EPIC、非 8 ビットワード幅プロセッサ、スケーラブルベクトル拡張)を経てなお有効であったことの検証期間として機能している点である。
2. 中心的発明: 単一パターン形式への還元
axx の最も本質的な主張は、機械語にメタレベルの複雑性を持つ EPIC/VLIW を除くすべての命令型アセンブリが、
instruction :: error_patterns :: binary_list
という構造に還元できる、というものである。エラーチェックを省けば instruction :: binary_list にまで単純化される。これは「アセンブラとは、命令の文法規則と、それに基づくバイナリ生成である」という定義の最小化であり、von Neumann アーキテクチャの命令型 ISA に共通する本質の抽出、ISA のメタモデル化、パターンマッチングによる形式化に相当する。
instruction は、文字列リテラル・整数値で置換可能なシンボル・整数式・整数因子・浮動小数点式の組み合わせとして定義される。この 5 要素の組み合わせだけで任意の命令型アセンブリ言語を処理できるというのが還元命題の内容であり、命令と機械語が一対一に対応するプロセッサであれば処理可能である。
例えば x86_64 の RET 命令は次の一行で完結する。
RET :: 0xc3
オペランドを持つ命令も、変数と式の組み合わせで一行に収まる。8048 の例では、
ADD A,R!n :: n>7;5 :: n|0x68
と書けば、add a,r1 から 0x69 が生成され、レジスタ番号が範囲を超えればエラーコード 5 (Register out of range) が返る。命令の構文、妥当性検査、コード生成が一行の対応関係として宣言される。この「一行 = 一命令パターン」という粒度が、仕様書からの転記可能性を保証している。
なお、実用上は binary_list に複雑な式計算、アライメント、値が 0 のとき出力を抑止する ; 前置修飾子、評価のみ行い出力しない ;; 前置修飾子が加わっているが、これらは最小モデルには不要な要素である。核はあくまで上記の一行である。
3. 自由構文パターン言語としての設計
3.1 文法を持たない文法
axx のパターン言語 (instruction 部) は DSL でありながら、固定された文法を持たない。文字列リテラル、シンボル、整数式、整数因子、浮動小数点式を組み合わせて、ユーザー自身が文法を作る自由構文言語である。この結果、伝統的な mnemonic operand 形式に縛られず、r1 = r2 + r3 のような代入風表記の ISA も、{v0.4s} のような ARM64 SIMD 表記も、同じ枠組みで記述できる。この性質により、axx はアセンブラであると同時に汎用バイナリ生成器としても機能する。
この点は、既存の大規模基盤との対比で際立つ。LLVM のアセンブラ生成機構 (TableGen/AsmMatcher) はニーモニックで始まる構文を前提としており、Hexagon のニーモニックを持たない r0 = r1 形式の転送構文を扱うために個別の対応を要した。axx にはそもそもその前提が組み込まれていない。
命名も設計思想に対応している。axx は「広く使える」という意味の general-purpose assembler ではなく、「すべてに共通する」という意味の general assembler である。
3.2 トークナイザレスの文字単位マッチング
axx は字句解析器を持たず、文字単位でパターンを照合する。これは意図的な設計判断である。ニーモニックに記号列を含むような構文(実在の ISA には . や $ や % を含むレジスタ名・ニーモニックが多数存在する)を扱うには、トークンの概念を事前に固定しない方が有利だからである。
規約は単純である。パターンファイル中の大文字・数字・記号は文字定数として扱われ(大文字はアセンブリ行の大文字・小文字の双方に一致する)、小文字で始まる名前(a も var1 も var_2 も同じ規則)は変数として扱われて、その位置にあるシンボルの値が代入される。! を前置すれば整数式、!! なら整数因子、!F / !D / !Q ならそれぞれ 32/64/128 ビット浮動小数点式の値が代入される。現行版ではこれに、テキスト置換モードで定義のないラベルを許して式を捕捉する !L、列挙オペランドリストの !E、集合の項目番号を捕捉する !Y、サブ表を参照する !S が加わっている(3.6 節、第 6 節)。どの書き方で捕捉した変数も、値と一緒にソース上の綴りを保持する。未代入の変数はすべて 0 に初期化される。字句と構文の境界の決定は、この最小限の規約のみを与えてパターン作成者に委ねられている。
3.3 文字集合そのものの可変性
トークナイザレス設計の帰結として、「識別子を構成する文字とは何か」もパターンファイル側から宣言できる。.symbolc はシンボルに使う文字集合を、.labelc はラベルに使う文字集合を拡張する。MIPS の $s5 や $v0 のようなレジスタ表記が特別扱いなしにシンボルとして書けるのはこの機構による。字句規則すら処理系に固定されていない点は、自由構文言語という設計の一貫した延長である。
3.4 特異度スコアによる順序非依存マッチング
パターンファイルにおいて、ディレクティブ (.setsym 等) は順序依存だが、パターン自体は順序に依存しない。axx は最初に一致したパターンで止まらず、一致したすべてのパターンに特異度スコア組 (n_expr, -n_lit, n_sym) を付けて最小のものを選ぶ。すなわち、式キャプチャが最も少なく、次にリテラル一致が最も多く、次にシンボルキャプチャが最も少ないパターンが勝つ。
MOV A,!d :: 0xAA,d
MOV A,B :: 0xBB
この二行は入れ替えても結果が変わらない。パターン作成者は「より特殊なパターンを先に書く」という従来のテーブル駆動アセンブラの暗黙の負担から解放される。この性質が最も効くのは、x86_64 のように特殊形が一般形から遠く離れた位置に置かれる大規模なパターンファイルである。
現行版では、この性質をディレクティブにまで広げることもできる。パターンファイルに .unordered を一行書くと、すべてのディレクティブがファイル全体に効き、書く位置もディレクティブ同士の並びも意味を持たなくなる。同じ名前への中身の違う定義や、.clrcheck のように「ここから先は」という位置の意味しか持たないディレクティブはエラーとなる。.setsym 同士の参照は依存関係の順に解決される。.map は変数ごとの名前と値の表を持つので、Z80 の C のように、レジスタとしては 1、条件(キャリー)としては 3 という名前を、.setsym を書き直さずに一つのファイルで書き分けられる。.unordered を書かないファイルは従来どおり上から順に読まれる。
3.5 シンボル体系 — 数値・文字列・配列・集合
.setsym::名前::値 は、値欄の見た目によって種類の異なるシンボルを定義する単一のディレクティブである。
| 値欄 | 定義されるもの |
|---|---|
0x20, #OTHER+1
|
数値シンボル |
"LD" |
文字列シンボル(テキストテンプレート用) |
[1,"A",#B] |
配列シンボル |
r0,r1,r2 |
集合 |
a&b, a|b, a^b, a+b, a-b
|
他の集合から計算された集合 |
同名の再定義は後勝ちであり、これを利用して「同じ文字が文脈によって異なる値を持つ」状況(Z80 のレジスタ C と条件フラグ C など)をパターンファイル上の記述順序で自然に表現できる。
集合は積・和・対称差・差の演算で組み合わせられ、結果は独立したコピーとして保持される。レジスタクラスを「汎用レジスタからスタックポインタを除いたもの」のように集合演算で定義できるため、仕様書に現れるオペランド制約をそのままの形で書き写せる。重要なのは、集合としての解釈が数値としての解釈より先に試されつつ、集合になりえない欄は必ず従来どおり数値として読まれることで、既存のパターンファイルの意味は変わらない点である。
3.6 位置の制約と捕捉
シンボル体系の上に、オペランド位置を制約し、捕捉するための機構が重ねられている。
.check::x::r1,r2,r3 は、変数 x の位置に現れてよいシンボルを列挙して制限する。これはレジスタクラスの型検査に相当する機構であり、AL/BL と AX/BX のように同じ用途で幅の異なるレジスタ群を、別々のパターンとして曖昧さなく書き分けることを可能にする。
.check::s::AL,BL
.check::t::AX,BX
MOV s,!a :: 0xb0|s,a
MOV t,!a :: 0xb8|t,a,a>>8
.map::x::R0,R1,R2,R3::1<<x は、名前の列に値を与えることとその位置の .check とを一行にまとめる。.enum は 68000 の MOVEM のようなレジスタ リスト(範囲指定 - を含む)を捕捉し、その構成を一つの値に畳み込む。.sub は、その位置に来られる選択肢を名前付きの パターンの表 にまとめ、!S{{名前}}変数 で参照させる。.check がシンボルを制限するのに対し、サブ表はその位置にパターンを置ける。
現行版で加わったシンボル捕捉子 !Y集合[変数] は、その位置で集合の項目名を一つ読み、その 番号 を変数に束縛する。
.setsym::x::AX,BX,CX
ENC !Yx[z] :: 0x40|z
.check が位置を制限するだけであるのに対し、!Y は制限したうえで「何番目だったか」を渡す。名前から番号へ、番号から名前へと引き当てられるため、同じ並びを持つ二つの集合を用意すれば、一方の綴りを他方の綴りへ写すことができる。これは第 6 節のソース間変換で中心的な役割を果たす。
最後に .free は、パターン層が保持するすべての表から指定した名前を解放し、どのディレクティブで定義したかを覚えていなくても名前を再利用できるようにする。大規模なパターンファイルをモジュールとして組み合わせる際の衛生を保つ機構である。
構造化が難しい ISA の部分は、これらの列挙と捕捉の機構によって解決される、というのが axx の一貫した方針である。
4. 計算能力の隔離 — 宣言的な核と、明示的に呼び出す計算
axx のパターン記法そのものはチューリング不完全である。binary_list が持つ制御構文は、代入 (:=)、三項演算子、; 前置修飾子、アライメント、@@[](反復展開)の 5 つに限定される。
これは能力の欠如ではなく設計上の選択である。記法そのものをチューリング完全にすれば DSL は全体が「プログラム」になり、パターンマッチングの停止性の保証が失われる。プロセッサのアーキテクチャは複雑にしようと思えばいくらでも複雑にできるため、チューリング完全なら任意のアーキテクチャに追随できるが、その代償としてパターンファイルは宣言的データではなくコードになってしまう。axx は「宣言的データであること」「停止性が保証されること」を選んだ。
ただし現行版は、この選択を逃げ場のない制約にはしていない。binary_list の要素として .call 名前(引数, …) と書いた場合に限り、.func … .endfunc で定義されたミニ言語の関数へ制御が渡る。ミニ言語は代入、.if/.elif/.else/.endif、.while、.for(数の範囲と配列の要素)、再帰、可変長配列、文字列を備えたチューリング完全な手続き型言語であり、.emit した値がその位置のワードになる。範囲外の値などを検出したときは .raise でエラーコードを報告でき、出力に影響しないデバッグ用の表示として .echo がある。ラベル、位置カウンタ、.setsym のシンボルは、アセンブラ自身の式評価器を通して読める。
BR !t :: .call rel8(t)
.func rel8(target)
d = target - $.
.if d < -128 || d > 127 .then
.raise 2
.endif
.emit(d & 0xff)
.return
.endfunc
実例として、AArch64 の論理即値は、ビットマスクを N:immr:imms に分解する探索を要し、固定の式では表しにくい符号化である。同梱の aarch64_logical_mini.axx はこれをミニ言語で書き、命令群全体を 86 行のパターンに収めている。
重要なのは、この計算能力がパターン記法に混ぜ込まれていないことである。計算は、書き手が .call と明示的に書いたときにだけ、名前で呼ばれる別の言語として現れる。照合は従来どおり必ず停止し、宣言的に書ける命令は宣言的なまま一行で残る。無制限の計算に踏み込むかどうかは行単位の選択であり、パターンファイル全体の性質ではない。
現行版では、ミニ言語の呼び出し口が binary_list 以外にも一つ増えた。ELF を書き出す際、REL 形式の欄への書き戻し (.elfencode) と r_info の組み立て (.elfrinfo) を、パターンファイルに書いた関数に委ねられる(第 7 節)。ここでも計算は名前で宣言された関数としてだけ現れ、ELF 書き出しのコード自体は機種を知らないまま保たれる。計算能力の隔離という原則が、符号化の外側にまで一貫して適用されていることを示す例である。
対象外の ISA は依然として存在するが、その理由は計算能力ではなく還元命題そのものにある。次の三つは「命令と機械語が一対一に対応する」というモデルの外にあるため、計算能力をいくら足しても届かない。
| プロセッサ | 対象外である理由 |
|---|---|
| Mill CPU | ベルトアーキテクチャ: オペランド参照が実行履歴に依存する |
| ZISC | 命令が存在しない |
| Thinking Machines | 超並列。命令ごとの符号化という対象がない |
さらに現行の技術マニュアルは、実装上の上限も明文化している。最小アドレス単位が 64 ビットを超える機械、3 進の Setun のような 2 進以外の機械は扱わず、浮動小数点データの変換は IEEE 754 形式(半精度〜4 倍精度)に限られる。適用可能性の境界をモデルと実装の両面で仕様として明文化している点は、「万能」を標榜しがちなツール設計に対する誠実な対照をなす。
5. 三層構造 — マクロ層・パターン層・ミニ言語
現行版の axx は、性質の異なる三つの層から成る。
-
マクロ層。 本体のアセンブラにソースが渡される前に走るソース間変換段。
!def/!if/!whileを持ち、構文上は汎用計算の能力がある。 - パターン層。 照合と符号化を担う宣言的な核。記法はチューリング不完全で、照合は必ず停止する。
- ミニ言語。 符号化の途中(および ELF 書き出しの途中)で、名前で呼ばれる手続き型言語。チューリング完全。
計算能力を持つ二つの層がいずれも パターン層とは独立した別の層として 追加されており、パターン層自身の宣言性が保たれている点が重要である。マクロ層は読み込みの前に、ミニ言語は符号化の時点に位置し、どちらも明示的な記法(! で始まる文、.call あるいは関数名の宣言)を書いたときだけ起動する。いずれも axx.py と caxx.c の双方に同一仕様で実装されている。
マクロ層の実用上の効果は大きい。AVX-512 までを含む x86_64 のパターン集合は、平に書けば 23,923 行になるが、マクロで記述した x86_64m.axx は 5,787 行と約 4 分の 1 であり、読み込み時にバイト単位で同一のパターン集合へ展開される。AArch64 (2,068 行 → 9,387 行)、Power ISA (452 行 → 4,774 行)、RISC-V 全体 (2,186 行 → 4,137 行) も同様にマクロ層で記述されている。
5.1 構文
すべての文は行頭の ! で始まる。
| 構文 | 意味 |
|---|---|
!def name(p1, p2, p3 = default) { ... } |
マクロ / コンパイル時関数定義 |
!return expr |
戻り値。本体からの早期脱出も兼ねる |
!if expr !then { ... } !elif ... !else { ... } |
条件分岐 |
!while expr { ... } |
ループ |
!break / !continue
|
ループ制御 |
!set / !local / !undef
|
変数の代入・宣言・削除 |
!include "file" |
マクロ展開時のテキスト取り込み |
!error / !warning / !echo
|
展開中断・診断出力 |
式中への埋め込みは !{expr}、Python 形式の書式指定を伴う !{expr:04x} で行う。書式指定は Python のフォーマットミニ言語に準じ、両実装は同じ指定を受け入れ、同じ指定を拒否する。値は整数と文字列に限定され、演算子は C 準拠(/ と % はゼロ方向切り捨て)である。マクロ内では呼び出しごとに一意な __id__(ローカルラベル生成用)と __name__ が暗黙に定義される。
5.2 停止性の扱いの差
マクロ層は !while と再帰を持つため、構文上は汎用計算の能力を持つ。しかし実行資源に上限が課されており、再帰 200 段、!while 100 万回、生成行数 200 万行、!include のネスト 64 段を超えるとエラーとなって以降の展開が中断される。ミニ言語も同様で、.call 一回あたり実行文 400 万、呼び出しネスト 128 段、出力ワード 1,048,576、配列長 1,048,576 を超えると、原因となった行を指してエラーになる。資源上限で守られるこの二つの層では、停止性は「言語の表現力を削ること」ではなく「資源上限を設けること」によって確保されている。
ここに三層構造の意味がある。パターン層は宣言的データのまま、照合の停止性を言語仕様として保証する。計算能力を必要とする部分は、前段のソース→ソース変換と、名前で呼ばれるミニ言語という別の層に隔離され、そちらは資源上限で守られる。第 4 節で述べた「宣言的な核」という選択は、これらの層の追加によって撤回されたのではなく、影響範囲を分離したうえで維持されている。
5.3 アセンブラ側の値の参照と、収束の担保
現行版のマクロ層は、ソース側(.s)に限りアセンブラ本体の値を参照できる。ラベル値と .equ 定義は裸の識別子で、マクロの識別子として書けない名前は label("...") で、位置カウンタは $ / $$ で読める。パターンファイル側のマクロはソースのアセンブル前に走るためこの対象外であり、宣言的なパターン層そのものはこの機能の影響を受けない。
ここで参照できるのは「今の値」ではない。マクロ展開はアドレスが確定するより前に走るので、その時点の値は原理的に存在しない。実際に読めるのは 前回の relaxation 反復 で得られた値であり、初回反復ではラベルは 0、defined() は偽、$ / $$ は 0 になる。
この設計が手放したものは、展開の冪等性である。展開結果がラベル値に依存する以上、展開はもはやソーステキストだけの関数ではなく、反復ごとに変わりうる。マクロ層は「アセンブル前に一度だけ走る前段」ではなく、relaxation の不動点反復に参加する構成要素になった。
収束の担保は、構造による保証から実行時の検出へ移る。relaxation ループは反復回数に上限を持ち、反復間でラベル配置が一致すれば収束と判断し、周期的な振動を検出し、収束しなければ出力ファイルを書かずにエラーで終了する。これは新設の安全網ではなく、可変長命令の前方参照という axx が最初から抱えていた問題に対して既に存在していた機構である。変更が増やしたのは危険の種類ではなく、その入口である。いずれの場合も、誤ったバイナリが黙って出力されることはない。
停止性・収束性の担保はいま三段になっている。パターン層の照合は言語仕様として、個々のマクロ展開とミニ言語の実行は資源上限として、そして展開の系列は relaxation の不動点として担保される。最初の二つが静的な保証であるのに対し、三つ目だけが実行時の検出である点は、設計上の非対称として明記しておくべきものだろう。この非対称は、同梱の axxsemantics が relaxation をラベル環境上の不動点として読む形式化を与えていることで、意味論的にも位置づけられている。
5.4 二実装間の意味論
axx.py と caxx.c はマクロ層について同一仕様であり、差異は数値表現のみである。Python 版は多倍長整数、C 版は int64 を用いるため、マクロ時計算が 64 ビットを超える場合にのみ結果が異なる。マクロ層の出力はソーステキストであるから、この差は本体の 256 ビット式評価には波及しない。差異の所在と波及範囲が明示されている点は、二実装体制を維持するうえで実務的に重要である。
5.5 現時点の制約
ソース側の .include はマクロ層を通過しないため、マクロ定義ファイルの取り込みには !include を用いる(パターン側の .include は層を通る)。対話モード(ソースファイル無指定時)はマクロ層を経由しない。!{a ? b : c:04x} のように三項演算子と書式指定を組み合わせた記法は現状では解析できない。マクロを含まないソースに対する出力は従来と同一であり、後方互換性は保たれている。--no-macro で層全体を無効化でき、-P / -p でソース側・パターン側それぞれの展開結果のみを出力できる。ただし -P はアセンブルを行わないため、アセンブラ側の値はすべて未確定として展開され、実際のアセンブル時の展開結果とは一致しない。
6. 二つの出力 — バイナリとテキスト
6.1 テキストテンプレート
現行版の binary_list の要素は、式の代わりにダブルクォートで囲んだ文字列にできる。文字列の中では {{ }} で囲んだ部分だけが評価され、{{e}}(10 進)、{{.hex(e)}}、{{.bin(e)}}、{{.float(e)}}(128 ビット浮動小数点、有効数字 34 桁)、文字列・配列シンボルの参照、そして捕捉した変数をソースに書かれたままの綴りで出す {{.exp(x)}} が使える。{{.exp(x)}} はどの捕捉子で捕捉した変数にも使え、同じ綴りはミニ言語の .call の引数 .exp(x) で文字列として関数へ渡すこともできる。
MOV R!r,!e:: "LD R{{r}},0x{{.hex(e)}}"
この一行は MOV R1,0x10 を LD R1,0x10 へ書き換える。同じテキストは 1 バイト 1 ワードとしてバイナリにも出力され、位置カウンタもその分進むため、テキストを出す行の後のラベルも正しく決まる。-V を付ければ組み立てたテキストが素のまま標準出力へ流れる。パターンファイルは、バイナリ生成器であると同時に ソース間変換器 になった。
6.2 テキスト置換モード
変換器としての使用を一つのディレクティブで整えるのが .textmode である。これは、どのパターンにも一致しない行をそのまま通す .passthru、ソースの一行を出力の一行にする .eol をまとめて有効にし、さらに !L が捕捉した式中の未定義ラベルをエラーにせず、ソースの ; コメントと行頭の字下げを書き換え後のテキストに引き継ぐ。書き換えたい行だけをパターンに書けば、残りは原文のまま流れる。
同梱の 8080toz80.axx は Intel 8080 のソースを Zilog Z80 の記法へ、intel2att.axx は x86-64 の Intel 記法を AT&T 記法へ翻訳する。後者は集合と !Y による綴りの写像と、マクロ層によるパターン生成を組み合わせ、60 行の記述が展開後 3,437 行のパターンになる。翻訳結果は GNU as に通る。a64tox64_axx.axx は AArch64 のアセンブリを axx の x86_64.axx の記法へ翻訳し、レジスタの対応、呼び出し規約のつなぎ、システムコールの引き直しをランタイムと組み合わせて行う。AArch64 で書いた Brainfuck インタプリタを翻訳し、axx だけで x86_64 の実行ファイルにでき、mandelbrot.bf を正しく走らせる。
6.3 意義
この拡張が示すのは、還元命題の右辺 binary_list が「機械語」に限られないということである。命令の構文を照合し、捕捉した値から別の表現を組み立てるという構造は、出力先がバイト列でもテキストでも変わらない。axx はこの拡張を、パターン層の宣言性もチューリング不完全性も変えずに実現している。第 12 節で述べるように、これは作者が axx2 構想として描いた「binary_list に文字列リテラルと文字列操作を加え、アセンブリ言語間の変換を可能にする」方向の、現行の核のままでの部分的な実現でもある。
7. ELF の一般化
7.1 原則: ELF を書くコードは機種を知らない
前版の時点で、-o による ELF64 リロケータブルオブジェクト出力は x86-64 を中心に動作していた。現行版では、これが任意の CPU に一般化されている。設計の要点は、ELF の組み立てに要る知識を二つに分け、それぞれを適切な場所に置いたことにある。
-
機種に依存するもの — リロケーション型、ELF クラス、RELA/REL、ELF ヘッダの欄、セクションヘッダの属性、命令語の中の欄の形、リロケーションの対や添え物、
r_infoの並び、加数の単位、セクショングループ、CFI の CIE の欄。これは パターンファイル に宣言する。 -
プログラムに依存するもの — シンボルの型・大きさ・束縛・可視性・共通シンボル、関数ごとの CFI (
.cfi_*)。これは ソースファイル に宣言する。
ELF を書き出すコードには、機種番号で分岐する処理が一切ない。axx は i386、M68K、PowerPC、PowerPC64、s390x、ARM、SuperH、SPARCV9、x86-64、AArch64、RISC-V の 11 機種ぶんの組み込み表を持つが、これらは宣言をあらかじめ書いておいたものにすぎず、.elfbuiltin::0 で表を使わずに宣言だけから組み立てることもできる。--elfdesc は、組み込み表と宣言を合わせた実効的な記述を、パターンファイルの宣言の形で書き出す。表と宣言が同じ表現力を持つことが、処理系自身によって確かめられる仕組みになっている。
7.2 宣言の語彙
宣言は、ELF の個々の要素に一対一で対応する小さな語彙として設計されている。
| 宣言 | 記述するもの |
|---|---|
.elftype |
リロケーション型の名前・番号・欄の幅・PC 相対か否か |
.elfclass / .elfrela / .elfheader
|
ELF32/64、RELA/REL、ELF ヘッダの欄 |
.elfsection / .elflink / .elfgroup
|
セクションの型・フラグ・整列・要素長、sh_link/sh_info、COMDAT グループ |
.elffield |
命令語の中のビット欄(マスク、オフセット、シフト、補正) |
.elfencode |
REL 形式で欄へ書き戻す関数(ミニ言語) |
.elfextra / .elfdiff
|
対になる・添えるリロケーション、ラベルの和と差 |
.elfrinfo |
r_info の組み立て(ミニ言語) |
.elfunit |
加数とシンボル値の単位(バイト/ワード) |
.elfcfi / .elfcfiinit / .elfcfireg
|
.eh_frame の CIE と初期命令、レジスタ名 |
これらによって、x86-64 や AArch64 のような RELA の機種だけでなく、REL で命令語の欄に加数を埋め戻す ARM や MIPS32、r_info に三つの型を詰める MIPS64 n64、リンカ緩和のために R_RISCV_RELAX を添え、ラベル差を ADD/SUB の対で表す RISC-V、1 ワード 16 ビットでアドレスがワード単位の架空機まで、同じ書き出しコードで扱える。組み込み表に無い MSP430 や MN10300 も、パターンファイルの宣言だけで ELF を出力する。リロケーション型の優先順位は「組み込み表 < パターンファイル < ソースファイル」と定められ、ソース側の .reloc / .extern / .global から型名を指定することもできる。
ELF32 と ELF64 の選択は -f で -m と独立に行え、-g を付ければ DWARF (.debug_info / .debug_abbrev / .debug_line) が付く。ソースに GNU as と同じ .cfi_* 指令を書けば .eh_frame が組み立てられる。セクション数に上限はなく、SHN_LORESERVE を超える場合は .symtab_shndx を用いる ELF の規約に従う。
7.3 検証
一般化の正しさは、外部のツールチェーンとの照合によって裏づけられている。同梱のテストフィクスチャについて、ARM (REL の命令欄)、RISC-V (対になるリロケーション、COMDAT、SHF_LINK_ORDER、リンカ緩和後のラベル差と CFI)、MIPS32 (R_MIPS_HI16 の書き戻し、リンク後の .text)、MIPS64 (r_info)、x86-64 (.eh_frame) の各出力が、llvm-mc / ld.lld のものと一致することが確認されている。実用パターンファイルでは、PowerPC64 の ELF が 64 ビット PowerPC ABI のリロケーション (REL24、REL14、@l / @ha / @high / @highest 系、D34 / PCREL34) を伴って GNU ld でリンクでき、RISC-V の ELF は ld -m elf64lriscv でリンクできる。
7.4 失敗の作法と範囲
ELF の一般化においても、axx は 黙って壊れたものを出さない という方針を貫いている。型の決まらない参照や宣言の無い幅のラベル差にはリロケーションを出さず (-d で報告)、宣言の型名が引けなければ一度だけ警告し、書き戻し関数の不在や引数の不一致はエラーにする。ELF32 の既定の r_info に収まらない型番号は警告される。これは 5.3 節の relaxation が収束しなければ出力を書かないことと同じ態度であり、axx 全体を通じた設計の一貫性を示している。
出力は再配置可能オブジェクト (ET_REL) に限られる。実行ファイル (ET_EXEC) や共有ライブラリ (ET_DYN) の生成は、リロケーションを型ごとの計算式で解決し、プログラムヘッダや動的リンクの表を組むリンカの仕事として、意図的に範囲の外に置かれている。番地の確定したイメージが必要な場合は -b の生バイナリで得られる。アセンブラの責務をアセンブラの範囲に正しく限定した一般化であると言える。
8. 拡張性の実証
最小の核が正しく切り出されていることの証左は、後付けの拡張が核の変更なしに載ることである。第 6 節・第 7 節に加え、axx では以下がそれを実証している。
VLIW/EPIC 対応。 .vliw::128::41::5::00 のようにバンドルビット数・命令ビット数・テンプレートビット数・NOP コードを宣言し、!!(命令連結)、!!!(連結命令数)、!!!!(ストップビット)という少数の記号追加のみで、Itanium 型 EPIC を含む VLIW プロセッサを扱う。EPIC パターンは第 4 のフィールドとしてインデックスコードを取り、テンプレートはインデックスの組み合わせから決まる。テンプレートビットは正数なら右端、負数なら左端に配置される。第 2 節で「EPIC/VLIW を除く」とされた還元命題の除外部分が、拡張によって回収されている。
非 8 ビットワード幅。 .bits::12 のような宣言で、ビットスライスプロセッサや機械語ワードがバイト単位でないプロセッサ(4 ビット、11 ビット、12 ビット等、1〜64 ビット)をエンディアン指定込みで扱える。.bits 指定時はアドレスがワード単位になり、ELF 出力でも .elfunit::word によって加数とシンボル値の単位をワードにできる。
浮動小数点即値。 !F (32bit)、!D (64bit)、!Q (128bit) により、浮動小数点式を整数ビットパターンとして評価する。ARM64 の vmov.f32 s0,#3.14 のような命令が一行のパターンで記述できる。
省略可能部。 instruction 中の [[ ]] で囲まれた部分は省略可能となり、省略時は変数の初期値 0 が用いられる。Z80 の inc (ix) と inc (ix+d) を一行で書けるのはこの機構による。
診断の記述。 .error::n::"本文" によりエラーコードに本文を与えられ、本文行に書く .echo でパターンファイルのデバッグ出力が得られる。診断もまたパターンファイルの側で宣言される。
独自演算子。 値の最高位ビット位置を返す前置演算子 @(ヘビマルマッタ演算子)、任意ビット位置からの符号拡張を行う二項演算子 '(SEX 演算子)、シンボル値参照 #、下位から n バイト目を取る *(x,y) など、バイナリ生成に特化した演算子群が式言語に統合されている。式とミニ言語の整数は 256 ビットで回り込む。これらはアドレッシングモードの符号化(例: x86_64 の LEAQ におけるスケール値のビット位置計算 ((@h)-1)<<6|t<<3|s)を一行の式に収めることを可能にしている。
9. 実装・検証・実用性
9.1 二つの実装
axx は Python 実装 (axx.py、通称 Paxx、リファレンス実装) と C 実装 (caxx.c、通称 Caxx) を持ち、FreeBSD および Linux 上で動作する。本稿執筆時点でそれぞれ約 1 万 5 千行、約 2 万 1 千行である。新機能は Paxx に先に入り、Caxx ははるかに高速である(約 24,000 パターンの x86_64 に対しても hello world のアセンブルは 1 秒未満で終わる)。二つの実装は、同じ入力に対してバイト単位で同一の出力を生成することを目標として維持されている。
9.2 二実装の突き合わせ
同梱の test1 は、52 組のパターン/ソースの対を両実装でアセンブルし、-b の生バイナリを cmp で比較する。.textmode を使う組では -V のテキストを、.echo の組では標準エラーの出力を、ELF の宣言を検証する組では -o のオブジェクトを、aarch64.axx などでは --elfdesc の出力をそれぞれ比較する。さらに中核の 16 組を -o(ELF64)、-m 3 -f 32 -o(ELF32)、-g -o(DWARF 付き)、-v(リスティング)、-V(テキスト出力)でも走らせ、比較は全部で 186 組に及ぶ。二つの独立な実装が、生バイナリからオブジェクトファイル、デバッグ情報、翻訳テキストまで一致し続けていることは、仕様が実装から独立に定まっていることの強い証拠である。
9.3 実用パターンファイル
実用向けとして同梱されるパターンファイルは、レガシー CPU から現行の主要 ISA まで広がった。
| ISA | 範囲 |
|---|---|
| x86_64 | x86_64-v3: セグメントアドレッシング、AVX/AVX2、BMI1/BMI2、x87、EVEX/AVX-512 |
| AArch64 | A64 全般、Advanced SIMD、暗号、スカラ拡張、SVE・SVE2、SME・SME2。GNU 式のリロケーション修飾子に対応 |
| PowerPC64 | Power ISA v3.1 (POWER10)、ビッグ/リトルエンディアン。VMX、VSX、四倍精度、MMA、前置命令(64 バイト境界を跨ぐ前に nop を挿入) |
| RISC-V | RV64 全体: I/M/A/F/D/Q/Zfh/C とその派生、ビット操作、スカラ暗号、CSR、特権命令・H 拡張、V 1.0 とそのベクトル暗号、GNU 疑似命令 |
| MIPS | MIPS I から MIPS64 Release 6 まで、ビッグ/リトルエンディアン、o32 と n64。整数・特権・FPU(ペアドシングル、COP1X)・COP2、拡張(DSP・MSA・MT・VZ・EVA・MIPS-3D・SmartMIPS・MCU・XPA・CRC・GINV)、Release 6 の新命令、GNU 疑似命令、%hi / %lo / %got / %pcrel などのリロケーション修飾子 |
| レガシー | Motorola 68000 / 6809 / 6800、MOS 6502、Zilog Z80、Intel 8080 / 8051 / 8048 / 4004 |
PowerPC64 と MIPS の基本命令は GNU as と、MIPS の拡張と Release 6、RISC-V は llvm-mc とバイト単位で照合されている。Brainfuck 仮想 CPU や、axx でアセンブルした Brainfuck インタプリタ(AArch64、PowerPC64、RISC-V 版。x86_64 版は別リポジトリ)といったデモも用意されている。
9.4 ツールチェーンとの接続
.global / .extern によるシンボルの外部連携、ソース側の ELF シンボル属性(.type・.size・.weak・.hidden・.protected・.comm など)、TSV 形式でのラベル・セクション情報のエクスポート/インポート (-e / -E / -i)、DWARF による gdb/lldb でのソースレベルデバッグが揃っている。生成したオブジェクトは GNU ld および LLVM ld.lld でリンクでき、C ライブラリ関数の呼び出しや共有オブジェクトの作成にも到達している。
実行プラットフォームは特定システムに依存しない。DOS 形式の行末 chr(13) は無視され、Python が動作する環境であれば Paxx は動く。
「一般アセンブラ」という抽象的な構想が、主要 ISA を含む実際のツールチェーンに接続する具体的な出口を持ったことは、構想の実在性を示すうえで重要である。
10. 系譜における位置づけ
テーブル駆動あるいはメタアセンブラという発想自体は計算機史に前例がある。1960 年代のメタアセンブラ、現代では LLVM の TableGen による命令記述、GNU binutils の一部を生成する CGEN、Rust 製の customasm が近い系譜にあたる。axx の独自性はこの系譜の中で次の点に求められる。
第一に、記述の粒度と自由度である。TableGen が C++ バックエンドと密結合し、実用的なターゲットでは数千行の手書き C++ を伴うのに対し、axx のパターンファイルは処理系から完全に分離した自由構文のテキストであり、仕様書の命令表とほぼ同型の見た目で書ける。CGEN が命令の 意味 を RTL 風に記述するのに対し、axx は表層の構文と符号化の対応を記述する。トークナイザレスの自由構文 DSL は前例がない。
第二に、チューリング不完全性の明示的な採用である。既存のメタアセンブラの多くはマクロ機構の拡張として汎用計算能力へ向かったが、axx は逆方向、すなわち最小性と停止性保証へ向かい、計算能力を必要とする部分は別層として切り出した。
第三に、順序非依存のパターン照合である。テーブル駆動方式で暗黙に要求される「定義順の工夫」を特異度スコアにより不要にした。.unordered を宣言すれば、ディレクティブも順序に依存しなくなる。
第四に、オブジェクト出力の一般化である。customasm は同じく ISA を宣言的に記述する思想を共有するが、出力はバイナリやダンプ形式にとどまり、再配置可能オブジェクトを持たない。LLVM MC は ELF・COFF・Mach-O を扱う本番用の基盤であるが、機種ごとのオブジェクト出力はコードとして実装される。axx は、リンク可能な ELF を、機種ごとのコードではなく宣言から生成する。宣言的なパターン記述と、宣言的な ELF 記述が同じファイルに同居している点は、この系譜の中で axx に固有のものである。
作者自身が述べるとおり、axx の目的は道具としての普及そのものではなく、全ての命令型アセンブリ言語が単一のパターン形式に押し込められるという学術的洞察の提示にある。axx は通常のアセンブラより一段低いレイヤ、すなわちアセンブラを定義するメタ層の道具として位置づけられる。同梱の axxsemantics は、この構造を Assembly Source → Pattern Matching → Environment → Binary → Object という意味付け関数として表示的意味論の立場から形式化しており、メタ層としての axx を理論的に記述する試みとなっている。
11. AI 時代における意義
巨大 ISA のパターンファイル作成は人間には労力が大きいが、仕様書からパターンファイルへの変換は形式が固定された転記作業であり、AI に適した作業である。一度作成されたパターンファイルはその ISA について完成品となり、使い回しが利く。現行版で AArch64 の SVE/SME2、Power ISA v3.1、RISC-V 全体といった巨大な命令集合が短期間にパターンファイル化され、外部のアセンブラとバイト単位で照合されたことは、この見通しが現実のものであることを示している。
アセンブラが元来「機械語を人間に理解しやすくする」ために生まれたものだとすれば、AI がコードを書く現代において、人間と計算機の双方に向けた一般化アセンブラという中間表現層が存在する意義は増している。パターンファイルとソースファイルの分離により、共通のソースから異なるプロセッサの機械語を生成することも原理的に可能であり、第 6 節のソース間変換と組み合わせれば、アセンブリ言語どうしを結ぶ簡易なリターゲッタブル基盤としての応用可能性も開かれる。
12. 今後 — axx2 構想と現行版の到達点
作者は次世代版 axx2 の構想を公開している。パターンファイルの記述言語を、パターンデータ形式からより記述的な複数行のメタ言語へ移行させ、binary_list を object_list に、パターンファイルを processor_specification_file に改称するというものである。文字列リテラル・文字列演算・制御構文を導入すれば、中間言語の生成やアセンブリ言語間の変換器の生成が視野に入る。この場合パターンファイル側はチューリング完全となり、Lisp マシンも原理的には扱えるようになるが、自己参照検査が必要になり、また処理系内部での無限ループはデバッグを困難にする。評価をパターンファイル内に限定すればデバッグ性を保ちつつループと分岐を導入できる、というのが作者の見立てである。
現行版は、この構想のうち二つの部分を、記述言語全体を作り替えることなく先に実現している。
一つは「ループと分岐をパターンファイル内に導入し、評価をその中に閉じ込める」という部分であり、これはミニ言語(第 4 節)として実現された。既存のパターン記法を宣言的なまま残したうえで、計算が要るところだけを名前で呼び出す形をとり、無限ループのデバッグ性という懸念には、資源上限を超えた時点で原因の行を名指しして止めることで応えている。
もう一つは「binary_list に文字列リテラルと文字列操作を加え、アセンブリ言語間の変換を可能にする」という部分であり、これはテキストテンプレートと .textmode(第 6 節)として実現された。binary_list は名称こそ変わらないが、出力先がバイト列でもテキストでもよいという意味で、すでに object_list の性格を帯びている。
残る課題について、作者は、パターンデータ形式のほうが直感的であること、記述的メタ言語化は大幅な書き換えを要することを挙げている。また、構造化・関数型アセンブリを命令型アセンブリへ変換する高性能マクロと最適化機能、ARM (A32/T32)・SPARC・32 ビット PowerPC・RV32 のパターンファイルは、実機やエミュレータでの検証を含めて個人で完遂するには大きいとし、協力者を歓迎している。これらは設計上の制約ではなく作業量の制約であり、パターンファイルの形式は完全に文書化されている。
13. 優れている点
ここまでの各節で述べたことのうち、axx を特徴づける優れた点を八つにまとめる。前の五つは着想と設計の核に、後の三つはその核が持っていた射程を引き出した到達点にあたる。
(1) 還元の一行。 すべての命令型アセンブリ言語を instruction :: error_patterns :: binary_list という一つの形に還元したこと(第 2 節)。これによりアセンブラは「ISA ごとに作るプログラム」から「ISA を記述するデータ」へと定義し直された。RET :: 0xc3 の一行で一命令分のアセンブラが完結するという単純さが、還元の深さをそのまま示している。
(2) トークナイザを持たない照合。 字句の区切りをあらかじめ固定せず、文字単位で照合するという判断(3.2 節)。この一つの判断によって、ニーモニックを持たない r1 = r2 + r3 のような構文も、$v0 のように記号を含む名前も、特別扱いなしに書ける。LLVM が個別の対応を要したものを、axx は最初から問題にしていない。
(3) 順序に依存しない照合。 一致したパターンのうち最も特殊なものを特異度スコアで選ぶこと(3.4 節)。「特殊なパターンを先に書く」という、テーブル駆動のアセンブラが暗黙に課してきた約束を仕組みによって不要にした。数万行に及ぶパターンファイルを書き続けられるのは、この性質による。
(4) 計算能力の隔離。 パターン層をチューリング不完全に保って照合の停止性を言語の性質として守り、計算が要るところだけを .call によって名前で呼ぶ別の層に渡すこと(第 4 節、第 5 節)。表現力と安全性を、層を分けることで両立させている。
(5) 38 年もった骨格。 1986 年の着想が、その後に現れた VLIW、EPIC、非 8 ビットのワード幅、スケーラブルベクトル拡張、テキスト翻訳、ELF の機種非依存化を、核を変えずに受け止めたこと(第 8 節)。後から加えたものが核を壊さずに載るのは、最初に抽出した本質が正しかったことの証拠である。
(6) 自由構文 DSL。 パターン言語が固定した文法を持たず、文字列・シンボル・整数式・整数因子・浮動小数点式の五要素を並べるだけで、書き手が文法そのものを作れること(3.1 節)。「ニーモニックとオペランド」というアセンブラの自明の前提を持たないことで、axx はアセンブラの枠を超えて汎用のバイナリ生成器になっている。トークナイザを持たない自由構文の DSL には前例がない。
(7) ELF の一般化。 「本体は機種を知らない」という原則を、命令の符号化だけでなくオブジェクトファイルの生成にまで貫いたこと(第 7 節)。リロケーション型、REL と RELA、命令語の中のビット欄、r_info の並び、CFI に至るまでをパターンファイルの宣言だけで書けるようにし、組み込みの 11 機種の表でさえ宣言をあらかじめ書いておいたものにすぎない。機種ごとのコードではなく宣言からリンク可能な ELF を生み出す点は、同種のツールに見当たらない到達点である。
(8) .textmode によるソース間変換。 binary_list の出力先をバイト列からテキストへ広げ、アセンブラをアセンブリ言語どうしの翻訳器にしたこと(第 6 節)。これは新しい仕組みを足したのではなく、「照合し、捕捉した値から出力を組み立てる」という元の構造が出力先を問わなかったことを示したものである。核の宣言性もチューリング不完全性も変えずに、axx2 構想の一部を先に実現している。
八つに共通するのは、新しい機能を外から足すのではなく、最初の着想がもともと持っていた射程を引き出しているという点である。そのため、どの到達点も核を壊すことなく載っている。ひとことで言えば、axx の最も優れた点は、アセンブラとは何かを小さく正確に定義したことにある。
14. 結論
axx の発明は、単一の還元命題(instruction :: error_patterns :: binary_list)、それを記述するための自由構文パターン言語、そして計算能力を宣言的な核から切り離すという設計判断の三点に集約される。現行版で加わったマクロ層とミニ言語は、この三点目を撤回するのではなく、それを具体化したものである。計算は前段のソース変換と、名前で呼んだときにだけ現れる別言語に隔離され、パターン層は宣言的データのまま、照合の停止性を言語仕様として保ち続けている。
前版以降の二つの到達点は、この設計の射程を大きく広げた。ELF の一般化は、「本体は機種を知らない」という原則を命令の符号化からオブジェクトファイルの生成にまで押し広げ、リンク可能な ELF を機種ごとのコードではなく宣言から生み出すことを可能にした。テキスト出力は、還元命題の右辺が機械語に限られないことを示し、パターンファイルをアセンブリ言語間の翻訳器にした。どちらも核の宣言性とチューリング不完全性を変えることなく載っており、どちらも「黙って誤った出力をしない」という態度を共有している。
1986 年の着想が 2024 年の実装で検証され、VLIW/EPIC、任意ワード幅、ELF32/64 オブジェクト出力とその機種非依存化、DWARF と CFI、マクロ層、ミニ言語、ソース間変換という拡張が核の設計を変えずに積み上がったこと、そして x86_64・AArch64・PowerPC64・RISC-V・MIPS という現行の主要 ISA がその上で外部のツールチェーンと一致する出力を得ていることは、抽出された「本質」が正しかったことの実証である。ホームメイドプロセッサからスーパーコンピュータまでを一つの照合エンジンで扱うという構想は、アセンブラを ISA ごとの実装物から、ISA を記述するためのメタ言語処理系へと再定義した。
参考
- GitHub リポジトリ: https://github.com/fygar256/axx
- 技術マニュアル:
Technical_Manual_ja.md/Technical_Manual.md - ELF の一般化:
elf_generalization.md/elf_generalization_en.md - マクロ層リファレンス:
MACRO.md/macro_en.md - ミニ言語リファレンス:
MINI.md/mini_en.md - 表示的意味論による形式化:
axxsemantics - x86_64 用パターンファイル: https://github.com/fygar256/x86_64_pattern_file_for_axx
- リロケータブル ELF 生成: https://github.com/fygar256/axx_relocatable_elf_generation
- axx でアセンブルした brainfuck インタプリタの実演: https://github.com/fygar256/brainfuck_interpreter_for_axx_on_freebsd_of_x86_64
- 日本語原記事 (Qiita: fygar256): https://qiita.com/fygar256/items/1d06fb757ac422796e31