Acessar uma matriz fora dos limites não dá erro, por quê?

177

Estou atribuindo valores em um programa C ++ fora dos limites como este:

#include <iostream>
using namespace std;
int main()
{
    int array[2];
    array[0] = 1;
    array[1] = 2;
    array[3] = 3;
    array[4] = 4;
    cout << array[3] << endl;
    cout << array[4] << endl;
    return 0;
}

O programa imprime 3e 4. Não deveria ser possível. Estou usando o g ++ 4.3.3

Aqui está o comando compilar e executar

$ g++ -W -Wall errorRange.cpp -o errorRange
$ ./errorRange
3
4

Somente ao atribuir array[3000]=3000isso me causa uma falha de segmentação.

Se o gcc não verificar os limites da matriz, como posso ter certeza se meu programa está correto, pois isso pode levar a alguns problemas sérios mais tarde?

Substituí o código acima por

vector<int> vint(2);
vint[0] = 0;
vint[1] = 1;
vint[2] = 2;
vint[5] = 5;
cout << vint[2] << endl;
cout << vint[5] << endl;

e este também não produz erro.

seg.server.fault
fonte
3
Pergunta relacionada: stackoverflow.com/questions/671703/…
TSomKes
16
O código é incorreto, é claro, mas gera um comportamento indefinido . Indefinido significa que pode ou não ser executado até a conclusão. Não há garantia de um acidente.
dmckee --- ex-moderador gatinho
4
Você pode ter certeza de que seu programa está correto, não mexendo com matrizes brutas. Os programadores de C ++ devem usar classes de contêiner, exceto na programação de sistemas operacionais / embarcados. Leia isso por motivos de contêineres do usuário. parashift.com/c++-faq-lite/containers.html
jkeys
8
Lembre-se de que os vetores não necessariamente checam o intervalo usando []. Usar .at () faz a mesma coisa que [], mas faz a verificação de intervalo.
22630 David Thornley
4
A vector não redimensiona automaticamente ao acessar elementos fora dos limites! É apenas UB!
Pavel Minaev 06/08/09

Respostas:

364

Bem-vindo ao melhor amigo de todos os programadores de C / C ++: comportamento indefinido .

Há muito que não é especificado pelo padrão de idioma, por vários motivos. Este é um deles.

Em geral, sempre que você encontra um comportamento indefinido, tudo pode acontecer. O aplicativo pode falhar, congelar, ejetar sua unidade de CD-ROM ou fazer com que demônios saiam do seu nariz. Pode formatar seu disco rígido ou enviar por e-mail todo o seu pornô para sua avó.

Pode até, se você tiver realmente azar, parecer funcionar corretamente.

A linguagem simplesmente diz o que deve acontecer se você acessar os elementos dentro dos limites de uma matriz. É deixado indefinido o que acontece se você sair dos limites. Pode parecer funcionar hoje, no seu compilador, mas não é legal em C ou C ++, e não há garantia de que ainda funcionará na próxima vez que você executar o programa. Ou que tem os dados essenciais não substituídos, mesmo agora, e você apenas não ter encontrado os problemas, que está indo para a causa - ainda.

Quanto ao motivo pelo qual não há verificação de limites, há alguns aspectos na resposta:

  • Uma matriz é uma sobra de C. As matrizes C são tão primitivas quanto você pode obter. Apenas uma sequência de elementos com endereços contíguos. Não há verificação de limites porque está simplesmente expondo a memória não processada. A implementação de um mecanismo robusto de verificação de limites teria sido quase impossível em C.
  • No C ++, a verificação de limites é possível em tipos de classe. Mas uma matriz ainda é a antiga compatível com C simples. Não é uma aula. Além disso, o C ++ também se baseia em outra regra que torna a verificação de limites não ideal. O princípio orientador do C ++ é "você não paga pelo que não usa". Se o seu código estiver correto, você não precisará de verificação de limites e não será obrigado a pagar pela sobrecarga da verificação de limites de tempo de execução.
  • Portanto, o C ++ oferece o std::vectormodelo de classe, que permite ambos. operator[]foi projetado para ser eficiente. O padrão de idioma não exige que ele execute verificação de limites (embora também não o proíba). Um vetor também tem a at()função de membro que é garantida para executar a verificação de limites. Portanto, em C ++, você obtém o melhor dos dois mundos se usar um vetor. Você obtém desempenho semelhante ao de uma matriz sem verificação de limites e pode usar o acesso verificado quando quiser.
jalf
fonte
5
@ Jaif: estamos usando essa coisa de matriz há tanto tempo, mas ainda por que não há nenhum teste para verificar um erro tão simples?
#
7
O princípio de design do C ++ era que ele não deveria ser mais lento que o código C equivalente e C não faz a verificação vinculada à matriz. O princípio do projeto C era basicamente a velocidade, pois era destinado à programação do sistema. A verificação vinculada à matriz leva tempo e, portanto, não é feita. Para a maioria dos usos em C ++, você deve usar um contêiner em vez de uma matriz de qualquer maneira e pode escolher entre verificação ou não, acessando um elemento via .at () ou [], respectivamente.
KTC
4
@seg Essa verificação custa algo. Se você escrever o código correto, não deseja pagar esse preço. Dito isto, eu me tornei um método completo de conversão para std :: vector at (), que é verificado. Usá-lo expôs alguns erros no que eu pensava ser o código "correto".
10
Acredito que versões antigas do GCC realmente lançaram o Emacs e uma simulação de Towers of Hanoi nele, quando encontrou certos tipos de comportamento indefinido. Como eu disse, tudo pode acontecer. ;)
jalf
4
Tudo já foi dito, portanto, isso justifica apenas um pequeno adendo. As compilações de depuração podem perdoar muito nessas circunstâncias quando comparadas às compilações de versão. Devido à inclusão de informações de depuração nos binários de depuração, há menos chances de que algo vital seja substituído. Às vezes, é por isso que as compilações de depuração parecem funcionar bem enquanto a compilação de versão falha.
675 Rich
31

Usando g ++, você pode adicionar a opção de linha de comando: -fstack-protector-all.

No seu exemplo, resultou no seguinte:

> g++ -o t -fstack-protector-all t.cc
> ./t
3
4
/bin/bash: line 1: 15450 Segmentation fault      ./t

Realmente não ajuda a encontrar ou resolver o problema, mas pelo menos o segfault permitirá que você saiba que algo está errado.

Richard Corden
fonte
10
Eu apenas encontrei uma opção ainda melhor: -fmudflap
Hi-Angel
1
@ Hi-Angel: O equivalente moderno é o -fsanitize=addressque pega esse bug no momento da compilação (se estiver otimizando) e no tempo de execução.
Nate Eldredge
@NateEldredge +1, hoje em dia eu até uso -fsanitize=undefined,address. Mas é importante notar que existem casos raros de canto com a biblioteca std, quando o acesso fora dos limites não é detectado pelo sanitizer . Por esse motivo, recomendo usar a -D_GLIBCXX_DEBUGopção adicional, que adiciona ainda mais verificações.
Olá Angel
12

O g ++ não verifica os limites da matriz e você pode substituir algo com 3,4, mas nada realmente importante; se você tentar com números mais altos, ocorrerá um acidente.

Você está apenas substituindo partes da pilha que não são usadas, você pode continuar até chegar ao final do espaço alocado para a pilha e ela poderá travar eventualmente

EDIT: Você não tem como lidar com isso, talvez um analisador de código estático possa revelar essas falhas, mas isso é simples demais, você pode ter falhas semelhantes (mas mais complexas) não detectadas, mesmo para analisadores estáticos

Arkaitz Jimenez
fonte
6
De onde você tira isso no endereço da matriz [3] e da matriz [4], não há "nada realmente importante"?
namezero 9/09/13
7

É um comportamento indefinido, tanto quanto eu sei. Execute um programa maior com isso e ele travará em algum lugar ao longo do caminho. A verificação de limites não faz parte de matrizes brutas (ou mesmo std :: vector).

Use std :: vector com std::vector::iterator's, para que você não precise se preocupar com isso.

Editar:

Apenas por diversão, execute isso e veja quanto tempo até você travar:

int main()
{
   int array[1];

   for (int i = 0; i != 100000; i++)
   {
       array[i] = i;
   }

   return 0; //will be lucky to ever reach this
}

Edit2:

Não corra isso.

Edit3:

OK, aqui está uma lição rápida sobre matrizes e seus relacionamentos com ponteiros:

Quando você usa a indexação de matriz, está realmente usando um ponteiro disfarçado (chamado de "referência"), que é automaticamente desreferenciado. É por isso que, em vez de * (matriz [1]), a matriz [1] retorna automaticamente o valor nesse valor.

Quando você tem um ponteiro para uma matriz, assim:

int array[5];
int *ptr = array;

Então a "matriz" na segunda declaração está realmente decaindo para um ponteiro para a primeira matriz. Esse é um comportamento equivalente a isso:

int *ptr = &array[0];

Quando você tenta acessar além do que você alocou, está apenas usando um ponteiro para outra memória (da qual o C ++ não se queixará). Tomando meu programa de exemplo acima, isso é equivalente a isso:

int main()
{
   int array[1];
   int *ptr = array;

   for (int i = 0; i != 100000; i++, ptr++)
   {
       *ptr++ = i;
   }

   return 0; //will be lucky to ever reach this
}

O compilador não irá reclamar porque, na programação, você geralmente precisa se comunicar com outros programas, especialmente o sistema operacional. Isso é feito com ponteiros um pouco.

jkeys
fonte
3
Eu acho que você esqueceu de incrementar "ptr" no seu último exemplo lá. Você produziu acidentalmente algum código bem definido.
11409 Jeff Lake
1
Haha, veja por que você não deveria usar matrizes brutas?
jkeys
"É por isso que em vez de * (matriz [1]), a matriz [1] retorna automaticamente o valor nesse valor." Você tem certeza de que * (array [1]) funcionará corretamente? Eu acho que deveria ser * (array + 1). ps: Lol, é como enviar uma mensagem para o passado. Mas, enfim:
muyustan 07/04
5

Sugestão

Se você quer ter matrizes de tamanho rápido de restrição com verificação de erro gama, tente usar boost :: variedade , (também std :: tr1 :: variedade de <tr1/array>será contêiner padrão na próxima especificação C ++). É muito mais rápido que o std :: vector. Ele reserva memória na pilha ou dentro da instância da classe, assim como int array [].
Este é um código de exemplo simples:

#include <iostream>
#include <boost/array.hpp>
int main()
{
    boost::array<int,2> array;
    array.at(0) = 1; // checking index is inside range
    array[1] = 2;    // no error check, as fast as int array[2];
    try
    {
       // index is inside range
       std::cout << "array.at(0) = " << array.at(0) << std::endl;

       // index is outside range, throwing exception
       std::cout << "array.at(2) = " << array.at(2) << std::endl; 

       // never comes here
       std::cout << "array.at(1) = " << array.at(1) << std::endl;  
    }
    catch(const std::out_of_range& r)
    {
        std::cout << "Something goes wrong: " << r.what() << std::endl;
    }
    return 0;
}

Este programa imprimirá:

array.at(0) = 1
Something goes wrong: array<>: index out of range
Arpegius
fonte
4

C ou C ++ não verificará os limites de um acesso à matriz.

Você está alocando a matriz na pilha. A indexação da matriz via array[3]é equivalente a * (array + 3), onde matriz é um ponteiro para & matriz [0]. Isso resultará em comportamento indefinido.

Uma maneira de capturar isso às vezes em C é usar um verificador estático, como splint . Se você executar:

splint +bounds array.c

em,

int main(void)
{
    int array[1];

    array[1] = 1;

    return 0;
}

então você receberá o aviso:

array.c: (na função principal) array.c: 5: 9: Provável armazenamento fora dos limites: array [1] Não é possível resolver a restrição: requer 0> = 1 necessário para satisfazer a pré-condição: requer maxSet (array @ array .c: 5: 9)> = 1 Uma gravação na memória pode gravar em um endereço além do buffer alocado.

Karl Voigtland
fonte
Correção: já foi alocado pelo sistema operacional ou outro programa. Ele está substituindo outra memória.
jkeys
1
Dizer que "C / C ++ não verificará os limites" não está totalmente correto - não há nada que impeça uma implementação compatível específica de fazê-lo, por padrão ou com alguns sinalizadores de compilação. É que nenhum deles se incomoda.
Pavel Minaev 06/08/09
3

Você certamente está substituindo sua pilha, mas o programa é simples o suficiente para que seus efeitos passem despercebidos.

Paul Dixon
fonte
2
A substituição ou não da pilha depende da plataforma.
Chris Cleeland
3

Execute isso no Valgrind e você poderá ver um erro.

Como Falaina apontou, o valgrind não detecta muitos casos de corrupção de pilha. Eu apenas tentei a amostra em valgrind, e ela realmente relata zero erros. No entanto, o Valgrind pode ser fundamental para encontrar muitos outros tipos de problemas de memória; nesse caso, não é particularmente útil, a menos que você modifique seu bulid para incluir a opção --stack-check. Se você construir e executar a amostra como

g++ --stack-check -W -Wall errorRange.cpp -o errorRange
valgrind ./errorRange

valgrind irá relatar um erro.

Todd Stout
fonte
2
Na verdade, o Valgrind é bastante fraco na determinação de acessos incorretos à matriz na pilha. (e justamente por isso, o melhor que pode fazer é marcar a pilha inteira como um local de gravação válido)
Falaina
@Falaina - bom ponto, mas o Valgrind pode detectar pelo menos alguns erros de pilha.
218 Todd Stout
E o valgrind não verá nada de errado com o código porque o compilador é inteligente o suficiente para otimizar a matriz e simplesmente gerar um literal 3 e 4. Essa otimização ocorre antes que o gcc verifique os limites da matriz, e é por isso que o aviso fora dos limites do gcc faz have não é mostrado.
Goswin von Brederlow
2

Comportamento indefinido trabalhando a seu favor. Qualquer que seja a memória que você esteja batendo, aparentemente não está segurando nada de importante. Observe que C e C ++ não fazem checagem de limites em matrizes; portanto, coisas assim não serão capturadas no tempo de compilação ou execução.

John Bode
fonte
5
Não, o comportamento indefinido "funciona a seu favor" quando falha de maneira limpa. Quando parece funcionar, esse é o pior cenário possível.
jalf
@JohnBode: Então seria melhor se você texto correto como por comentário jalf de
Destructor
1

Quando você inicializa a matriz com int array[2], o espaço para 2 números inteiros é alocado; mas o identificador arraysimplesmente aponta para o início desse espaço. Quando você acessa array[3]e array[4], o compilador simplesmente incrementa esse endereço para apontar para onde esses valores estariam, se a matriz fosse longa o suficiente; tente acessar algo como array[42]sem inicializá-lo primeiro, você acabará recebendo o valor que já estava na memória naquele local.

Editar:

Mais informações sobre ponteiros / matrizes: http://home.netcom.com/~tjensen/ptr/pointers.htm

Nathan Clark
fonte
0

quando você declara int array [2]; você reserva 2 espaços de memória de 4 bytes cada (programa de 32 bits). se você digitar array [4] no seu código, ele ainda corresponderá a uma chamada válida, mas somente em tempo de execução será lançada uma exceção não tratada. C ++ usa gerenciamento manual de memória. Esta é realmente uma falha de segurança que foi usada para programas de hackers

isso pode ajudar a entender:

int * somepointer;

somepointer [0] = somepointer [5];

yan bellavance
fonte
0

Pelo que entendi, as variáveis ​​locais são alocadas na pilha; portanto, sair dos limites da sua própria pilha só pode sobrescrever alguma outra variável local, a menos que você exagere demais e exceda o tamanho da pilha. Como você não possui outras variáveis ​​declaradas em sua função - isso não causa efeitos colaterais. Tente declarar outra variável / matriz logo após a sua primeira e veja o que acontecerá com ela.

Vorber
fonte
0

Quando você escreve 'array [index]' em C, ele o converte nas instruções da máquina.

A tradução é algo como:

  1. 'obtém o endereço da matriz'
  2. 'obtém o tamanho do tipo de objeto que a matriz é composta'
  3. 'multiplique o tamanho do tipo pelo índice'
  4. 'adicione o resultado ao endereço da matriz'
  5. 'leia o que está no endereço resultante'

O resultado aborda algo que pode ou não fazer parte da matriz. Em troca da velocidade impressionante das instruções da máquina, você perde a rede de segurança do computador que verifica as coisas para você. Se você é meticuloso e cuidadoso, não há problema. Se você é desleixado ou comete um erro, se queima. Às vezes, pode gerar uma instrução inválida que causa uma exceção, às vezes não.

Jay
fonte
0

Uma boa abordagem que eu já vi muitas vezes e que tinha sido usada na verdade é injetar algum elemento do tipo NULL (ou um criado, como uint THIS_IS_INFINITY = 82862863263;) no final da matriz.

Então, na verificação da condição do loop, TYPE *pagesWordshá algum tipo de matriz de ponteiro:

int pagesWordsLength = sizeof(pagesWords) / sizeof(pagesWords[0]);

realloc (pagesWords, sizeof(pagesWords[0]) * (pagesWordsLength + 1);

pagesWords[pagesWordsLength] = MY_NULL;

for (uint i = 0; i < 1000; i++)
{
  if (pagesWords[i] == MY_NULL)
  {
    break;
  }
}

Essa solução não funcionará se a matriz for preenchida com structtipos.

xudre
fonte
0

Como mencionado agora na pergunta, usando std :: vector :: at resolverá o problema e fará uma verificação vinculada antes de acessar.

Se você precisar de uma matriz de tamanho constante localizada na pilha como seu primeiro código, use o novo container std :: array do C ++ 11; como vetor, existe a função std :: array :: at. De fato, a função existe em todos os contêineres padrão nos quais tem um significado, ou seja, onde o operador [] é definido :( deque, map, unordered_map), com exceção do std :: bitset no qual é chamado std :: bitset: :teste.

Mohamed El-Nakib
fonte
0

A libstdc ++, que faz parte do gcc, possui um modo de depuração especial para verificação de erros. É ativado pelo sinalizador do compilador -D_GLIBCXX_DEBUG. Entre outras coisas, ele limita a verificação std::vectorao custo do desempenho. Aqui está uma demonstração on - line com a versão recente do gcc.

Portanto, na verdade, você pode verificar os limites com o modo de depuração libstdc ++, mas deve fazê-lo apenas durante o teste, pois custa um desempenho notável comparado ao modo libstdc ++ normal.

ks1322
fonte
0

Se você mudar um pouco o seu programa:

#include <iostream>
using namespace std;
int main()
{
    int array[2];
    INT NOTHING;
    CHAR FOO[4];
    STRCPY(FOO, "BAR");
    array[0] = 1;
    array[1] = 2;
    array[3] = 3;
    array[4] = 4;
    cout << array[3] << endl;
    cout << array[4] << endl;
    COUT << FOO << ENDL;
    return 0;
}

(Alterações em maiúsculas - coloque-as em minúsculas, se você quiser fazer isso.)

Você verá que a variável foo foi lixeira. Seu código irá armazenar valores na matriz inexistente [3] e array [4], e ser capaz de recuperá-los corretamente, mas o armazenamento real usado será de foo .

Portanto, você pode "exceder" os limites da matriz em seu exemplo original, mas com o custo de causar danos em outros lugares - danos que podem ser muito difíceis de diagnosticar.

Por que não há verificação automática de limites - um programa escrito corretamente não precisa dele. Feito isso, não há razão para verificar os limites de tempo de execução, pois isso atrasaria o programa. É melhor entender tudo isso durante o design e a codificação.

O C ++ é baseado em C, que foi projetado para ser o mais próximo possível da linguagem assembly.

Jennifer
fonte