web-dev-qa-db-ja.com

8ビット整数から8ビットを超える値を取得するにはどうすればよいですか?

この小さな宝石の後ろに隠れている非常に厄介なバグを見つけました。 C++仕様では、符号付きオーバーフローは未定義の動作ですが、値がビット幅sizeof(int)に拡張されたときにオーバーフローが発生する場合のみです。私が理解しているように、charのインクリメントは、sizeof(char) < sizeof(int)である限り、未定義の動作であってはなりません。しかし、それはcimpossible値を取得する方法を説明しません。 8ビット整数として、どのようにしてcがそのビット幅より大きい値を保持できますか?

コード

// Compiled with gcc-4.7.2
#include <cstdio>
#include <stdint.h>
#include <climits>

int main()
{
   int8_t c = 0;
   printf("SCHAR_MIN: %i\n", SCHAR_MIN);
   printf("SCHAR_MAX: %i\n", SCHAR_MAX);

   for (int32_t i = 0; i <= 300; i++)
      printf("c: %i\n", c--);

   printf("c: %i\n", c);

   return 0;
}

出力

SCHAR_MIN: -128
SCHAR_MAX: 127
c: 0
c: -1
c: -2
c: -3
...
c: -127
c: -128  // <= The next value should still be an 8-bit value.
c: -129  // <= What? That's more than 8 bits!
c: -130  // <= Uh...
c: -131
...
c: -297
c: -298  // <= Getting ridiculous now.
c: -299
c: -300
c: -45   // <= ..........

ideoneで確認してください。

118
Unsigned

これはコンパイラのバグです。

未定義の動作に対して不可能な結果を​​得ることは有効な結果ですが、実際にはコードには未定義の動作はありません。何が起こっているのかというと、コンパイラは動作を未定義と考えており、それに応じて最適化します。

cint8_tとして定義され、int8_tintにプロモートされる場合、c--は、int算術演算でc - 1の減算を実行し、結果をint8_tに変換します。 intの減算はオーバーフローせず、範囲外の整数値を別の整数型に変換することは有効です。宛先タイプが署名されている場合、結果は実装定義ですが、宛先タイプの有効な値でなければなりません。 (また、宛先タイプが符号なしの場合、結果は明確に定義されますが、ここでは適用されません。)

111
user743382

コンパイラには、他の要件があるため、標準に準拠していない以外のバグがある場合があります。コンパイラは、それ自体の他のバージョンと互換性がある必要があります。また、他のコンパイラと何らかの方法で互換性があり、そのユーザーベースの大部分が保持する動作に関するいくつかの信念に準拠することも期待される場合があります。

この場合、それは適合バグのようです。式c--は、c = c - 1と同様の方法でcを操作する必要があります。ここでは、右側のcの値がint型に昇格され、その後減算が行われます。 cint8_tの範囲にあるため、この減算はオーバーフローしませんが、int8_tの範囲外の値を生成する場合があります。この値が割り当てられると、変換はint8_t型に戻されるため、結果はcに戻ります。範囲外の場合、変換には実装定義の値があります。 ただし、int8_tの範囲外の値は実装定義の有効な値ではありません。実装は、8ビット型が突然9ビット以上を保持することを「定義」できません。値の場合実装定義であるとは、int8_tの範囲内の何かが生成され、プログラムが継続することを意味します。 C標準では、飽和演算(DSPで一般的)またはラップアラウンド(メインストリームアーキテクチャ)などの動作が可能です。

コンパイラは、int8_tcharなどの小さな整数型の値を操作するときに、より広い基礎となるマシン型を使用しています。算術が実行されると、短整数型の範囲外の結果は、この幅の広い型で確実にキャプチャできます。変数が8ビット型であるという外部から見える動作を維持するには、より広い結果を8ビットの範囲に切り捨てる必要があります。マシンの保存場所(レジスタ)は8ビットより広く、大きな値に満足しているため、これを行うには明示的なコードが必要です。ここでは、コンパイラ正規化を無視値をそのままprintfに渡します。 printf%i変換指定子は、引数が元々int8_t計算に由来することを知りません。 int引数を使用しているだけです。

15
Kaz

これをコメントに収めることができないため、回答として投稿しています

非常に奇妙な理由で、--演算子が原因であることがあります。

Ideoneに投稿されたコードをテストし、c--c = c - 1に置き換えました。値は[-128 ... 127]の範囲内に留まりました。

c: -123
c: -124
c: -125
c: -126
c: -127
c: -128 // about to overflow
c: 127  // woop
c: 126
c: 125
c: 124
c: 123
c: 122

気紛れ? i++i--のような式に対してコンパイラーが何をするかについてはあまり知りません。戻り値をintに昇格して渡す可能性があります。あなたが実際に8ビットに収まらない値を取得しているので、それが私が思いつくことができる唯一の論理的な結論です。

14

基礎となるハードウェアは、まだint8_tを保持するために32ビットレジスタを使用していると思います。仕様ではオーバーフローの動作が強制されないため、実装ではオーバーフローのチェックは行われず、より大きな値も保存できます。


ローカル変数をvolatileとしてマークすると、そのためにメモリを使用することを強制し、その結果、範囲内の期待値を取得します。

12
Zoltán

アセンブラーコードは問題を明らかにします:

:loop
mov esi, ebx
xor eax, eax
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
sub ebx, 1
call    printf
cmp ebx, -301
jne loop

mov esi, -45
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
xor eax, eax
call    printf

EBXはFFポストデクリメントでandedするか、EBXの残りをクリアしてBLのみを使用する必要があります。 decの代わりにsubを使用することに興味があります。 -45は完全に神秘的です。 300と255 = 44のビット単位の反転です。-45=〜44。どこかに接続があります。

C = c-1を使用してさらに多くの作業を行います。

mov eax, ebx
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
add ebx, 1
not eax
movsx   ebp, al                 ;uses only the lower 8 bits
xor eax, eax
mov esi, ebp

次に、RAXの低い部分のみを使用するため、-128〜127に制限されます。コンパイラオプション "-g -O2"。

最適化なしで、正しいコードが生成されます。

movzx   eax, BYTE PTR [rbp-1]
sub eax, 1
mov BYTE PTR [rbp-1], al
movsx   edx, BYTE PTR [rbp-1]
mov eax, OFFSET FLAT:.LC2   ;"c: %i\n"
mov esi, edx

したがって、オプティマイザーのバグです。

11
user2513931

使用する %hhd の代わりに %i!あなたの問題を解決するはずです。

コンパイラーの最適化の結果は、32ビットの数値を出力するようにprintfに指示し、(実際にはポインターサイズの)スタックに(おそらく8ビットの)数値をプッシュすることと組み合わせた結果です。

4
Zotta

これはコードの最適化によって行われていると思います:

_for (int32_t i = 0; i <= 300; i++)
      printf("c: %i\n", c--);
_

コンパイラーは、icの両方に_int32_t i_変数を使用します。最適化をオフにするか、直接キャストしますprintf("c: %i\n", (int8_t)c--);

3
Vsevolod

c自体はint8_tとして定義されていますが、++または-- over int8_tを操作する場合、最初にint操作の結果に暗黙的に変換されます代わりにcの内部値がprintfで出力されますが、これはたまたまintになります。

ループ全体の後のc実際の値、特に最後のデクリメントの後を参照してください

-301 + 256 = -45 (since it revolved entire 8 bit range once)

動作に似た正しい値-128 + 1 = 127

cは、intサイズのメモリの使用を開始しますが、int8_tのみを使用してそれ自体として印刷すると、8 bitsとして印刷されます。 intとして使用される場合、すべての32 bitsを利用します

[コンパイラのバグ]

1
Izhar Aazmi

Int iが300になり、cが-300になるまでループが続くためだと思います。そして最後の価値は

printf("c: %i\n", c);
0
r.mirzojonov