EarOwlの日記: エンディアン・アライメント・int型のサイズ 7
日記 by
EarOwl
C プログラマ、特に組込系のプログラマにとって、移植性の高いコードを書く上で重要でありながら意外とハマる人が多いのが、タイトルに挙げた 3点だと思う。
エンディアンの変換で重要なのは、以下の 2点。
- ビッグ・リトルといった固定のエンディアンと『CPU のネイティブなエンディアン』を分けて考える
- どのエンディアンからどのエンディアンへ変換したいのかが明確になるような関数名/マクロ関数名にする
swap16/32 という関数/マクロ関数名をよく見かけるが、これだとどのエンディアンからどのエンディアンへ変更したいのかが分からないため、移植時に問題となる可能性がある。
アライメントに関しては、構造体のパディングは理解していても、例えば以下のようなコードで問題が発生する場合があることを理解していない人が多いように思う。
void fill_buffer(uint8_t *buf, uint32_t n)
{
*(uint32_t *)buf = n;
}
C 言語の仕様として、 buf が uint32_t 型の境界にアラインされていない場合の動作は未定義となる。例えば、 CPU によってはアドレスの下位 2ビットが無視されてしまい、誤ったアドレスに値を書き込んでしまう結果となることがある。
int 型のサイズに関しては、理解していてもなかなか難しい。私も以下のようなコードを書いてハマったことがある。
uint32_t n;
uint8_t buf[4];
/* … */
n = buf[0] * 0x1 + buf[1] * 0x100 + buf[2] * 0x10000 + buf[3] * 0x1000000;
演算子の優先順位、型変換の規則をきっちり理解して、コードを書く際には常に注意を払うことが必要だと思う。
片方だけ (スコア:2)
入力時の変換は入力側、出力時は出力側だけ表す方が楽だと書いてみるテスト。
Re:片方だけ (スコア:1)
確かに入力なら『どのエンディアンへ』の方が、出力なら『どのエンディアンから』の方が CPU のネイティブエンディアンになるのが普通だから、それで十分ですね。
私は基本的にエンディアンやアライメントの異なるデータは uint8_t の配列で受け渡しを行い、 uint16/32_t 型の変数は必ず CPU のネイティブなエンディアンとなるようにしています。
正解は? (スコア:0)
uint32_t n;
uint8_t buf[4];
/* … */
n = buf[0] * 0x1 + buf[1] * 0x100 + buf[2] * 0x10000 + buf[3] * 0x1000000;
int=int32_tの処理系だとして、定数の部分はUL(unsigned long=uint32_tとして)をつけておかないと乗算の結果がオーバーフローしてそれが問題になるのかもしれないですが、どうでしょう?
正解もこっそり教えて下さい。
Re:正解は? (スコア:1)
普通はシフト演算でやらね?
n = buf[0] | buf[1] << 8 | buf[2] << 16 | buf[3] << 24;
Re:正解は? (スコア:1)
int が 16bit のとき、 buf[1] * 0x100 が負の値となってしまう場合があります。
これを 32bit に型変換する際に上位 16bit が 0x0000 になって欲しいところが
0xFFFF になってしまい問題となります。
ビットシフトで行う場合も、 fl さんのコメント中のコードでは int が 16bit の場合に
16ビット及び 24ビットのシフトがビット幅以上のシフトになってしまい、結果は
未定義となります。
Re:正解は? (スコア:1)
書き忘れ。
対策として、私の場合は以下のようなコードにしています。
n = buf[0] * 0x1ul + buf[1] * 0x100ul + buf[2] * 0x10000ul + buf[3] * 0x1000000ul;
Re:正解は? (スコア:1)
16bitは考えてなかった。なるほどね。
32bitからuint64_t使うときも同じ問題に当たるから、忘れないようにしよう。