119

この小さな宝石の背後に隠れている非常に厄介なバグを突き止めました。C++ 仕様では、符号付きオーバーフローは未定義の動作であることを認識していますが、値が bit-width に拡張されたときにオーバーフローが発生した場合のみsizeof(int)です。私が理解しているように、 a のインクリメントは、 であるchar限り未定義の動作であってはなりませんsizeof(char) < sizeof(int)しかし、それは不可能なcを取得する方法を説明していません。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 で確認してください。

4

9 に答える 9

111

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

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

cが として定義されint8_t、 にint8_t昇格した場合int、は算術でc--減算を実行し、結果を に変換することになっています。での減算はオーバーフローせず、範囲外の整数値を別の整数型に変換することは有効です。宛先の型が署名されている場合、結果は実装定義ですが、宛先の型に対して有効な値である必要があります。(宛先の型が符号なしの場合、結果は明確に定義されていますが、ここでは当てはまりません。)c - 1intint8_tint

于 2013-04-20T22:24:04.650 に答える
15

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

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

int8_tコンパイラは、またはのような小さな整数型の値を操作するときに、より広い基礎となるマシン タイプを使用していますchar。演算が実行されると、短整数型の範囲外の結果を、このより広い型で確実にキャプチャできます。変数が 8 ビット型であるという外部から見える動作を維持するには、より広い結果を 8 ビット範囲に切り詰める必要があります。マシンの格納場所 (レジスタ) は 8 ビットよりも広く、より大きな値に満足しているため、これを行うには明示的なコードが必要です。ここでは、コンパイラは値の正規化を怠り、単純にそのまま渡しましたprintf。の変換指定子は、引数が元々計算から来たものであることを認識%iしていません。それはちょうどで働いていますprintfint8_tint口論。

于 2013-04-21T04:01:41.940 に答える
14

これはコメントに収まらないので、回答として投稿します。

非常に奇妙な理由で、--オペレーターがたまたま犯人でした。

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--。戻り値を an に昇格させてint渡す可能性があります。実際には 8 ビットに収まらない値を取得しているため、これが私が思いつく唯一の論理的結論です。

于 2013-04-20T22:16:18.993 に答える
12

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


強制的にメモリを使用するようにローカル変数をマークするvolatileと、その結果、範囲内の期待値が得られます。

于 2013-04-20T22:12:44.090 に答える
11

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

: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 ポスト デクリメントで AND 演算するか、BL のみを使用して残りの EBX をクリアする必要があります。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

つまり、オプティマイザーのバグです。

于 2013-06-23T18:49:16.047 に答える
4

!%hhdの代わりに使用します。%iあなたの問題を解決する必要があります。

ここに表示されるのは、コンパイラの最適化と、32 ビットの数値を出力するように printf に指示し、(おそらく 8 ビット) の数値をスタックにプッシュした結果です。これは、実際にはポインター サイズです。これは、x86 のプッシュ オペコードがどのように機能するかによるものです。

于 2013-05-05T17:25:31.273 に答える
3

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

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

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

于 2013-05-05T10:36:25.287 に答える
0

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

printf("c: %i\n", c);
于 2013-06-20T07:28:00.510 に答える