私は非常に古い学校のプログラマーによって書かれたコードを持っています:-)。それはこのようなものになります
typedef struct ts_request
{
ts_request_buffer_header_def header;
char package[1];
} ts_request_def;
ts_request_def* request_buffer =
malloc(sizeof(ts_request_def) + (2 * 1024 * 1024));
プログラマーは基本的にバッファーオーバーフローの概念に取り組んでいます。私はコードが怪しげに見えるのを知っています。だから私の質問は:
Mallocは常に連続したメモリブロックを割り当てますか?このコードでは、ブロックが隣接していない場合、コードは大きな時間で失敗します
Free(request_buffer)を実行すると、mallocによって割り当てられたすべてのバイト、つまりsizeof(ts_request_def)+(2 * 1024 * 1024)、または構造体sizeof(ts_request_def)のサイズのバイトのみが解放されます
このアプローチに明らかな問題がありますか。上司と話し合う必要があり、このアプローチの抜け穴を指摘したいと思います
あなたの番号付きのポイントに答えるために。
最新のC標準であるISO/IEC 9899:1999(非公式にはC99)では、 フレキシブルアレイメンバー を使用できます。
この例は次のとおりです。
int main(void)
{
struct { size_t x; char a[]; } *p;
p = malloc(sizeof *p + 100);
if (p)
{
/* You can now access up to p->a[99] safely */
}
}
この標準化された機能により、質問で説明する一般的だが非標準的な実装拡張機能の使用を回避できました。厳密に言えば、柔軟性のない配列メンバーを使用してその境界を超えてアクセスすることは未定義の動作ですが、多くの実装ではドキュメント化して推奨しています。
さらに、拡張機能として gcc は zero-length array を許可します。長さ0の配列は標準Cでは不正ですが、C99が柔軟な配列メンバーを提供する前にgccがこの機能を導入しました。
コメントへの応答として、以下のスニペットが技術的に未定義の動作である理由を説明します。私が引用するセクション番号はC99(ISO/IEC 9899:1999)を参照しています
struct {
char arr[1];
} *x;
x = malloc(sizeof *x + 1024);
x->arr[23] = 42;
まず、6.5.2.1#2はa [i]が(*((a)+(i)))と同一であることを示しているため、x-> arr [23]は(*((x-> arr)+( 23)))。ここで、6.5.6#8(ポインターと整数の追加について)は次のように述べています。
「ポインターオペランドと結果の両方が同じ配列オブジェクトの要素を指す場合、または配列オブジェクトの最後の要素の1つ後を指す場合、評価はオーバーフローを生成しません。それ以外の場合は、動作未定義です。」
このため、x-> arr [23]は配列内にないため、動作は未定義です。 malloc()が配列が拡張されたことを意味するため、それでも問題ないと思うかもしれませんが、厳密にはそうではありません。有益な附属書J.2(未定義の動作の例をリスト)は、例を使用してさらに明確化します。
オブジェクトが特定の添え字で明らかにアクセス可能であっても、配列の添え字は範囲外です(宣言int a [4] [5]が与えられたlvalue式a [1] [7]のように)(6.5.6)。
3-これは、構造体の最後に動的配列を割り当てるためのかなり一般的なCのトリックです。もう1つの方法は、ポインタを構造体に入れてから、配列を個別に割り当てることです。解放することも忘れないでください。サイズが2MBに固定されていることは少し変わっているようですが。
1)はい、そうです。または、十分な大きさの連続したブロックが利用できない場合、mallocは失敗します。 (mallocでの失敗はNULLポインターを返します)
2)はい。内部メモリ割り当ては、そのポインタ値で割り当てられたメモリ量を追跡し、そのすべてを解放します。
3)言語のハックのようなものであり、その使用については少し疑わしいです。それでもバッファオーバーフローが発生する可能性があり、攻撃者がそれを引き起こすペイロードを見つけるまでに少し長くかかる場合があります。 「保護」のコストもかなり高額です(本当に要求バッファーあたり2MB以上必要ですか?)。上司がその議論を理解していないかもしれませんが、それも非常に醜いです:)
これは標準のCトリックであり、他のどのバッファよりも危険ではありません。
「非常に古い学校のプログラマー」より頭が良いことを上司に見せようとしているのであれば、このコードは当てはまりません。オールドスクールは必ずしも悪いわけではありません。 「古い学校」の人はメモリ管理について十分に知っているようです;)
私は、既存の答えがこの問題の本質にかなり到達することはないと思います。あなたは昔ながらのプログラマーがこのようなことをしていると言います。
typedef struct ts_request
{
ts_request_buffer_header_def header;
char package[1];
} ts_request_def;
ts_request_buffer_def* request_buffer =
malloc(sizeof(ts_request_def) + (2 * 1024 * 1024));
彼がまさにそれをしたかったのであれば、トリックを必要としない簡略化された同等のコードでそれを行うことができるので、私は彼がまさにそれをしている可能性は低いと思います。
typedef struct ts_request
{
ts_request_buffer_header_def header;
char package[2*1024*1024 + 1];
} ts_request_def;
ts_request_buffer_def* request_buffer =
malloc(sizeof(ts_request_def));
彼が実際にやっていることは次のようなものだと思う。
typedef struct ts_request
{
ts_request_buffer_header_def header;
char package[1]; // effectively package[x]
} ts_request_def;
ts_request_buffer_def* request_buffer =
malloc( sizeof(ts_request_def) + x );
彼が達成したいのは、可変パッケージサイズxでのリクエストの割り当てです。もちろん、配列のサイズを変数で宣言することは違法なので、彼はトリックでこれを回避しています。まるで彼が私に何をしているのかを知っているように見えますが、そのトリックは、Cトリックのスケールの立派で実用的な終わりに向かっているのです。
はい。 mallocは単一のポインターのみを返します-要求を満たすために複数の不連続なブロックを割り当てたことをリクエスターに伝えるにはどうすればよいでしょうか?
#3については、コードを追加しないと答えるのが困難です。それが何度も起こっていない限り、私はそれで何も悪いとは思わない。つまり、常に2MBのメモリチャンクを割り当てる必要はありません。また、それを不必要に実行したくない場合もあります。 2kのみを使用する場合。
何らかの理由でそれが気に入らないという事実は、それに反対したり、完全に書き直すことを正当化するのに十分ではありません。私は使用法を注意深く調べ、元のプログラマーが何を考えていたかを理解しようとし、このメモリーを使用するコードで(workmad3が指摘したように)バッファーオーバーフローを注意深く調べます。
よくある間違いがたくさんあります。たとえば、コードはmalloc()が成功したことを確認するためにチェックしますか?
エクスプロイト(質問3)は、実際にこの構造のインターフェースにかかっています。コンテキストでは、この割り当ては理にかなっている可能性があり、それ以上の情報がなければ、それが安全であるかどうかを言うことは不可能です。
しかし、構造よりも大きなメモリの割り当てに関する問題を意味する場合、これは決して悪いC設計ではありません(私はそれが古い学校だとは言いませんが;))
ここでの最後の注意-char [1]を持つことのポイントは、終端のNULLが常に宣言された構造体にあるということです。つまり、バッファには2 * 1024 * 1024文字があり、 NULLを「+1」で説明する必要はありません。小さな偉業のように見えるかもしれませんが、私は指摘したかっただけです。
私はこのパターンを頻繁に見て使用しました。
その利点は、メモリ管理を簡素化し、メモリリークのリスクを回避することです。必要なのは、mallocされたブロックを解放することだけです。二次バッファを使用すると、2つの空きが必要になります。ただし、この操作をカプセル化するには、デストラクタ関数を定義して使用する必要があります。これにより、セカンダリバッファに切り替えたり、構造を削除するときに実行する操作を追加したりするなど、常にその動作を変更できます。
配列要素へのアクセスも少し効率的ですが、最近のコンピューターではそれほど重要ではありません。
非常に頻繁に発生するため、さまざまなコンパイラーを使用して、構造内のメモリー配置が変更された場合にも、コードは正しく機能します。
私が目にする唯一の潜在的な問題は、コンパイラーがメンバー変数のストレージの順序を入れ替えるかどうかです。このトリックでは、パッケージフィールドがストレージの最後に残る必要があるためです。 C標準で順列が禁止されているかどうかはわかりません。
また、割り当てられたバッファのサイズは必要以上に大きくなる可能性が高いことに注意してください。少なくとも1バイト、パディングバイトがある場合は追加されます。
それは一般的ではありませんが、Windows APIはそのような用途でいっぱいなので、私はそれを標準的なプラクティスと呼ぶかもしれません。
たとえば、非常に一般的なBITMAPヘッダー構造を確認してください。
http://msdn.Microsoft.com/en-us/library/aa921550.aspx
最後のRBGクワッドは1サイズの配列で、これはまさにこの手法に依存します。
3番目の質問への回答。
free
は常に、一度に割り当てられたすべてのメモリを解放します。
int* i = (int*) malloc(1024*2);
free(i+1024); // gives error because the pointer 'i' is offset
free(i); // releases all the 2KB memory
この一般的なCのトリックは このStackOverflowの質問(誰かがsolarisのdirent構造体のこの定義を説明できますか?) でも説明されています。
質問1と2の答えは「はい」です
醜さ(すなわち質問3)について、プログラマはその割り当てられたメモリを何にしようとしていますか?
ここで理解すべきことは、malloc
はこれで行われている計算を認識しないことです
malloc(sizeof(ts_request_def) + (2 * 1024 * 1024));
それと同じ
int sz = sizeof(ts_request_def) + (2 * 1024 * 1024);
malloc(sz);
あなたは、メモリの2つのチャンクを割り当てることを考えているかもしれません。しかし、mallocはそれをまったく認識しません。