こんにちは!IBMメインフレームの世界へようこそ。
JavaやCOBOLといったモダン、あるいは従来型のビジネス言語の経験がおありの方にとって、PL/I(ピーエルアイ)という名前を聞くだけで「なんだか難解そうだな……」「レガシーの要塞みたいで怖そう……」と身構えてしまうかもしれませんよね。
でも、どうぞ安心してください。どんなに古い仕組みであっても、基本のルールを一つずつ紐解いていけば、決して恐ろしいものではありません。今日は、メインフレームの運用現場でエンジニアたちの冷や汗を誘う、あの有名なエラー「CEE3207S データ例外(S0C7)」について、優しく、そして深く掘り下げてお話ししていきたいと思います。
—
1. そもそもPL/Iの変数名には「予約語」がないって本当?
本題に入る前に、PL/Iという言語のちょっとユニークで、JavaやCOBOL出身者が思わず二度見してしまう仕様について少し触れておきましょう。
JavaやCOBOLには、`if` や `class`、`MOVE` といった「予約語(言語の命令として独占されている単語)」がたくさんありますよね。そのため、変数名にうっかり予約語を使ってしまうとコンパイルエラーになってしまいます。
しかし、PL/Iには「予約語」という概念が基本的に存在しません。
例えば、PL/Iではこんな信じられないような変数宣言ができてしまいます。
1
/ PL/Iの奇妙で懐の深い変数宣言の例 /
DCL IF CHAR(10); / 「IF」という名前の文字型変数 /
DCL THEN FIXED DEC(5); / 「THEN」という名前の数値型変数 /
DCL ELSE FLOAT; / 「ELSE」という名前の浮動小数点変数 /
「えっ、じゃあコンパイラはどうやって `IF` が条件分岐なのか、変数なのかを見分けているの?」と思いますよね。
これはPL/Iのコンパイラが「文脈(Context)」を incredibly 賢く読み取っているからです。「あ、この位置にある `IF` は後に続く条件式を判定する構文だな」「ここは代入文の左辺だから変数の `IF` だな」と、前後の文脈で判断してくれています。
初心者泣かせではありますが、「プログラマの自由度を極限まで高めよう」とした当時の設計思想の現れです。怖がらなくて大丈夫ですよ。
—
2. 恐怖の「S0C7(CEE3207S)」!データ例外の正体とは?
さて、ここからが本日のメインテーマです。
バッチ処理が突如として異常終了し、ログ出力(Sysout)を覗いたときに、以下のようなメッセージを見つけて背筋が凍ったことはありませんか?
> CEE3207S 割り込みが発生しました。システム・アブダクション: S0C7
この `S0C7(エスゼロシーナナ)` または `CEE3207S` こそが、メインフレームの歴史において数々のエンジニアを夜な夜な悩ませてきた「データ例外(Data Exception)」です。
Javaで言えば `NumberFormatException` や `NullPointerException` のようなものですが、インパクトはその比ではありません。プログラムがその場で即死します。
パック10進数(COMP-3)ってどんなデータ?
このエラーを引き起こす主役が、「パック10進数(PL/Iでは `FIXED DECIMAL`、COBOLでは `COMP-3` と呼ばれる形式)」です。
メインフレームでは、数値を効率よく、かつ10進数の誤差なく計算するために、1バイト(8ビット)のなかに2つの数字(10進数の桁)をギュッと詰め込むパッキング形式を使います。さらに、最後の4ビット(半バイト)には、符号(プラスなら `C` や `F`、マイナスなら `D` など)が入る仕組みになっています。
- 正しいパックデータの例(数値の「1234」):
ストレージ上では `12 34 3F` のように格納されます(最後の `F` はプラス符号の意味)。
人間が見れば「1234だね」と分かりますが、もしこれがファイル破損や文字化け、あるいは未初期化の領域(スペース= `40` や、ゼロクリア漏れの `00`)のまま計算に使われたらどうなるでしょうか?
- 不正なパックデータの例:
末尾の符号部分に、うっかり文字のスペースである `40` や、単なる文字の `A` なんかが紛れ込んでしまったとします(例:`12 34 3A`)。
CPUがこのデータを読み込んで「さあ足し算するぞ!」と演算器に放り込んだ瞬間、CPUはこう叫びます。
「おい!最後の4ビットに正しい符号(プラス・マイナス)が入ってないぞ!これじゃ計算できない!」
これが、S0C7データ例外のメカニズムです。
—
3. 実務で遭遇するPL/Iコードとバグの芽
実際に、PL/Iでどのようなコードを書いたときにこれが起こるのか、簡単なサンプルを見てみましょう。
1
——————————————————————
- パック10進数を使った危険な計算処理のサンプル
——————————————————————
CALC_SAMPLE: PROC OPTIONS(MAIN);
/ 宣言部 /
DCL WS_OLD_AMT FIXED DEC(7,2) INIT(0); / 正常な金額項目 /
DCL WS_ADD_AMT FIXED DEC(7,2); / 加算する金額(未初期化!) /
DCL WS_TOTAL FIXED DEC(9,2); / 合計金額 /
/ ファイルからデータを読み込んだつもり…だが、 /
/ 外部ファイル側がブランク(空白=’40’x)だったとする /
WS_ADD_AMT = UNSP_READ_DATA(); / 外部からの不正データ想定 /
/ ここで計算を実行! /
/ 未初期化、あるいはスペースが入った変数同士で演算 /
WS_TOTAL = WS_OLD_AMT + WS_ADD_AMT; / ←ここでドン!S0C7が発生 /
PUT SKIP LIST (‘合計金額は: ‘, WS_TOTAL);
END CALC_SAMPLE;
Javaであれば、変数が `null` であれば `NullPointerException` が安全にキャッチできますし、COBOLでもコンパイラオプションやNUMPROC設定によっては救われるケースがあります。しかし、PL/Iで数値項目が正しく初期化されていなかったり、外部から文字データが混入したまま `FIXED DECIMAL` として演算に使うと、容赦なくハードウェア割り込み(S0C7)が発生します。
—
4. ダンプ(CEEDUMP)から原因箇所を特定する方法
もし本番稼働中にこのエラーを踏んでしまっても、慌てず騒がず、以下のステップで原因を特定していきましょう。
1. SYSOUTのCEEDUMPを確認する
エラー発生時に出力される `CEEDUMP` またはスナップダンプを開きます。
2. PSW(Program Status Word)のアドレスを探す
ダンプの冒頭にあるレジスタ情報やPSWから、異常終了を引き起こした機械語命令のメモリアドレスを見つけます。
3. 周辺の変数を逆引きする
PL/Iのリストティング(コンパイルリスト)と突合させ、「どの変数を処理している最中にその命令が実行されたか」を特定します。大抵の場合、変数の値が `40404040`(スペース)や `0000000F` 以外のゴミデータになっているレジスタが見つかります。
対策と予防:怖くない、こう書けばいいんです!
原因が分かれば対策はシンプルです。現場の先輩たちが実践している鉄則をご紹介します。
- 変数は必ず初期化する
`INIT(0)` を忘れないこと。宣言時にゼロで満たしておくだけで、未初期化に起因するS0C7の8割は防げます。
- 外部からの入力は必ず検証(バリデーション)する
ファイルや画面から受け取ったデータが、本当に正しい数字(パック形式に変換可能な文字)であるかを、計算前に `TEST` ビルトイン関数などでチェックするか、文字型(CHAR)として受け取ってから安全に変換する癖をつけましょう。
—
まとめ
いかがでしたでしょうか?
PL/Iの「予約語がない自由な世界」や、パック10進数が引き起こす「S0C7データ例外」の裏側を知ると、レガシーシステムの裏で動いているコンピュータの息遣いが少し感じられたのではないでしょうか。
エラーコードやダンプの見栄えは少し無機質で圧倒されるかもしれませんが、中身を紐解けば「あ、CPUが計算できなくて怒ってるんだな」と優しく理解できるようになります。
基幹システムのモダナイゼーションや保守において、この知識があなたの心強い武器になることを願っています。それでは、次回のレガシー探訪もお楽しみに!
