Por que esse loop for encerrado em algumas plataformas e não em outras?

240

Comecei recentemente a aprender C e estou tendo uma aula com C como disciplina. Atualmente, estou brincando com loops e estou tendo um comportamento estranho que não sei explicar.

#include <stdio.h>

int main()
{
  int array[10],i;

  for (i = 0; i <=10 ; i++)
  {
    array[i]=0; /*code should never terminate*/
    printf("test \n");

  }
  printf("%d \n", sizeof(array)/sizeof(int));
  return 0;
}

No meu laptop executando o Ubuntu 14.04, esse código não quebra. É executado até a conclusão. No computador da minha escola executando o CentOS 6.6, ele também funciona bem. No Windows 8.1, o loop nunca termina.

O que é ainda mais estranho é que, quando edito a condição do forloop para:, i <= 11o código só termina no meu laptop executando o Ubuntu. Ele nunca termina no CentOS e no Windows.

Alguém pode explicar o que está acontecendo na memória e por que os diferentes sistemas operacionais executando o mesmo código fornecem resultados diferentes?

EDIT: Eu sei que o loop for sai dos limites. Eu estou fazendo isso intencionalmente. Eu simplesmente não consigo descobrir como o comportamento pode ser diferente em diferentes sistemas operacionais e computadores.

JonCav
fonte
147
Como você está substituindo a matriz, ocorre um comportamento indefinido. Comportamento indefinido significa que tudo pode acontecer, incluindo que parece funcionar. Portanto, "o código nunca deve terminar" não é uma expectativa válida.
kaylum
37
Precisamente, para acolher C. A sua matriz tem elementos 10 - numeradas de 0 a 9.
Yetti99
14
@ JonCav Você quebrou o código. Você está recebendo um comportamento indefinido que é um código quebrado.
kaylum
50
Bem, o ponto principal é que o comportamento indefinido é exatamente isso. Você não pode testá-lo com segurança e provar que algo definido acontecerá. O que provavelmente está acontecendo na sua máquina Windows é que a variável ié armazenada logo após o final de arraye você a substitui array[10]=0;. Pode não ser o caso de uma construção otimizada na mesma plataforma, que pode armazenar iem um registro e nunca se referir a ele na memória.
arroz
46
Porque a não previsibilidade é uma propriedade fundamental do comportamento indefinido. Você precisa entender isso ... Absolutamente todas as apostas estão fora.
arroz

Respostas:

356

No meu laptop executando o Ubuntu 14.04, esse código não quebra, ele é executado até a conclusão. No computador da minha escola executando o CentOS 6.6, ele também funciona bem. No Windows 8.1, o loop nunca termina.

O que é mais estranho é quando eu edito o condicional do forloop para:, i <= 11o código só termina no meu laptop executando o Ubuntu. O CentOS e o Windows nunca terminam.

Você acabou de descobrir a falta de memória. Você pode ler mais sobre isso aqui: O que é um “stomp de memória”?

Quando você aloca int array[10],i;, essas variáveis ​​são armazenadas na memória (especificamente, elas são alocadas na pilha, que é um bloco de memória associado à função). array[]e iprovavelmente estão adjacentes um ao outro na memória. Parece que no Windows 8.1, iestá localizado em array[10]. No CentOS, iestá localizado em array[11]. E no Ubuntu, ele não está em nenhum lugar (talvez esteja array[-1]?).

Tente adicionar essas instruções de depuração ao seu código. Você deve observar que, na iteração 10 ou 11, array[i]aponta para i.

#include <stdio.h>
 
int main() 
{ 
  int array[10],i; 
 
  printf ("array: %p, &i: %p\n", array, &i); 
  printf ("i is offset %d from array\n", &i - array);

  for (i = 0; i <=11 ; i++) 
  { 
    printf ("%d: Writing 0 to address %p\n", i, &array[i]); 
    array[i]=0; /*code should never terminate*/ 
  } 
  return 0; 
} 
QuestionC
fonte
6
Ei, obrigado! Isso realmente explicou um pouco. No Windows, declara que i se desloca 10 da matriz, enquanto no CentOS e Ubuntu é -1. O mais estranho é que, se eu comentar o código do depurador, o CentOS não poderá executar o código (ele trava), mas com o código de depuração ele será executado. C parece ser um idioma muito distante X_x
JonCav
12
@JonCav "travar" pode acontecer se a gravação array[10]destruir o quadro da pilha, por exemplo. Como pode haver uma diferença entre o código com ou sem a saída de depuração? Se o endereço de inunca for necessário, o compilador pode otimizar i. em um registrador, alterando assim o layout de memória na pilha ...
Hagen von Eitzen
2
Eu não acho que está travando, acho que está em um loop infinito porque está recarregando o contador de loop da memória (que acabou de ser zerado array[10]=0. Se você compilou seu código com otimização, isso provavelmente não aconteceria. (Como C tem regras de aliasing que limitam que tipos de acesso à memória devem se sobrepor potencialmente a outra memória.Como uma variável local da qual você nunca usa o endereço, acho que um compilador deve ser capaz de assumir que nada o aliase. de uma matriz é um comportamento indefinido tente sempre difícil evitar dependendo isso..
Peter Cordes
4
Outra alternativa é que um compilador otimizador remove completamente a matriz, pois não tem efeito observável (no código original da pergunta). Portanto, o código resultante poderia simplesmente imprimir essa sequência constante onze vezes, seguido pela impressão do tamanho constante e, assim, tornar o estouro completamente imperceptível.
Holger
9
@ JonCav Eu diria que, em geral, você não precisa saber mais sobre gerenciamento de memória e simplesmente não sabe escrever código indefinido, especificamente, não escreve além do final de uma matriz ...
T. Kiley
98

O erro está entre estes trechos de código:

int array[10],i;

for (i = 0; i <=10 ; i++)

array[i]=0;

Como arraypossui apenas 10 elementos, na última iteração array[10] = 0;há um estouro de buffer. Os estouros de buffer são COMPORTAMENTO NÃO DEFINIDO , o que significa que eles podem formatar seu disco rígido ou causar demônios a sair do seu nariz.

É bastante comum que todas as variáveis ​​de pilha sejam dispostas adjacentes umas às outras. Se iestiver localizado onde array[10]grava, o UB será redefinido ipara 0, levando ao loop não terminado.

Para corrigir, altere a condição do loop para i < 10.

o11c
fonte
6
Nitpick: Na verdade, você não pode formatar o disco rígido em nenhum sistema operacional sadio do mercado, a menos que esteja executando como root (ou equivalente).
24515 Kevin Kevin
26
@ Kevin, quando invoca o UB, desiste de qualquer pretensão de sanidade.
o11c 24/06/2015
7
Não importa se o seu código é sensato. O sistema operacional não permitirá que você faça isso.
Kevin
2
@ Kevin O exemplo com a formatação do disco rígido se originou muito antes disso. Até os unixes da época (onde C se originou) ficaram muito felizes em permitir que você fizesse coisas assim - e ainda hoje muitas das distribuições permitem que você comece a excluir tudo rm -rf /mesmo quando você não é root, não "formatar" toda a unidade, é claro, mas ainda assim destruindo todos os seus dados. Ai.
Luaan
5
O @Kevin, mas um comportamento indefinido, pode explorar uma vulnerabilidade do sistema operacional e, em seguida, elevar-se para instalar um novo driver de disco rígido e começar a esfregar a unidade.
catraca aberração
38

Na qual deve ser a última execução do loop, você escreve array[10], mas existem apenas 10 elementos na matriz, numerados de 0 a 9. A especificação da linguagem C diz que esse é um "comportamento indefinido". O que isso significa na prática é que seu programa tentará gravar na intparte de tamanho de memória que fica imediatamente depois arrayna memória. O que acontece, então, depende do que realmente existe, e isso depende não apenas do sistema operacional, mas também do compilador, das opções do compilador (como configurações de otimização), da arquitetura do processador, do código circundante , etc. Pode até variar de execução para execução, por exemplo, devido à aleatorização do espaço de endereço (provavelmente não neste exemplo de brinquedo, mas acontece na vida real). Algumas possibilidades incluem:

  • A localização não foi usada. O loop termina normalmente.
  • A localização foi usada para algo que passou a ter o valor 0. O loop termina normalmente.
  • A localização continha o endereço de retorno da função. O loop termina normalmente, mas o programa falha porque tenta pular para o endereço 0.
  • A localização contém a variável i. O loop nunca termina porque ireinicia em 0.
  • A localização contém outra variável. O loop termina normalmente, mas depois acontecem coisas "interessantes".
  • O local é um endereço de memória inválido, por exemplo, porque arrayfica no final de uma página de memória virtual e a página seguinte não é mapeada.
  • Demônios voam do seu nariz . Felizmente, a maioria dos computadores não possui o hardware necessário.

O que você observou no Windows foi que o compilador decidiu colocar a variável iimediatamente após a matriz na memória e array[10] = 0acabou atribuindo-a i. No Ubuntu e CentOS, o compilador não foi colocado ilá. Quase todas as implementações C agrupam variáveis ​​locais na memória, em uma pilha de memória , com uma grande exceção: algumas variáveis ​​locais podem ser colocadas inteiramente em registradores . Mesmo que a variável esteja na pilha, a ordem das variáveis ​​é determinada pelo compilador e pode depender não apenas da ordem no arquivo de origem, mas também de seus tipos (para evitar desperdiçar memória com restrições de alinhamento que deixariam buracos) , em seus nomes, em algum valor de hash usado na estrutura de dados interna de um compilador, etc.

Se você quiser descobrir o que o seu compilador decidiu fazer, você pode pedir para mostrar o código do assembler. Ah, e aprenda a decifrar o assembler (é mais fácil do que escrevê-lo). Com o GCC (e alguns outros compiladores, especialmente no mundo Unix), passe a opção -Sde produzir código assembler em vez de binário. Por exemplo, aqui está o snippet do assembler para o loop compilar com o GCC no amd64 com a opção de otimização -O0(sem otimização), com comentários adicionados manualmente:

.L3:
    movl    -52(%rbp), %eax           ; load i to register eax
    cltq
    movl    $0, -48(%rbp,%rax,4)      ; set array[i] to 0
    movl    $.LC0, %edi
    call    puts                      ; printf of a constant string was optimized to puts
    addl    $1, -52(%rbp)             ; add 1 to i
.L2:
    cmpl    $10, -52(%rbp)            ; compare i to 10
    jle     .L3

Aqui, a variável iestá 52 bytes abaixo do topo da pilha, enquanto a matriz inicia 48 bytes abaixo do topo da pilha. Portanto, esse compilador foi colocado ilogo antes da matriz; você substituiria ise escrevesse array[-1]. Se você mudar array[i]=0para array[9-i]=0, obterá um loop infinito nessa plataforma específica com essas opções específicas do compilador.

Agora vamos compilar seu programa com gcc -O1.

    movl    $11, %ebx
.L3:
    movl    $.LC0, %edi
    call    puts
    subl    $1, %ebx
    jne     .L3

Isso é mais curto! O compilador não apenas se recusou a alocar um local de pilha i- apenas está armazenado no registro ebx- como também não se preocupou em alocar memória para arrayou gerar código para definir seus elementos, porque percebeu que nenhum dos elementos são sempre usados.

Para tornar este exemplo mais revelador, vamos garantir que as atribuições da matriz sejam executadas, fornecendo ao compilador algo que não é possível otimizar. Uma maneira fácil de fazer isso é usar a matriz de outro arquivo - por causa da compilação separada, o compilador não sabe o que acontece em outro arquivo (a menos que seja otimizado no momento do link, o que ocorre gcc -O0ou gcc -O1não). Crie um arquivo de origem use_array.ccontendo

void use_array(int *array) {}

e mude seu código fonte para

#include <stdio.h>
void use_array(int *array);

int main()
{
  int array[10],i;

  for (i = 0; i <=10 ; i++)
  {
    array[i]=0; /*code should never terminate*/
    printf("test \n");

  }
  printf("%zd \n", sizeof(array)/sizeof(int));
  use_array(array);
  return 0;
}

Ajuntar com

gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o

Desta vez, o código do assembler fica assim:

    movq    %rsp, %rbx
    leaq    44(%rsp), %rbp
.L3:
    movl    $0, (%rbx)
    movl    $.LC0, %edi
    call    puts
    addq    $4, %rbx
    cmpq    %rbp, %rbx
    jne     .L3

Agora a matriz está na pilha, a 44 bytes da parte superior. Que tal i? Não aparece em lugar nenhum! Mas o contador de loop é mantido no registro rbx. Não é exatamente i, mas o endereço do array[i]. O compilador decidiu que, como o valor de inunca foi usado diretamente, não havia sentido em executar aritmética para calcular onde armazenar 0 durante cada execução do loop. Em vez disso, esse endereço é a variável do loop, e a aritmética para determinar os limites foi realizada em parte no tempo de compilação (multiplique 11 iterações por 4 bytes por elemento da matriz para obter 44) e parcialmente no tempo de execução, mas de uma vez por todas antes do início do loop ( faça uma subtração para obter o valor inicial).

Mesmo neste exemplo muito simples, vimos como alterar as opções do compilador (ativar a otimização) ou alterar algo menor ( array[i]para array[9-i]) ou até alterar algo aparentemente não relacionado (adicionar a chamada para use_array) pode fazer uma diferença significativa no que o programa executável gerou pelo compilador faz. As otimizações do compilador podem fazer muitas coisas que podem parecer não intuitivas em programas que invocam um comportamento indefinido . É por isso que o comportamento indefinido é deixado completamente indefinido. Quando você se desvia um pouco das trilhas, em programas do mundo real, pode ser muito difícil entender a relação entre o que o código faz e o que deveria ter feito, mesmo para programadores experientes.

Gilles 'SO- parar de ser mau'
fonte
25

Ao contrário de Java, C não faz verificação de limite de matriz, ou seja, não há ArrayIndexOutOfBoundsException, o trabalho de garantir que o índice da matriz é válido é deixado para o programador. Fazer isso de propósito leva a um comportamento indefinido, qualquer coisa pode acontecer.


Para uma matriz:

int array[10]

índices são válidos apenas no intervalo 0de 9. No entanto, você está tentando:

for (i = 0; i <=10 ; i++)

acesse array[10]aqui, altere a condição parai < 10

Yu Hao
fonte
6
Fazer isso de propósito também leva a um comportamento indefinido - o compilador não pode dizer! ;-)
Toby Speight
1
Basta usar uma macro para lançar seus erros como avisos: #define UNINTENDED_MISTAKE (EXP) printf ("Aviso:" #EXP "erro \ n");
lkraider
1
Quer dizer, se você está fazendo um erro de propósito assim como você pode identificá-lo como tal e torná-la segura para evitar o comportamento indefinido; D
lkraider
19

Você tem uma violação de limites e, nas plataformas sem terminação, acredito que você está inadvertidamente configurando icomo zero no final do loop, para que ele reinicie novamente.

array[10]é inválido; contém 10 elementos, array[0]até array[9]e array[10]é o 11º. Seu loop deve ser escrito para parar antes 10 , da seguinte maneira:

for (i = 0; i < 10; i++)

Onde o array[10]land é definido pela implementação e, de maneira divertida, em duas de suas plataformas, ele aterra no iqual essas plataformas aparentemente colocam diretamente em seguida array. ié definido como zero e o loop continua para sempre. Para suas outras plataformas, ipode estar localizado antes arrayou arraypode ter algum preenchimento depois dele.

Derek T. Jones
fonte
Não acho que o valgrind possa entender isso, pois ainda é um local válido, mas a ASAN pode.
o11c
13

Você declara que o int array[10]meio arraypossui índice 0para 9(total de 10elementos inteiros que ele pode conter). Mas o seguinte loop,

for (i = 0; i <=10 ; i++)

fará um loop 0para 10significa 11tempo. Portanto, quando i = 10ele excederá o buffer e causará um comportamento indefinido .

Então tente o seguinte:

for (i = 0; i < 10 ; i++)

ou,

for (i = 0; i <= 9 ; i++)
rakeb.mazharul
fonte
7

Ele é indefinido em array[10]e fornece um comportamento indefinido como descrito anteriormente. Pense assim:

Tenho 10 itens no meu carrinho de compras. Eles são:

0: Uma caixa de cereal
1: Pão
2: Leite
3: Torta
4: Ovos
5: Bolo
6: A 2 litro de refrigerante
7: Salada
8: Hambúrgueres
9: Sorvete

cart[10]é indefinido e pode dar uma exceção fora dos limites em alguns compiladores. Mas, aparentemente, muitas não. O décimo primeiro item aparente é um item que não está realmente no carrinho. O décimo primeiro item está apontando para o que chamarei de "item poltergeist". Isso nunca existiu, mas estava lá.

Por que alguns compiladores fornecem ium índice de array[10]ou array[11]ou mesmo array[-1]é por causa da sua declaração de inicialização / declaração. Alguns compiladores interpretam isso como:

  • "Aloque 10 blocos de ints para array[10]e outro intbloco. Para facilitar, coloque-os um ao lado do outro."
  • O mesmo que antes, mas afaste-o um espaço ou dois, para que array[10]isso não aponte i.
  • Faça o mesmo que antes, mas aloque iem array[-1](porque um índice de uma matriz não pode ou não deveria ser negativo) ou aloque-o em um local completamente diferente, porque o sistema operacional pode lidar com isso e é mais seguro.

Alguns compiladores querem que as coisas aconteçam mais rapidamente e outros preferem segurança. É tudo sobre o contexto. Se eu estivesse desenvolvendo um aplicativo para o antigo BREW OS (o sistema operacional de um telefone básico), por exemplo, ele não se importaria com a segurança. Se eu estivesse desenvolvendo para um iPhone 6, ele poderia correr rápido, não importando o que fosse, então eu precisaria enfatizar a segurança. (Sério, você leu as Diretrizes da App Store da Apple ou leu o desenvolvimento do Swift e Swift 2.0?)

DDPWNAGE
fonte
Nota: digitei a lista para que ela seja "0, 1, 2, 3, 4, 5, 6, 7, 8, 9", mas a linguagem de marcação da SO fixou as posições da minha lista ordenada.
DDPWNAGE
6

Como você criou uma matriz de tamanho 10, a condição de loop deve ser a seguinte:

int array[10],i;

for (i = 0; i <10 ; i++)
{

Atualmente, você está tentando acessar o local não atribuído a partir da memória array[10]e está causando o comportamento indefinido . Comportamento indefinido significa que seu programa se comportará de maneira indeterminada, para que ele possa fornecer resultados diferentes em cada execução.

Steephen
fonte
5

Bem, o compilador C tradicionalmente não verifica limites. Você pode receber uma falha de segmentação caso se refira a um local que não "pertence" ao seu processo. No entanto, as variáveis ​​locais são alocadas na pilha e, dependendo da maneira como a memória é alocada, a área logo após a matriz ( array[10]) pode pertencer ao segmento de memória do processo. Portanto, nenhuma armadilha de falha de segmentação é lançada e é isso que você parece experimentar. Como outros já apontaram, esse é um comportamento indefinido em C e seu código pode ser considerado irregular. Como você está aprendendo C, é melhor você adquirir o hábito de verificar se há limites no seu código.

unxnut
fonte
4

Além da possibilidade de que a memória possa ser disposta para que uma tentativa de gravação seja a[10]realmente substituída i, também seria possível que um compilador de otimização determine que o teste de loop não pode ser alcançado com um valor imaior que dez sem que o código tenha acessado primeiro o elemento de matriz inexistente a[10].

Como uma tentativa de acessar esse elemento seria um comportamento indefinido, o compilador não teria obrigações com relação ao que o programa poderia fazer após esse ponto. Mais especificamente, como o compilador não teria obrigação de gerar código para verificar o índice do loop em nenhum caso em que pudesse ser maior que dez, não teria obrigação de gerar código para verificá-lo; em vez disso, poderia assumir que o <=10teste sempre produzirá verdade. Observe que isso seria verdade mesmo se o código fosse lido em a[10]vez de gravado.

supercat
fonte
3

Quando você repete, i==9você atribui zero aos 'itens da matriz' que estão realmente localizados após a matriz , para substituir outros dados. Provavelmente você sobrescreve a ivariável, que está localizada depois a[]. Dessa forma, você simplesmente redefine a ivariável para zero e, assim, reinicia o loop.

Você pode descobrir isso sozinho se imprimisse ino loop:

      printf("test i=%d\n", i);

em vez de apenas

      printf("test \n");

É claro que esse resultado depende fortemente da alocação de memória para suas variáveis, que por sua vez depende de um compilador e de suas configurações, por isso geralmente é um comportamento indefinido - é por isso que os resultados em diferentes máquinas ou sistemas operacionais ou em diferentes compiladores podem ser diferentes.

CiaPan
fonte
0

o erro está no array de porções [10] w / c também é o endereço de i (int array [10], i;). quando o array [10] é definido como 0, o i seria 0 w / c redefine o loop inteiro e causa o loop infinito. haverá loop infinito se o array [10] estiver entre 0-10. o loop correto deve ser para (i = 0; i <10; i ++) {...} int array [10], i; para (i = 0; i <= 10; i ++) matriz [i] = 0;

Jonelle H. Castaneda
fonte
0

Vou sugerir algo que não encontro acima:

Tente atribuir matriz [i] = 20;

Eu acho que isso deve terminar o código em todos os lugares .. (desde que você mantenha i <= 10 ou ll)

Se isso funcionar, você pode decidir com firmeza que as respostas especificadas aqui já estão corretas [a resposta relacionada à memória que apaga uma por ex.]

Chovendo fogo
fonte
-9

Há duas coisas erradas aqui. O int i é na verdade um elemento do array, array [10], como visto na pilha. Como você permitiu que a indexação realmente fizesse a matriz [10] = 0, o índice do loop, i, nunca excederá 10. Faça isso for(i=0; i<10; i+=1).

O i ++ é, como K&R o chamaria, 'estilo ruim'. Ele está incrementando i pelo tamanho de i, e não 1. i ++ é para matemática de ponteiros e i + = 1 é para álgebra. Embora isso dependa do compilador, não é uma boa convenção para portabilidade.

SkipBerne
fonte
5
-1 Completamente errado. Variável iNÃO é um elemento da matriz a[10], não há obrigação ou mesmo sugestão para um compilador colocá-lo na pilha imediatamente após a[] - ele também pode estar localizado antes da matriz ou separado com algum espaço adicional. Pode até ser alocado fora da memória principal, por exemplo, em um registro da CPU. Também é falso que ++seja para ponteiros e não para números inteiros. Completamente errado é 'i ++ está aumentando i pelo tamanho de i' - leia a descrição do operador na definição da linguagem!
CiaPan
é por isso que funciona em algumas plataformas e não em outras. é a única explicação lógica para o motivo de ele repetir para sempre no Windows. no que diz respeito ao I ++, é ponteiro matemático, não inteiro. leia as Escrituras ... a 'linguagem de programação C'. por Kernigan e Ritche, se você quiser eu tenho uma cópia autografada, e ter sido programação em C desde 1981.
SkipBerne
1
Leia o código fonte pelo OP e encontre a declaração da variável i- é do inttipo. É um número inteiro , não um ponteiro; um inteiro, usado como um índice para array,.
CiaPan
1
Eu fiz e é por isso que comentei. talvez você deva perceber que, a menos que o compilador inclua verificações de pilha e, nesse caso, não importaria como referência de pilha quando I = 10 realmente referenciasse, em algumas compilações, o índice da matriz e que esteja dentro dos limites da região da pilha. compiladores não podem consertar estúpido. compilações podem fazer uma correção da maneira que parece, mas uma interpretação pura da linguagem de programação c não suportaria essa convenção e, como o OP disse, resultaria em resultados não portáteis.
SkipBerne
@ SkipBerne: considere excluir sua resposta antes de ser "premiado" com mais pontos negativos.
Peter VARGA