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


118

この小さな宝石の後ろに隠れている非常に厄介なバグを見つけました。C ++仕様によると、符号付きオーバーフローは未定義の動作ですが、値がbit-widthに拡張されたときにオーバーフローが発生した場合のみsizeof(int)です。私が理解しているように、のインクリメントは、で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で確認してください。


61
「C ++仕様に従って、符号付きオーバーフローは未定義であることを認識しています。」 - 正しい。正確には、が未定義であるだけでなく、動作も定義されています。物理的に不可能な結果が得られたように見えることは、妥当な結果です。

@hvd一般的なC ++実装がこの動作を引き起こす方法について誰かが説明していると確信しています。おそらくそれは整列に関係しているのでしょうprintf()か、それとも変換はどのようにしていますか?
rliu 2013

他は主な問題に対処しました。私のコメントはより一般的であり、診断アプローチに関連しています。あなたがこのようなパズルを見つけた理由の一部は、それが可能だった根本的な信念であると信じています。明らかに、それは不可能ではないので、それを受け入れてもう一度見てください
Tim X

@TimX-私はその振る舞いを観察し、明らかにその意味では不可能ではないという結論を導き出しました。私の言葉の使用は、9ビット値を保持する8ビット整数を指していました。これは、定義上不可能です。これが発生したという事実は、それが8ビット値として扱われていないことを示唆しています。他の人が対処したように、これはコンパイラのバグが原因です。ここに見える唯一の不可能性は、8ビット空間の9ビット値であり、この明らかな不可能性は、実際に報告されているよりも「大きい」空間によって説明されます。
署名なし2013

私はそれを私の機械でテストしたところ、結果はまさにそれがあるべきものです。c:-120 c:-121 c:-122 c:-123 c:-124 c:-125 c:-126 c:-127 c:-128 c:127 c:126 c:125 c:124 c: 123 c:122 c:121 c:120 c:119 c:118 c:117そして私の環境は:Ubuntu-12.10 gcc-4.7.2
VELVETDETH

回答:


111

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

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

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


私はそれを「バグ」とは言いません。符号付きオーバーフローは未定義の動作を引き起こすため、コンパイラーはそれが起こらないと想定し、ループを最適化して中間値をcより広い型に保持する権利があります。おそらく、それがここで起こっていることです。
マイクシーモア

4
@MikeSeymour:ここでの唯一のオーバーフローは(暗黙の)変換です。符号付き変換のオーバーフローには未定義の動作はありません。実装定義の結果が生成されるだけです(または実装定義の信号が発生しますが、ここでは発生していないようです)。算術演算と変換の定義の違いは奇妙ですが、それが言語標準で定義されている方法です。
キース・トンプソン

2
@KeithThompsonこれはCとC ++で異なるものです。Cは実装定義の信号を許可しますが、C ++は許可しません。C ++は、「宛先タイプが署名されている場合、宛先タイプ(およびビットフィールド幅)で表すことができる場合、値は変更されません。それ以外の場合、値は実装定義です。」

たまたま、g ++ 4.8.0では奇妙な動作を再現できません。
Daniel Landau

2
@DanielLandauそのバグのコメント38を参照してください:「4.8.0で修正済み」。:)

15

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

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

コンパイラは、int8_tまたはのような小さな整数型の値を操作するときに、より幅広い基本となるマシンタイプを使用していますchar。算術を実行すると、この広い型で短整数型の範囲外の結果を確実に取り込むことができます。変数が8ビット型であるという外部から見える動作を維持するには、より広い結果を8ビットの範囲に切り捨てる必要があります。マシンの格納場所(レジスタ)は8ビットより広く、より大きな値で十分なので、これを行うには明示的なコードが必要です。ここで、コンパイラーは値を正規化することを怠り、そのままそのまま渡しましたprintf。の変換指定子%iprintf、引数が元々int8_t計算に由来するものであることを認識していません。それだけで働いていますint 引数。


これは明快な説明です。
David Healy

コンパイラーは、オプティマイザーをオフにして適切なコードを生成します。したがって、「ルール」と「定義」を使用した説明は適用されません。これはオプティマイザのバグです。

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

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


4
不可欠なプロモーションのため、をc = c - 1意味しc = (int8_t) ((int)c - 1ます。範囲外intへの変換では、int8_t動作は定義されていますが、実装で定義された結果です。実際、c--同じ変換を実行することになっているのではない ですか?

12

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


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


1
ああすごい。コンパイルされたアセンブリがローカル変数をレジスタに格納できることを忘れていました。これprintfsizeof、フォーマット値を気にしないことと一緒に最も可能性の高い答えのようです。
rliu 2013

3
@roliu g ++ -O2 -S code.cppを実行すると、アセンブリが表示されます。さらに、printf()は可変引数関数であるため、ランクがintより小さい引数はintに昇格されます。
nos

@nosしたいです。私は自分のマシンでarchlinuxを実行するためにUEFIブートローダー(特にrEFInd)をインストールできなかったため、実際には長い間GNUツールでコーディングしていませんでした。私はそれに行きます...最終的に。今のところ、それはVSのC#であり、Cを覚えようとしている/ C ++を学習しようとしている:)
rliu

@rollu VirtualBoxなどの仮想マシンで実行
nos

@nosトピックを狂わせたくないが、そうだ。BIOSブートローダーを使用してLinuxをインストールすることもできます。私は頑固ですが、UEFIブートローダーで動作させることができない場合は、おそらくまったく動作しません:P。
rliu 2013

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ポストデクリメントを適用するか、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

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


4

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

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


1
を使用して、システムで元の動作を再現できg++ -O3ます。に変更%i%hhdても何も変わりません。
キーストンプソン

3

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

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

compilatorは、使用int32_t iのために、両方の変数をic。最適化をオフにするか、直接キャストします printf("c: %i\n", (int8_t)c--);


次に、最適化をオフにします。または、このような何かを:(int8_t)(c & 0x0000ffff)--
フセヴォロド

1

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

ループ全体、特に最後のデクリメント後の実際の値を確認するc

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

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

cintサイズメモリを使用し始めますが、int8_tのみを使用してそれ自体として印刷されたときのように印刷されます8 bits32 bitsとして使用されたときにすべてを利用するint

【コンパイラバグ】


0

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

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

「c」は8ビット値であるため、-300までの数値を保持することは不可能です。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.