C言語のbit-fieldのメモリレイアウトについて


3行まとめ

  • Mach-Oの仕様にbit-fieldを使って定義されている箇所がある
  • bit-fieldを使用している構造体のメモリレイアウトはどうも実装依存ぽい
  • そしたらオブジェクトファイルのbyte layour決まらないのでは?何か見逃してる??分からない!助けて!!

助けてください

本文

MachOフォーマットのパースをしている中で、 relocation_info という構造体にbit-fieldが使われていた。

struct relocation_info {
    int32_t     r_address;
    uint32_t    r_symbolnum:24,
                r_pcrel:1,
                r_length:2,
                r_extern:1,
                r_type:4;
};

で、問題は、この構造体はどのようなメモリレイアウトになるのかということ。

最初、勝手に以下のようなレイアウトだと思い込んでいた。

<------------- 24bit -----------><- 1bit -><-- 2bit --><- 1bit -><---- 4bit ---->

けどどうも違うっぽい。あと24bitの r_symbolnum とかはどういうエンディアンで保存されるんだ。

プラットフォームのエンディアンに従うのか?それともビッグエンディアン(素直な配置)なのか?

軽く調べてみると、

  • bit-fieldの配置はプラットフォームのエンディアンによって2通りある

という主張と、

  • bit-fieldの配置は完全にコンパイラ依存である

という主張の2つが見つかった。

え、コンパイラ依存だとしたら、クロスアセンブルした場合におかしなことになってしまうのでは??

もしかしてMachOの仕様にメモリレイアウト書いてあるのか?と思いながら仕様を読み返したけど書いてないっぽい。(見逃しているだけかも)

こうなったら標準仕様を参照するしかないと思い、Cの標準規格である ISO/IEC 9899 を参照すると、以下のように書いてある。

An implementation may allocate any addressable storage unit large enough to hold a bit-field. If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit. If insufficient space remains, whether a bit-field that does not fit is put into the next unit or overlaps adjacent units is implementation-defined. The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined. The alignment of the addressable storage unit is unspecified.

関係ありそうな部分を太字にした。つまり、あるbit-fieldの直後にあるbit-fieldは、そのbit-fieldと同じstorage unit の中の、そのbit-fieldの隣の bit に置かれるらしい。ただし、置かれる順番は実装依存。

いや結局実装依存なんかい。

まあでもとりあえず、今回のbit-fieldのメモリレイアウトは以下の2パターンに絞られた。

  • 24 - 1 - 2 - 1 - 4
  • 4 - 1 - 2 - 1 - 24

で、最初のパターンが間違いだったんだから2つめのパターンが正解なんだけど、問題はこれがどの環境でも同じなのか、ということ。

今回試したのはリトルエンディアン環境で生成されたオブジェクトファイル(リトルエンディアン環境でコンパイルされたアセンブラを使って生成されたオブジェクトファイル)だったからこんな感じになったのか。もしくはやはりMachOの標準で規定されていて、どのような環境でもこの順番になるのか。もしくはアセンブラをコンパイルしたコンパイラ次第で変わってしまうのか。

llvm-epiというプロジェクトの、この辺のcommitとかをみると、どの環境でも 4 - 1 - 2 - 1 - 24 のパターンとしてパースしているっぽい。

linuxでもbit-fieldが使われているっぽいけど、どうしているんだろうか。

LXR linux/include/linux/ip.h

追記

reloc.h ファイルを眺めていて気がついたけど、 scattered_relocation_info 構造体は、ビッグエンディアンとリトルエンディアンでbit-fieldの定義を逆にしているな。

struct scattered_relocation_info {
#ifdef __BIG_ENDIAN__
   uint32_t r_scattered:1,
        r_pcrel:1,
        r_length:2,
        r_type:4,
    r_address:24;
   int32_t  r_value;
#endif /* __BIG_ENDIAN__ */
#ifdef __LITTLE_ENDIAN__
   uint32_t r_address:24,
        r_type:4,
        r_length:2,
        r_pcrel:1,
        r_scattered:1;
   int32_t  r_value;
#endif /* __LITTLE_ENDIAN__ */
};

scattered_relocation_info では、そのバイト列が通常の relocation_info ではなく scattered_relocation_info であることを明示するために、MSB(最上位ビット)を 1 にする必要がある。

そのため、ビッグエンディアン環境でもリトルエンディアン環境でも同じメモリレイアウトになるようにする必要がある。それがこのような実装になっているのだと思う。

ということはつまり、bit-fieldのメモリ上の順番は実装依存ではなく、エンディアンによって決定されるということだろうか。少なくともこの実装はそれを前提にしている。

標準仕様は実装依存としているけど、事実上エンディアンで決まるよね、っていうことなのだろうか。