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

ゼロ除算の悪夢:PL/I基幹バッチにおけるZERODIVIDE例外とダンプ解析の深層

メインフレームの夜間バッチが突如として沈黙する。オペレータから「S806」や「ASRA」といったお馴染みの、しかし胃が痛くなるようなアベンドコードの連絡が入る。ログを辿ると、そこには見慣れたメッセージが鎮座している。

`IBM0271S ONCODE = 峇10` —— ZERODIVIDE。

JavaやC#といったモダン言語の世界であれば、ゼロ除算はせいぜい `ArithmeticException` がスローされ、スタックトレースを吐いてトランザクションがロールバックされるだけの「想定内の例外」に過ぎない。しかし、我々が日夜対峙しているIBMメインフレームのPL/Iワールド、特にコンパイラの最適化の洗礼を受けたレガシーコードにおいて、`ZERODIVIDE` 条件の発生は、単なる算術エラーにとどまらない「システム全体の信頼性を揺るがす深淵」への入り口である。

今回は、PL/Iにおける固定小数点数(`FIXED BINARY` / `FIXED DECIMAL`)の精度と表現の罠、そしてゼロ除算が引き起こすメカニズムと、アベンド時のシステムダンプから真の原因を秒速で特定するための実践知を、マニュアルの行間から紐解いていこう。

—

1. 固定小数点演算とZERODIVIDEの発生メカニズム

PL/Iのデータ定義において、`FIXED DECIMAL`(パック十進数:COMP-3)や `FIXED BINARY`(2進固定小数点数)は、金融計算や基幹システムの要である。しかし、このデータ型は「見た目の桁数」と「内部の表現・演算精度」の間に、コンパイラの最適化という名の魔物が潜んでいる。

ゼロ除算は「なぜ」検出されるのか?

CPU( z/Architecture)レベルで見れば、ゼロによる除算命令(例えば `DR` や `DSG` などのハードウェア命令)が実行された瞬間、ハードウェア割り込みが発生する。PL/Iランタイム環境(Language Environment: LE)はこの割り込みをキャッチし、ON条件である `ZERODIVIDE` を raise(発生)させる。

ここで厄介なのは、「除数が明示的な `0` でなくても発生する」という点だ。

例えば、外部ファイルから読み込んだパッチデータや、DB2からフェッチしたカラム値が、何らかの理由でスペース(空白)や上位ニブルの不正なゾーン、あるいはヌルバイト(X’00’)であった場合を考えてみてほしい。パックデシマルの内部表現において、正しくない符号ビットやパディングの崩れによって、コンパイラが生成した展開コードが「実質的なゼロ」とみなして除算を実行し、容赦なくアベンドを引き起こす。

1
DCL WS-TOTAL-AMT FIXED DEC(11,2) INIT(100000.00);
DCL WS-COUNT FIXED DEC(5,0) INIT(0);
DCL WS-AVERAGE FIXED DEC(11,2);

/ ここでゼロ除算が発生する典型的なケース /
ON ZERODIVIDE BEGIN;
PUT SKIP EDIT (‘【警告】ゼロ除算を検知しました。デフォルト値に置き換えます。’) (A);
WS-AVERAGE = 0;
GOTO CALC-EXIT;
END;

WS-AVERAGE = WS-TOTAL-AMT / WS-COUNT; / WS-COUNTが0のためZERODIVIDE発火 /

CALC-EXIT:

このコードは `ON` ユニットでトラップできているように見える。しかし、コンパイラオプションに `OPTIMIZE(2)` や `TEST` が絡むと、オプティマイザが除算命令の直前で行うべきゼロチェックのコードを最適化(削除・移動)してしまい、`ON` ユニットが意図通りに機能しない、あるいは例外アドレスがずれるという、ベテランをも唸らせる現象に遭遇する。

—

2. ポインタ操作と動的メモリ領域における「見えないゼロ」

基幹システムの高度なメモリ管理において、`BASED` 変数と `POINTER` を駆使した動的ストレージの割り当て(`ALLOCATE` / `FREE`)は日常茶飯事だ。ここで発生するゼロ除算の多くは、ポインタが指し示す先の中身がゴミ(ガベージ)であること、あるいは未初期化領域へのアクセスに起因する。

以下の実務的スニペットを見てほしい。ここでは、動的に取得したストレージ内の構造体フィールドに対して演算を行っているが、ポインタの制御を誤った場合に地雷を踏む。

1
/ 構造体テンプレートの定義 /
DCL 1 ACCOUNT-REC BASED(P-ACCT),
2 ACCT-ID CHAR(8),
2 ACCT-VAL1 FIXED BIN(31),
2 ACCT-VAL2 FIXED BIN(31),
2 ACCT-RATIO FIXED DEC(5,4);

DCL P-ACCT POINTER;
DCL WORK-DIVISOR FIXED BIN(31);

/ 領域の動的獲得 /
ALLOCATE ACCOUNT-REC;

/ うっかり初期化を忘れた、あるいはポインタが狂った状態を想定 /
P-ACCT -> ACCT-VAL1 = 5000;
P-ACCT -> ACCT-VAL2 = 0; / ここに明示的、あるいは意図せぬゼロ /

/ 安全に見える除算処理 /
IF P-ACCT -> ACCT-VAL2 ^= 0 THEN
P-ACCT -> ACCT-RATIO = P-ACCT -> ACCT-VAL1 / P-ACCT -> ACCT-VAL2;
ELSE
P-ACCT -> ACCT-RATIO = 0;

一見、`IF` 文でガードしているため安全に見える。しかし、マイグレーションやレガシー改修の現場では、この `P-ACCT` 自体が不正なアドレスを指していたり、マルチスレッド(CICS環境下のタスク間通信など)で別のトランザクションがメモリ領域を書き換えた瞬間(いわゆる非同期競合)に、`ACCT-VAL2` が突如としてゼロに化けることがある。

この場合、ガード条件をすり抜けてハードウェア例外としての `ZERODIVIDE` が直撃する。

—

3. コンパイラオプションと最適化の罠

PL/Iコンパイラ(Enterprise PL/I for z/OS)の最適化レベル(`OPTIMIZE`)は、パフォーマンスチューニングの強力な武器だが、例外処理のデバッグにおいては諸刃の剣となる。

  • `STG(NONE)` vs `STG(FULL)` / `OPTIMIZE(2)`:

オプティマイザは、レジスタ割付の効率化や命令の並び替え(レオーダリング)を行う。もし `TRAP(ON)` を指定していなければ、ゼロ除算が発生した瞬間にLanguage Environmentがプロセスを異常終了させ、ダンプを吐く。

  • `CHECK` / `NOCHECK`:

添字範囲外チェックやオーバーフローチェックと同様に、除算時の厳密なチェックを外していると、ハードウェアレベルの例外トラップが意図した挙動を示さないことがある。

マイグレーションプロジェクト(PL/IからJavaやC#へのリライト)において最も頭を悩ませるのが、「レガシー側ではオプティマイザの妙技によって偶然スルーされていた(あるいは特定のダンプで握り潰されていた)潜在的バグが、移行先の厳格なモダン言語環境で一斉に例外として表面化する」という現象である。移行前のアセスメント段階で、ソースコード上のすべての除算演算子(`/`)と組み込み関数(`DIVIDE`)を静的解析し、除数がゼロになり得るパスをすべて洗い出すことが、アーキテクトとしての最初の仕事となる。

—

4. アベンド発生時のシステムダンプ解析(CEEDUMP / SYSUDUMP)

本番稼働中に `ZERODIVIDE` によるアベンド(例: `ABEND S0C7` や `CEE3209S`)が発生した際、我々はSYSUDUMPやCEEDUMPを手がかりに、犯人を特定しなければならない。

ダンプ解析のステップ

1. CEEDUMPのトレーサーバック(Traceback)の確認:
どのモジュールの、何行目(Statement Number)で例外が起きたかを特定する。PL/Iのシンボル情報(`TEST(SYM)` オプション付きでコンパイルされていることが前提)があれば、変数の名前まで一発で特定できる。
2. PSW(Program Status Word)の確認:
アベンド時の命令アドレス(Instruction Address)を特定し、ロードモジュール上のオフセットとリンケージエディトマップを突き合わせる。どのマシン語命令(`DR` や `DSG` など)で割り込みがかかったかを確認する。
3. レジスタ内容とワークエリアのダンプ:
除数(divisor)を保持していた汎用レジスタや、パックデシマル演算が行われていたストレージ領域(Storage Dump)を16進数で目視し、値が `X’00’` なのか、それとも不正なパック符号(例: `X’12343F’` のようなゾーン・符号の崩れ)なのかを判別する。

—

5. エッジケース対策:DB2(埋め込みSQL)とCICS環境での極意

最後に、基幹システムの現場特有の、データベースとオンライン環境が絡むエッジケースに言及しておこう。

A. DB2(埋め込みSQL)におけるNULLとゼロ除算

SQLの `SUM` や `AVG` などの集計関数をフェッチする際、対象データが1件もヒットしない場合、DB2は計算結果として `NULL` を返す。これをPL/Iのホスト変数(例: `FIXED DEC`)にそのまま受け取ろうとすると、インジケータ変数(Indicator Variable)を適切にハンドリングしていない限り、データ転送時やその後の演算で予期せぬ挙動を引き起こす。
さらに、分母となる集計値が `NULL`(PL/I側でゼロとみなされる、あるいは未定義値)のまま割り算の分母に組み込まれると、高確率で `ZERODIVIDE` の餌食になる。

B. CICSオンラインにおけるタスク異常終了の隔離

CICS(Customer Information Control System)のトランザクション内で `ZERODIVIDE` が発生した場合、対策をしていないとCICS領域全体、あるいは当該タスクがアベンドし、セッションが強制切断される。オンライン画面の向こう側にいるエンドユーザーに不快な思いをさせないため、CICS環境では `HANDLE CONDITION` や `EXEC CICS CATCH` に相当するエラーハンドリング、あるいはPL/I側の `ON ZERODIVIDE` を適切に配置し、トランザクションを安全にロールバック(`EXEC CICS SYNCPOINT ROLLBACK`)させる設計が不可欠である。

1
/ CICSオンラインプログラムにおける安全な除算ブロックの例 /
ON ZERODIVIDE BEGIN;
/ ログ出力やエラーメッセージの設定 /
WS-ERR-MSG = ‘演算エラーが発生しました。管理者にお問い合わせください。’;
/ 画面へのエラー返却処理へジャンプ /
GOTO CICS-ERROR-ROUTINE;
END;

/ 危険を伴う除算の実行 /
WS-RESULT = WS-AMOUNT / WS-DIVISOR;

—

結びに代えて

PL/Iにおける `ZERODIVIDE` 条件は、単なる「数学的なゼロ割りのエラー」ではない。それは、データの品質不良、ポインタの迷子、コンパイラの最適化、そしてデータベースやオンライン制御との境界領域における「システムの矛盾」が先鋭化した結果である。

私たちシステムアーキテクトやマイグレーションの担い手は、単にコードを新しい言語に機械的に翻訳するのではなく、このレガシーコードが孕む「例外の哲学」と「ハードウェアへの深い依存性」を完全に理解し、次世代の堅牢なアーキテクチャへと昇華させなければならない。

次にあの忌々しい `IBM0271S` に遭遇した時、慌ててソースコードの分母を見るだけの素人であってはならない。レジスタを読み、ダンプの海から真犯人を炙り出す——それこそが、メインフレームの番人たる我々の矜持である。

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