配列およびその他のオブジェクトの境界の外にあるアクセスの検出
説明
ポインタ式からのアクセスがオブジェクトの境界内にあるかチェックします。オブジェクトはどんな型でもよく、グローバルやスタックまたはヒープ上など、どこにでも存在することができます。
チェックを実行する理由
このチェックは、アプリケーションが本来は許可されていない場所にリードやライトを行うときに便利です。以下に例を示します。
int arr[10] = {0};
int f(int i)
{
return arr[i];
}
int g(void)
{
return f(20); /* arr[20 is out of bounds] */
}使用方法
コンパイラオプション :‑‑runtime_checking bounds
IDE で、 [プロジェクト]>[オプション]>[ランタイム解析]>[境界チェックを有効化]
これによって、境界チェックをグローバルで有効化します。グローバルの境界チェック、および各ソースファイルのチェックを微調整するためのサブオプションが使用できます。
仕組み
ポインタ境界が追跡されているコード内。
ポインタ値が渡されるたびに、そのポインタ値の境界も渡されます。
何らかのオブジェクトをポイントするようにポインタが初期化される場合、そのポインタの境界はオブジェクトの境界に設定されます。オブジェクトが配列の場合は、境界は配列全体をカバーします。単独のインスタンスであれば、境界はその単独のインスタンスをカバーします。
ポインタが絶対アドレスに初期化される場合、ポインタは指定した型の単一のオブジェクトをポイントするという前提になります。以下に例を示します。
uint32_t * p = (uint_32_t *)0x100;
この場合、
pはアドレス0x100で32ビットの符号なしの整数をポイントし、境界は0x100および0x104となります。nullポインタには、いずれのアクセスもカバーしない境界が割り当てられます。つまり、それを通したアクセスは間違ったものになります。
ポインタの値がパラメータとして関数に渡されるとき、境界は追加のパラメータとして渡されます。
ポインタの値が関数から返されるとき、返される値と境界は実際のリターン値として
structで渡されます。ポインタ経由でアクセス可能な形でポインタの値がメモリに保存される場合、その境界はグローバル境界テーブルに格納されます。ポインタの値がアクセスされるときは、グローバル境界テーブルにある関連の境界も常に読み込まれます。グローバル境界テーブルのサイズは、[エントリ数](リンカオプション
‑‑bounds_table_size number_of_records[:number_of_buckets]|(number_of_bytes))を使用すれば変更できます。その他の場合は、境界は追加のローカル変数内に格納されます。
すべての浮動小数点ライブラリ関数は、softfp インターフェースを使用します。
ポインタ式を介してアクセスがあるたびに、計算されたアドレス、計算済みアドレスにアクセスサイズを足したものが境界に対してチェックされます。2 つのアドレスのうちどちらかが境界の外にある場合、C-RUN メッセージが生成されます。
任意のパラメータでポインタを受け取る関数、またはポインタ値を返す関数は、2 つの派生型(境界チェックありと境界チェックなし)を持つことができます。
リソースの使用
境界チェックのオーバヘッドによって、使用可能な ROM や RAM にアプリケーションが配置できなくなることがあります。この問題を解決するには、以下の方法があります。
間接的にアクセスされるポイントがアプリケーションに多くない場合、グローバル境界テーブルを縮小して、使用されるRAMの量を少なくします。 ‑‑bounds_table_sizeを参照してください(IDEのエントリ数)。
デフォルトでは、約190 KBを必要とする4 KBのエントリが使用されます。
一部のモジュールでは実際の境界チェックをオフにすることができます。これによって、実装によって追加されるコードの量が少なくなります。
一部のモジュールではポインタの境界追跡をオフにすることができます。これにより、それらのモジュールにおけるコードサイズが大きくなることは一切なくなりますが、ポインタ境界を追跡するコードと追跡しないコード間のインターフェースに問題が発生します。これについて詳しくは、次のセクションを参照してください。
チェックされていないコード
アプリケーション全体で境界チェックを有効化できない場合もあります。たとえば、アプリケーションの一部が外部でビルドされたライブラリであったり、アセンブラで記述されているときです。境界チェックのためにコードが機能するように何らかのソースコードを追加する場合、プリプロセッサシンボル__AS_BOUNDS__を使用して追加のソースコードを条件付きにしてください。このような場合は以下を検討してください。
境界を追跡するコードから境界を追跡しないコードを呼び出す
これは、パラメータまたはリターン型としてポインタを持つ関数のみに影響します。
宣言で
#pragma no_boundsまたは#pragma default_no_boundsを使用することにより、特定の関数でポインタ境界を追跡しないように指定できます。ポインタ境界を追跡しないコードからこうした関数を呼び出す場合、追加の非表示パラメータは渡されず、オプション[C-RUNが有効でない関数からのポインタをチェック]が使用されているかどうかに応じて(コンパイラオプション‑‑ignore_uninstrumented_pointers)、返されたポインタはすべて「安全でない」(こうしたポインタ経由のチェック済みアクセスはすべてエラーとなります)または「安全」(このポインタを介したアクセスは失敗しません)のどちらかと見なされます。このような値に明示的に境界を指定するには、ビルトイン演算子__as_make_boundsを使用します。以下に例を示します。
#pragma no_bounds struct X * f1(void); ... { struct X *px = f1(); /* Set bounds to allow acesses to a single X struct. (If the pointer can be NULL, you must check for that.) */ if (px) px = __as_make_bounds(px, 1); /* From here, any accesses via the pointer will be checked to ensure taht they are within the struct. */境界を追跡しないコードから境界を追跡するコードを呼び出す
境界を追跡する関数、パラメータとしてポインタを持つ関数、境界を追跡しないコードからポインタを返す関数を呼び出す場合、リンクの際には通常
undefined externalエラーが表示されます。このような呼び出しを有効にするには、#pragma generate_entry_without_boundsまたはオプション[C-RUNが有効でないコードから呼び出し可能な関数を生成](コンパイラオプション‑‑generate_entries_without_bounds)を使用して、境界を追跡しないコードから呼び出し可能な1つまたは複数の追加関数を出力するようコンパイラに指示することができます。こうした関数は単に、それぞれのデフォルトの境界を持つ関数を呼び出します。デフォルトの境界は、オプション[有効でない関数からのポインタ](コンパイラオプション‑‑ignore_uninstrumented_pointers)を使用しているかどうかに応じて、"safe"(このポインタを介したアクセスではエラーは一切発生しません)または"unsafe"(このポインタを介したアクセスでは常にエラーが発生します)のどちらかです。この場合に、より正確な境界を指定するには、
#pragma define_without_boundsを使用します。このプラグマディレクティブには2つの使用方法があります。問題の関数がポインタ境界を追跡しないコードからのみ呼び出され、境界が既知であるか、他のパラメータから推測できる場合、2つの関数は必要ありません。
#pragma define_without_boundsを使用して定義を変更するだけですみます。以下に例を示します。
#pragma define_without_bounds int f2(int * p, int n) { p = __as_make_bounds(p, n); /* Give p bounds */ ... }この例では、
pはn個の整数からなる配列をポイントすると想定します。割当ての後、pの境界はpおよびp + nとなります。関数がポインタ境界を追跡するコードと追跡しないコードの両方から呼び出せる場合、代わりに
#pragma define_without_boundsを使用して、境界情報を持たない関数の追加の派生型で境界情報を持つ派生型を呼び出すものを定義することができます。同じ翻訳単位に、境界を持たない派生型と境界を持つ派生型を定義することはできません。
以下に例を示します。
#pragma define_without_bounds int f3(int * p, int n) { return f3(__as_make_bounds(p, n), n); }この例では、
pはn個の整数からなる配列をポイントすると想定します。ここで定義されている追加の境界情報を持たないf3の派生型は、追加の境界情報("f3[with bounds]")を持つf3の派生型を呼び出し、ポインタのパラメータにpとp + nの境界を渡します。境界を追跡しないコードで定義されるポインタを持ったグローバル変数
これらのポインタは、あらゆるアクセスに対してエラーを発信する境界、あるいはリンクの際にオプション[C-RUNが有効でない関数からのポインタをチェック](リンカオプション
‑‑ignore_unistrumented_pointers)が使用される場合は、発信するエラーが一切発生しない境界のどちらかを取得します。より具体的な境界が必要な場合は、__as_make_boundsを使用してください。以下に例を示します。
extern struct x * gptr; int main(void) { /* Give gptr bounds with size N. */ gptr = __as_make_bounds(gptr, N); ... }RTOSタスク
タスクを実装する関数は、ポインタであるパラメータにより呼び出されることがあります。RTOS自体がポインタ境界を追跡しない場合は、
#pragma define_without_boundsおよび__as_make_boundsを使用して正しい境界情報を取得する必要があります。以下に例を示します。
#pragma define_without_bounds void task1(struct Arg * p) { /* p points to a single Arg struct */ p = __as_make_bounds(p, 1); ... }
一部の制限事項 :
関数ポインタ
関数ポインタを境界を追跡するコードと追跡しないコード間で共有すると、問題が発生する可能性があります。
境界を追跡するコードと追跡しないコードでは、型に違いがありません。どちらの種類の関数も関数ポインタに割り当てたり、関数ポインタのパラメータを受け入れる関数に渡すことができます。ただし、シグネチャにポインタを含む関数が一致しないコンテキストで呼び出された場合(境界を追跡しないコードから境界を追跡する関数を呼び出したり、その逆)、正しく機能しなくなります。最も理想的な場合には、これは紛らわしい境界の違反ということになりますが、これらの関数は間違った数の引数によって呼び出されているため、どんな動作でも実際に起こる可能性があります。
正しく機能するためには、ポインタがシグネチャに含まれるすべての関数、および関数ポインタを介して呼び出されるすべての関数が適切な種類でなければなりません。境界を追跡しないライブラリからのコールバックの単純なケースでは、適切な関数上で
#pragma no_boundsを使用すれば通常は十分です。K&R関数
K&R関数は使用しないでください。すべての関数に正しいプロトタイプがあることを確認するには、
‑‑require_prototypesと共有のヘッダファイルを使用します。Cではvoid f()はK&R関数ですが、f(void)はそうではない点に注意してください。境界を追跡しないコードにより更新されるポインタ
ポインタに新しい境界を設定しないコードによってポインタが更新される場合は、常に潜在的な問題があります。新しいポインタ値が古いポインタ値と同じオブジェクトにポイントしない場合、境界が正しいものではなくなり、チェック済みコードでこのポインタを介したアクセスはエラーを発信します。
絶対アドレス
#pragma location または @ 演算子を使用して絶対アドレスに変数を配置する場合、これらの変数へのポインタは、他の任意の変数へのポインタと同じように正しい境界を取得します。
整数からポインタへ明示的なキャストを使用する場合、ポインタは境界を取得します。このポインタが指定した型の 1 つのオブジェクトをポイントすることが前提となります。他の境界が必要な場合は、__as_make_boundsを使用します。
以下に例を示します。
/* p will get bounds that assume it points to a single struct Port at address 0x1000. */ p = (struct Port *)0x1000; /* If it points to an array of 3 struct you can add */ p = __as_make_bounds(p, 3);
例
C-RUNランタイムエラー解析を使用するにあたってに記載された手順に従ってください。[境界チェック]オプションを使用します。
これは、実行時に識別されるソースコードの一例です。
C-RUN は、境界の外のアクセスまたは無効な関数ポインタのどちらかを報告します。これは、リストされるメッセージの一例です。
