Solaris 動的トレースガイド

第 5 章 ポインタと配列

ポインタは、オペレーティングシステムカーネル内またはユーザープロセスのアドレス空間内のデータオブジェクトのメモリーアドレスです。D では、ポインタを作成、操作して、変数や連想配列内に格納できます。この章では、ポインタの D 構文、ポインタの作成やアクセスに使用する演算子、およびポインタと固定サイズのスカラー配列との関係について説明します。また、異なるアドレス空間でのポインタの使用についても説明します。


注 –

D ポインタ構文は、対応する ANSI-C 構文と同じです。C や C++ のプログラミング経験をお持ちのユーザーは、この章は、ざっと目を通すだけでかまいません。ただし、「DTrace オブジェクトのポインタ」「ポインタとアドレス空間」は目を通すようにしてください。DTrace に固有の機能や問題が説明されています。


ポインタとアドレス

Solaris オペレーティングシステムでは、「仮想メモリー」により、ユーザープロセスごとに、システム上のメモリーリソースについて固有の仮想表示が提供されます。メモリーリソースの仮想表示を「アドレス空間」と呼びます。アドレス空間は、アドレス範囲 (32 ビットの場合 [0 ... 0xffffffff]、64 ビットの場合 [0 ... 0xffffffffffffffff]) と、翻訳セットを関連付けます。翻訳セットは、オペレーティングシステムやハードウェアが個々の仮想アドレスを対応する物理メモリー配置に変換するために使用されます。D のポインタはデータオブジェクトで、整数型の仮想アドレス値を格納したあと、対応するメモリー配置に格納されているデータの形式を説明する D 型と関連付けます。

D 変数をポインタ型として宣言するには、参照されるデータの型を指定し、型名にポインタ型の宣言であることを示すアスタリスク (*) を付加します。たとえば次のような宣言があるとします。

int *p;

この宣言は、p という名前の D 大域変数が、整数のポインタであることを示しています。この宣言から、p 自体が 32 ビットまたは 64 ビットの整数で、その値はメモリー内にある別の整数のアドレスであることがわかります。D コードは、コンパイル後、オペレーティングシステムカーネル内でプローブ起動時に実行されます。このため、D ポインタは通常、カーネルのアドレス空間に関連付けられています。稼働中のオペレーティングシステムカーネルのポインタのビット数を確認するには、isainfo(1) -b コマンドを実行します。

カーネル内のデータオブジェクトのポインタを作成したい場合は、& 演算子を使って、そのアドレスを計算します。たとえば、オペレーティングシステムカーネルのソースコードに、チューニング可能なパラメータ int kmem_flags が宣言されています。この int のアドレスをトレースするには、D でのオブジェクト名に & 演算子を付加した結果をトレースします。

trace(&`kmem_flags);

* 演算子は、ポインタによってアドレス指定されたオブジェクトを参照するときに使用します。この演算子は、& 演算子と正反対の機能を持っています。たとえば、次の 2 つの D コードの抜粋は、意味的に同じです。

p = &`kmem_flags;				trace(`kmem_flags);
trace(*p);

左側の抜粋コードでは、D 大域変数ポインタ p が作成されています。kmem_flagsint 型のオブジェクトなので、&`kmem_flags の結果の型は int * (int へのポインタ) になります。左側の抜粋コードでは、ポインタをデータオブジェクト kmem_flags に移してから、*p の値をトレースしています。したがって、この抜粋コードは、データオブジェクトの名前を指定して直接その値をトレースする右側の抜粋コードと同じになります。

ポインタの安全性

C や C++ の知識をお持ちのユーザーは、前の節をお読みになって、ポインタの使い方を間違えるとプログラムがクラッシュするのではないかと、少し不安になったかもしれません。DTrace は堅牢で安全な環境であるため、このような間違いのある D プログラムを実行しても、プログラムがクラッシュする心配はありません。作成した D プログラムにバグがあっても、無効な D ポインタアクセスが原因で DTrace やオペレーティングシステムカーネルに障害やクラッシュが起きることはありません。DTrace は、無効なポインタアクセスを検出すると、計測機能を無効にし、デバッグに役立つ情報を報告します。

Java プログラミングの経験をお持ちであればご存知でしょうが、Java 言語では、このような安全面の理由から、ポインタはサポートされていません。D では、C で記述されたオペレーティングシステムの実装の本質的な部分として、ポインタを使用する必要があります。しかし、DTrace には、Java プログラミング言語と同様の保護機構が実装されているため、プログラムにバグがあっても、そのプログラム自体やその他のプログラムに危害を与えることはありません。DTrace のエラー報告機能は、Java プログラミング言語の実行環境と同じように、プログラミングエラーを検出し、例外を報告します。

以下では、DTrace のエラー処理およびエラー報告機能について確認するため、ポインタを使って、意図的に不正な D プログラムを作成してみましょう。エディタで以下の D プログラムを入力し、badptr.d という名前のファイルに保存してください。


例 5–1 badptr.d: DTrace のエラー処理機能のデモ

BEGIN
{
	x = (int *)NULL;
	y = *x;
	trace(y);
}

badptr.d プログラムで作成されている D ポインタ x は、int へのポインタです。このポインタには、特殊かつ無効なポインタ値 NULL が割り当てられていますが、この値はアドレス 0 を表す組み込みの別名です。慣例上、アドレス 0 は常に無効と定義されます。このため、C プログラムや D プログラムでは、NULL は標識値として使用できます。このプログラムでは、キャスト式により、NULL が整数ポインタに変換されています。このポインタは式 *x によって間接参照され、その結果が別の変数 y に割り当てられます。その後、この変数 y のトレースが試みられます。この D プログラムを実行すると、y = *x という文が実行されたところで無効なポインタアクセスが検出され、エラーが報告されます。


# dtrace -s badptr.d
dtrace: script '/dev/stdin' matched 1 probe
CPU     ID                    FUNCTION:NAME
dtrace: error on enabled probe ID 1 (ID 1: dtrace:::BEGIN): invalid address
(0x0) in action #2 at DIF offset 4
dtrace: 1 error on CPU 0
^C
#

プログラムで無効なポインタを使用した場合、「配置エラー」の問題が起きることもあります。構造上、整数をはじめとする基本データオブジェクトは、そのサイズに従ってメモリー内に配置されます。たとえば、2 バイトの整数は 2 の倍数のアドレス、4 バイトの整数は 4 の倍数のアドレスというように配置されます。4 バイトの整数のポインタを間接参照するとき、このポインタが 4 の倍数以外の無効な値のアドレスに配置されていると、アクセスに失敗し、配置エラーが返されます。たいていの場合、D の配置エラーは、D プログラム内のバグによってポインタの値が無効になったか、壊れていることを示します。配置エラーの例として、ソースコード badptr.d で、NULL の代わりにアドレス (int *)2 を指定してみてください。int は 4 バイトで、2 は 4 の倍数ではないので、式 *x は DTrace 配置エラーになります。

DTrace のエラー機構の詳細は、ERROR プローブ」を参照してください。

配列宣言と記憶域

D では、第 3 章で説明した動的連想配列のほかに、「スカラー配列」もサポートされています。スカラー配列は、それぞれに同じ型の値が格納される、固定長の連続するメモリー配 置の集まりです。スカラー配列にアクセスするには、ゼロで始まる整数を使って、それぞれの位置を参照します。スカラー配列と C や C++ の配列の概念と構文は、直接対応しています。D では、スカラー配列は、連想配列やその応用である「集積体」ほどは多用されませんが、C で宣言された既存のオペレーティングシステムの配列データ構造にアクセスするとき、スカラー配列が必要になる場合があります。集積体については、第 9 章集積体で説明します。

以下では、int 型の 5 つの整数から成る D スカラー配列を宣言します。宣言の末尾には、接尾辞として、要素数を角括弧で囲んだものを付加します。

int a[5];

この配列の記憶域を視覚的に表現すると、次の図のようになります。

図 5–1 スカラー配列

5 つのオブジェクトから成る配列を示す図です。

D 式 a[0] は最初の配列要素、a[1] は 2 番目の配列要素 (以下同様) を参照しています。構文だけ見ると、スカラー配列は連想配列と非常によく似ています。たとえば、単一の整数キーによって参照される 5 つの整数から成る連想配列は、次のように宣言できます。

int a[int];

この配列は、式 a[0] を使って参照することもできます。一方、記憶域と実装について見ると、スカラー配列と連想配列は、まったく別物です。静的配列 a は、ゼロから順に番号付けされた連続した 5 つのメモリー配置で構成されています。インデックスは、この配列に割り当てられた記憶域内のオフセットを参照しています。これに対して、連想配列の場合、サイズは事前定義されておらず、要素は連続したメモリー配置には格納されません。また、連想配列のキーと対応する値の記憶域の位置には、何の関連性もありません。連想配列の要素 a[0]a[-5] にアクセスすると、DTrace により 2 ワード分のみの記憶域が割り当てられますが、これらの記憶域は連続しているとはかぎりません。連想配列のキーは、対応する値の抽象名になっており、値の記憶域の位置とは無関係です。

配列の作成時に初期値を割り当て、単一の整数式を配列インデックスとして使用した場合 (例: a[0] = 2)、式だけ見れば a をスカラー配列への代入と見なすことも可能ですが、D コンパイラは必ず、新しい連想配列を生成します。この場合、D コンパイラにこの配列をスカラー配列と判断させるには、あらかじめスカラー配列を宣言して、コンパイラに配列のサイズの定義を認識させる必要があります。

ポインタと配列の関係

D でも、ANSI-C の場合と同様に、ポインタと配列には特別な関係があります。配列を表す変数は、最初の記憶域の位置を示すアドレスに関連付けられています。ポインタも、記憶域の位置を示すアドレスであり、あらかじめ型定義されています。このため、D では、ポインタ変数でも配列変数でも、配列インデックスの表記 [ ] を使用できます。たとえば、次の 2 つの D コードの抜粋は、意味的に同じです。

p = &a[0];				trace(a[2]);
trace(p[2]);

左側の抜粋コードでは、式 a[0]& 演算子を適用することにより、ポインタ pa 内の最初の配列要素のアドレスに割り当てています。式 p[2] では、3 番目の配列要素 (インデックス 2) の値をトレースします。p には、a に関連付けられたアドレスが含まれるため、この式からは、右側の抜粋コードの a[2] と同じ値が導き出されます。このような等価性により、C と D では、任意のポインタまたは配列の任意のインデックスへのアクセスが可能です。コンパイラも DTrace 実行環境も、配列境界チェックは行いません。配列に事前定義された値の範囲外のメモリーにアクセスすると、予想外の結果になるか、先ほどの例のように、DTrace から、アドレスが無効であることを示すエラーが報告されます。DTrace そのものやオペレーティングシステムに影響はありませんが、D プログラムをデバッグする必要があります。

ポインタと配列の違いは、ポインタ変数が別の記憶域の整数アドレスを含む記憶域を参照する点です。配列変数は、配列の位置を含む整数アドレスではなく、配列の記憶域そのものを指定します。以下に、この違いを図示します。

図 5–2 ポインタと配列の記憶域

5 つのオブジェクトから成る配列のポインタを示す図です。

この違いは、ポインタを割り当てるときとスカラー配列を割り当てるときの D 構文から明らかです。xy がポインタ変数である場合、式 x = y は正当です。この式では、y 内のポインタアドレスが、x で指定された記憶域の位置にコピーされます。xy がスカラー変数である場合、式 x = y は不正です。D では配列そのものを割り当てることはできません。ただし、配列変数やシンボル名は、ポインタが可能なあらゆる状況で使用できます。p がポインタで a が配列である場合、p = a という文は使用可能です。この文は、p = &a[0] と同等です。

ポインタ演算

ポインタは、メモリー内の別のオブジェクトのアドレスとして使用される整数に過ぎません。このため、D には、ポインタ演算機能が用意されています。ただし、ポインタ演算は、整数の演算とは別のものです。ポインタ演算では、ポインタで参照されている型のサイズとオペランドの乗除によって、配下のアドレスが暗黙的に変更されます。以下に、この属性を示す D コードの抜粋を示します。

int *x;

BEGIN
{
	trace(x);
	trace(x + 1);
	trace(x + 2);
}

このコードの抜粋では、整数ポインタ x が作成され、その値とその値を 1 増分した値、さらにその値を 2 増分した値がトレースされます。このプログラムを作成して実行したとき、DTrace から返される整数値は、0、4、8 です。

x は整数 (サイズは 4 バイト) のポインタなので、x の値を 1 増分すると、配下のポインタ値は 4 大きくなります。この特性は、ポインタを使って配列などの連続した記憶域の位置を参照する場合に便利です。たとえば、x図 5–2 のような配列 a のアドレスに割り当てた場合、式 x + 1 は式 &a[1] と同じことになります。同様に、式 *(x + 1) は、値 a[1] を参照します。演算子 +=+、または ++ によってポインタ値が増分されている箇所では、D コンパイラにより、ポインタ演算が行われます。

ポインタ演算は、左側のポインタから整数が減算されている箇所、ポインタから別のポインタが減算されている箇所、ポインタに演算子 -- が適用されている箇所でも行われます。たとえば、次の D プログラムは、結果として 2 をトレースします。

int *x, *y;
int a[5];

BEGIN
{
	x = &a[0];
	y = &a[2];
	trace(y - x);
}

汎用ポインタ

D プログラムでは、ポインタが参照するデータ型を指定せず、汎用ポインタアドレスを表現または操作すると便利な場合があります。汎用ポインタは、void * 型か、組み込みの型の別名 uintptr_t を使って指定します。キーワード void は、特定の型情報が存在しないことを示します。また、型の別名 uintptr_t は、現在のデータモデル内のポインタに適したサイズの符号なし整数型を示します。void * 型のオブジェクトにポインタ演算を適用することはできません。これらのポインタを間接参照するには、まず別の型にキャストする必要があります。ポインタ値に対して整数演算を行いたい場合は、ポインタを uintptr_t 型にキャストします。

void へのポインタは、連想配列の組の式や代入文の右式など、別のデータ型へのポインタが必要なとき、いつでも使用できます。同様に、void へのポインタが必要なときは、任意のデータ型のポインタを使用できます。void 以外のあるポインタ型の代わりに、void 以外の別の型のポインタを使用したい場合は、明示的キャストが必要になります。明示的キャストにより、ポインタを uintptr_t などの整数型に変換するか、これらの整数を適切なポインタ型に変換して戻す必要があります。

多次元配列

D では、多次元のスカラー配列は、あまり使用されません。多次元のスカラー配列は、ANSI-C との互換性を実現するためのものです。この機能を使って、C で作成されたオペレーティングシステムデータ構造を監視したり、使用したりできます。多次元配列は、基本型の後ろに、連続するスカラー配列サイズを角括弧 ([ ]) で囲んだ形式で宣言されます。たとえば、12 行 × 34 列の整数値から成る固定サイズの 2 次元四角形配列は、次のように宣言します。

int a[12][34];

多次元スカラー配列にも、同様の表記法でアクセスできます。たとえば、行 0 列 1 に格納されている値にアクセスしたい場合、次のような D 式を記述します。

a[0][1]

多次元スカラー配列値の記憶域の位置は、「行番号」×「宣言された列数の合計」+「列番号」で計算されます。

多次元配列の構文と、連想配列アクセスの D 構文を混同しないでください。a[0][1]a[0, 1] は、まったく別の構文です。連想配列に互換性のない組を使用したり、スカラー配列の連想配列アクセスを試行したりすると、D コンパイラからエラーメッセージが表示され、プログラムのコンパイルが拒否されます。

DTrace オブジェクトのポインタ

D コンパイラでは、連想配列、組み込み関数、変数などの DTrace オブジェクトへのポインタを、& 演算子を使って取得することは禁じられています。DTrace 実行環境では、プログラムに必要なメモリーをより効果的に管理するため、次のプローブが起動するまでの間に、必要に応じて変数のアドレスが再配置されます。このため、これらの変数のアドレスを取得することは禁じられています。複合構造を作成すれば、DTrace オブジェクト記憶域のカーネルアドレスを取得する式を作成することは可能です。このような式を D プログラム内で作成することはなるべく避けてください。こうした式を使用する必要がある場合は、プローブ起動時にアドレスをキャッシュしないようにしてください。

ANSI-C では、ポインタを使って、間接的な関数呼び出しや代入を実行できます。たとえば、代入演算子の左側に、単項の間接参照演算子 * を配置できます。D では、ポインタを使ったこのような式は許可されていません。値は、名前を指定するか、D スカラー配列または連想配列に配列インデックス演算子 [] を適用することにより、D 変数に直接代入しなければなりません。第 10 章アクションとサブルーチンに指定されているように、DTrace 環境に定義されている関数は名前を指定して呼び出す必要があります。D では、ポインタを使った間接的な関数呼び出しは許可されていません。

ポインタとアドレス空間

ポインタは、「仮想アドレス空間」内で物理メモリーへの翻訳を提供するアドレスです。DTrace は、オペレーティングシステムカーネル自体のアドレス空間内で D プログラムを実行します。Solaris システム全体で、多数のアドレス空間が管理されています。 オペレーティングシステムカーネル用のアドレス空間と、個々のユーザープロセス用のアドレス空間があります。各アドレス空間は、システム上のすべてのメモリーにアクセスできるように見えるので、複数のアドレス空間で同じ仮想アドレスポインタ値を再利用し、それぞれ異なる物理メモリーに翻訳できます。したがって、ポインタを含む D プログラムを作成するときは、使用するポインタに対応するアドレス空間を意識する必要があります。

たとえば、syscall プロバイダを使って、引数として整数ポインタまたは整数配列ポインタを取るシステムコール (例: pipe(2)) の開始を計測する場合、演算子 *[] を使ってそのポインタまたは配列を間接参照することはできません。これは、そのアドレスが、システムコールを実行したユーザープロセスのアドレス空間内のアドレスだからです。D で、このアドレスに演算子 * または [] を適用すると、カーネルアドレス空間がアクセスされ、アドレスが無効であるというエラーメッセージが表示されます。または、このアドレスが有効なカーネルアドレスとたまたま同じだった場合、D プログラムに予想外のデータが返されます。

DTrace プローブからユーザープロセスメモリーにアクセスするには、第 10 章アクションとサブルーチンに記載されている copyin()copyinstr()copyinto() のうちいずれかの関数を、ユーザーアドレス空間ポインタに適用する必要があります。D プログラムの作成時には、混乱を防ぐため、ユーザーアドレスを適切に格納する変数を指定し、コメントを付けてください。ユーザーアドレスを間接参照するような D コードを誤ってコンパイルすることがないように、ユーザーアドレスを uintptr_t として格納することもできます。ユーザープロセスで DTrace を使用するテクニックについては、第 33 章ユーザープロセスのトレースを参照してください。