【テクニカル・上級編】CEE3201S(ゼロ除算)のダンプ解析とPSWの特定 – PL/Iの基本構文とデータ制御実践ガイド

夜間バッチの海原で、突如として赤く染まるオペレータコンソール。
CEE3201S THE SYSTEM INTERRUPT SIGNAL 0C7-A WAS RECEIVED.」あるいは「0C9-I」。

メインフレームの現場において、この「CEE3201S」という見慣れた(そして忌々しい)メッセージほど、我々テックリードの背筋を凍らせるものはない。Language Environment(LE)が吐き出すこのエラーは、要するに「お前の書いたプログラムが、禁忌であるゼロ除算(またはそれに類する演算例外)をやらかしたぞ」という冷徹な宣告だ。

JavaやC#のモダンな世界であれば、スタックトレースが親切に「何行目のどのメソッドで `ArithmeticException` が起きたか」を教えてくれる。だが、レガシーの牙城であるPL/IとOS/390、あるいはz/OSの世界ではそうはいかない。特にコンパイラの最適化(`OPTIMIZE`)が効いた状態でこのアベンドを踏んだとき、バイナリの海から真の原因箇所を自力で釣り上げられなければ、システムアーキテクトとしての飯の食い上げだ。

今回は、この「CEE3201S(ゼロ除算)」をテーマに、PSW(プログラム状態語:Program Status Word)の読解から、コンパイラの裏側、さらにはモダナイゼーション(Java等へのマイグレーション)における地雷原の回避策まで、骨の髄まで解説しよう。

1. なぜCEE3201Sが起きるのか:PL/I演算の罠

PL/Iは、FORTRANの数値計算能力とCOBOLの事務処理能力、そしてAlgolの構造化プログラミングを統合した、極めて強力かつ「危険な」言語である。識別子の命名規則に予約語の縛りが緩やか(コンテキストによってキーワードが解釈されるため、変数名に `IF` すら使えてしまう変態的な柔軟性を持つ)なのと同様に、データ型と算術演算の暗黙の型変換も非常にアグレッシブに行われる。

例えば、以下のような動的メモリ操作やポインタ演算、あるいは埋め込みSQL(DB2)から取得したデータをそのまま計算に使う場面を想像してほしい。

//
/ 顧客マスタの売上集計処理におけるゼロ除算の危険な一例 /
/————————————————/
DCL 1 CUST_REC BASED(P_CUST),
2 CUST_ID CHAR(5),
2 TOTAL_SALES DEC FIXED(11,2),
2 ITEM_COUNT FIXED BIN(31); / 項目数(ここがゼロになり得る)/

DCL AVG_SALES DEC FIXED(9,2);
DCL P_CUST PTR;

/ ポインタを通じた動的メモリ上のレコード参照 /
P_CUST = GET_TRANSACTION_BUFFER();

/ 致命的なゼロ除算の罠 /
/ ITEM_COUNT が 0 の場合、ハードウェア例外(System Code 0C9)が発生 /
AVG_SALES = CUST_REC.TOTAL_SALES / CUST_REC.ITEM_COUNT;

一見して何の問題もないコードに見えるかもしれない。しかし、基幹システムの現場データは我々の想像を遥かに超えて汚染されている。バッチの前提条件である「マスタの整合性」が何らかの原因で崩れ、`ITEM_COUNT` にバイナリの `0` が入り込んでいた瞬間、ハードウェアの算術論理ユニット(ALU)は容赦なく例外割り込みを発生させる。

これがLEによって捕捉され、 `CEE3201S` というシグナルとしてジョブログに刻まれるのだ。

2. ダンプ解析の極意:PSWから「真の発生源」を特定する

CEEドキュメントやCEE-DUMPが出力されたとき、素人はメッセージの要約だけを見て頭を抱える。しかし、真のアーキテクトが見るべきは、ダンプリスト(あるいはSYSUDUMP/CEEDUMP)の頭取にある PSW(Program Status Word) と、ロードモジュールのアドレスマップだ。

PSWの構造とインストラクション・アドレスの抽出

z/OSの64ビットモード(あるいは32ビットモード)において、PSWの右半分(または特定のオフセット)には、次に実行するはずだった、あるいは例外を引き起こした機械語命令のアドレス(Instruction Address)が格納されている。

例えば、CEEDUMPのブレークダウンセクションに以下のようなレジスタおよびPSWのスナップショットがあったとする。

PSW at event: 078D1400 80000000 00000000 000184A2

この末尾の `000184A2` がPSWインストラクション・アドレスだ。

1. ロードマップとの突き合わせ
リンク編集(Linkage Editor / Binder)時のコンパイラリスト(Listing)を参照し、プログラムのベースアドレス(Entry Point)を確認する。
2. 相対アドレスの計算
PSWアドレスからエントリポイントのアドレスを引くことで、モジュール内オフセット(例: `+0004A2`)を算出する。
3. リスト上の命令コード(Machine Instructions)の逆引き
PL/Iコンパイラが出力するリスティングの「Object Code」欄と照合する。

ゼロ除算(System 0C9)を引き起こす代表的なS/390アセンブラ命令は以下の通りだ。

  • `DR` (Divide Register)
  • `D` (Divide)
  • `EDIV` (Divide Extended)
  • あるいはDECIMAL演算でのデータ例外(0C7)を引き起こす `AP`, `SP`, `MP`, `DP`。

コンパイラが `OPTIMIZE(FULL)` や `OPTIMIZE(TIME)` で最適化を行っている場合、ソースコード上のどの行がどのマシン語に対応しているかが巧妙にシャッフルされる。そのため、「このレジスタ `R4` にロードされている値がゼロだったから `DR` 命令で落ちた」という事実を、逆アセンブルされた命令とコンパイラリストのステートメント番号(Stmt)から逆算するスキルが不可欠となる。

3. コンパイラオプションとエッジケース対策

この種の障害を防ぐ、あるいは解析を容易にするために、我々アーキテクトはPL/Iコンパイラのオプション設計に細心の注意を払わなければならない。

テスト・本番環境における推奨オプション

  • `TEST(SYM)` vs `NOTEST`:

本番性能を極限まで高めるために `NOTEST` を採用しがちだが、マイグレーション期や重要基幹バッチにおいては、CEEDUMPにより豊かな変数情報を残すために `TEST(COBOL,SYM)` 相当のデバッグ情報を付与するか、 حداقل `STMT` オプションを有効にしておくことが、障害シューティングの時間を数分に短縮する鍵となる。

/ コンパイル時プロシージャのイメージ /
//SYSIN DD
OPTIONS(RENT, STMT, XREF, FLAG(I,U))
/

  • TRAP(ON) と 異常終了ハンドリング:

LEのランタイムオプションである `TRAP(ON)` により、OSレベルの割り込みをLEがキャッチし、温かみのある(?)CEEメッセージに変換してくれる。これを `TRAP(OFF)` にすると、システムアベンド(S0C9等)がそのまま直撃し、より低レベルなスナップダンプしか得られなくなるため原則として `ON` を維持する。

4. マイグレーション(Java / C# / 現代的アーキテクチャへの移行)への教訓

さて、我々が直面する現在の最大関心事は、こうしたPL/Iレガシーシステムを、いかに安全にJava(Spring Bootなど)やC#へモダナイズ(マイグレーション)するかだ。

ここで重大なエッジケースが浮上する。「PL/I特有の暗黙の数値丸めとゼロ除算の挙動の違い」である。

1. ゼロ除算の例外処理の差異

  • PL/I(メインフレームハードウェア)では、ゼロ除算は即座にハードウェア例外(ABEND)となり、バッチ全体が異常終了する。
  • 一方、Javaの `double` や `float` 演算では、ゼロ除算は `Infinity` または `-Infinity` または `NaN` (非数)となり、例外を投げずに処理が続行されてしまうことがある。
  • しかし、Javaの整数演算(`int`, `long`)では `ArithmeticException` が発生する。PL/Iの `FIXED BIN` や `DEC FIXED` をJavaへ移行する際、この「例外で止まるべきか、無限大として伝播すべきか、あるいはデフォルト値(0)にフォールバックすべきか」の仕様定義を誤ると、移行後に「バッチは正常終了したが、集計金額がとんでもないことになっている」という、アベンドよりもタチの悪いサイレント・データ破損を引き起こす。

2. パックデシマルの内部符号反転バグの継承
PL/Iの `DEC FIXED` は、EBCDICのゾーン・パック形式を厳密に解釈する。レガシーDB2やVSAMファイルに潜む「符号ニブルの不正(例: 正常な `C` や `D` 以外が入っている)」が、マイグレーション先のRDB(PostgreSQLやOracle)への移行プロセスでパースエラーや予期せぬゼロ除算を引き起こすケースが後を絶たない。

マイグレーション設計におけるアーキテクトの指針

  • 移行先コード(Java等)の生成にあたっては、旧PL/Iプログラムが暗黙的に行っていた演算精度(スケールとプレシジョン)を完全再現するラッパーライブラリを設計すること。
  • 除算を行う箇所には、必ずガード条件(`if (divisor == 0)` による安全なハンドリングまたはログ出力)を自動変換ツール、あるいは手動のリファクタリングで網羅的に挿入するルールを徹底する。

5. おわりに

CEE3201Sというたった一つのエラーコードの裏側には、ハードウェアのアーキテクチャ、コンパイラの最適化ロジック、そして何十年と蓄積されたビジネスデータの歴史が複雑に絡み合っている。

「画面の向こうで動いているコードが、CPUのどのレジスタを叩き、どのメモリアドレスを参照して爆発したのか」──この感覚を失ったとき、システムアーキテクトは単なる「設定屋」に成り下がってしまう。

レガシーの牙城を現代的なクラウドやオープン系へ引き継ぐその日まで、我々はバイトの1ビット、PSWの1ワードに宿る真実を見据え続けなければならない。

タイトルとURLをコピーしました