Quão perigoso é acessar uma matriz fora de seus limites (em C)? Às vezes, pode acontecer que eu leio de fora da matriz (agora entendo que, então, acedo à memória usada por outras partes do meu programa ou mesmo além dela) ou estou tentando definir um valor para um índice fora da matriz. Às vezes, o programa trava, mas às vezes é executado, fornecendo apenas resultados inesperados.
Agora, o que eu gostaria de saber é, o quão perigoso isso é realmente? Se danificar meu programa, não é tão ruim. Se, por outro lado, quebrar algo fora do meu programa, porque de alguma forma consegui acessar alguma memória totalmente não relacionada, então é muito ruim, imagino. Eu li muito 'tudo pode acontecer', 'segmentação pode ser o problema menos ruim' , 'seu disco rígido pode ficar rosa e unicórnios podem estar cantando sob sua janela', o que é legal, mas qual é realmente o perigo?
Minhas perguntas:
- A leitura de valores de fora da matriz pode prejudicar algo além do meu programa? Eu imagino que apenas olhar as coisas não muda nada ou, por exemplo, alteraria o atributo 'última vez que foi aberto' de um arquivo que encontrei?
- A definição de valores fora da matriz pode prejudicar algo além do meu programa? A partir desta questão de estouro de pilha, concluo que é possível acessar qualquer local de memória, que não há garantia de segurança.
- Agora eu executo meus pequenos programas no XCode. Isso fornece alguma proteção extra ao redor do meu programa, onde ele não pode alcançar fora de sua própria memória? Isso pode prejudicar o XCode?
- Alguma recomendação sobre como executar meu código inerentemente incorreto?
Eu uso o OSX 10.7, Xcode 4.6.
Respostas:
No que diz respeito ao padrão ISO C (a definição oficial do idioma), o acesso a uma matriz fora de seus limites tem " comportamento indefinido ". O significado literal disso é:
Uma nota não normativa expande sobre isso:
Então essa é a teoria. Qual é a realidade?
No "melhor" caso, você acessará uma parte da memória que pertence ao seu programa em execução no momento (o que pode causar um mau comportamento do programa) ou que não pertence ao seu programa em execução no momento (o que provavelmente fará com que o programa falha com algo como uma falha de segmentação). Ou você pode tentar gravar na memória que seu programa possui, mas isso é marcado como somente leitura; isso provavelmente também fará com que seu programa falhe.
Isso pressupõe que o seu programa esteja sendo executado em um sistema operacional que tenta proteger os processos em execução simultaneamente. Se o seu código estiver sendo executado no "bare metal", diga se faz parte de um kernel do SO ou de um sistema incorporado, então não existe essa proteção; seu código de mau comportamento é o que deveria fornecer essa proteção. Nesse caso, as possibilidades de danos são consideravelmente maiores, incluindo, em alguns casos, danos físicos ao hardware (ou a objetos ou pessoas próximas).
Mesmo em um ambiente protegido, nem sempre as proteções são 100%. Existem erros no sistema operacional que permitem que programas sem privilégios obtenham acesso root (administrativo), por exemplo. Mesmo com privilégios comuns de usuário, um programa com defeito pode consumir recursos excessivos (CPU, memória, disco), possivelmente derrubando todo o sistema. Muitos malwares (vírus, etc.) exploram a saturação de buffer para obter acesso não autorizado ao sistema.
(Um exemplo histórico: ouvi dizer que em alguns sistemas antigos com memória central , o acesso repetido a um único local de memória em um loop apertado pode literalmente fazer com que esse pedaço de memória derreta. Outras possibilidades incluem destruir uma tela CRT e mover a leitura / escreva a cabeça de uma unidade de disco com a frequência harmônica do gabinete da unidade, fazendo com que ela atravesse uma mesa e caia no chão.)
E sempre há Skynet com que se preocupar.
O ponto principal é o seguinte: se você pode escrever um programa para fazer algo ruim deliberadamente , é pelo menos teoricamente possível que um programa de buggy possa fazer a mesma coisa acidentalmente .
Na prática, é muito improvável que o seu programa de bugs rodando em um sistema MacOS X faça algo mais sério do que travar. Mas não é possível impedir completamente que o código de buggy faça coisas realmente ruins.
fonte
Em geral, os sistemas operacionais de hoje (os populares de qualquer maneira) executam todos os aplicativos em regiões de memória protegidas usando um gerenciador de memória virtual. Acontece que não é muito FÁCIL (digamos) simplesmente ler ou gravar em um local que exista no espaço REAL, fora da região (s) atribuída / alocada ao seu processo.
Respostas diretas:
1) A leitura quase nunca danifica diretamente outro processo, mas pode danificar indiretamente um processo se você ler um valor KEY usado para criptografar, descriptografar ou validar um programa / processo. A leitura fora dos limites pode ter efeitos adversos / inesperados no seu código se você estiver tomando decisões com base nos dados que está lendo
2) A única maneira de você realmente DANIFICAR algo gravando em um local acessível por um endereço de memória é se esse endereço de memória para o qual você está gravando for realmente um registro de hardware (um local que na verdade não é para armazenamento de dados, mas para controlar alguma peça de hardware) não é um local de RAM. De fato, você ainda normalmente não danifica algo, a menos que esteja escrevendo um local programável único que não seja regravável (ou algo dessa natureza).
3) Geralmente, a execução de dentro do depurador executa o código no modo de depuração. A execução no modo de depuração Tende a (mas nem sempre) interrompe seu código mais rapidamente quando você faz algo considerado fora da prática ou totalmente ilegal.
4) Nunca use macros, use estruturas de dados que já possuam verificação de limites de índice de matriz incorporada, etc.
ADICIONAL Devo acrescentar que as informações acima são realmente apenas para sistemas que usam um sistema operacional com janelas de proteção de memória. Se escrever código para um sistema incorporado ou mesmo para um sistema operacional (em tempo real ou outro) que não possua janelas de proteção de memória (ou janelas endereçadas virtuais), deve-se ter muito mais cuidado ao ler e gravar na memória. Também nesses casos, práticas de codificação SAFE e SECURE devem sempre ser empregadas para evitar problemas de segurança.
fonte
Não verificar os limites pode levar a efeitos colaterais feios, incluindo brechas na segurança. Um dos mais feios é a execução arbitrária de códigos . No exemplo clássico: se você possui uma matriz de tamanho fixo e usa
strcpy()
para colocar uma string fornecida pelo usuário, ela pode fornecer uma string que transborda o buffer e sobrescreve outros locais de memória, incluindo o endereço de código onde a CPU deve retornar quando sua função termina.O que significa que seu usuário pode enviar uma string que fará com que seu programa chame essencialmente
exec("/bin/sh")
, o que o transformará em shell, executando o que ele quiser no seu sistema, incluindo a coleta de todos os dados e a transformação da máquina em um nó de botnet.Veja Smashing The Stack For Fun And Profit para obter detalhes sobre como isso pode ser feito.
fonte
foo[0]
atravésfoo[len-1]
depois de ter utilizado anteriormente um cheque delen
contra o comprimento da matriz, quer executar ou saltar um pedaço de código, o compilador deve se sentir livre para executar esse outro código incondicionalmente mesmo se o aplicativo possuir o armazenamento após a matriz e os efeitos da leitura, seria benigno, mas o efeito de invocar o outro código não seria.Você escreve:
Vamos colocar dessa maneira: carregar uma arma. Aponte para fora da janela sem nenhum objetivo específico e fogo. Qual é o perigo?
A questão é que você não sabe. Se o seu código sobrescrever algo que trava seu programa, você está bem, pois ele o interromperá em um estado definido. No entanto, se não travar, os problemas começam a surgir. Quais recursos estão sob controle do seu programa e o que isso pode fazer com eles? Quais recursos podem ficar sob controle do seu programa e o que isso pode fazer com eles? Conheço pelo menos um problema importante causado por esse estouro. O problema estava em uma função estatística aparentemente sem sentido que atrapalhou uma tabela de conversão não relacionada para um banco de dados de produção. O resultado foi uma limpeza muito cara depois. Na verdade, teria sido muito mais barato e mais fácil de lidar se esse problema tivesse formatado os discos rígidos ... com outras palavras: unicórnios rosa podem ser o seu menor problema.
A idéia de que seu sistema operacional o protegerá é otimista. Se possível, tente evitar escrever fora dos limites.
fonte
Não executar o programa como root ou qualquer outro usuário privilegiado não prejudicará o sistema, portanto, geralmente isso pode ser uma boa idéia.
Ao gravar dados em algum local aleatório da memória, você não "danifica" diretamente nenhum outro programa em execução no seu computador, pois cada processo é executado em seu próprio espaço de memória.
Se você tentar acessar qualquer memória não alocada para o processo, o sistema operacional interromperá a execução do programa com uma falha de segmentação.
Então, diretamente (sem executar como root e acessar diretamente arquivos como / dev / mem), não há perigo de que seu programa interfira com qualquer outro programa em execução no sistema operacional.
No entanto - e provavelmente é sobre isso que você já ouviu falar em termos de perigo - escrevendo dados aleatórios cegamente em locais aleatórios de memória por acidente, com certeza você pode danificar qualquer coisa que possa danificar.
Por exemplo, seu programa pode querer excluir um arquivo específico fornecido por um nome de arquivo armazenado em algum lugar do programa. Se, por acidente, você simplesmente substituir o local em que o nome do arquivo está armazenado, poderá excluir um arquivo muito diferente.
fonte
NSArray
s no Objective-C recebem um bloco específico de memória. Exceder os limites da matriz significa que você acessaria a memória que não está atribuída à matriz. Isso significa:Pelo aspecto do seu programa, você sempre quer saber quando o seu código está excedendo os limites de uma matriz. Isso pode levar à devolução de valores desconhecidos, causando uma falha no aplicativo ou fornecendo dados inválidos.
fonte
NSArrays
tem exceções fora dos limites. E esta pergunta parece ser sobre o array C.Você pode tentar usar a
memcheck
ferramenta no Valgrind ao testar seu código - ele não detectará violações de limites de matriz individuais dentro de um quadro de pilha, mas deve detectar muitos outros tipos de problemas de memória, incluindo aqueles que causariam sutis e mais amplos problemas fora do escopo de uma única função.Do manual:
ETA: Embora, como diz a resposta de Kaz, não seja uma panacéia e nem sempre produza os resultados mais úteis, especialmente quando você está usando padrões de acesso interessantes .
fonte
Se você fizer uma programação no nível de sistemas ou de sistemas embarcados, coisas muito ruins podem acontecer se você gravar em locais de memória aleatórios. Os sistemas mais antigos e muitos microcontroladores usam E / S mapeadas na memória; portanto, gravar em um local de memória que mapeia para um registro periférico pode causar estragos, especialmente se for feito de forma assíncrona.
Um exemplo é a programação da memória flash. O modo de programação nos chips de memória é ativado gravando uma sequência específica de valores em locais específicos dentro do intervalo de endereços do chip. Se outro processo fosse gravado em qualquer outro local no chip enquanto isso acontecesse, isso causaria falha no ciclo de programação.
Em alguns casos, o hardware envolve os endereços (os bits / bytes de endereço mais significativos são ignorados); portanto, a gravação em um endereço além do final do espaço de endereço físico resultará na gravação de dados no meio das coisas.
E, finalmente, CPUs mais antigas como o MC68000 podem bloquear até o ponto em que apenas uma redefinição de hardware pode fazê-las funcionar novamente. Não trabalhamos com eles há algumas décadas, mas acredito que foi quando ele encontrou um erro de barramento (memória inexistente) ao tentar lidar com uma exceção, ele simplesmente seria interrompido até que a redefinição do hardware fosse confirmada.
Minha maior recomendação é um plágio flagrante para um produto, mas não tenho interesse pessoal e não sou afiliado a eles de nenhuma maneira - mas com base em algumas décadas de programação C e sistemas embarcados onde a confiabilidade era crítica, o PC de Gimpel O Lint não apenas detecta esse tipo de erro, mas também cria um programador C / C ++ melhor, constantemente falando sobre maus hábitos.
Também recomendo a leitura do padrão de codificação MISRA C, se você conseguir uma cópia de alguém. Eu não vi nenhum recente, mas em alguns dias eles deram uma boa explicação de por que você não deveria / deveria fazer as coisas que eles cobrem.
Não sei quanto a você, mas, na segunda ou na terceira vez, recebo um coredump ou hangup de qualquer aplicativo, minha opinião sobre qualquer empresa que produziu diminui pela metade. Na quarta ou quinta vez, e o que quer que seja o pacote, torna-se uma prateleira e eu enfio uma estaca de madeira no centro do pacote / disco em que ele veio, apenas para garantir que ele nunca mais volte para me assombrar.
fonte
Eu estou trabalhando com um compilador para um chip DSP que deliberadamente gera código que acessa um após o final de uma matriz fora do código C que não!
Isso ocorre porque os loops são estruturados para que o final de uma iteração prefira alguns dados para a próxima iteração. Portanto, o dado pré-buscado no final da última iteração nunca é realmente usado.
Escrever código C assim invoca um comportamento indefinido, mas isso é apenas uma formalidade de um documento de normas que se preocupa com a portabilidade máxima.
Mais frequentemente, um programa que acessa fora dos limites não é otimizado de maneira inteligente. É simplesmente buggy. O código busca algum valor de lixo e, diferentemente dos loops otimizados do compilador mencionado acima, o código usa o valor em cálculos subsequentes, corrompendo-os.
Vale a pena capturar bugs como esse e, portanto, vale a pena tornar o comportamento indefinido apenas por esse motivo: para que o tempo de execução possa produzir uma mensagem de diagnóstico como "array overrun na linha 42 de main.c".
Em sistemas com memória virtual, uma matriz pode ser alocada de modo que o endereço a seguir esteja em uma área não mapeada da memória virtual. O acesso bombardeará o programa.
No entanto, o acesso a valores não inicializados ou fora dos limites às vezes é uma técnica de otimização válida, mesmo que não seja maximamente portátil. É por exemplo, por que a ferramenta Valgrind não relata acessos a dados não inicializados quando esses acessos acontecem, mas somente quando o valor é usado posteriormente de alguma maneira que possa afetar o resultado do programa. Você obtém um diagnóstico como "ramificação condicional em xxx: nnn depende de valor não inicializado" e às vezes pode ser difícil rastrear sua origem. Se todos esses acessos fossem capturados imediatamente, haveria muitos falsos positivos decorrentes do código otimizado do compilador, bem como do código otimizado manualmente.
Falando nisso, eu estava trabalhando com algum codec de um fornecedor que estava emitindo esses erros quando portado para Linux e executado no Valgrind. Mas o fornecedor me convenceu de que apenas vários bitso valor usado realmente veio da memória não inicializada e esses bits foram cuidadosamente evitados pela lógica. Somente os bons bits do valor estavam sendo usados e o Valgrind não tem a capacidade de rastrear o bit individual. O material não inicializado veio da leitura de uma palavra após o final de um fluxo de bits de dados codificados, mas o código sabe quantos bits estão no fluxo e não usará mais bits do que realmente são. Como o acesso além do final da matriz de fluxo de bits não causa nenhum dano à arquitetura DSP (não há memória virtual após a matriz, nenhuma porta mapeada na memória e o endereço não quebra), é uma técnica de otimização válida.
"Comportamento indefinido" não significa muito, porque, de acordo com a ISO C, simplesmente incluir um cabeçalho que não está definido no padrão C ou chamar uma função que não está definida no próprio programa ou no padrão C são exemplos de indefinidos. comportamento. Comportamento indefinido não significa "não definido por ninguém no planeta" apenas "não definido pelo padrão ISO C". Mas, claro, o comportamento às vezes indefinido realmente é absolutamente não definido por qualquer pessoa.
fonte
Além do seu próprio programa, acho que você não quebrará nada, na pior das hipóteses você tentará ler ou escrever a partir de um endereço de memória que corresponda a uma página que o kernel não atribuiu aos seus processos, gerando a exceção adequada e ser morto (quero dizer, seu processo).
fonte