おい、最近夜間バッチのログを見ていて、突然の`S0C4`アベンドに冷や汗をかいたことはないか?
「またか……」とストレージダンプ(SYSUDUMPやCEEDUMP)を開き、見慣れないレジスタの羅列と格闘する。メインフレームの保守メンバなら、誰もが一度は通る洗礼だな。
特にPL/I環境において、文字列操作の代表格である`SUBSTR`関数の使いどころを誤ると、容赦なくシステムエラーの王様であるS0C4(保護例外:Addressing Exception)を引き起こす。
今回は、なぜ`SUBSTR`の引数指定ミスが致命的なS0C4を招くのか、そして現場でどうやって瞬時にオフセットを特定し、二度と再発させない堅牢なコードを書くべきか、俺の経験を総動員して叩き込んでやろう。心して読め。
—
1. なぜPL/IのSUBSTRはS0C4を引き起こすのか?
PL/Iという言語は、C言語のように「プログラマが全てを管理しろ、配列外アクセスなんて知ったこっちゃない」という野蛮な言語ではないが、かといって近年のスクリプト言語のように親切に境界チェックをデフォルトで全方位してくれるわけでもない。特にパフォーマンスを重視するメインフレームの基幹バッチでは、コンパイラオプションのチューニング次第で、境界チェックが甘くなることもある。
S0C4アベンドのメカニズム
`SUBSTR(ターゲット文字列, 開始位置, 長さ)` を実行した際、以下の条件エンティティのいずれかが崩壊していると、CPUは「存在しないメモリ領域(またはアクセス権限のない領域)」を指し示し、ハードウェア割り込みを発生させる。これがS0C4の正体だ。
1. 開始位置(Offset)が0以下、あるいは定義された文字列長を大幅に超えている。
2. 取得長(Length)の指定が長すぎて、宣言された変数の領域をはみ出してポインタが暴走する。
3. `DCL`(宣言)時の文字列長指定と、実際のデータ長(特别是不変長と可変長 Varying の混同)が噛み合っていない。
PL/IにはC言語のような「予約語」という概念が非常に少なく、識別子の自由度が高い反面、コーディング規約がしっかりしていないと、こうしたポインタやオフセットの計算ミスを見逃しやすい。
—
2. 現場で役立つ!実用的なPL/Iコードとアンチパターン
百聞は一見に如かずだ。まずは、実際にやってしまいがちな「危なっかしいコード」と、それを如何にして堅牢に書き直すかを見比べてみよう。
以下のサンプルは、VSAMから読み込んだレコード(固定長)に対して、特定の業務コードを切り出すバッチ処理の一コマを想定している。
【アンチパターン】マジックナンバーと甘い文字数指定による危ういコード
1
/ ————————————————– /
/ 危険なコード例:不適切なSUBSTR指定 /
/ ————————————————– /
DCL IN-RECORD CHAR(200); / 入力レコード 200バイト /
DCL WORK-CODE CHAR(10); / 作業用コード /
DCL IDX FIXED BIN(31) INIT(0);
/ ここで外部からの不正データによりIDXが狂う、あるいは誤った計算をする /
IDX = 195;
/ 200バイトの領域に対して 195 + 15 = 210バイト目を参照しようとする /
WORK-CODE = SUBSTR(IN-RECORD, IDX, 15);
/ -> ここでバッファオーバーランを起こし、S0C4アベンドの餌食になる! /
【模範解答】BUILTIN関数の活用と安全な境界チェック
実務では、`LENGTH` や `MAX`、そして `MIN` などのビルトイン関数を賢く使い、絶対にメモリ境界を越えない防衛的プログラミング(Defensive Programming)を徹底すべきだ。
1
/ ————————————————– /
/ 堅牢なコード例:ビルトイン関数と事前チェック /
/ ————————————————– /
SAFE-SUBSTR-SAMPLE: PROC OPTIONS(MAIN);
DCL IN-RECORD CHAR(200) INIT(‘ ‘);
DCL WORK-CODE CHAR(10) VARYING;
DCL REC-LEN FIXED BIN(15);
DCL TARGET-POS FIXED BIN(15);
DCL TARGET-LEN FIXED BIN(15);
/ 組み込み関数の明示的宣言 /
DCL (LENGTH, SUBSTR, MIN, MAX) BUILTIN;
/ 1. レコード長および取得パラメータの定義 /
REC-LEN = LENGTH(IN-RECORD);
TARGET-POS = 195;
TARGET-LEN = 10;
/ 2. 境界チェック(防衛的ロジック) /
IF TARGET-POS > 0 & TARGET-POS <= REC-LEN THEN DO;
/ 取得長がレコードの残り長さを超えないようMINでガードする /
DCL ALLOW-LEN FIXED BIN(15);
ALLOW-LEN = MIN(TARGET-LEN, (REC-LEN - TARGET-POS + 1));
WORK-CODE = SUBSTR(IN-RECORD, TARGET-POS, ALLOW-LEN);
DISPLAY('正常取得コード: ' || WORK-CODE);
END;
ELSE DO;
DISPLAY('エラー: 指定された開始位置がレコード範囲外です。POS=' || TARGET-POS);
/ 独自の異常終了処理へ分岐 /
SIGNAL ERROR;
END;
END SAFE-SUBSTR-SAMPLE;
どうだ?これなら、たとえ入力データのオフセットが狂っていたとしても、`MIN` 関数が物理的なメモリ境界を守り抜くため、S0C4でジョブが突然死する最悪の事態を防ぐことができる。
---
3. ダンプ解析:S0C4発生時のオフセット特定手順
それでもなお、他システムから連携されたレガシーなソースや、巨大な既存プログラムの改修漏れでS0C4を踏んでしまったときの手順を授けよう。慌てずに以下のステップを踏むんだ。
ステップ1:CEEDUMPまたはSYSUDUMPからPSW(Program Status Word)を抑える
ダンプリストを開き、アベンド発生時の PSW(プログラム・ステータス・ワード) のアドレスを確認する。
大抵の場合、最後の4バイト〜8バイトに、例外が発生した瞬間のマシン語命令のアドレスが残っている。
ステップ2: コンパイラリスト(Listing)のクロスマップと突合する
PL/Iのコンパイル時に出力されるリスト(セクション・マップおよびストレージ・マップ)を取り出し、PSWのアドレスと対応するプログラムのセクションオフセットを比較する。
- ヒント: `STMT`(ステートメント番号)がダンプのCEE3204Sなどのメッセージから特定できればラッキーだ。「どの行の、どの変数代入でコケたか」が一発で分かる。
ステップ3: 該当行のSUBSTRの引数を疑う
ステートメント番号が特定できたら、ソースコードの該当行を見る。
- 変数の定義長を超えた固定値が第2、第3引数に入っていないか?
- `BASED` 変数やポインタ (`POINTER`) を使った構造体マップで、領域そのものが割り当てられていない(アロケートされていない、あるいはアンアロケートされた)のに `SUBSTR` を叩いていないか?
特に `BASED` 変数とポインタを組み合わせて使っている現場は要注意だ。メモリの割り当て忘れ(`ALLOCATE` 漏れ)によるS0C4は、PL/Iプログラマの永年の宿敵だからな。
—
4. ベテランからのアドバイス:保守性を高めるコーディング標準
最後に、今後のバッチ改修で後輩たちが路頭に迷わないための「現場の鉄則」をいくつか残しておこう。
1. マジックナンバーの排除:
コード中に直接 `SUBSTR(REC, 150, 20)` のように数字を直書きするな。COPY句や構造体定義(DECLARE)を使い、フィールドのオフセットと長さはシンボリックに管理しろ。
2. ON-UNITによる例外捕捉の準備:
予期せぬデータ異常に対して、`ON ERROR` や `ON SUBSCRG`(添字・範囲エラー用のONユニット。※コンパイラオプション依存)を適切に配置し、単なるS0C4のハードウェア異常ではなく、分かりやすいユーザ異常コード(Uxxxxなど)でABENDさせる設計を取り入れること。システムが優雅に(Graceful)落ちるのもプロフェッショナルの仕事だ。
3. コンパイルオプションの確認:
テスト環境や本番移行時は、可能な限りデバッグオプションや境界チェックが有効になる設定(例: `STGCHCK`, `CHECK`など、プロジェクトの標準規約に従う)でビルドし、早期にバグを炙り出すことだ。
メインフレームの寿命は長い。そして私たちが書くPL/Iのコードもまた、次の世代へと受け継がれていく。
「なぜここでこの長さを指定しているのか」を後世のエンジニアが理解できるような、美しく、そして堅牢なコードを書き続けてくれ。健闘を祈る!
