はじめに
メインフレーム案件に入って最初の週、先輩からソースコードの入ったフォルダを渡された。
「まずこれを読んでみてください」
開いたファイルの中身はこうだった。
STM 14,12,12(13)
LR 12,15
USING *,12
LA 13,SAVEAREA
L 2,=F'1000000'
L 3,BALANCE
AR 3,2
ST 3,BALANCE
LM 14,12,12(13)
BR 14
「……これはコードなのか?」
Javaしか書いたことがなかった自分には、何一つ読めなかった。命令の意味も、数字の意味も、なぜ12(13)という記法が出てくるのかも。
この記事では、Javaエンジニアが初めてHLASM(High Level Assembler)を読んだときに「ここで詰まる」という5つのポイントを、Javaとの対比で解説する。全部わかったとき、冒頭のコードが「口座残高に100万円を加算して保存する処理」だと読めるようになる。
その1. レジスタが15本しかない──「変数」という概念がない
Javaエンジニアが最初に感じる違和感
Javaでは変数を自由に宣言できる。
int balance = getBalance();
int deposit = 1000000;
int newBalance = balance + deposit;
saveBalance(newBalance);
balance、deposit、newBalanceという名前を持った入れ物が、好きなだけ作れる。
HLASMにはそれがない。
レジスタとは何か
HLASMで「演算」に使える入れ物は、**汎用レジスタ(General Purpose Register)**と呼ばれる16本(R0〜R15)だけだ。
R0 ── 特殊用途(アドレス計算・分岐条件など)
R1 ── パラメータ渡し
R2 ── 汎用(自由に使う)
R3 ── 汎用(自由に使う)
︙
R12 ── ベースレジスタ(慣習)
R13 ── セーブエリアポインタ(慣習)
R14 ── リターンアドレス
R15 ── エントリポイントアドレス・戻り値
Javaの変数は「名前付きの入れ物」だが、レジスタは「番号付きの入れ物」だ。しかも16本しかないため、複雑な処理をするときはレジスタの使い回し(スピル)が必要になる。
スタックフレームがない
Javaではメソッドを呼び出すと自動的にスタックフレームが積まれ、ローカル変数が管理される。HLASMにはそれがない。
代わりに、セーブエリアと呼ばれる固定サイズのメモリ領域を自分で確保し、呼び出し元のレジスタ状態を手動で退避・復元する。
STM 14,12,12(13) * 呼び出し元のR14〜R12を退避(R13が指すアドレス+12バイト目から)
LR 12,15 * R15(エントリポイント)をベースレジスタR12にコピー
USING *,12 * アセンブラに「R12がベースレジスタ」と宣言
LA 13,SAVEAREA * R13を自分のセーブエリアに切り替え
冒頭4行の意味がこれだ。Javaのメソッド呼び出しで「自動でやってくれていること」を、手動で書いている。
なぜベースレジスタを宣言しなければならないのか
ここで「なぜこんな面倒なことを?」と思うのは自然だ。理由はハードウェアの制約にある。
メインフレームの機械語命令(RX形式など)は、メモリアドレスを「ベースレジスタ(4ビット)+インデックスレジスタ(4ビット)+ディスプレースメント(12ビット)」で表現する。12ビットで表現できる範囲は最大4,096(4KB)だ。
つまり1命令で「ここから5,000バイト先」が書けない。4KBを超えるプログラムやデータを扱うには、ベースレジスタを基地(アンカー)として複数配置し、「R12から800バイト先」「R11から200バイト先」という形で位置を表現しなければならない。
これが移行時の静的解析ツールを悩ませる原因の一つでもある。ベースレジスタが動的に書き換えられると、「この命令が実際にどのアドレスを指しているか」がソースコードだけからは確定できなくなる。
その2. 命令がすべて3〜5文字──読めるが意味がわからない
ニーモニックの壁
HLASMの命令(ニーモニック)は英語の略語で構成されている。知っていれば読めるが、初見では暗号に見える。
| ニーモニック | 正式名称 | Javaで言うと |
|---|---|---|
L |
Load |
reg = mem[addr](メモリからレジスタへ) |
ST |
Store |
mem[addr] = reg(レジスタからメモリへ) |
LR |
Load Register |
reg1 = reg2(レジスタ間コピー) |
LA |
Load Address |
reg = addr(アドレス値をロード) |
AR |
Add Register | reg1 += reg2 |
SR |
Subtract Register | reg1 -= reg2 |
MR |
Multiply Register |
reg1 *= reg2(64bit結果に注意) |
DR |
Divide Register |
reg1 /= reg2(商と余り) |
MVC |
Move Characters |
memcpy(dst, src, len)(文字列コピー) |
CLC |
Compare Logical Characters |
memcmp(a, b, len)(文字列比較) |
リテラルとリテラルプール
命令と合わせて覚えておきたいのがリテラルの記法だ。
L 2,=F'1000000' * R2 ← 定数1,000,000(Fは4バイト整数)
=F'1000000'は「4バイト整数の定数1,000,000」を意味する。=C'ACTIVE'なら文字定数、=V(SUBPROG)なら外部プログラムSUBPROGのアドレスだ。
ここで重要なのは、これらのリテラルが**コードの裏側に自動生成される「リテラルプール」**にまとめて配置される点だ。=F'1000000'はその場に定数が書かれているのではなく、「アセンブラがソースの末尾(またはLTORG命令の位置)に配置したアドレスをベース+ディスプレースメントで参照している」という動きになる。C言語のstatic constに近いが、物理的な配置がアセンブラに完全に委ねられている点が異なる。
これを踏まえると、冒頭コードの中盤が読めてくる。
L 2,=F'1000000' * R2 ← 定数1,000,000(リテラルプールから参照)
L 3,BALANCE * R3 ← BALANCEラベルのメモリ値
AR 3,2 * R3 ← R3 + R2(加算)
ST 3,BALANCE * BALANCE ← R3(結果を保存)
Javaに翻訳すると:
int r2 = 1000000;
int r3 = balance;
r3 = r3 + r2;
balance = r3;
読める。ただし変数名の代わりにレジスタ番号を頭で追い続ける必要がある。長い処理になると、「R3は今何の値を持っているのか」を常に脳内で管理しなければならない。
その3. 分岐がすべてGOTO──構造化プログラミングがない
if文もfor文もない
Javaにはif、for、while、switchがある。HLASMにはない。あるのは分岐命令だけだ。
CLC STATUS,=C'ACTIVE' * STATUSとリテラル'ACTIVE'を比較
BNE SKIP * 等しくなければSKIPラベルへジャンプ
A 3,BONUS * R3 += BONUS(等しい場合だけ実行)
SKIP EQU * * ここに合流
これはJavaで言うと:
if (status.equals("ACTIVE")) {
r3 += bonus;
}
代表的な分岐命令の一覧:
| 命令 | 意味 | Javaで言うと |
|---|---|---|
B |
Branch(無条件) | goto label |
BE |
Branch if Equal | if (a == b) |
BNE |
Branch if Not Equal | if (a != b) |
BH |
Branch if High | if (a > b) |
BL |
Branch if Low | if (a < b) |
BCT |
Branch on Count | if (--counter != 0) |
BXH |
Branch on Index High | ループインデックス制御 |
ループはこう書く
Javaのforループ:
for (int i = 10; i > 0; i--) {
// 処理
}
HLASMでは:
LA 4,10 * R4 ← 10(カウンタ初期化)
LOOP EQU * * ループ先頭ラベル
* (ループ本体の処理)
BCT 4,LOOP * R4をデクリメント、0でなければLOOPへ
BCT(Branch on Count)は「R4を1減らし、0でなければジャンプ」という命令だ。
見た目はGOTOの連続だが、制御フローグラフ(CFG)で可視化すると、構造化プログラムと同じループ構造が現れる。HLASMは「構造がない」のではなく、「構造を明示しない」言語だ。
その4. メモリを直接触る──アドレス計算がコードに露出している
12(13) という記法の謎
冒頭コードに戻る。
STM 14,12,12(13)
この12(13)が最初に理解できなかった部分だ。
HLASMのメモリアクセスは**ベース+ディスプレースメント(Base + Displacement)**という記法で書く。
D(X,B) または D(B)
D = Displacement(0〜4095のオフセット)
X = インデックスレジスタ(省略可)
B = ベースレジスタ
つまり12(13)は「R13が指すアドレスから12バイト後ろ」を意味する。
Javaで言えばポインタ演算に相当するが、Javaには存在しない概念だ。C言語の経験者なら*(ptr + 12)に近いと言えばイメージしやすいかもしれない。
Javaの参照型との根本差
| Java | HLASM | |
|---|---|---|
| メモリアクセス | JVMが抽象化(参照型) | ベース+ディスプレースメントで直接指定 |
| アドレス計算 | コードに現れない |
LA、A、STなどで明示的に計算 |
| メモリ保護 | JVMのガベージコレクタ | なし(自己責任) |
| 境界外アクセス | ArrayIndexOutOfBoundsException |
データ破壊またはS0C4(保護例外)で即死 |
HLASMでは、1バイトでもアドレスを間違えると、別の変数を上書きするか、プログラム例外(アベンド)で即終了する。Javaエンジニアが「安全」だと思っているメモリアクセスの大半は、JVMが裏側でやってくれていることだ。
DSECTによる構造体マッピング
メモリを直接触るHLASMだが、構造化する手段はある。DSECT(Dummy Section)だ。
CUSTAREA DSECT
CUSTID DS CL8 * 顧客ID(8バイト文字)
CUSTNAME DS CL20 * 顧客名(20バイト文字)
BALANCE DS F * 残高(4バイト整数)
STATUS DS CL1 * ステータス(1バイト)
これはJavaで言うと:
class Customer {
String custId; // 8バイト
String custName; // 20バイト
int balance; // 4バイト
char status; // 1バイト
}
DSECTは「メモリのこの位置にこの構造が来る」という宣言だ。実体はメモリの連続した領域であり、アクセスはベース+ディスプレースメントで行われる。Javaのフィールドアクセスが、アドレス計算として露出している。
移行現場での恐怖:S0C7アベンドの量産
メモリを直接触ることに関連して、移行プロジェクトで頻発するバグを一つ紹介しておく。
HLASMのパック10進数(COMP-3形式)は、末尾の4ビットが符号(C=正、D=負、F=符号なし)で終わる必要がある。この符号ビットが不正な値の場合、AP(加算)やSP(減算)などの10進演算命令を実行した瞬間に、CPUが**S0C7(データ例外)**を発生させてプログラムを強制終了させる。
問題はどこで起きるかだ。初期化されていないメモリ領域(スペースX'40'で埋まっている状態など)に対して何も考えずにAP命令を打ち込むと、この即死が起きる。
* 危険なパターン:初期化されていない領域に直接加算
AP AMOUNT,=P'100' * AMOUNTがスペース初期化されていたらS0C7で即死
Javaで書き直すとき、「このフィールドをそのままバッファにコピーすれば動く」という単純な思い込みが、S0C7の量産につながる。メモリの初期化状態とデータ型の整合性を、バイト単位で確認する習慣がないと、移行後のシステムが実データで初めて動いた瞬間に大量のアベンドが出る。
その5. EX命令──コードが実行時に書き換わる
最大の衝撃
5つの中で最も「Javaエンジニアの常識が通用しない」のが、EX命令(Execute)だ。
LA 5,LENGTH-1 * R5 ← (移動バイト数 - 1)
EX 5,MVCMODEL * R5の下位8ビットをMVCMODELに埋め込んで実行
B NEXT
MVCMODEL MVC TARGET(0),SOURCE * ← 長さフィールドが0(EXで動的に設定される)
NEXT EQU *
これが何をしているのかを説明する。
EX R5, MVCMODELは「MVCMODEL命令の第2バイト(長さフィールド)をR5の下位8ビットで上書きしてから実行する」という命令だ。
つまり実行時に命令の内容が変わる。
MVCMODELのMVC TARGET(0),SOURCEは、長さ0のMVCとして書かれているが、EX命令によって実際の長さが埋め込まれる。長さが可変の文字列コピーを、自己書き換えで実現している。
なぜ静的解析ツールが詰まるのか
ソースコードを静的に解析するとき、MVCMODELを単体で読むと「長さ0のMVC」に見える。しかし実行時には別の命令として動く。
これがモダナイゼーションの現場で「静的解析の限界」として立ちはだかる問題だ。
ソースコードに書かれていること:MVC TARGET(0),SOURCE
実行時に実際に動くこと: MVC TARGET(N),SOURCE ← Nは実行時にR5から決まる
AST(抽象構文木)を構築してもCFG(制御フローグラフ)を描いても、EX命令の「実際の動作」はソースコードから読み取れない。動かしてみて初めてわかる。
EX命令が使われる主なパターン
| パターン | 目的 |
|---|---|
EX R,MVC |
可変長文字列コピー |
EX R,CLC |
可変長文字列比較 |
EX R,TR |
可変長変換(文字コード変換など) |
EX R,TRT |
可変長スキャン |
いずれも「長さが実行時に決まる」操作を実現するための慣用句だ。EX命令が存在するHLASMコードを機械的に別言語へ変換しようとすると、長さの動的決定ロジックを追跡して「実際に何バイト操作するのか」を推定するか、実行時情報を取得しなければならない。
理解できなかったことが、理解できたとき
最初に見たコードを、もう一度見てほしい。
STM 14,12,12(13) * R14〜R12を呼び出し元のセーブエリアに退避
LR 12,15 * R15(エントリポイント)をベースレジスタにコピー
USING *,12 * R12がベースレジスタであることをアセンブラに宣言
LA 13,SAVEAREA * R13を自セーブエリアに切り替え
L 2,=F'1000000' * R2 ← 1,000,000(リテラルプールから参照)
L 3,BALANCE * R3 ← BALANCE(残高)
AR 3,2 * R3 ← R3 + R2
ST 3,BALANCE * BALANCE ← R3
LM 14,12,12(13) * 退避したレジスタを復元
BR 14 * 呼び出し元に戻る(R14がリターンアドレス)
1行目:セーブエリアへのレジスタ退避。
2〜4行目:プログラムの初期化(ベースレジスタ設定・セーブエリア切り替え)。
5〜8行目:本体処理(残高に100万円を加算して保存)。
9〜10行目:レジスタ復元とリターン。
Javaで書けば5行のメソッドが、HLASMでは10行になる。増えた5行は、JVMが自動でやってくれていたことを手書きしているだけだ。
HLASMは難解なのではなく、抽象化がない。 JVMがやってくれていたことが全部コードに見えている。それだけの話だ。
おわりに
HLASMを読めるようになると、メインフレームのコードに対する見方が変わる。
「古いコード」と見ていたものが、「50年間、1円の誤差も出さずに動き続けてきた工学の結晶」に見えてくる。
金融系・公共系のモダナイゼーション案件に関わるエンジニアにとって、HLASMを「読めないもの」として遠ざけることは、移行設計の根拠を持てないことを意味する。少なくとも今回の5つを知っていれば、ソースコードを前に完全に迷子になることはなくなるはずだ。
なお、本記事ではHLASMの読み方の入口を扱ったが、**「なぜHLASMの静的解析はコンパイラ理論的に困難なのか」「EX命令・自己書き換えコード・ベースレジスタの動的書き換えをどうIRに抽象化するか」「HLASM→Kotlin/Javaの変換パイプラインの実装詳細」**については、Zennにて連載記事を公開している。
また、**「富士通・日立・IBM各社の撤退タイムラインと現場への影響」「2030/2035年問題のキャパシティ・クランチ」**については、noteの連載で詳しく論じている。
第0章 日本の汎用機市場の現在地と2035年問題(note)
アセンブラ体系編(Zenn)