【テクニカル・上級編】ZERODIVIDE条件の発生メカニズムとONCODEの識別 – PL/Iの基本構文とデータ制御実践ガイド

ゼロ除算(ZERODIVIDE)の悪夢と、IBMメインフレームが隠す「例外の深淵」

基幹システムの夜間バッチが突然のU4038やS0C7で沈黙したとき、運用担当者の背筋が凍る瞬間だ。JavaやC#といったモダン言語の世界であれば、ゼロ除算(Division by Zero)は高確率で `ArithmeticException` として安全にキャッチされ、スタックトレースを残して优雅にログに記録される。しかし、IBM z/OS上のPL/I(Enterprise PL/I)が支配するレガシーの世界は、そんなに甘くはない。

PL/Iにおける `ZERODIVIDE` 条件は、単なる数理的エラーではない。それはハードウェア(S/390、z/Architecture)の浮動小数点演算器や固定小数点演算器が発する悲鳴であり、ランタイム環境(Language Environment: LE)のトラップ機構、そしてコンパイラの最適化オプションが複雑に絡み合う、極めてプリミティブなシステムイベントなのだ。

今回は、テックリードやマイグレーションアーキテクトが実務で必ず直面する、ゼロ除算発生時のメカニズムとONCODEによる緻密なエラーハンドリング、そしてJava等への近代化移行における「隠れた地雷」について、現場の知見を総動員して解説する。

1. ゼロ除算発生のメカニズム:ハードウェア割り込みからPL/Iランタイムへ

PL/Iプログラム内で、例えば以下のようなコードを実行したとする。

1
DCL RESULT FIXED DEC(5,0);
DCL DIVISOR FIXED DEC(5,0) INIT(0);
DCL VALUE FIXED DEC(5,0) INIT(10050);

/ ここでゼロ除算が発生する /
RESULT = VALUE / DIVISOR;

この瞬間、何が起きているのか。CPU(CP:Central Processor)の内部では、`DP`(Divide Packed)などのマシン語命令が実行される。ハードウェアは「除数がゼロである」ことを検知すると、即座にプログラム割り込み(Program Interruption)を発生させる。固定小数点(パック十進数)の場合はコード000B(Decimal Divide Exception)、浮動点演算の場合はコード000F(Floating-Point Divide Exception)などの割り込みコードがCPUステータスに刻まれる。

通常、OS(z/OS)のプロテクションが働けば、このハードウェア割り込みはそのままジョブのアベンド(System Abend: S0C7やS0C9など)に直結する。しかし、LE(Language Environment)とPL/Iランタイムが適切に初期化されている環境下では、このハードウェア割り込みがLEの条件ハンドラーに捕捉され、PL/Iの `ZERODIVIDE` 条件(Condition)へと昇華される。

ここで重要なのは、コンパイラの最適化オプション(`OPT(2)` や `OPT(3)`)が有効な場合、「実際にゼロで割る命令を実行する前に、コンパイラが除算コード自体を最適化で消し去ったり、順序を入れ替えたりする」という現象が起きる点だ。デバッグ時に「なぜか期待した場所で `ZERODIVIDE` が発生せず、数行ずれた場所でS0C4(Protection Exception)や予期せぬデータ例外が起きる」というトラブルに遭遇したベテランも多いはずだ。最適化と例外送出の因果関係を把握しておくことは、ダンプ解析の第一歩となる。

2. ONCODEによる例外トラップと、実務で使うべきハンドリング設計

PL/Iの真骨頂は、エラーが発生したときにプログラムを即座にクラッシュさせるのではなく、`ON` ユニットを用いて開発者が自らリカバリーパスを構築できる点にある。

以下のコードは、`ZERODIVIDE` を捕捉し、動的メモリ(ベース変数とポインタ)を安全に解放しつつ、ログにONCODEを出力して異常終了を回避する実例である。

1
/ —————————————————————- /
/ ZERODIVIDE例外ハンドリングの堅牢な実装例 /
/ —————————————————————- /
DEMO_ZD: PROC OPTIONS(MAIN);

Dcl Ptr Pointer;
Dcl 1 Buffer Based(Ptr),
2 ItemId Fixed Bin(31),
2 DataVal Dec Float(16);

Dcl ErrorCode Fixed Bin(15);

/ 動的メモリの獲得 /
Allocate Buffer Set(Ptr);
ItemId = 9999;
DataVal = 0.0E0; / 意図しないゼロ値 /

/ ZERODIVIDE条件の捕捉(ONユニットの定義) /
ON ZERODIVIDE BEGIN;
ErrorCode = ONCODE();
Put Skip List(‘ 警告: ゼロ除算を検知しました. ONCODE = ‘, ErrorCode);

/ 例外発生時の安全なデフォルト値へのフォールバック /
DataVal = 1.0E0;

/ 動的メモリの解放などクリーンアップ処理をここに記述 /
Free Buffer;

/ 処理を続行させる場合はGOTOでリトライ先を指定するか、そのまま抜ける /
GOTO NORMAL_EXIT;
END;

/ ゼロ除算を引き起こす危険な演算 /
Put Skip List(‘演算開始…’);
DataVal = 100.0E0 / DataVal; / DataValが0なのでここでZERODIVIDE発生 /

Put Skip List(‘演算結果: ‘, DataVal);

NORMAL_EXIT:
Put Skip List(‘プログラムを正常に終了します.’);

/ 多重解放を防ぐためのポインタ無効化 /
Ptr = Null();

END DEMO_ZD;

ONCODEの識別と実務的意味

`ONCODE()` ビルトイン関数が返すコード番号を正確に把握しておくことが、保守エンジニアの腕の見せ所だ。

  • ONCODE 320: 固定小数点数(FIXED DECIMAL / FIXED BINARY)におけるゼロ除算。
  • ONCODE 330: 浮動小数点数(FLOAT)におけるゼロ除算。

レガシーシステムのバッチ改修において、外部から流入するマスタデータの欠損(空白やゼロ)により、突然 `ONCODE 320` が頻発するケースは後を絶たない。単に `ON` ユニットで潰すだけでなく、どのキー項目(ItemIdなど)の処理中に発生したかをダンプやログに必ず出力する設計にすべきである。

3. レガシー移行(Java/C#等へのマイグレーション)におけるエッジケースの罠

現在、多くの企業がIBM汎用機からオープン系(Java、C#、あるいはクラウドネイティブ環境)へのマイグレーションを推進している。しかし、この「ゼロ除算の挙動の違い」は、移行プロジェクトの最終フェーズで必ずと言っていいほどテスト工程の足を引っ張る。

① パックデシマルの内部符号反転バグとゼロ除算の複合災害

PL/Iの `FIXED DECIMAL` は、IBMメインフレーム固有のパック十進数(Packed Decimal / COMP-3)としてメモリ上に存在する。稀に、外部ファイル(VSAMやSequential)の文字化けや不正なJCLソート、あるいは古いCOBOL/PL/I混載プログラムのメモリ破壊により、ゾーン/パックの符号ニブル(最下位バイトの右側4ビット)が破壊され、`C`(正)でも `D`(負)でもない不正な値(例えば `F` や `A`)になることがある。

この「ゾンビデータ」を分母にして演算を行った場合、PL/I環境ではデータ例外(S0C7)や `ZERODIVIDE` が発生する。
しかし、これをそのままJavaの `BigDecimal` に置き換えてマイグレーションするとどうなるか。Java側では不正なゾーン符号は単なるパースエラー(`NumberFormatException`)として弾かれるか、あるいはデータベース(DB2 for z/OSからPostgreSQL等への移行)のロード時にインテグリティエラーを引き起こす。「PL/Iではゼロ除算またはデータ例外だったものが、移行後にはDB登録時のトランザクションエラーに化ける」という非互換に、アーキテクトは立ち向かわなければならない。

② 埋め込みSQL(DB2)とCICSオンライン処理のエッジケース

CICSオンラインの画面からの入力値がブランク(スペース)のまま送信され、それをPL/I側で `FIXED DEC` に変数値として受け渡して演算した瞬間、`ZERODIVIDE` が走ることがある。
さらに、DB2の `SQLCA` を用いた動的SQL実行時において、SQLの集約関数(`SUM` や `AVG`)の結果がゼロであり、それをさらにPL/I側で別の係数で割るような処理がある場合、DB2側はそもそもSQLリターンコード(`SQLCODE`)でハンドリングすべきか、PL/Iの `ZERODIVIDE` に任せるべきかの設計思想の乖離が生じる。

Java(Spring Boot + MyBatis/JPA等)へ書き換える際は、PL/Iの `ON ZERODIVIDE` が担っていた暗黙のフォールバック動作を、明示的なバリデーション層(Bean Validation等)やAOP(アスペクト指向プログラミング)による例外ハンドリングとして再構築する必要がある。

4. スペシャリストからの提言:ダンプ解析とコード設計の極意

最後に、現行PL/Iシステムの保守、および将来の移行を見据えたアーキテクトへの提言を記す。

1. ダンプを恐れるな、CEEDUMPを読め
アベンド時に出力されるSYSUDUMPやCEEDUMPには、`ZERODIVIDE` が発生した瞬間のPSW(Program Status Word)、インストラクションアドレス、そして各変数のスナップショットが残されている。特にLE環境下のCEEDUMPの「Traceback」セクションを見れば、どのモジュールの何番地のステートメントで例外が起きたのかがミリ秒単位で特定できる。これを読めるかどうかが、プロのメインフレームエンジニアの分かれ目だ。
2. 安易なON-unitの乱用を戒めよ
エラーを隠蔽するためにコード全体を `ON ZERODIVIDE BEGIN; … END;` で覆い隠す設計は、バグの温床となる。例外はあくまで異常系であり、データの事前検証(Pre-validation)によって分母がゼロにならないことを担保するのが本来の正攻法である。
3. 移行時は「ゼロ」と「NULL(欠損)」のセマンティクスを一致させよ
PL/Iの数値変数の初期化ゼロと、オープン系データベースの `NULL` は意味が異なる。ゼロ除算の発生源を断つためには、マイグレーション設計の初期段階で「分母となり得る項目に対する厳格なドメイン定義」をJava/C#側に移植しなければならない。

基幹システムの心臓部を支えるPL/Iの例外制御。その挙動の裏にあるハードウェアとランタイムの対話を深く理解し、予測不可能なクラッシュを完全に出し切るシステム設計こそが、真のレガシーモダナイゼーションの第一歩なのである。

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