VisualStudio(VC++)での符号あり定義時のビットフィールドの取り扱いについて
2015/09/09
ここで書くのはVisualStudio(2013)のVC++での振る舞いについてであり、他の環境ではどうなるかはわかりません。
この、他の環境ではどうなるかわからない(実装依存)というところが、言及しているサイトが少ない理由なのかもしれません。
今回何故調べようと思ったのかというと、人の書いたプログラムでsigned intで定義されているプログラムがあり、その時の挙動がなんでそうなるの?と調べだしたためです。
色々検索してはみたんですが大体のサイトが「signed intの場合や、int, signed int, unsigned int以外の型の場合は実装依存です」で終わってしまっていて、実際どうなるのか書かれていないため、そうは言われても気持ちが悪いので調べることにしました。
まずはunsigned intの場合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <stdio.h> typedef struct _ST { unsigned int bField00 : 1; unsigned int bField01 : 1; unsigned int bField02 : 1; unsigned int bField03 : 1; unsigned int bField04 : 1; unsigned int bField05 : 1; unsigned int bField06 : 1; unsigned int bField07 : 1; } stTest; int main(void) { stTest a; a.bField00 = 1; printf("%d", a.bField00); return 0; } |
これの結果としてはこうなります。
1
至って普通です。
では、signed intの場合どうなるでしょうか?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <stdio.h> typedef struct _ST { signed int bField00 : 1; signed int bField01 : 1; signed int bField02 : 1; signed int bField03 : 1; signed int bField04 : 1; signed int bField05 : 1; signed int bField06 : 1; signed int bField07 : 1; } stTest; int main(void) { stTest a; a.bField00 = 1; printf("%d", a.bField00); return 0; } |
これの実行結果は以下のようになります。
-1
???
直前でビットフィールドbField00に1を入れていますが、a.bField00をprintfしてみると-1になります。
判定をしてみても-1になってるんですが、試しに構造体を共用体にしてint型の値として見ると、目的の場所のビットはきちんと立っていました。
が、値の判定や表示は-1になります。
どこをどうしたらこうなるのかさっぱりわかりません。
しかたがないので逆アセンブルしてみることにしました。
unsigned intの場合の逆アセンブルリスト(抜粋)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
; 16 : ; 17 : stTest a; ; 18 : ; 19 : a.bField00 = 1; mov eax, DWORD PTR _a$[ebp] or eax, 1 mov DWORD PTR _a$[ebp], eax ; 20 : ; 21 : printf("%d", a.bField00); mov eax, DWORD PTR _a$[ebp] and eax, 1 mov esi, esp push eax push OFFSET ??_C@_02DPKJAMEF@?$CFd?$AA@ call DWORD PTR __imp__printf add esp, 8 cmp esi, esp call __RTC_CheckEsp ; 22 : ; 23 : return 0; xor eax, eax |
値を取り出しているのはmov eax, DWORD PTR _a$[ebp]です。
で、bField00はbit0なので、次のand eax,1でbit0を残して全て0でクリアしています。
そしてprintfへ値を渡して出力しにいっています。
まあ、これはわかります。
次、signed intです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
; 16 : ; 17 : stTest a; ; 18 : ; 19 : a.bField00 = 1; mov eax, DWORD PTR _a$[ebp] or eax, 1 mov DWORD PTR _a$[ebp], eax ; 20 : ; 21 : printf("%d", a.bField00); mov eax, DWORD PTR _a$[ebp] shl eax, 31 ; 0000001fH sar eax, 31 ; 0000001fH mov esi, esp push eax push OFFSET ??_C@_02DPKJAMEF@?$CFd?$AA@ call DWORD PTR __imp__printf add esp, 8 cmp esi, esp call __RTC_CheckEsp ; 22 : ; 23 : return 0; xor eax, eax |
今回の現象の原因は以下にあります。
1 2 |
shl eax, 31 ; 0000001fH sar eax, 31 ; 0000001fH |
bit0を取り出すためにまずshl eax, 31で左に31ビット算術シフトし、次に右に31ビット論理シフトを行なっています。
まあこの部分を見るとわかる人はわかると思いますが、bit0に1が立っている場合に31ビット左に算術シフトすると、
00000000 00000000 00000000 00000001 (0x00000001)
これが、
10000000 00000000 00000000 00000000 (0x80000000)
という風になります。
次にsar eax, 31で右31ビット論理シフトするわけですが、上記0x80000000は符号を表す最上位ビットが立った状態になっているため、sarを行なうと1ビットシフトする度に最上位ビットに1が立っていきます。
つまり、31ビット右シフトした後は以下のような値となります。
11111111 11111111 11111111 11111111 (0xFFFFFFFF)
ということで-1となりました。
signedで宣言した場合、符号を維持しようとしてsarを行ないますが、1ビットのビットフィールドを定義した状態だと符号ビットなどというものは存在しないか、符号ビットしかないような状態になるため、それを値として扱いたい側と符号として扱う側とで矛盾が発生し、このような結果になっていると思われます。
おそらくコンパイラ側からすれば「1ビットで定義するんなら符号ありなんかで定義すんなよ!」ということなのでしょうが、使う側からしたら「察してくれよ!」とか思ってしまいますね・・・。もしくはこのような状態になるのであればWarningかなんかを出して欲しいです。
これが1bitより大きいビットフィールドだと、意図通り最上位ビットが符号ビットとして扱われ、負数を入れたい場合などでもきちんと使えるのだと思いますが、1ビットでは値が保存できなくなってしまいます。
もしくは0か-1かしか保持しないものだと理解した上で敢えてsignedで定義するかですね(爆)
まあ、そもそもビットフィールド自体が環境依存が激しいので出来れば使いたくないというのが正直なところですが、既に大分作り込んでしまっているような人のソースの場合困ってしまいますね・・・。
ということで、Visual Studio(2013)でのsigned intでのビットフィールドの扱い方の一部をご紹介いたしました。
ではでは。