バイナリのデコードで困ったことが起きたので記録として残します。
バイナリと言っても、byte単位で意味の持つものだったので、すでにバイト列に変換されていたところからの話です。
メモリ上の値を扱いを型キャストで変更できるので、バイナリ列の先頭のアドレスを別の型にキャストしてやろう、つまり、下のように書いたらうまくいくだろうと思った。
struct T {...};
std::vector<uint8_t> buffer{...};
T* t = reinterpret_cast<T*>(buffer.data());
これだとうまくいかなかった。
結論
先に結論。
アラインメントはバイナリデータをC/C++から使う時に意識しないといけない。
アイランメントとは、決められたメモリ配置ルールに乗っ取って変数を配置すること。
処理系によって変わる可能性はあるが、intでは4byte境界に、shortでは2byte境界に、charでは1byte境界に配置される必要がある。
struct T {
short a; // 2byte
int b; // 4byte
short c; // 2byte
};
構造体Tでは
a a _ _
b b b b
c c _ _
というメモリ配置になるので、
2+4+2=8byteではなく、10byte12byteのサイズの構造体になる。
(cの後にも2byte入るので、全体で12byteになるみたいです。)
なので、そのままではうまくいかない。
解決策はメモリ境界を1byteにする。
+ #pragma pack(1)
struct T {
short a;
int b;
short c;
};
+ #pragma pack()
std::vector<uint8_t> buffer{...}; // 8byte
T* t = reinterpret_cast<T*>(buffer.data());
本編
メモリについておさらい
知っている人にとっては当たり前すぎるかもしれないが、メモリについておさらい。
以下のような場合の数字のメモリ配置を考える。
int main()
{
vector<uint8_t> d = {0x12, 0x34, 0x56, 0x78, 0x9a};
d.data()
}
cppref-vectorによると
vector はシーケンスコンテナの一種で、各要素は線形に、順序を保ったまま格納される。
vector コンテナは可変長配列として実装される。通常の( new [] で確保した)配列と同じように、 vector の各要素は連続して配置されるため、イテレータだけでなく添字による要素のランダムアクセスも高速である。
とのことで、vectorではデータを連続して配置する。
また、cppref - vector::dataによると
配列の先頭へのポインタを返す。
とのことなので、今の状態を図に書くと以下のイメージ。5byte分データが入ってます。(実際にvectorが確保してるメモリサイズはvector::capacity()で確認でき、それはvector::size()と同じかそれ以上になっている。)
間違った認識
勘違いしていた内容について書きます。恥ずかしいですが、誰かのためになればと思って書きます。
vectorの中のbyte列を意味のある形として取り出したい(構造体に格納したい)から、構造体を定義して、d.data()をキャストしよう!(このとき、僕は「我ながらスマートな方法を思いついた」と思ってた。)
typedef struct {
uint8_t a;
uint16_t b;
uint8_t c;
uint8_t d;
} Packet_t;
int main()
{
vector<uint8_t> d = {0x12, 0x34, 0x56, 0x78, 0x9a};
Packet_t* p = reinterpret_cast<Packet_t*>(d.data());
cout << "a: " << std::hex << (int)p->a << endl;
cout << "b: " << std::hex << (int)p->b << endl;
cout << "c: " << std::hex << (int)p->c << endl;
cout << "d: " << std::hex << (int)p->d << endl;
}
そりゃデータがうまく取り出せるはずだ!!(大間違い)
$ g++ main.cpp -std=c++11 && ./a.out
a: 0x12
b: 0x7856
c: 0x9a
d: 0x0
$
は?0x34どこに消えた???
デバッグ
意味がわからないので、それぞれの値のアドレスも出力する。
- cout << "a: " << std::hex << (int)p->a << endl;
- cout << "b: " << std::hex << (int)p->b << endl;
- cout << "c: " << std::hex << (int)p->c << endl;
- cout << "d: " << std::hex << (int)p->d << endl;
+ cout << "a: " << std::hex << (void*)&p->a << ": 0x" << (int)p->a << endl;
+ cout << "b: " << std::hex << (void*)&p->b << ": 0x" << (int)p->b << endl;
+ cout << "c: " << std::hex << (void*)&p->c << ": 0x" << (int)p->c << endl;
+ cout << "d: " << std::hex << (void*)&p->d << ": 0x" << (int)p->d << endl;
$ g++ main.cpp -std=c++11 && ./a.out
a: 0x15A6067A0: 0x12
b: 0x15A6067A2: 0x7856
c: 0x15A6067A4: 0x9a
d: 0x15A6067A5: 0x0
ん?0x15A6067A1の値である0x34どこ行った???
ってかこれだと構造体のサイズ6byteになってないか?
cout << sizeof(Packet_t) << endl;
$ g++ main.cpp -std=c++11 && ./a.out
6
うんやっぱり6byteになってる。困ってたらアラインメントって先輩が教えてくれた。
メモリ配置にルールがあるらしい。
構造体に#pragma packでメモリ境界を変更することができるらしい。
#pragma pack(1)
struct Packet_t {
uint8_t a;
uint16_t b;
uint8_t c;
uint8_t d;
};
#pragma pack()
int main()
{
cout << sizeof(Packet_t) << endl; // -> 5
}
#pragma pack(1) でメモリ境界を1byteにする
#pragma pack() でメモリ境界をデフォルトに戻す
参考
- https://learn.microsoft.com/ja-jp/cpp/cpp/alignment-cpp-declarations?view=msvc-170
- https://www.ibm.com/docs/ja/zos/2.3.0?topic=descriptions-pragma-pack
- https://qiita.com/hoboaki/items/46700f03b522193e9747
補足
他の人も言っている通り,無意味に無理矢理アライメントをいじるのはよくないです.どうしても上述のようにしないといけない場合もあるかもしれないですが,そうでない場合は,余計なパディングが入らないようにデータを定義した方がよいはずです.
また,普通にキャストする場合,以下のようにするのがいいと思います.
struct Packet_t {
uint8_t a;
uint16_t b;
uint8_t c;
uint8_t d;
};
vector<uint8_t> d = {0x12, 0x34, 0x56, 0x78, 0x9a};
Packet_t packet
auto p = d.data();
memcpy(packet.a, p, sizeof(packet.a))
p += sizeof(packet.a)
/* b, c, d も同様 */