LoginSignup
1
0

バイナリをデコードするときにメモリアライメントを知らなくて困った話(C/C++)

Last updated at Posted at 2024-03-20

バイナリのデコードで困ったことが起きたので記録として残します。
バイナリと言っても、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にする。

cpp
+ #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()と同じかそれ以上になっている。)

memory_1.png

間違った認識

勘違いしていた内容について書きます。恥ずかしいですが、誰かのためになればと思って書きます。

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;
}

この時の自分の脳内のイメージ図はこんな感じ。
memory_2.png

そりゃデータがうまく取り出せるはずだ!!(大間違い)

実行結果
$ 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() でメモリ境界をデフォルトに戻す

参考

1
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0