夜間バッチのピークを過ぎた静まり返ったオフィス、あるいはリモートアクセスの向こう側で、突然鳴り響くオペレーターからの内線――「おい、夜間一括更新ジョブが異常終了したぞ! `CEE3201S`が出てる!」
メインフレームの保守現場において、この「CEE3201S」という文字列ほど、エンジニアの心拍数を跳ね上げる呪文はありません。これは言語ランタイム(Language Environment: LE)が発報する「固定小数点数によるゼロ除算(Fixed-point binary/decimal divide exception)」のシグナルです。
今回は、数々の修羅場をくぐり抜けてきたシニアアーキテクトの視点から、この「ゼロ除算」がなぜ起きるのか、そしてダンプからPSW(Program Status Word)を突き止め、一瞬で犯人のソースコード行を特定するプロの技を伝授します。
—
1. そもそもなぜPL/Iで「ゼロ除算」が起きるのか?
PL/Iの言語仕様には、CやJavaのような「予約語(Keyword)の概念がない」という特異な美学があります。`IF`や`DO`といった制御文字ですら、文脈によってはただの変数名として定義できてしまうほど自由度が高い。しかし、その自由度の高さゆえに、データ定義(DECLARE)の甘さがそのまま致命的なバグにつながります。
特に、外部ファイル(VSAMや順編成ファイル)から読み込んだパック十進数(DECIMAL FIXED)やバイナリ(BINARY FIXED)のフィールドが、スペースやローデータ(未初期化領域)のまま計算式に巻き込まれたとき、悲劇は起きます。
「いやいや、ちゃんとマスタから取ってるよ」と言いたくなる気持ちは分かりますが、夜間バッチの現場では、前日までのデータ以上・以下が平気で飛んできます。「件数」や「単価」を分母(右側)にした割り算を実装している箇所は、すべてゼロ除算の潜在的リスクを抱えているのです。
—
2. 現場で役立つ実用コード:安全な除算とONユニットの作法
まずは、今回のテーマである「ゼロ除算」を安全にハンドリングする、あるいはあえてダンプを採取するためのPL/Iサンプルコードを見てみましょう。大文字ベースの記述、適切なインデント、そしてビルトイン関数(BUILTIN)の活用に注目してください。
CALC_SAMPLE: PROC OPTIONS(MAIN);
/————————————————-/
/ 宣言部:データ属性の明示と初期化 /
/————————————————-/
DCL WK_DIVIDEND DEC FIXED(9,2) INIT(0); / 被除数 /
DCL WK_DIVISOR DEC FIXED(5,2) INIT(0); / 除数(分母) /
DCL WK_RESULT DEC FIXED(9,2) INIT(0); / 結果 /
DCL ERR_MSG CHAR(80) INIT(‘ ‘);
/ 簡易的な入力を模擬したデータ代入 /
WK_DIVIDEND = 15000.00;
WK_DIVISOR = 0.00; / ここでゼロが入る想定外のケース /
/————————————————-/
/ ONユニットによる例外条件の捕捉 /
/————————————————-/
ON ZERODIVIDE
BEGIN;
PUT SKIP EDIT (‘ 警告: ゼロ除算を検知しました ‘)(A);
/ ここで代替処理を行うか、あえてダンプを落とすかを選択 /
WK_RESULT = 0;
GOTO BYPASS_CALC;
END;
/————————————————-/
/ 演算処理 /
/————————————————-/
PUT SKIP EDIT (‘計算開始: 被除数=’, WK_DIVIDEND, ‘ 除数=’, WK_DIVISOR)
(A, F(9,2), A, F(5,2));
/ ゼロ除算が発生するメインの割り算処理 /
WK_RESULT = WK_DIVIDEND / WK_DIVISOR;
BYPASS_CALC:
PUT SKIP EDIT (‘計算結果: ‘, WK_RESULT)(A, F(9,2));
/————————————————-/
/ 意図的にシステムダンプを要求する場合のビルトイン /
/————————————————-/
IF WK_RESULT = 0 & WK_DIVISOR = 0 THEN
DO;
/ 異常終了コードを指定してLE環境にダンプを吐かせる /
/ 実際の現場ではCEE3201Sは自動でダンプを生成します /
END;
END CALC_SAMPLE;
このコードでは `ON ZERODIVIDE` を使ってトラップしていますが、実際の障害調査では「このONユニットが書かれておらず、容赦なくLEがジョブを異常終了させた」ケースがほとんどです。その結果出力されるのが、あの分厚いスプール出力、すなわちCEEDUMPです。
—
3. CEE3201S発生!CEEDUMPからPSWを特定する手順
ジョブが `CEE3201S` でアベンドしたとき、シスログ(SYSLOG)やジョブログには以下のようなメッセージが残ります。
CEE3201S The system detected an exception (Data exception/Fixed-point divide).
From entry point CALC_SAMPLE at compile unit CALC_SAMPLE at offset +00000A2C
ここからがメインフレームエンジニアの腕の見所です。メッセージにあるオフセットだけでも大体の当たりはつけられますが、確実な証拠を押さえるためにはCEEDUMPを紐解く必要があります。
ステップ1: CEEDUMPの「Traceback」セクションを探す
CEEDUMPの冒頭部分にある `Traceback`(トレースバック)セクションを開いてください。アベンドが発生した瞬間に、どのモジュール、どのエントリーポイントの、何バイト目でプログラムが力尽きたのかがコールスタックの形で記録されています。
ステップ2: PSW(プログラム状態語)の抽出
アベンド時のレジスタとPSWスナップ(Registers / PSW at abend)を探します。
31ビットアドレッシング(AMODE(31))の世界では、PSWの下位アドレス部分が「異常発生命令の次のアドレス(または直上のアドレス)」を示しています。
PSW at abend: 078D1400 80000000 00000000 00015A2C
^^^^^^^^
インストラクションアドレス
この下位アドレス `00015A2C` が、メモリ上の実行アドレスです。
ステップ3: リスト(コンパイルリスト)との突き合わせ
ここからが最も重要です。PL/Iのコンパイル時に出力された「オブジェクティング・リスト(Listing)」を用意します。
リストの「Offset(オフセット)」列と、先ほどのダンプから得られたオフセット(あるいはPSWのアドレスからベースアドレスを引いた相対値)を照らし合わせます。
例えば、リスト上で `WK_RESULT = WK_DIVIDEND / WK_DIVISOR;` が生成しているマシン語コード(`AP` 命令や `DR` 命令など)の開始オフセットが `+00000A2C` であれば、「まさにこの割り算命令を実行した瞬間に分母がゼロだった」ことが100%の確証をもって証明されます。
—
4. 現場のプロが教える再発防止とデバッグの極意
こうしたゼロ除算やデータ例外(S0C7など)に直面したとき、単に「データを直して再実行」して終わりにしてはいけません。基幹システムの信頼性を守るために、以下のプラクティスを必ずチームのコーディング標準に組み込んでください。
1. 入力値検証(バリデーション)の徹底
外部からインプットされる数値データは、計算に使う前に必ず `WHILE` や `IF` でゼロでないこと、かつ有効な数値(NUMERICビルトイン関数など)であるかを検証するルーチンを通すこと。
2. ONユニットの適切なスコープ管理
予期せぬ例外に対して全体を一網打尽にするだけでなく、トランザクション単位で `ON ZERODIVIDE` を適切に配置し、エラーログを出力した上で安全にリカバー(スキップ)する設計思想を持つこと。
3. コンパイルリストの保管義務
「ソースがあるからリストはいらない」という若手がたまにいますが、メインフレーム開発においてコンパイルリストはダンプ解析の羅針盤です。ソースの行番号と機械語のオフセットの対応表がなければ、高度な障害解析はただの勘当になってしまいます。必ずソースとセットでリポジトリや保管庫に残すこと。
メインフレームのPL/Iは古くからある言語ですが、その挙動は極めてロジカルで、機械は嘘をつきません。ダンプとPSWが示す真実を正しく読み解くスキルは、どんなにオープン系やクラウドの技術が進んでも、基幹システムを支えるプロフェッショナルにとって最強の武器であり続けます。
さあ、冷たいコーヒーでも飲んで、次のバッチサイクルの監視に戻りましょうか。
