8 bitlik bir tam sayıdan 8 bitten daha büyük bir değeri nasıl elde ettim?


118

Bu küçük mücevherin arkasında saklanan son derece kötü bir böceği buldum. C ++ spesifikasyonuna göre, işaretli taşmaların tanımsız davranışlar olduğunu, ancak yalnızca değer bit genişliğine genişletildiğinde taşma gerçekleştiğinde farkındayım sizeof(int). Anladığım kadarıyla, a'yı artırmak charasla tanımsız bir davranış olmamalı sizeof(char) < sizeof(int). Ancak bu, nasıl imkansız bir değer celde edildiğini açıklamıyor . 8 bitlik bir tamsayı olarak, bit genişliğinden daha büyük değerleri nasıl tutabilir?c

kod

// 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;
}

Çıktı

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   // <= ..........

İdeone'da kontrol edin.


61
"C ++ spesifikasyonuna göre, imzalı taşmaların tanımsız olduğunun farkındayım." -- Sağ. Kesin olmak gerekirse, sadece değer tanımsız değil, davranış da öyle. Fiziksel olarak imkansız sonuçlar alıyormuş gibi görünmek geçerli bir sonuçtur.

@hvd Eminim birisinin bu davranışa ne kadar yaygın C ++ uygulamalarının neden olduğuna dair bir açıklaması vardır. Belki hizalama ile ilgisi var veya printf()dönüşüm nasıl oluyor ?
rliu

Diğerleri ana sorunu ele aldı. Yorumum daha geneldir ve teşhis yaklaşımlarıyla ilgilidir. Bunu neden böyle bir bulmacayı bulduğunuzun bir kısmının, mümkün olduğu temelde yatan inanç olduğuna inanıyorum. Açıkçası, imkansız değil, öyleyse bunu kabul edin ve tekrar bakın
Tim X

@TimX - Davranışı gözlemledim ve belli ki bu anlamda imkansız olmadığı sonucuna vardım. Bu sözcüğü kullanmam, tanım gereği imkansız olan 9 bitlik bir değeri tutan 8 bitlik bir tam sayıya atıfta bulundu. Bunun olması gerçeği, 8 bitlik bir değer olarak değerlendirilmediğini gösteriyor. Diğerlerinin de belirttiği gibi, bu bir derleyici hatasından kaynaklanmaktadır. Burada görünen tek imkansızlık, 8 bitlik bir uzayda 9 bitlik bir değerdir ve bu görünürdeki imkansızlık, uzayın gerçekte bildirilenden "daha büyük" olmasıyla açıklanır.
İmzasız

Bunu makinemde test ettim ve sonuç tam da olması gerektiği gibi. 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 Ve
ortamım

Yanıtlar:


111

Bu bir derleyici hatasıdır.

Tanımlanmamış davranış için imkansız sonuçlar elde etmek geçerli bir sonuç olsa da, aslında kodunuzda tanımsız davranış yoktur. Olan şey, derleyicinin davranışın tanımsız olduğunu düşünmesi ve buna göre optimize etmesidir.

Eğer colarak tanımlanır int8_tve int8_tiçin teşvik int, sonra c--çıkarma yerine düşünülen c - 1içinde intaritmetik ve sonuç geri dönüştürmek int8_t. Çıkarma işlemi inttaşmaz ve aralık dışı integral değerlerinin başka bir integral tipine dönüştürülmesi geçerlidir. Hedef türü imzalanmışsa, sonuç uygulama tanımlıdır, ancak hedef türü için geçerli bir değer olmalıdır. (Hedef türü işaretsiz ise, sonuç iyi tanımlanmıştır, ancak bu burada geçerli değildir.)


Bunu bir "böcek" olarak tanımlamazdım. İşaretli taşma tanımlanmamış davranışa neden olduğundan, derleyici bunun olmayacağını varsayma ve ara değerleri cdaha geniş bir türde tutmak için döngüyü optimize etme hakkına sahiptir . Muhtemelen burada olan budur.
Mike Seymour

4
@MikeSeymour: Buradaki tek taşma (örtük) dönüşümle ilgili. İmzalı dönüştürmedeki taşma tanımsız davranışa sahip değildir; yalnızca uygulama tanımlı bir sonuç verir (veya uygulama tanımlı bir sinyal yükseltir, ancak bu burada gerçekleşmiyor gibi görünüyor). Aritmetik işlemler ve dönüştürmeler arasındaki tanımlılık farkı tuhaftır, ancak dil standardının tanımladığı yol budur.
Keith Thompson

2
@KeithThompson Bu, C ile C ++ arasında farklılık gösteren bir şeydir: C, uygulama tanımlı bir sinyale izin verir, C ++ bunu yapmaz. C ++ sadece "Hedef türü imzalanmışsa, hedef türünde (ve bit alanı genişliğinde) gösterilebiliyorsa değer değişmez; aksi takdirde değer uygulama tanımlıdır."

Olduğu gibi, g ++ 4.8.0'da garip davranışı yeniden oluşturamıyorum.
Daniel Landau

2
@DanielLandau Bu hatadaki 38 numaralı yoruma bakın: "4.8.0 için düzeltildi." :)

15

Bir derleyici, standarda uymayanlardan farklı hatalara sahip olabilir, çünkü başka gereksinimler de vardır. Bir derleyici, kendisinin diğer sürümleriyle uyumlu olmalıdır. Ayrıca diğer derleyicilerle bazı yönlerden uyumlu olması ve kullanıcı tabanının çoğunluğunun sahip olduğu davranışlarla ilgili bazı inançlara uyması da beklenebilir.

Bu durumda, bir uyum hatası gibi görünmektedir. İfadesi c--manipüle gerektiğini cbenzer bir şekilde c = c - 1. Burada, csağdaki değeri yazmaya yükseltilir intve ardından çıkarma gerçekleşir. Yana caralığındadır int8_tbu çıkarma taşması olmaz, ama aralığının dışında bir değer üretebilir int8_t. Bu değer atandığında, int8_tsonucun tekrar uyum sağlaması için türe geri dönüşüm gerçekleşir c. Aralık dışı durumda, dönüşüm uygulama tanımlı bir değere sahiptir. Ancak aralığın dışındaki bir değer int8_t, uygulama tanımlı geçerli bir değer değildir. Bir uygulama, 8 bitlik bir türün aniden 9 veya daha fazla bit tuttuğunu "tanımlayamaz". Değerin uygulama tanımlı olması, aralığında bir int8_tşeyin üretildiği ve programın devam ettiği anlamına gelir . C standardı böylece doygunluk aritmetiği (DSP'lerde ortaktır) veya sarmalı (ana akım mimariler) gibi davranışlara izin verir.

Derleyici, int8_tveya gibi küçük tamsayı türlerinin değerlerini işlerken daha geniş bir temel makine türü kullanıyor char. Aritmetik uygulandığında, küçük tamsayı türünün kapsamı dışında kalan sonuçlar bu daha geniş türde güvenilir bir şekilde yakalanabilir. Değişkenin 8 bitlik bir tip olduğu harici olarak görülebilir davranışı korumak için, daha geniş sonucun 8 bitlik aralıkta kesilmesi gerekir. Makine depolama konumları (kayıtlar) 8 bitten daha geniş olduğundan ve daha büyük değerlerden memnun olduğundan, bunu yapmak için açık kod gereklidir. Burada, derleyici değeri normalleştirmeyi ihmal etti ve olduğu printfgibi basitçe iletti . Dönüşüm belirteci %iiçinde printfargüman aslen geldiğini hiçbir fikri yok int8_thesaplamalar; sadece bir ile çalışıyorint argüman.


Bu anlaşılır bir açıklamadır.
David Healy

Derleyici, optimize edici kapalıyken iyi kod üretir. Bu nedenle, "kurallar" ve "tanımlar" kullanan açıklamalar geçerli değildir. İyileştiricideki bir hata.

14

Bunu bir yoruma sığdıramıyorum, bu yüzden bir cevap olarak gönderiyorum.

Garip bir nedenden ötürü, --suçlu operatör olur.

Ben kodu Ideone yayınlanan ve yerine test c--ile c = c - 1ve değerler aralığında [-128 ... 127] içinde kalmıştır:

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

Acayip ey? Derleyicinin i++veya gibi ifadelere ne yaptığını bilmiyorum i--. Muhtemelen dönüş değerini bir değerine intyükseltir ve onu geçer. Bulabildiğim tek mantıklı sonuç bu, çünkü aslında 8-bite sığmayan değerler alıyorsunuz.


4
İntegral promosyonlar nedeniyle, c = c - 1anlamına gelir c = (int8_t) ((int)c - 1. Dışı bir aralık dönüştürme intiçin int8_ttanımlamıştır davranışı ancak bir uygulama tanımlı bir sonuç. Aslında, c--aynı dönüşümleri de gerçekleştirmesi gerekmiyor mu?

12

Sanırım temeldeki donanım bu int8_t'yi tutmak için hala 32 bitlik bir kayıt kullanıyor. Spesifikasyon taşma için bir davranış empoze etmediğinden, uygulama taşmayı kontrol etmez ve daha büyük değerlerin de saklanmasına izin verir.


Yerel değişkeni volatileonun için bellek kullanmaya zorlarken işaretlerseniz ve sonuç olarak aralık içinde beklenen değerleri elde ederseniz.


1
Vay canına. Derlenen derlemenin eğer yapabiliyorsa yerel değişkenleri kayıtlarda saklayacağını unuttum. Bu , biçim değerlerini printfönemsememekle birlikte en olası yanıt gibi görünüyor sizeof.
rliu

3
@roliu g ++ -O2 -S code.cpp çalıştırın ve derlemeyi göreceksiniz. Ayrıca, printf () bir değişken bağımsız değişken işlevidir, bu nedenle sıralaması bir int'ten küçük olan bağımsız değişkenler bir int'e yükseltilir.
nos

@nos istiyorum. Makinemde archlinux'u çalıştırmak için bir UEFI önyükleyici (özellikle rEFInd) yükleyemedim, bu yüzden aslında uzun süredir GNU araçlarıyla kodlamadım. Eninde sonunda ... ona ulaşacağım. Şimdilik sadece
VS'de

@rollu Sanal bir makinede çalıştırın, örneğin VirtualBox
nos

@nos Konuyu raydan çıkarmak istemiyorum, ama evet, yapabilirim. Ayrıca bir BIOS bootloader ile linux kurabilirim. Ben sadece inatçıyım ve eğer onu bir UEFI önyükleyici ile çalıştıramazsam, muhtemelen onu hiç çalıştırmayacağım: P.
rliu

11

Assembler kodu sorunu ortaya çıkarır:

: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 sonrası azaltma ile ve uygulanmalıdır veya EBX temizliğinin geri kalanıyla yalnızca BL kullanılmalıdır. Dec yerine sub kullanması merak ediyor. -45 tamamen gizemlidir. 300 & 255 = 44'ün bitsel ters çevrilmesidir. -45 = ~ 44. Bir yerde bir bağlantı var.

C = c - 1 kullanarak çok daha fazla iş yapar:

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

Daha sonra RAX'in yalnızca düşük kısmını kullanır, bu nedenle -128 ila 127 ile sınırlıdır. Derleyici seçenekleri "-g -O2".

Optimizasyon olmadan doğru kodu üretir:

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

Yani bu, optimize edicideki bir hata.


4

kullanım %hhdBunun yerine%i ! Sorununuzu çözmelisiniz.

Gördüğünüz şey, size printf'e 32 bitlik bir sayı yazdırmasını söylemeniz ve ardından yığına bir (sözde 8 bit) sayı göndermenizle birleştirilen derleyici optimizasyonlarının sonucudur, bu gerçekten işaretçi boyutundadır, çünkü x86'daki push opcode bu şekilde çalışır.


1
Sistemimdeki orijinal davranışı kullanarak yeniden oluşturabiliyorum g++ -O3 . Değişen %ihiç %hhdbir şey değiştirmez.
Keith Thompson

3

Bunun kodun optimizasyonu ile yapıldığını düşünüyorum:

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

Compilator kullanmak int32_t iiçin hem değişken ive c. Optimizasyonu kapatın veya doğrudan yayın yapın printf("c: %i\n", (int8_t)c--);


Ardından optimizasyonu kapatın. veya bunun gibi bir şey yapın:(int8_t)(c & 0x0000ffff)--
Vsevolod

1

ckendisi olarak tanımlanır int8_t, ancak çalışırken ++veya --üzerinde int8_tdolaylı olarak önce intve işlemin sonucuna dönüştürülür . bunun yerine bunun yerine c'nin dahili değeri olan printf ile yazdırılır int.

Bkz gerçek değerini arasında cözellikle, tüm döngü sonra son eksiltme sonra

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

davranışa benzeyen doğru değer -128 + 1 = 127

cintboyut belleğini kullanmaya başlar, ancak int8_tyalnızca kendisi kullanılarak yazdırıldığı gibi yazdırılır 8 bits. Hepsini kullanır32 bitsOlarak kullanıldığında kullanırint

[Derleyici Hatası]


0

Sanırım bu gerçekleşti çünkü döngünüz int i 300 ve c -300 olana kadar devam edecek. Ve son değer çünkü

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

'c' 8 bitlik bir değerdir, bu nedenle -300 kadar büyük bir sayıyı tutması imkansızdır.
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.