💡

PL/Iで学ぶメモリ管理の落とし穴:FREEと空間分割で防ぐシステム障害

に公開

はじめに

こんにちは。最近メインフレーム技術に興味を持ち、PL/Iの学習を始めた初学者です。

私がこの言語に最初に触れて「面白い!」と感じたのは、ON条件というエラーハンドリングの仕組みでした。特に、金融計算で致命的となる**「桁溢れ(FIXEDOVERFLOW)」**を検知できる堅牢さに、基幹システムで使われ続ける理由の一端を見た気がします。

この発見をきっかけに、過去の金融システムの重大障害を調べてみると、その多くが「桁溢れ」や「メモリ管理」といった、一見地味なテーマに起因することを知りました。しかし、私には実機環境がありません。どうすれば、この技術の重要性をリアルに学ぶことができるだろうか?

そう考えた末、**「公開されている言語仕様と過去の障害事例を照らし合わせ、コードレベルで思考実験をしてみよう」**と思い立ちました。

この記事は、その学習プロセスを虫食い問題形式でアウトプットした、私の調査記録です。実機がない私でも、過去の事件から「なぜバグが起きたのか」「自分ならどう防ぐか」を考えることで、多くの学びがありました。

同じようにメインフレーム技術を学ぶ方の参考になれば幸いです。また、内容に誤りや改善点があれば、ぜひコメントでご指摘いただけると嬉しいです。

Question 1:金融計算の第一歩「桁溢れ」を防ぐ

私が最初に学んだFIXED DECIMAL型。これは金額計算などで精度を保証する強力な型ですが、一つ間違えると「桁溢れ」というバグを生みます。このコードでは、予期せぬ桁溢れが発生します。どうすればそれを検知し、安全に処理できるでしょうか?

/* 勘定残高を計算するプログラム */
PROCEDURE OPTIONS(MAIN);
   DECLARE BALANCE   FIXED DECIMAL(10, 2) INITIAL(99999999.99);
   DECLARE DEPOSIT   FIXED DECIMAL(10, 2) INITIAL(      10.00);

   /* --- ここに防御コードを追加したい --- */

   BALANCE = BALANCE + DEPOSIT; /* ← ここで桁溢れが発生! */

   PUT LIST ('Final Balance:', BALANCE);
END;

<details>
<summary>私の考察とコード解説</summary>

桁溢れ(FIXEDOVERFLOW)とは?

FIXED DECIMAL(10, 2)は、小数点以下2桁、全体で10桁までを表現できる型です。
99,999,999.9910.00 を足すと 100,000,009.99 となり、全体の桁数が11桁になってしまいます。このとき、変数の器から数値が溢れてしまうのが「桁溢れ」だと理解しました。これを放置すると、計算結果が狂い、金融システムでは致命的な問題になります。

PL/Iの賢い防御策:ON FIXEDOVERFLOW

PL/Iには、この桁溢れを検知するためのON FIXEDOVERFLOW(短縮形はON FOFL)という素晴らしい仕組みがあることを知りました。これを使えば、桁溢れが発生した瞬間に特定の処理へ分岐させることができます。

PROCEDURE OPTIONS(MAIN);
   DECLARE BALANCE   FIXED DECIMAL(10, 2) INITIAL(99999999.99);
   DECLARE DEPOSIT   FIXED DECIMAL(10, 2) INITIAL(      10.00);

   /* --- 防御コード --- */
   ON FIXEDOVERFLOW BEGIN;
      PUT SKIP LIST ('Error: FIXEDOVERFLOW occurred!');
      STOP;
   END;
   /* --------------- */

   BALANCE = BALANCE + DEPOSIT; /* ← 桁溢れを検知し、ONブロックが実行される */

   PUT LIST ('Final Balance:', BALANCE); /* この行は実行されない */
END;

このON条件という考え方は、プログラム全体にセーフティネットを張るようで、非常に堅牢な設計思想だと感じました。この学びが、次のテーマである「メモリ管理」への興味に繋がっていきます。

</details>

調査の深掘り:メモリ管理の重要性

桁溢れのような単純な計算ミスだけでなく、より複雑で発見しにくいのが「メモリ管理」のバグです。

  • 全銀ネット システム障害(2023年): メモリ上のインデックステーブル破損が原因とされています。メモリ領域の設計・テストの難しさが伺えます。
  • Knight Capital社 事件(2012年): 古いコードの再利用ミスから、意図しないメモリアクセスが発生し、約4億ドルもの損失に繋がったと言われています。

これらの事故は、メモリというリソースを正しく確保し、使い、解放する原則の難しさを示しています。このテーマを深掘りするため、PL/Iのメモリ管理についてコードを書きながら考えてみることにしました。

Question 2:野良ポインターの恐怖を追体験する

ここからはメモリの話です。最も古典的で危険な「野良ポインター」。Knight Capital社の事件も、突き詰めれば意図しないメモリアクセスが引き金だったと言われています。このコードのどこに、そしてなぜ危険が潜んでいるのでしょうか。

DECLARE PTR POINTER;
DECLARE 1 WORK_AREA BASED(PTR),
          2 DATA_FIELD CHAR(10);

/* --- 処理A --- */
ALLOCATE WORK_AREA SET(PTR);
PTR -> DATA_FIELD = 'TASK_A_DATA';

/* --- 処理B --- */
FREE WORK_AREA;

/* --- 処理C --- */
PTR -> DATA_FIELD = 'TASK_B_DATA'; /* ⚠️ ここが危険? */

<details>
<summary>私の考察とコード解説</summary>

危険だと考えた理由

FREE命令は、ポインターが指す先のメモリ領域を解放するだけで、ポインター変数自体を無効にしてはくれない、という点がポイントのようです。

FREE実行後も、PTR変数は解放済み領域のアドレスを持ち続けてしまいます。これが**野良ポインター(Dangling Pointer)**と呼ばれる状態だと理解しました。

この状態を、自分なりに図で整理してみました。

解放されたメモリ領域は、すぐに別のプログラムが使い始める可能性があります。そこに書き込みを行う処理Cは、他のプログラムのデータを破壊したり、最悪の場合システム全体をクラッシュさせたりする、非常に危険な操作だと考えられます。

どうすれば防げるか?

解放処理とポインターの無効化は、必ずセットで行うべきだと学びました。

FREE WORK_AREA;
PTR = NULL(); /* ポインターを明示的に無効化する */

/* これで、以降のアクセスはエラーとして検知できる */
IF PTR ^= NULL() THEN
   PTR -> DATA_FIELD = 'SAFE_DATA';

</details>

Question 3:解放漏れを防ぐPL/Iの賢い仕組み

次に、解放漏れ(メモリリーク)を防ぐためのPL/Iの賢い仕組み CONTROLLED 属性について調べました。これを使えば安全性が高まるはずですが、本当でしょうか。

設問A

手動でALLOCATEでき、かつブロックを抜けるときに自動でFREEしてくれる便利なストレージ属性は何でしょうか?

<details>
<summary>解答</summary>
CONTROLLED
</details>

設問B

このCONTROLLED属性を使えば、FREEを書き忘れてもメモリリークは起きないのでしょうか?以下のコードを例に考えてみます。

PROCEDURE OPTIONS(MAIN);

   BEGIN; /* ブロック開始 */
      DECLARE BANK_NAMES CONTROLLED CHARACTER(20);
      
      ALLOCATE BANK_NAMES;
      BANK_NAMES = '■■■■ BANK';
      
      /* FREEを書き忘れてしまった! */
      
   END; /* ← ここで何が起きる? */
   
END PROCEDURE;

<details>
<summary>私の考察とコード解説</summary>

言語リファレンスを調べたところ、CONTROLLED属性を持つ変数は、宣言されたブロックのENDステートメントに到達した時点で、システムが自動的に解放してくれるようです。

つまり、上記のコードでは、プログラマがFREEを忘れてもEND;で自動的に解放されるため、メモリリークは発生しません。手動確保の柔軟性と自動解放の安全性を両立した、非常に優れた仕組みだと感じました。

</details>

Question 4:GOTOがメモリ管理を複雑にする罠

CONTROLLED属性は安全に見えます。しかし、レガシーコードで頻出するGOTO文と組み合わせると、どうなるでしょうか。全銀ネットの障害報告書を読んで、エラーハンドリングの複雑さが気になりました。このコードには、一見すると安全に見えて、実はメモリリークの危険が潜んでいます。

PROCEDURE OPTIONS(MAIN);
LABEL_1: ;

   BEGIN;
      DECLARE TEMP_DATA CONTROLLED;
      ALLOCATE TEMP_DATA;
      
      /* ... 何らかの処理 ... */
      
      IF error_condition THEN 
         GOTO LABEL_1; /* ⚠️ ブロックを飛び越える */
      
      FREE TEMP_DATA;
   END;

END PROCEDURE;

<details>
<summary>私の考察とコード解説</summary>

懸念事項

このコードの罠は、GOTO文がEND;ステートメントを飛び越えてしまう点にあると考えました。

CONTROLLED属性の自動解放が機能するのは、あくまでEND;に到達した場合です。GOTO LABEL_1;が実行されると、ブロックの終端を通過しないため、自動解放は行われません。さらに、FREE TEMP_DATA;も実行されないため、確保されたメモリは解放されないまま宙に浮いてしまいます。

これを繰り返すと、深刻なメモリリークを引き起こし、最終的にはシステム全体のメモリ不足に繋がる危険があります。

どうすれば防げるか?

やはり、可能な限りGOTOを避けるべきだと感じました。どうしても必要な場合は、脱出する全ての経路でFREEが実行されることを保証する設計が必要なようです。
ON ERRORという例外処理の仕組みもあるので、そちらでエラー時のクリーンナップ処理を一元化するのが現代的なアプローチなのかもしれません。

</details>

思考実験:もし自分が担当者だったら

ここからは、調査した知識を元に、実際の障害事例を想定したコードを書いてみる思考実験です。もし自分がこのシステムの担当者だったら、どういう防御コードを書くべきか考えてみました。

シナリオ:全銀ネット障害から学ぶテーブル管理

メモリ不足でALLOCATEが失敗する状況を想定し、システムを安全に停止させるコードを考えます。ここでもON条件が活躍しそうです。

/* 空欄を埋めて、メモリ確保エラーを捕捉するコードを完成させる */
%PROCESS OPTIONS(MAIN);
MAIN: PROCEDURE OPTIONS(MAIN);
   /* ... 変数宣言は省略 ... */
   DECLARE BANK_NAMES DYNAMIC BASED(PTR) CHARACTER(20) DIM(1000);
   DECLARE PTR POINTER;

   _______ STORAGE _______;
      GOTO ERROR_HANDLER;
   END;

   _______ BANK_NAMES SET(PTR);
   /* ... テーブル構築処理 ... */
   _______ BANK_NAMES;
   RETURN;

ERROR_HANDLER:;
   PUT SKIP LIST ('Storage Error: Memory allocation failed');
   _______;
END MAIN;

<details>
<summary>私が考えたコード</summary>

%PROCESS OPTIONS(MAIN);
MAIN: PROCEDURE OPTIONS(MAIN);
   /* ... 変数宣言は省略 ... */
   DECLARE BANK_NAMES DYNAMIC BASED(PTR) CHARACTER(20) DIM(1000);
   DECLARE PTR POINTER;

   ON STORAGE DO;
      GOTO ERROR_HANDLER;
   END;

   ALLOCATE BANK_NAMES SET(PTR);
   /* ... テーブル構築処理 ... */
   FREE BANK_NAMES;
   RETURN;

ERROR_HANDLER:;
   PUT SKIP LIST ('Storage Error: Memory allocation failed');
   STOP;
END MAIN;

ポイント: ON FIXEDOVERFLOWと同じON構文の仲間であるON STORAGE条件でメモリ確保失敗を検知し、安全なエラー処理ルーチンに飛ばすことで、システムの暴走を防ぐことを意図しました。

</details>

まとめ:初学者が学んだPL/Iの安全設計思想

今回の調査と思考実験を通じて、実機がなくとも多くの学びがありました。特に、私が重要だと感じたポイントは以下の通りです。

  1. ON条件で防御する: ON FIXEDOVERFLOWON STORAGEを使い、プログラム全体に「エラーが起きても大丈夫なように」セーフティネットを張る。
  2. 確保と解放は必ずペアで考える: ALLOCATEを書いたら、その場で対応するFREEを書く癖をつける。
  3. FREEの後はNULL(): 解放したポインターは、すぐに無効化して「野良」にしない。
  4. GOTOの先のメモリを意識する: GOTOでジャンプする時、「確保したままのメモリはないか?」と自問する。

おわりに

最初に学んだON FIXEDOVERFLOWから始まった今回の調査。机上の学習ではありますが、PL/Iの言語仕様と過去の障害事例を繋ぎ合わせることで、金融システムを支える技術の奥深さと責任の重さを垣間見ることができた気がします。実機がなくても、公開情報からこれだけの学びが得られるというのは大きな発見でした。

この記事が、私と同じようにメインフレーム技術を学ぶ誰かの、思考のきっかけになれば嬉しいです。


この記事は、あくまで初学者が公開情報を元に調査・考察したものです。内容の正確性については、専門家のレビューをいただけると幸いです。

#zennfes2025free

Discussion