はじめに:PL/Iにおける「予約語なし」という孤高の思想と最適化の深淵
こんにちは。基幹システムの現場を長年守り続けてきたシステムアーキテクトの端くれとして、今日のメインフレーム談義を始めよう。
JavaやC#、あるいは近年のモダンな言語に慣れ親しんだエンジニアが最初にPL/Iのコードベースを見たとき、決まって驚くポイントがある。それは「PL/Iには厳密な意味での予約語(Reserved Words)が存在しない」という事実だ。
例えば、`IF`や`THEN`、さらには変数名として使いそうな`READ`や`WRITE`すら、コンテキスト(文脈)によって変数名にもなればキーワードにもなる。`IF IF = THEN THEN THEN = ELSE;` という、一見すると悪夢のような構文すら、PL/Iのコンパイラは文法解析のコンテキストから完璧に解釈しきる。
この柔軟で懐の深い言語仕様の裏側で、IBMの偉大なコンパイラエンジニアたちは、機械語(ナノ秒単位の実行効率)への翻訳において想像を絶する最適化のパズルを解き続けてきた。
今回はその中でも、データ制御の要でありながら、誤った使い方をすれば瞬時にS0C4アベンドやサイレントなデータ破壊を引き起こす`SUBSTR`関数の内部実装とアセンブラ展開のメカニズムに焦点を当てる。
テックリードとしてJavaやC#へのマイグレーション(レガシー移行)を指揮する立場にあるなら、この「文字操作の基本」が鉄火場のメインフレーム上でどう動いているかを知らないと、移行後の性能劣化やデータ不整合という地雷を踏むことになる。
—
1. `SUBSTR`関数の内部実装:コンパイラはアセンブラのどの命令を吐くのか?
PL/Iの `SUBSTR(string, position, length)` は、一見すると高レベルな文字列操作関数に見える。しかし、Enterprise PL/Iコンパイラ(あるいはその祖先である古いオプタイザー)は、これを極限まで削ぎ落とされたSystem zの機械語命令へと昇華させる。
コンパイル時にオフセットや長さがリテラル(定数)として確定している場合、コンパイラは余計なレジスタ演算を一切行わない。
静的ケース:リテラルによるオフセット指定
DCL WK_AREA CHAR(100) INIT(‘ ‘);
DCL WK_SUB CHAR(10) INIT(‘ ‘);
/ コンパイル時に位置と長さが静的に確定しているケース /
WK_SUB = SUBSTR(WK_AREA, 15, 10);
このコードが生成するアセンブラ(Objectコードの逆アセンブルイメージ)の核心部は、およそ以下のような動きをする。
1. ベースアドレスの解決: `WK_AREA`の先頭アドレスが格納されている基底レジスタ(Base Register)に対し、静的オフセット(この場合は $15 – 1 = 14$ バイト目)を加算する。
2. MVC(Move Character)命令への直結:
MVC WK_SUB(10),14(R_WK_AREA)
コンパイラは、関数呼び出しのオーバーヘッドを完全に排除し、ただ1発の `MVC` 命令(最大256バイトまで転送可能なストレージ間転送命令)へとインライン展開する。
動的ケース:変数によるオフセット指定と `MVCL` の呪縛
では、ポジションや長さに変数(動的値)が指定された場合はどうなるか。
DCL WK_AREA CHAR(2000) INIT(‘ ‘);
DCL WK_SUB CHAR(50) INIT(‘ ‘);
DCL POS_VAR FIXED BIN(31) INIT(100);
/ オフセットが変数で動的に変動するケース /
WK_SUB = SUBSTR(WK_AREA, POS_VAR, 50);
ここでコンパイラの最適化ロジックが真価を発揮する。
1. `POS_VAR` の値(31ビット固定小数点数)をワークレジスタ(例: R15)にロード。
2. 1-originから0-originへの変換($POS\_VAR – 1$)のための減算命令(`S` または `AH`)を実行。
3. ベースアドレスにそのオフセットレジスタを加算。
4. 転送長が長大である場合や、可変長(VARYING)文字列が絡む場合、コンパイラは `MVCL`(Move Character Long)命令を選択し、レジスタペア(長さとアドレス)を用いたループレスな高速転送を組み立てる。
—
2. ベース変数とポインタを用いた動的メモリ操作の罠
PL/Iの真骨頂は、C言語のポインタを凌駕する強力かつ危険なベース変数(Based Variable)の概念にある。`SUBSTR` をこのポインタ操作と組み合わせたとき、メインフレームの生のメモリ空間を直接ハックするようなコードが書けてしまう。
実務で遭遇した、最悪のバグの一例を見てみよう。
DCL P_BUF POINTER;
DCL BASED_DATA CHAR(32767) BASED(P_BUF);
DCL TARGET_STR CHAR(100);
/ 外部から渡された不安全なアドレスをベース変数に割り当て /
P_BUF = GET_UNSAFE_STORAGE_ADDRESS();
/ 動的なオフセット計算とSUBSTRの組み合わせ /
IF SUBSTR(BASED_DATA, 500, 10) = ‘ABEND_KEY’ THEN
DO;
/ ここで領域外を参照してS0C4が発生するリスク /
TARGET_STR = SUBSTR(BASED_DATA, 400, 100);
END;
なぜこれが「地雷」なのか?
JavaやC#であれば、配列境界チェック(Bounds Checking)が働き、範囲外アクセスは安全に例外(`IndexOutOfBoundsException`など)としてキャッチされる。
しかし、PL/Iのデフォルト、あるいは最適化オプション(`LIMIT`や`NOCHECK`)によっては、境界チェックのコードが生成されない。結果として何が起きるか?
1. S0C4アベンド(Protection Exception): 割り当てられた仮想記憶の境界を超えてストレージを読み込もうとした瞬間、OS(z/OS)のハードウェア例外が走り、ジョブは容赦なく異常終了する。
2. サイレントなデータ破壊: さらにタチが悪いのは、読み込みではなく書き込み(代入の左辺への `SUBSTR`)で境界を超えた場合だ。隣接する別のトランザクションの制御ブロックや、DB2のホスト変数領域を静かに上書きし、数時間後に全く関係ないモジュールで謎のS0C7(データ例外)を引き起こす。
—
3. アベンド(ABEND)発生時のダンプ解析とエッジケース
深夜、運用担当者から「バッチが `S0C4` で落ちた」と連絡が入る。シスダンプ(System Dump)をIPCS(Interactive Problem Control System)で解析するシチュエーションを想像してほしい。
PSW(Program Status Word)が指し示すアドレスの逆アセンブル結果を見ると、大抵犯人は `MVC` または `EX`(Execute)命令のオペランド異常、あるいは `SUBSTR` の演算結果による不正アドレス参照だ。
パックデシマルの内部符号反転バグとの複合汚染
基幹システム特有の厄介な問題として、COBOLから移行してきた、あるいはCOBOLとデータ領域を共有している(COMP-3形式の)パックデシマルデータを、誤って `CHAR` 型の領域として `SUBSTR` で切り出してしまうケースがある。
DCL 1 CUST_REC,
3 CUST_ID FIXED DECIMAL(7,0), / 4バイトのパックデシマル /
3 CUST_NAME CHAR(30);
DCL WORK_STR CHAR(10);
/ 意図せずパックデシマルのバイナリ領域を文字としてSUBSTRで切り出す /
WORK_STR = SUBSTR(CUST_REC, 1, 4);
パックデシマルの下位4ビットには符号(`C`, `D`, `F` など)が入っている。これを通常の文字データとして処理したり、さらにはマイグレーション先のJavaでそのままUTF-8に変換しようとすると、文字化けどころか、アプリケーション層でのパースエラーを引き起こす。
メインフレーム上ではエラーにならずとも、この「バイナリのゴミ」を含んだ `SUBSTR` の結果がCICSの画面に出力されたり、DB2のVARCHAR列に格納されたりすることで、下流システムに毒が回り続けることになる。
—
4. 埋め込みSQL(DB2)とCICSオンライン処理におけるエッジケース
オンラインシステム(CICS)やバッチのデータベースアクセス(DB2)において、`SUBSTR` の使い方はパフォーマンスに直結する。
DB2ホスト変数への `SUBSTR` 適用
DB2のストアドプロシージャや動的SQLとのインタフェースにおいて、ホスト変数の部分文字列を条件に使う場合、PL/I側で無駄な `SUBSTR` を多用すると、DB2オプティマイザがインデックススキャンを諦め、全表スキャン(Table Space Scan)に格下げしてしまうケースがある。
/ 悪い例:ホスト変数の加工をPL/I側で行う /
EXEC SQL
SELECT FROM CUSTOMER
WHERE ACCT_NO = :FULL_ACCT;
/ ではなく、SQL側でプレディケートを工夫すべきだが、
やむを得ずPL/I側で部分一致をやる場合 /
IF SUBSTR(FULL_ACCT, 1, 4) = ‘9999’ THEN …
特に、CICSの通信領域(`DFHCOMMAREA`)やTDキュー(Transient Data Queue)を扱う際、ストレージのレイアウトを `SUBSTR` でパースするレガシーコードは山のように存在する。
CICS環境下でのS0C4やASRA(CICSのアベンドコード)の大部分は、COMMAREAの長さ(`EIBCALEN`)を検証せずに `SUBSTR` で固定長分のデータを切り出そうとした結果、未初期化または割り当て外の領域にアクセスしたことが原因である。
—
5. マイグレーション(Java / C#への移行)におけるアーキテクチャ設計の指針
さて、我々システムアーキテクトが最も頭を悩ませるのが、これらPL/I特有の「アセンブラ最適化されたメモリハックと `SUBSTR` の挙動」を、どうやってモダンな言語(JavaやC#)に安全かつ効率的に移行するかという点だ。
1. 「暗黙の切り捨て」と「パディング」の差異への対応
PL/Iの `SUBSTR` や代入では、代入先の長さに応じてスペース埋め(Padding)や切り捨て(Truncation)が暗黙に行われる。
Javaの `String.substring()` は、範囲外を指定すれば容赦なく `StringIndexOutOfBoundsException` を吐くし、固定長へのパディングも自前で実装する必要がある。
移行ツール(自動変換ツール)の多くはこの差異を単純な文字列操作メソッドに置き換えるが、元のPL/Iコードが依存していた「あふれたデータの切り捨て挙動」や「空白埋めのセマンティクス」を見落とし、データ不整合の温床となる。
2. ポインタとベース変数の抽象化
JavaやC#にはポインタ(メモリアドレスの直接操作)の概念がない(あるいはunsafeコンテキストが必要)。
したがって、レガシーな `BASED` 変数と `SUBSTR` を駆使したバイナリパーシングのロジックは、そのまま機械的に変換することは不可能だ。
マイグレーション設計においては、バイト配列(`byte[]`)とオフセット管理を行う専用のラッパークラス(例えば、レガシーのレイアウトを模した「レコードバッファクラス」)を設計し、その内部で安全なバイト単位のスライス操作を行うよう、アーキテクチャレベルでリファクタリングを施す必要がある。
—
おわりに:レガシーの美学を理解した者だけが移行を制す
PL/Iの `SUBSTR` 関数と、それが生み出すアセンブラコード、そしてベース変数によるメモリ操作は、ハードウェアの性能を極限まで引き出すための先人たちの知恵の結晶である。
「古い言語だから」「誰も読めないから」と、その内部挙動を理解せずにモダン言語へ単純置き換え(リフト&シフト)を試みるプロジェクトは、必ずテストフェーズの終盤で底なし沼にはまることになる。
メインフレームのアーキテクチャがハードウェアレベルで何をしていたのかを正確に把握し、その本質をJavaやC#の安全なオブジェクト指向・メモリ管理モデルへと再構築すること――それこそが、我々レガシー移行スペシャリストに課された使命なのだ。
基幹システムの未来を担う次世代のアーキテクチャ設計に向けて、今日の知見が少しでもあなたの現場の羅針盤となれば幸いである。
