バッファ内のC ++オブジェクトの管理、アライメントとメモリレイアウトの仮定を考慮
-
03-07-2019 - |
質問
オブジェクトをバッファに保存しています。これで、オブジェクトのメモリレイアウトについて推測できないことがわかりました。
オブジェクトの全体的なサイズがわかっている場合、このメモリへのポインタを作成し、そのメモリ上の関数を呼び出すことは受け入れられますか?
e.g。次のクラスがあるとします:
[int,int,int,int,char,padding*3bytes,unsigned short int*]
1) このクラスのサイズが24であり、メモリ内で開始するアドレスがわかっている場合 これをポインタにキャストし、これらのメンバーにアクセスするこのオブジェクトの関数を呼び出すことは、メモリレイアウトが受け入れられると仮定するのは安全ではありませんか? (c ++は何らかの魔法によってメンバーの正しい位置を知っていますか?)
2) これが安全ではない場合、すべての引数を取るコンストラクタを使用して、バッファから各引数を一度に1つずつ引き出す以外の方法はありますか?
編集:タイトルを変更して、私が求めているものにより適したものにしました。
解決
すべてのメンバーを取得して割り当てるコンストラクターを作成してから、新しい配置を使用できます。
class Foo
{
int a;int b;int c;int d;char e;unsigned short int*f;
public:
Foo(int A,int B,int C,int D,char E,unsigned short int*F) : a(A), b(B), c(C), d(D), e(E), f(F) {}
};
...
char *buf = new char[sizeof(Foo)]; //pre-allocated buffer
Foo *f = new (buf) Foo(a,b,c,d,e,f);
これには、vテーブルでも正しく生成されるという利点があります。ただし、これをシリアル化に使用している場合、何らかの方法を使用してポインターをオフセットに変換してから元に戻す場合を除き、unsigned short intポインターは逆シリアル化するときに有用なものを指すことはありません。
this
ポインターの個々のメソッドは静的にリンクされ、明示的なパラメーターの前の最初のパラメーターである this
を使用した単純な関数呼び出しです。
メンバー変数は、 this
ポインターからのオフセットを使用して参照されます。オブジェクトが次のようにレイアウトされている場合:
0: vtable
4: a
8: b
12: c
etc...
a
は、 this + 4 bytes
を逆参照することによりアクセスされます。
他のヒント
基本的に、あなたが提案していることは、(ランダムではなく)大量のバイトを読み取り、それらを既知のオブジェクトにキャストし、そのオブジェクトのクラスメソッドを呼び出すことです。これらのバイトは" this"で終わるため、実際に動作する可能性があります。そのクラスメソッド内のポインタ。しかし、あなたは、コンパイルされたコードが期待する場所にないことで本当のチャンスを取っています。また、JavaやC#とは異なり、実際の「ランタイム」はありません。これらの種類の問題をキャッチするために、せいぜいコアダンプを取得し、最悪の場合、破損したメモリを取得します。
Javaのシリアライゼーション/デシリアライゼーションのC ++バージョンが必要なようです。おそらくそれを行うためのライブラリがあります。
非仮想関数呼び出しは、C関数のように直接リンクされます。オブジェクト(this)ポインターが最初の引数として渡されます。関数を呼び出すためにオブジェクトレイアウトの知識は必要ありません。
オブジェクト自体をバッファに保存するのではなく、オブジェクトを構成するデータを保存するようです。
このデータがメモリ内にある場合、フィールドはクラス内で定義され(プラットフォームに適切なパディングを使用)、 タイプは POD から、データを memcpy
できますあなたの型へのポインタへのバッファ(または、おそらくそれをキャストしますが、異なる型のポインタへのキャストを持つプラットフォーム固有の落とし穴があることに注意してください)。
クラスがPODではない場合、フィールドのメモリ内レイアウトは保証されません。また、再コンパイルごとに変更が許可されるため、観察された順序に依存しないでください。
ただし、PODのデータを使用して非PODを初期化できます。
非仮想関数が配置されているアドレスに関しては、コンパイル時に、型のすべてのインスタンスで同じコードセグメント内のある場所に静的にリンクされます。 「ランタイム」はないことに注意してください。関与した。このようなコードを書くとき:
class Foo{
int a;
int b;
public:
void DoSomething(int x);
};
void Foo::DoSomething(int x){a = x * 2; b = x + a;}
int main(){
Foo f;
f.DoSomething(42);
return 0;
}
コンパイラは次のようなコードを生成します:
- 関数
main
:- オブジェクトのスタックに8バイトを割り当てる"
f
" - クラスのデフォルトの初期化子を呼び出す"
Foo
" (この場合は何もしません) - 引数値
42
をスタックにプッシュ - オブジェクトへのプッシュポインター"
f
"スタック上 - 関数
Foo_i_DoSomething @ 4
を呼び出します(実際の名前は通常、より複雑です) - アキュムレータレジスタに戻り値
0
をロード - 呼び出し元に戻る
- オブジェクトのスタックに8バイトを割り当てる"
- 関数
Foo_i_DoSomething @ 4
(コードセグメントの他の場所にあります)- load"
x
"スタックからの値(呼び出し元によってプッシュされます) - 2で乗算
- load"
this
"スタックからのポインター(呼び出し元によってプッシュされます) - フィールドのオフセットを計算"
a
"Foo
オブジェクト内 - 手順3で読み込んだ
this
ポインターに計算オフセットを追加します - ステップ2で計算した製品を、ステップ5で計算したオフセットに保存します
- load"
x
"スタックからの値、再び - load"
this
"再びスタックからのポインター - フィールドのオフセットを計算"
a
"Foo
オブジェクト内で、再び - 手順8で読み込んだ
this
ポインターに計算オフセットを追加します - load"
a
"オフセットに保存された値、 - 追加"
a
" intステップ12にロードされた値、"x
"手順7で読み込んだ値 - load"
this
"再びスタックからのポインター - フィールドのオフセットを計算"
b
"Foo
オブジェクト内 - 手順14で読み込んだ
this
ポインターに計算されたオフセットを追加します - ステップ16で計算されたオフセットに、ステップ13で計算された合計を保存する
- 呼び出し元に戻る
- load"
言い換えれば、これはあなたがこれを書いたのとほぼ同じコードになります(DoSomething関数の名前や this
ポインタを渡す方法などの詳細はコンパイラ次第です) :
class Foo{
int a;
int b;
friend void Foo_DoSomething(Foo *f, int x);
};
void Foo_DoSomething(Foo *f, int x){
f->a = x * 2;
f->b = x + f->a;
}
int main(){
Foo f;
Foo_DoSomething(&f, 42);
return 0;
}
-
この場合、PODタイプのオブジェクトはすでに作成されており(newを呼び出すかどうか。必要なストレージの割り当てで十分です)、その関数を呼び出すなど、そのメンバーにアクセスできます。オブジェクト。ただし、必要なTのアライメント、Tのサイズ(バッファーはそれより小さくない場合があります)、およびTのすべてのメンバーのアライメントを正確に知っている場合にのみ機能します。ポッドタイプの場合でも、コンパイラーは必要に応じて、メンバー間にパディングバイトを挿入できます。非POD型の場合、型に仮想関数または基本クラス、ユーザー定義コンストラクター(もちろん)がなく、ベースとそのすべての非静的メンバーにも適用される場合、同じ運があります。
-
他のすべてのタイプでは、すべてのベットはオフです。最初にPODで値を読み取り、次にそのデータで非POD型を初期化する必要があります。
オブジェクトをバッファに保存しています。 ...オブジェクトの全体的なサイズがわかっている場合、このメモリへのポインタを作成し、そのメモリで関数を呼び出すことは受け入れられますか?
これは、キャストの使用が許容される範囲で許容されます:
#include <iostream>
namespace {
class A {
int i;
int j;
public:
int value()
{
return i + j;
}
};
}
int main()
{
char buffer[] = { 1, 2 };
std::cout << reinterpret_cast<A*>(buffer)->value() << '\n';
}
生のメモリのようなものにオブジェクトをキャストし、再び戻すことは、特にCの世界では実際にかなり一般的です。ただし、クラス階層を使用している場合は、メンバー関数へのポインターを使用する方が理にかなっています。
次のクラスがあるとします:...
このクラスのサイズが24で、メモリ内の開始位置のアドレスがわかっている場合...
これは事態が困難になる場所です。オブジェクトのサイズには、そのデータメンバー(およびすべての基本クラスのすべてのデータメンバー)のサイズに加えて、パディングと関数ポインターまたは実装依存の情報、特定のサイズ最適化(空の基本クラスの最適化)から保存されたものが含まれます。結果の数値が0バイトの場合、オブジェクトは少なくとも1バイトのメモリを取得する必要があります。これらは、言語の問題と、メモリアクセスに関してほとんどのCPUが持っている一般的な要件の組み合わせです。 物事を適切に機能させようとするのは非常に苦痛です。
オブジェクトを割り当てて生メモリとの間でキャストするだけの場合、これらの問題は無視できます。しかし、オブジェクトの内部をある種のバッファにコピーすると、かなり早く頭を伸ばします。上記のコードは、アライメントに関するいくつかの一般的なルールに依存しています(つまり、クラスAがintと同じアライメント制限を持っていることを知っているので、配列を安全にAにキャストできますが、必ずしも保証することはできませんでした配列の一部をAにキャストし、一部を他のデータメンバーを持つ他のクラスにキャストする場合も同じです。
ああ、オブジェクトをコピーするときは、ポインタを適切に処理していることを確認する必要があります。
Googleのプロトコルバッファまたは FacebookのThrift 。
はい、これらの問題は難しいです。そして、はい、いくつかのプログラミング言語はそれらを敷き詰めています。 しかし、非常に多くのものがあります敷物の下に掃きました:
SunのHotSpot JVMでは、オブジェクトストレージは最も近い64ビット境界に揃えられます。さらに、すべてのオブジェクトにはメモリ内に2ワードのヘッダーがあります。 JVMのワードサイズは通常、プラットフォームのネイティブポインターサイズです。 (32ビットのintと64ビットのdouble(96ビットのデータのみ)で構成されるオブジェクトには、オブジェクトヘッダーに2ワード、intに1ワード、doubleに2ワードが必要です。それは5ワードです:160ビット。整列のため、このオブジェクトは192ビットのメモリを占有します。
これは、Sunがメモリアライメントの問題に対して比較的単純な戦略に依存しているためです(架空のプロセッサでは、charは任意のメモリ位置、4で割り切れる任意の位置のint、およびdouble 32で割り切れるメモリロケーションにのみ割り当てる必要がある場合がありますが、最も厳しいアライメント要件は他のすべてのアライメント要件も満たしているため、Sunはすべてのアライメントを最も厳しいロケーションに従って行います。
- クラスに仮想関数が含まれていない(したがって、クラスインスタンスにvptrがない)場合、およびクラスのメンバーデータがメモリに配置される方法について正しい仮定を行う場合、あなたが提案していることをすることはうまくいくかもしれません(しかし、移植性がないかもしれません)。
- はい、別の方法(より慣用的ですが、あまり安全ではありません...クラスがそのデータをどのようにレイアウトするかを知る必要があります)は、いわゆる「配置演算子new」を使用することです。およびデフォルトのコンストラクタ。
これは、「安全」の意味に依存します。この方法でメモリアドレスをポイントにキャストすると、コンパイラーが提供するタイプセーフ機能をバイパスし、自分自身に責任を負います。 Chrisが示唆するように、メモリレイアウトまたはコンパイラの実装の詳細について誤った仮定をした場合、予期しない結果が生じ、移植性が失われます。
「安全」について心配しているため、このプログラミングスタイルの場合、既存のライブラリなどの移植可能なタイプセーフなメソッドを調査したり、その目的のためにコンストラクターまたは代入演算子を記述したりする価値があるでしょう。