【PL/I学習|初心者向け】PL/Iで学ぶ「バイトスワップ」ハック:POSITIONとDEFINEDの魔法

導入

メインフレームの世界では、データの「並び方」が非常に重要になることがあります。特に、数値データをバイト単位で操作したり、異なるシステム間でデータをやり取りしたりする際に、「エンディアン」という概念が関わってきます。今回は、PL/Iの強力な機能である `POSITION` と `DEFINED` を使って、4バイトの数値を1バイトずつに分解(または再構成)する「バイトスワップ」というテクニックを、初心者の方にも分かりやすく解説します。このテクニックを理解することで、低レベルなデータ操作の柔軟性を肌で感じることができます。

基礎知識

エンディアンとは?

コンピュータが数値をメモリに格納する際、バイトの並び順には2つの方式があります。

  • ビッグエンディアン (Big-Endian): 最上位バイト(最も大きな値を持つバイト)が先頭に格納されます。人間が数値を読む順番に近いイメージです。
  • リトルエンディアン (Little-Endian): 最下位バイト(最も小さな値を持つバイト)が先頭に格納されます。

例えば、4バイトの数値 `0x12345678` を考えてみましょう。

  • ビッグエンディアンでは、メモリ上は `12 34 56 78` の順に格納されます。
  • リトルエンディアンでは、メモリ上は `78 56 34 12` の順に格納されます。

PL/Iなどのメインフレーム言語では、通常、システムがデフォルトのエンディアン方式を採用していますが、プログラムによっては明示的にバイトの並び順を意識したり、変換したりする必要が出てきます。

DEFINED と POSITION

PL/Iの `DEFINED` 属性は、ある領域のメモリを、別のデータ構造として「再定義」するための強力な機能です。これにより、同じメモリ領域を異なる視点から参照できるようになります。

`POSITION` 属性は、`DEFINED` 属性と組み合わせて使われ、定義されたデータ構造が、元のデータ領域のどこから始まるかを指定します。

これらの属性を組み合わせることで、まるで「メモリの特定の場所を、全く別の型や構造として解釈する」ようなことが可能になります。今回のバイトスワップでは、この「メモリの再解釈」の力を利用します。

実装/解決策:バイトスワップの実現

4バイトの整数 (`FIXED BIN(31)`) を、4つの1バイトの文字 (`CHAR(1)`) の配列として扱う方法を見てみましょう。これは、あたかも4バイトの数値が4つの独立したバイトであるかのように扱うことを意味します。

まず、4バイトの数値を格納するための領域を宣言します。ここでは `FULL_INT` と名付けましょう。
次に、その `FULL_INT` の領域を、4つの1バイト文字の配列として「定義」する領域を宣言します。これが `BYTE_ARR` です。

1
DCL FULL_INT FIXED BIN(31); / 4バイトの固定小数点2進数 /
DCL BYTE_ARR(4) CHAR(1) DEFINED(FULL_INT); / FULL_INTと同じメモリ領域を4つのCHAR(1)として再定義 /

この宣言により、`FULL_INT` に格納された4バイトの数値データは、`BYTE_ARR(1)`、`BYTE_ARR(2)`、`BYTE_ARR(3)`、`BYTE_ARR(4)` という4つの要素に分割されてアクセスできるようになります。

例えば、`FULL_INT` に `X’12345678’` という値が格納されているとします。

  • システムがビッグエンディアンの場合、メモリ上は `12 34 56 78` と格納されていると想定されます。
  • `BYTE_ARR(1)` には `X’12’` が格納されます。
  • `BYTE_ARR(2)` には `X’34’` が格納されます。
  • `BYTE_ARR(3)` には `X’56’` が格納されます。
  • `BYTE_ARR(4)` には `X’78’` が格納されます。
  • システムがリトルエンディアンの場合、メモリ上は `78 56 34 12` と格納されていると想定されます。
  • `BYTE_ARR(1)` には `X’78’` が格納されます。
  • `BYTE_ARR(2)` には `X’56’` が格納されます。
  • `BYTE_ARR(3)` には `X’34’` が格納されます。
  • `BYTE_ARR(4)` には `X’12’` が格納されます。

このように、`DEFINED` 属性を使うことで、同じメモリ領域を異なるデータ型として参照し、バイト単位での操作が容易になります。

サンプルプログラム

ここでは、4バイトの数値を定義し、それをバイト配列として表示する簡単なPL/Iプログラムを作成します。

1
/ TEST_BYTE_SWAP: DEFINEDとPOSITIONを使ったバイトスワップのデモ /
TEST_BYTE_SWAP: PROC OPTIONS(MAIN);

/ 4バイトの整数を格納するための変数 /
DCL MY_NUMBER FIXED BIN(31);

/ MY_NUMBERと同じメモリ領域を、4つの1バイト文字の配列として再定義 /
/ DEFINED属性は、MY_NUMBERのメモリ領域を参照することを意味します。 /
/ CHAR(1)は1バイトの文字型です。 /
/ (4)は配列のサイズ(要素数)を指定します。 /
DCL MY_BYTES(4) CHAR(1) DEFINED(MY_NUMBER);

/ 処理の開始 /
PUT SKIP LIST(‘— バイトスワップ デモ —‘);

/ 4バイトの整数に値を代入 /
/ 例として、ビッグエンディアンで 0x12345678 を表現する値を設定 /
MY_NUMBER = X’12345678’;
PUT SKIP LIST(‘代入した4バイト整数 (16進数):’, HEX(MY_NUMBER));

/ DEFINED属性を使って、整数をバイト配列としてアクセスし、表示 /
/ MY_BYTES(1)は、MY_NUMBERの最も左(または最も右、エンディアンによる)のバイト /
PUT SKIP LIST(‘バイト配列としてアクセス (16進数):’);
PUT SKIP LIST(‘ Byte 1:’, HEX(MY_BYTES(1)));
PUT SKIP LIST(‘ Byte 2:’, HEX(MY_BYTES(2)));
PUT SKIP LIST(‘ Byte 3:’, HEX(MY_BYTES(3)));
PUT SKIP LIST(‘ Byte 4:’, HEX(MY_BYTES(4)));

/ 別の例: リトルエンディアンで 0x12345678 を表現する値を設定 /
/ これは、リトルエンディアンシステムで 0x78563412 という値に相当します。 /
MY_NUMBER = X’78563412′;
PUT SKIP LIST(‘ ‘); / 空行 /
PUT SKIP LIST(‘別の4バイト整数 (16進数):’, HEX(MY_NUMBER));

PUT SKIP LIST(‘バイト配列としてアクセス (16進数):’);
PUT SKIP LIST(‘ Byte 1:’, HEX(MY_BYTES(1)));
PUT SKIP LIST(‘ Byte 2:’, HEX(MY_BYTES(2)));
PUT SKIP LIST(‘ Byte 3:’, HEX(MY_BYTES(3)));
PUT SKIP LIST(‘ Byte 4:’, HEX(MY_BYTES(4)));

/ プログラム終了 /
PUT SKIP LIST(‘— デモ終了 —‘);

RETURN;

END TEST_BYTE_SWAP;

実行結果の例 (ビッグエンディアンシステムを想定)

— バイトスワップ デモ —
代入した4バイト整数 (16進数): 12345678
バイト配列としてアクセス (16進数):
Byte 1: 12
Byte 2: 34
Byte 3: 56
Byte 4: 78

別の4バイト整数 (16進数): 78563412
バイト配列としてアクセス (16進数):
Byte 1: 78
Byte 2: 56
Byte 3: 34
Byte 4: 12
— デモ終了 —

このサンプルでは、`MY_NUMBER` に値を代入する際に `X’…’` を使用していますが、これは16進数リテラルです。`HEX()` 関数は、数値を16進数文字列として表示するために使用しています。

応用・注意点

エンディアン変換の応用

この `DEFINED` 属性を使ったテクニックは、異なるエンディアン方式を持つシステム間でデータをやり取りする際に、エンディアン変換を「宣言レベル」で行うのに役立ちます。

例えば、ビッグエンディアンのシステムで作成されたデータを、リトルエンディアンのシステムで読み込む場合を考えてみましょう。

1. ビッグエンディアンのシステムで、4バイトの数値を `X’12345678’` として保存します。
2. リトルエンディアンのシステムで、このデータを読み込みます。
3. `DEFINED` 属性を使って、読み込んだ4バイトの領域を `CHAR(1)` の配列として宣言します。
4. リトルエンディアンシステムでは、メモリ上は `78 56 34 12` と読み込まれます。
5. もし、このデータが本来ビッグエンディアンで `12 34 56 78` であることを知っていれば、`MY_BYTES(1)` から `MY_BYTES(4)` を順番に読み込み、それを別の4バイト整数変数に格納する際に、バイトの順番を入れ替える(例えば、`MY_BYTES(4)` を最初のバイトに、`MY_BYTES(3)` を次のバイトに…というように)ことで、正しい値 `X’12345678’` を復元できます。

陥りやすいバグと回避策

  • エンディアンの誤解: 最も注意すべきは、プログラムが実行されているシステムのエンディアン方式を理解していないことです。`DEFINED` 属性はあくまで「メモリ上のバイトの並び」を参照するだけで、バイトの並び順を自動的に変換してくれるわけではありません。ご自身のシステムがビッグエンディアンかリトルエンディアンかを確認し、それに合わせたロジックを組む必要があります。
  • データ型の不一致: `DEFINED` 属性で定義するデータ型と、元のデータ領域のサイズが一致しているか確認してください。例えば、4バイトの整数を3バイトの文字配列として定義しようとすると、予期しない結果やエラーの原因となります。
  • `POSITION` 属性の誤用: 今回の例では `POSITION` 属性を明示的に使用していませんが、より複雑なデータ構造を定義する際には、`POSITION` 属性でオフセットを正確に指定することが重要です。指定を誤ると、意図しないメモリ領域を参照してしまう可能性があります。

現代のプログラミング言語では、`BitConverter` (C#) や `ByteBuffer.order()` (Java) のような、より安全で高レベルなAPIが提供されています。しかし、PL/Iにおける `DEFINED` と `POSITION` を理解することは、メインフレームで低レベルなデータ操作を行う上での基礎となり、データの構造を深く理解する助けとなります。これらの機能を安全に活用することで、メインフレームならではの強力なデータ操作が可能になります。

コメント

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