Como obtive um valor maior do que 8 bits de um número inteiro de 8 bits?

118

Eu rastreei um inseto extremamente desagradável escondido atrás desta pequena joia. Estou ciente de que, de acordo com a especificação C ++, os estouros assinados são um comportamento indefinido, mas somente quando o estouro ocorre quando o valor é estendido para a largura de bits sizeof(int). Pelo que entendi, incrementar um charnão deve ser um comportamento indefinido, contanto que sizeof(char) < sizeof(int). Mas isso não explica como cobter um valor impossível . Como um número inteiro de 8 bits, como pode cconter valores maiores do que sua largura de bits?

Código

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

Resultado

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

Verifique no ideone.

Sem sinal
fonte
61
"Estou ciente de que, de acordo com a especificação C ++, os estouros assinados são indefinidos." -- Certo. Para ser preciso, não apenas o valor é indefinido, o comportamento é. Parecer obter resultados fisicamente impossíveis é uma consequência válida.
@hvd Tenho certeza de que alguém tem uma explicação de como as implementações comuns de C ++ causam esse comportamento. Talvez tenha a ver com alinhamento ou como printf()ocorre a conversão?
rliu de
Outros abordaram a questão principal. Meu comentário é mais geral e se refere a abordagens diagnósticas. Acredito que parte do motivo pelo qual você encontrou esse enigma é a crença subjacente de que era possível. Obviamente, não é impossível, então aceite isso e olhe novamente
Tim X
@TimX - Observei o comportamento e obviamente tirei a conclusão de que não era impossível naquele sentido. Meu uso da palavra se refere a um inteiro de 8 bits contendo um valor de 9 bits, o que é uma impossibilidade por definição. O fato de isso ter acontecido sugere que não está sendo tratado como um valor de 8 bits. Como outros já trataram, isso se deve a um bug do compilador. A única impossibilidade aparente aqui é um valor de 9 bits em um espaço de 8 bits, e essa aparente impossibilidade é explicada pelo espaço ser realmente "maior" do que o relatado.
Não assinado de
Acabei de testá-lo no meu mecanismo e o resultado é o que deveria ser. 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 E meu ambiente é: Ubuntu-12.10 gcc-4.7.2
VELVETDETH

Respostas:

111

Este é um bug do compilador.

Embora a obtenção de resultados impossíveis para comportamento indefinido seja uma consequência válida, na verdade não há comportamento indefinido em seu código. O que está acontecendo é que o compilador pensa que o comportamento é indefinido e otimiza de acordo.

Se cfor definido como int8_te int8_tpromover a int, então c--deverá realizar a subtração c - 1em intaritmética e converter o resultado de volta para int8_t. A subtração em intnão transborda e a conversão de valores integrais fora do intervalo em outro tipo integral é válida. Se o tipo de destino for assinado, o resultado será definido pela implementação, mas deve ser um valor válido para o tipo de destino. (E se o tipo de destino não tiver sinal, o resultado será bem definido, mas isso não se aplica aqui.)


fonte
Eu não o descreveria como um "bug". Uma vez que o estouro assinado causa um comportamento indefinido, o compilador tem o direito de assumir que isso não acontecerá e otimizar o loop para manter os valores intermediários de cum tipo mais amplo. Presumivelmente, é o que está acontecendo aqui.
Mike Seymour de
4
@MikeSeymour: O único estouro aqui é na conversão (implícita). O estouro na conversão assinada não tem comportamento indefinido; meramente produz um resultado definido pela implementação (ou levanta um sinal definido pela implementação, mas isso não parece estar acontecendo aqui). A diferença de definição entre operações aritméticas e conversões é estranha, mas é assim que o padrão da linguagem a define.
Keith Thompson,
2
@KeithThompson Isso é algo que difere entre C e C ++: C permite um sinal definido pela implementação, C ++ não. C ++ apenas diz "Se o tipo de destino for assinado, o valor não será alterado se puder ser representado no tipo de destino (e largura do campo de bits); caso contrário, o valor é definido pela implementação."
Acontece que não consigo reproduzir o comportamento estranho no g ++ 4.8.0.
Daniel Landau
2
@DanielLandau Veja o comentário 38 nesse bug: "Corrigido para 4.8.0." :)
15

Um compilador pode ter bugs que não estão em conformidade com o padrão, porque existem outros requisitos. Um compilador deve ser compatível com outras versões de si mesmo. Também pode ser esperado que seja compatível de algumas maneiras com outros compiladores, e também que esteja de acordo com algumas crenças sobre comportamento que são mantidas pela maioria de sua base de usuários.

Nesse caso, parece ser um bug de conformidade. A expressão c--deve ser manipulada de cmaneira semelhante a c = c - 1. Aqui, o valor de cà direita é promovido a tipo inte, em seguida, ocorre a subtração. Como cestá na faixa de int8_t, essa subtração não irá estourar, mas pode produzir um valor que está fora da faixa de int8_t. Quando esse valor é atribuído, ocorre uma conversão de volta ao tipo int8_tpara que o resultado se ajuste novamente c. No caso fora do intervalo, a conversão tem um valor definido pela implementação. Mas um valor fora do intervalo de int8_tnão é um valor definido pela implementação válido. Uma implementação não pode "definir" que um tipo de 8 bits de repente contém 9 ou mais bits. Para o valor a ser definido pela implementação significa que algo na faixa de int8_té produzido e o programa continua. O padrão C, portanto, permite comportamentos como aritmética de saturação (comum em DSP's) ou wrap-around (arquiteturas convencionais).

O compilador está usando um tipo de máquina subjacente mais amplo ao manipular valores de pequenos tipos inteiros como int8_tou char. Quando a aritmética é realizada, os resultados que estão fora da faixa do tipo inteiro pequeno podem ser capturados com segurança neste tipo mais amplo. Para preservar o comportamento externamente visível de que a variável é do tipo de 8 bits, o resultado mais amplo deve ser truncado no intervalo de 8 bits. O código explícito é necessário para fazer isso, uma vez que os locais de armazenamento da máquina (registros) são maiores do que 8 bits e estão satisfeitos com os valores maiores. Aqui, o compilador negligenciou normalizar o valor e simplesmente o passou printfcomo está. O especificador de conversão %iem printfnão tem ideia de que o argumento veio originalmente de int8_tcálculos; é apenas trabalhar com umint argumento.

Kaz
fonte
Esta é uma explicação lúcida.
David Healy
O compilador produz um bom código com o otimizador desligado. Portanto, as explicações que usam "regras" e "definições" não são aplicáveis. É um bug no otimizador.
14

Não consigo colocar isso em um comentário, então estou postando como uma resposta.

Por alguma razão muito estranha, o --operador é o culpado.

Testei o código postado no Ideone e troquei c--por c = c - 1e os valores permaneceram dentro do intervalo [-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

Olhos estranhos? Não sei muito sobre o que o compilador faz com expressões como i++ou i--. Provavelmente está promovendo o valor de retorno para um inte passando-o. Essa é a única conclusão lógica que posso tirar porque você ESTÁ de fato obtendo valores que não cabem em 8 bits.

usuário123
fonte
4
Por causa das promoções integrais, c = c - 1meios c = (int8_t) ((int)c - 1. A conversão de um fora do intervalo intpara int8_ttem um comportamento definido, mas um resultado definido pela implementação. Na verdade, não c--deveria realizar essas mesmas conversões também?
12

Eu acho que o hardware subjacente ainda está usando um registro de 32 bits para manter esse int8_t. Como a especificação não impõe um comportamento de estouro, a implementação não verifica o estouro e permite que valores maiores também sejam armazenados.


Se você marcar a variável local, volatileestará forçando o uso de memória para ela e, consequentemente, obter os valores esperados dentro do intervalo.

Zoltán
fonte
1
Oh uau. Esqueci que o assembly compilado armazenará variáveis ​​locais em registradores, se puder. Esta parece ser a resposta mais provável, além de printfnão se importar com os sizeofvalores do formato.
rliu de
3
@roliu Execute g ++ -O2 -S code.cpp e você verá a montagem. Além disso, printf () é uma função de argumento variável, então os argumentos cuja classificação é menor que um int serão promovidos a um int.
nos de
@nos eu gostaria. Não fui capaz de instalar um carregador de inicialização UEFI (rEFInd em particular) para fazer o archlinux rodar na minha máquina, então eu não tenho codificado com ferramentas GNU há muito tempo. Eu vou chegar lá ... eventualmente. Por enquanto é apenas C # no VS e tentando lembrar C / aprender um pouco de C ++ :)
rliu
@rollu Execute-o em uma máquina virtual, por exemplo, VirtualBox
nos
@nos Não quero atrapalhar o assunto, mas sim, eu poderia. Eu também poderia simplesmente instalar o Linux com um bootloader BIOS. Sou apenas teimoso e se não consigo fazer funcionar com um bootloader UEFI, provavelmente não o farei funcionar: P.
rliu de
11

O código assembler revela o problema:

: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 deve ser editado com FF pós-decremento, ou apenas BL deve ser usado com o restante de EBX transparente. Curioso que use sub em vez de dec. O -45 é totalmente misterioso. É a inversão bit a bit de 300 & 255 = 44. -45 = ~ 44. Existe uma conexão em algum lugar.

Ele passa por muito mais trabalho usando 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

Em seguida, ele usa apenas a parte inferior do RAX, portanto, é restrito de -128 a 127. Opções do compilador "-g -O2".

Sem otimização, ele produz o código correto:

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

Portanto, é um bug no otimizador.


fonte
4

Use em %hhdvez de %i! Deve resolver seu problema.

O que você vê ali é o resultado de otimizações do compilador combinadas com você dizendo a printf para imprimir um número de 32 bits e depois colocando um número (supostamente de 8 bits) na pilha, que é realmente do tamanho de um ponteiro, porque é assim que o opcode push em x86 funciona.

Zotta
fonte
1
Consigo reproduzir o comportamento original em meu sistema usando g++ -O3. Mudar %ipara %hhdnão muda nada.
Keith Thompson
3

Acho que isso é feito por meio da otimização do código:

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

O compilador usa a int32_t ivariável para ie c. Desative a otimização ou faça transmissão direta printf("c: %i\n", (int8_t)c--);

Vsevolod
fonte
Em seguida, desative a otimização. ou faça algo assim:(int8_t)(c & 0x0000ffff)--
Vsevolod
1

cé ele próprio definido como int8_t, mas ao operar ++ou --sobre int8_tele é implicitamente convertido primeiro em inte o resultado da operação, em vez disso, o valor interno de c é impresso com printf, que por acaso é int.

Veja o valor real de capós todo o loop, especialmente após o último decremento

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

é o valor correto que se assemelha ao comportamento -128 + 1 = 127

ccomeça a uso intde memória tamanho, mas impresso como int8_tquando impresso como a própria usando apenas 8 bits. Utiliza tudo 32 bitsquando usado comoint

[Bug do compilador]

Izhar Aazmi
fonte
0

Acho que aconteceu porque o seu loop irá até o int i se tornar 300 ec se tornar -300. E o último valor é porque

printf("c: %i\n", c);
r.mirzojonov
fonte
'c' é um valor de 8 bits, portanto, é impossível conter um número tão grande quanto -300.