Ponteiros de teste para validade (C / C ++)

90

Existe alguma maneira de determinar (programaticamente, é claro) se um determinado ponteiro é "válido"? Verificar se há NULL é fácil, mas e coisas como 0x00001234? Ao tentar desreferenciar este tipo de ponteiro, ocorre uma exceção / falha.

É preferível um método de plataforma cruzada, mas específico de plataforma (para Windows e Linux) também está ok.

Atualização para esclarecimento: O problema não é com ponteiros obsoletos / liberados / não inicializados; em vez disso, estou implementando uma API que recebe ponteiros do chamador (como um ponteiro para uma string, um identificador de arquivo, etc.). O chamador pode enviar (propositalmente ou por engano) um valor inválido como o ponteiro. Como posso evitar uma falha?

noamtm
fonte
Consulte também stackoverflow.com/questions/496034/…
ChrisW de
Acho que a melhor resposta positiva para o linux é dada por George Carrette. Se isso não for suficiente, considere construir a tabela de símbolos de função na biblioteca, ou mesmo outro nível de tabela de bibliotecas disponíveis com suas próprias tabelas de funções. Em seguida, verifique essas tabelas exatas. Obviamente, essas respostas negativas também estão corretas: você não pode realmente ter 100% de certeza se um ponteiro de função é válido ou não, a menos que coloque muitas restrições adicionais para o aplicativo do usuário.
minghua
A Especificação da API realmente especifica essa obrigação a ser cumprida pela implementação? A propósito, finjo não ter sido assumido que você é o desenvolvedor e o designer. Meu ponto é, eu não acho que uma API especificaria algo como "No caso de um ponteiro inválido sendo passado como um argumento, a função deve lidar com o problema e retorna NULL.". Uma API assume a obrigação de fornecer um serviço sob condições de uso adequadas, não por hacks. No entanto, não faz mal ser um pouco à prova de estupidez. Usar uma referência faz com que tais casos se espalhem menos. :)
Poniros

Respostas:

75

Atualização para esclarecimento: O problema não é com ponteiros obsoletos, liberados ou não inicializados; em vez disso, estou implementando uma API que recebe ponteiros do chamador (como um ponteiro para uma string, um identificador de arquivo, etc.). O chamador pode enviar (propositalmente ou por engano) um valor inválido como o ponteiro. Como posso evitar uma falha?

Você não pode fazer essa verificação. Simplesmente não há como verificar se um ponteiro é "válido". Você tem que confiar que, quando as pessoas usam uma função que recebe um ponteiro, essas pessoas sabem o que estão fazendo. Se eles passarem a você 0x4211 como um valor de ponteiro, você deve confiar que ele aponta para o endereço 0x4211. E se eles "acidentalmente" atingirem um objeto, mesmo que você use alguma função assustadora do sistema operacional (IsValidPtr ou qualquer outra), você ainda escorregará para um bug e não falhará rapidamente.

Comece a usar ponteiros nulos para sinalizar esse tipo de coisa e diga ao usuário de sua biblioteca que ele não deve usar ponteiros se eles tendem a passar ponteiros inválidos acidentalmente, sério :)

Johannes Schaub - litb
fonte
Esta é provavelmente a resposta certa, mas acho que uma função simples que verifica as localizações de memória hexspeak comuns seria útil para depuração geral ... No momento eu tenho um ponteiro que às vezes aponta para 0xfeeefeee e se eu tivesse uma função simples, eu poderia use para apimentar afirmações ao redor Isso tornaria muito mais fácil encontrar o culpado ... EDITAR: Embora não seja difícil escrever um para você, eu acho ..
quant
@quant o problema é que algum código C e C ++ poderia fazer aritmética de ponteiro em um endereço inválido sem verificação (com base no princípio garbage-in, garbage-out) e, assim, passar um ponteiro "aritmeticamente modificado" de um desses também - endereços inválidos conhecidos. Casos comuns são a procura de um método de uma vtable inexistente com base em um endereço de objeto inválido ou de um tipo incorreto, ou simplesmente lendo campos de um ponteiro para uma estrutura que não aponta para um.
rwong
Isso basicamente significa que você só pode obter índices de array do mundo externo. Uma API que deve se defender do chamador simplesmente não pode ter ponteiros na interface. No entanto, ainda seria bom ter macros para usar em asserções sobre a validade de ponteiros (que você deve ter internamente). Se é garantido que um ponteiro aponta para dentro de uma matriz cujo ponto inicial e comprimento são conhecidos, isso pode ser verificado explicitamente. É melhor morrer de uma violação de declaração (erro documentado) do que um deref (erro não documentado).
Rob
34

Aqui estão três maneiras fáceis para um programa C no Linux se tornar introspectivo sobre o status da memória na qual está sendo executado e por que a pergunta tem respostas sofisticadas apropriadas em alguns contextos.

  1. Depois de chamar getpagesize () e arredondar o ponteiro para um limite de página, você pode chamar mincore () para descobrir se uma página é válida e se ela faz parte do conjunto de trabalho do processo. Observe que isso requer alguns recursos do kernel, então você deve compará-lo e determinar se chamar esta função é realmente apropriado em sua API. Se sua api for manipular interrupções ou ler de portas seriais na memória, é apropriado chamar isso para evitar comportamentos imprevisíveis.
  2. Depois de chamar stat () para determinar se há um diretório / proc / self disponível, você pode abrir e ler / proc / self / maps para encontrar informações sobre a região na qual um ponteiro reside. Estude a página man do proc, o sistema de pseudo-arquivos de informações do processo. Obviamente, isso é relativamente caro, mas você pode conseguir se safar com o cache do resultado da análise em um array que possa pesquisar com eficiência usando uma pesquisa binária. Considere também / proc / self / smaps. Se sua api for para computação de alto desempenho, o programa irá querer saber sobre o / proc / self / numa que está documentado na página de manual para numa, a arquitetura de memória não uniforme.
  3. A chamada get_mempolicy (MPOL_F_ADDR) é apropriada para trabalho de API de computação de alto desempenho, onde há vários threads de execução e você está gerenciando seu trabalho para ter afinidade com memória não uniforme no que se refere aos núcleos de cpu e recursos de soquete. Obviamente, essa API também informará se um ponteiro é válido.

No Microsoft Windows, há a função QueryWorkingSetEx documentada na API de status do processo (também na API NUMA). Como corolário da programação sofisticada da API NUMA, essa função também permitirá que você faça um simples trabalho de "teste de validade (C / C ++)", como tal, é improvável que seja descontinuado por pelo menos 15 anos.

George Carrette
fonte
12
Primeira resposta que não tenta ser moral sobre a pergunta em si e, na verdade, a responde perfeitamente. As pessoas às vezes não percebem que é realmente necessário este tipo de abordagem de depuração para encontrar bugs em, por exemplo, bibliotecas de terceiros ou em código legado, porque até mesmo o valgrind só encontra ponteiros selvagens quando realmente os acessa, não por exemplo, se você quiser verificar regularmente ponteiros para validade em uma tabela de cache que foi sobrescrita de algum outro lugar em seu código ...
lumpidu
Esta deve ser a resposta aceita. Eu fiz isso de forma semelhante em uma plataforma não-linux. Fundamentalmente, é expor as informações do processo ao próprio processo. Com este aspecto, parece que o Windows faz um trabalho melhor do que o Linux, expondo as informações mais significativas por meio da API de status do processo.
minghua
31

Prevenir um travamento causado pelo chamador enviando um ponteiro inválido é uma boa maneira de criar bugs silenciosos que são difíceis de encontrar.

Não é melhor para o programador que usa sua API obter uma mensagem clara de que seu código é falso, travando-o em vez de ocultá-lo?

Nailer
fonte
8
Em alguns casos, porém, verificar se há um ponteiro inválido imediatamente quando a API é chamada é a causa da falha precoce. Por exemplo, o que aconteceria se a API armazenasse o ponteiro em uma estrutura de dados onde ele só seria adiado posteriormente? Então, passar um ponteiro inválido para a API causará um travamento em algum ponto posterior aleatório. Nesse caso, seria melhor falhar mais cedo, na chamada da API em que o valor inválido foi originalmente introduzido.
peterflynn
28

No Win32 / 64, existe uma maneira de fazer isso. Tente ler o ponteiro e capturar a execução SEH resultante que será lançada em caso de falha. Se não lançar, é um ponteiro válido.

O problema com esse método é que ele apenas retorna se você pode ou não ler os dados do ponteiro. Não oferece nenhuma garantia sobre a segurança de tipo ou qualquer número de outras invariantes. Em geral, esse método serve apenas para dizer "sim, posso ler aquele lugar específico na memória em um momento que já passou".

Resumindo, não faça isso;)

Raymond Chen tem uma postagem no blog sobre este assunto: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx

JaredPar
fonte
3
@Tim, não há como fazer isso em C ++.
JaredPar de
6
É apenas a "resposta certa" se você definir "ponteiro válido" como "não causa uma violação de acesso / segfault". Eu prefiro defini-lo como "pontos para dados significativos alocados para o propósito que você vai usar". Eu diria que essa é uma definição melhor de validade de ponteiro ...;)
jalf
Mesmo se o ponteiro for válido, não pode ser verificado desta forma. Pense em thread1 () {.. if (IsValidPtr (p)) * p = 7; ...} thread2 () {sleep (1); delete p; ...}
Christopher de
2
@Christopher, é verdade. Eu deveria ter dito "Posso ler aquele lugar específico na memória em um momento que já passou"
JaredPar
@JaredPar: Sugestão muito ruim. Pode acionar uma página de proteção, para que a pilha não seja expandida posteriormente ou algo igualmente bom.
Deduplicator
16

AFAIK não há como. Você deve tentar evitar essa situação, sempre definindo os ponteiros para NULL após liberar memória.

Ferdinand Beyer
fonte
4
Definir um ponteiro como nulo não oferece nada, exceto talvez uma falsa sensação de segurança.
Isso não é verdade. Especialmente em C ++, você pode determinar se deseja excluir objetos de membro verificando se há nulo. Observe também que, em C ++, é válido excluir ponteiros nulos, portanto, excluir incondicionalmente objetos em destruidores é popular.
Ferdinand Beyer
4
int * p = novo int (0); int * p2 = p; delete p; p = NULL; delete p2; // falha
1
zabzonk e ?? o que ele disse é que você pode deletar um ponteiro nulo. p2 não é um ponteiro nulo, mas é um ponteiro inválido. você deve defini-lo como nulo antes.
Johannes Schaub - litb
2
Se você tiver apelidos para a memória apontada, apenas um deles será definido como NULL, outros apelidos estão pendurados ao redor.
jdehaan
7

Dê uma olhada nesta e nesta questão. Também dê uma olhada nas dicas inteligentes .

Tunnuz
fonte
7

Com relação à resposta um pouco acima neste tópico:

IsBadReadPtr (), IsBadWritePtr (), IsBadCodePtr (), IsBadStringPtr () para Windows.

Meu conselho é ficar longe deles, alguém já postou este aqui: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx

Outra postagem sobre o mesmo tópico e do mesmo autor (eu acho) é esta: http://blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx ("IsBadXxxPtr deveria realmente ser chamado CrashProgramRandomly ").

Se os usuários de sua API enviarem dados inválidos, deixe-o travar. Se o problema for que os dados transmitidos não são usados ​​até mais tarde (e isso torna mais difícil encontrar a causa), adicione um modo de depuração em que as strings etc. são registradas na entrada. Se forem ruins, será óbvio (e provavelmente falhará). Se estiver acontecendo com frequência, pode valer a pena tirar a API do processo e deixá-los travar o processo da API em vez do processo principal.

Fredrik
fonte
Provavelmente outra maneira é usar _CrtIsValidHeapPointer . Esta função retornará TRUE se o ponteiro for válido e lançará uma exceção quando o ponteiro for liberado. Conforme documentado, esta função está disponível apenas no CRT de depuração.
Crend King
6

Em primeiro lugar, não vejo sentido em tentar se proteger do chamador que tenta causar uma falha deliberadamente. Eles poderiam fazer isso facilmente tentando acessar por meio de um ponteiro inválido. Existem muitas outras maneiras - elas podem apenas sobrescrever sua memória ou a pilha. Se você precisa se proteger contra esse tipo de coisa, você precisa estar executando em um processo separado usando soquetes ou algum outro IPC para comunicação.

Escrevemos muitos softwares que permitem que parceiros / clientes / usuários estendam a funcionalidade. Inevitavelmente, qualquer bug é relatado para nós primeiro, por isso é útil ser capaz de mostrar facilmente que o problema está no código do plug-in. Além disso, existem questões de segurança e alguns usuários são mais confiáveis ​​do que outros.

Usamos vários métodos diferentes, dependendo dos requisitos de desempenho / taxa de transferência e confiabilidade. Dos mais preferidos:

  • processos separados usando soquetes (geralmente passando dados como texto).

  • processos separados usando memória compartilhada (se grandes quantidades de dados passarem).

  • o mesmo processo separa threads por meio da fila de mensagens (se houver mensagens curtas frequentes).

  • mesmo processo, encadeamentos separados, todos os dados transmitidos alocados de um pool de memória.

  • mesmo processo via chamada de procedimento direta - todos os dados passados ​​alocados de um pool de memória.

Tentamos nunca recorrer ao que você está tentando fazer ao lidar com software de terceiros - especialmente quando recebemos os plug-ins / biblioteca como binários em vez de código-fonte.

O uso de um pool de memória é bastante fácil na maioria das circunstâncias e não precisa ser ineficiente. Se VOCÊ alocar os dados em primeiro lugar, será trivial verificar os ponteiros com os valores alocados. Você também pode armazenar o comprimento alocado e adicionar valores "mágicos" antes e depois dos dados para verificar o tipo de dados válido e saturações de dados.

Vareta de medição
fonte
4

Tenho muita simpatia por sua pergunta, pois também estou em uma posição quase idêntica. Eu aprecio o que muitas das respostas estão dizendo, e elas estão corretas - a rotina que fornece o ponteiro deve fornecer um ponteiro válido. No meu caso, é quase inconcebível que eles pudessem ter corrompido o ponteiro - mas se eles tivessem conseguido, seria o MEU software que travaria e eu que ficaria com a culpa :-(

Minha exigência não é que eu continue após uma falha de segmentação - isso seria perigoso - eu só quero relatar o que aconteceu ao cliente antes de encerrar, para que ele possa corrigir o código em vez de me culpar!

É assim que descobri fazer isso (no Windows): http://www.cplusplus.com/reference/clibrary/csignal/signal/

Para dar uma sinopse:

#include <signal.h>

using namespace std;

void terminate(int param)
/// Function executed if a segmentation fault is encountered during the cast to an instance.
{
  cerr << "\nThe function received a corrupted reference - please check the user-supplied  dll.\n";
  cerr << "Terminating program...\n";
  exit(1);
}

...
void MyFunction()
{
    void (*previous_sigsegv_function)(int);
    previous_sigsegv_function = signal(SIGSEGV, terminate);

    <-- insert risky stuff here -->

    signal(SIGSEGV, previous_sigsegv_function);
}

Agora, isso parece se comportar como eu esperava (ele imprime a mensagem de erro e, em seguida, encerra o programa) - mas se alguém detectar uma falha, por favor me avise!

Mike Sadler
fonte
Não use exit(), ele contorna RAII e, portanto, pode causar vazamento de recursos.
Sebastian Mach
Interessante - há outra maneira de encerrar perfeitamente nessa situação? E a instrução de saída é o único problema em fazer isso assim? Percebi que adquiri um "-1" - isso é apenas por causa da 'saída'?
Mike Sadler
Ops, percebo que esta é uma situação bastante excepcional. Acabei de ver exit()e meu alarme portátil C ++ começou a tocar. Deve estar tudo bem nesta situação específica do Linux, onde seu programa sairia de qualquer maneira, desculpe pelo barulho.
Sebastian Mach
1
o sinal (2) não é portátil. Use sigaction (2). man 2 signalno Linux tem um parágrafo explicando o porquê.
rptb1,
1
Nessa situação, eu normalmente chamaria abort (3) em vez de exit (3) porque é mais provável que produza algum tipo de backtrace de depuração que você pode usar para diagnosticar o problema post-mortem. Na maioria dos Unixen, abort (3) irá despejar o core (se core dumps forem permitidos) e no Windows irá oferecer para iniciar um depurador se instalado.
rptb1,
4

No Unix, você deve ser capaz de utilizar um syscall do kernel que verifica o ponteiro e retorna EFAULT, como:

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdbool.h>

bool isPointerBad( void * p )
{
   int fh = open( p, 0, 0 );
   int e = errno;

   if ( -1 == fh && e == EFAULT )
   {
      printf( "bad pointer: %p\n", p );
      return true;
   }
   else if ( fh != -1 )
   {
      close( fh );
   }

   printf( "good pointer: %p\n", p );
   return false;
}

int main()
{
   int good = 4;
   isPointerBad( (void *)3 );
   isPointerBad( &good );
   isPointerBad( "/tmp/blah" );

   return 0;
}

retornando:

bad pointer: 0x3
good pointer: 0x7fff375fd49c
good pointer: 0x400793

Provavelmente existe um syscall melhor para usar do que open () [talvez access], uma vez que há uma chance de que isso leve ao codepath de criação de arquivo real e a um subsequente requisito de fechamento.

Peeter Joot
fonte
Este é um hack brilhante. Eu adoraria ver conselhos sobre diferentes syscalls para validar intervalos de memória, especialmente se eles não tiverem efeitos colaterais. Você pode manter um descritor de arquivo aberto para gravar em / dev / null para testar se os buffers estão na memória legível, mas provavelmente existem soluções mais simples. O melhor que posso encontrar é o link simbólico (ptr, "") que definirá errno como 14 em um endereço incorreto ou 2 em um endereço válido, mas as alterações do kernel podem mudar a ordem de verificação.
Preston
@Preston No DB2, acho que costumávamos usar access () de unistd.h. Usei open () acima porque é um pouco menos obscuro, mas você provavelmente está certo ao dizer que existem várias syscalls possíveis para usar. O Windows costumava ter uma API de verificação de ponteiro explícita, mas acabou não sendo thread-safe (acho que ele usou SEH para tentar escrever e restaurar os limites do intervalo de memória.)
Peeter Joot
2

Não há provisões em C ++ para testar a validade de um ponteiro como um caso geral. Obviamente, pode-se presumir que NULL (0x00000000) é ruim, e vários compiladores e bibliotecas gostam de usar "valores especiais" aqui e ali para tornar a depuração mais fácil (por exemplo, se eu vir um ponteiro aparecer como 0xCECECECE no visual studio, eu sei Eu fiz algo errado), mas a verdade é que, como um ponteiro é apenas um índice na memória, é quase impossível dizer apenas olhando para o ponteiro se é o índice "certo".

Existem vários truques que você pode fazer com dynamic_cast e RTTI para garantir que o objeto apontado seja do tipo que você deseja, mas todos eles requerem que você esteja apontando para algo válido em primeiro lugar.

Se você quiser garantir que seu programa possa detectar ponteiros "inválidos", meu conselho é este: Defina cada ponteiro que você declara como NULL ou um endereço válido imediatamente após a criação e defina-o como NULL imediatamente após liberar a memória para a qual ele aponta. Se você for diligente quanto a essa prática, verificar se há NULL é tudo de que você precisa.

Toji
fonte
Uma constante de ponteiro nulo em C ++ (ou C, nesse caso), é representada por um zero integral constante. Muitas implementações usam zeros totalmente binários para representá-lo, mas não é algo com o qual contar.
David Thornley
2

Não existe uma maneira portátil de fazer isso, e fazer isso para plataformas específicas pode ser algo entre difícil e impossível. Em qualquer caso, você nunca deve escrever código que dependa dessa verificação - não deixe que os ponteiros assumam valores inválidos em primeiro lugar.


fonte
2

Definir o ponteiro como NULL antes e depois de usar é uma boa técnica. Isso é fácil de fazer em C ++ se você gerencia ponteiros dentro de uma classe, por exemplo (uma string):

class SomeClass
{
public:
    SomeClass();
    ~SomeClass();

    void SetText( const char *text);
    char *GetText() const { return MyText; }
    void Clear();

private:
    char * MyText;
};


SomeClass::SomeClass()
{
    MyText = NULL;
}


SomeClass::~SomeClass()
{
    Clear();
}

void SomeClass::Clear()
{
    if (MyText)
        free( MyText);

    MyText = NULL;
}



void SomeClass::Settext( const char *text)
{
    Clear();

    MyText = malloc( strlen(text));

    if (MyText)
        strcpy( MyText, text);
}
Tim Ring
fonte
A pergunta atualizada torna minha resposta errada, é claro (ou pelo menos uma resposta para outra pergunta). Eu concordo com as respostas que basicamente dizem: deixe-os travar se abusarem da API. Você não pode impedir as pessoas de se baterem no polegar com um martelo ...
Tim Ring
2

Não é uma política muito boa aceitar ponteiros arbitrários como parâmetros de entrada em uma API pública. É melhor ter tipos de "dados simples" como um inteiro, uma string ou uma estrutura (quero dizer uma estrutura clássica com dados simples dentro, é claro; oficialmente, qualquer coisa pode ser uma estrutura).

Por quê? Bem, porque, como outros dizem, não existe uma maneira padrão de saber se você recebeu um indicador válido ou que aponta para o lixo.

Mas às vezes você não tem escolha - sua API deve aceitar um ponteiro.

Nestes casos, é dever do chamador passar um bom ponteiro. NULL pode ser aceito como um valor, mas não um ponteiro para lixo.

Você pode verificar novamente de alguma forma? Bem, o que eu fiz em um caso como esse foi definir um invariante para o tipo para o qual o ponteiro aponta e chamá-lo quando você o obtiver (no modo de depuração). Pelo menos se o invariante falhar (ou travar), você sabe que recebeu um valor incorreto.

// API that does not allow NULL
void PublicApiFunction1(Person* in_person)
{
  assert(in_person != NULL);
  assert(in_person->Invariant());

  // Actual code...
}

// API that allows NULL
void PublicApiFunction2(Person* in_person)
{
  assert(in_person == NULL || in_person->Invariant());

  // Actual code (must keep in mind that in_person may be NULL)
}
Daniel Daranas
fonte
re: "passar um tipo de dados simples ... como uma string" Mas em C ++ strings são mais frequentemente passadas como ponteiros para caracteres, (char *) ou (const char *), então você está de volta aos ponteiros de passagem. E seu exemplo passa in_person como uma referência, não como um ponteiro, então a comparação (in_person! = NULL) implica que há algumas comparações de objeto / ponteiro definidas na classe Person.
Jesse Chisholm
@JesseChisholm Por string eu quis dizer uma string, ou seja, um std :: string. De forma alguma estou recomendando usar char * como uma forma de armazenar strings ou distribuí-las. Não faça isso.
Daniel Daranas
@JesseChisholm Por algum motivo, cometi um erro ao responder a essa pergunta há cinco anos. Claramente, não faz sentido verificar se Person & é NULL. Isso nem mesmo compilaria. Eu queria usar ponteiros, não referências. Eu consertei agora.
Daniel Daranas
1

Como já foi dito, você não pode detectar com segurança um ponteiro inválido. Considere algumas das formas que um ponteiro inválido pode assumir:

Você pode ter um ponteiro nulo. Isso você pode facilmente verificar e fazer algo a respeito.

Você pode ter um ponteiro para algum lugar fora da memória válida. O que constitui a memória válida varia dependendo de como o ambiente de tempo de execução do sistema configura o espaço de endereço. Em sistemas Unix, geralmente é um espaço de endereço virtual começando em 0 e indo para um grande número de megabytes. Em sistemas embarcados, pode ser bem pequeno. Pode não começar em 0, em qualquer caso. Se o seu aplicativo estiver sendo executado no modo de supervisor ou equivalente, seu ponteiro pode fazer referência a um endereço real, que pode ou não ter backup com memória real.

Você poderia ter um ponteiro para algum lugar dentro de sua memória válida, mesmo dentro de seu segmento de dados, bss, pilha ou heap, mas não apontando para um objeto válido. Uma variante disso é um ponteiro que costumava apontar para um objeto válido, antes que algo de ruim acontecesse ao objeto. Coisas ruins neste contexto incluem desalocação, corrupção de memória ou corrupção de ponteiro.

Você poderia ter um ponteiro totalmente ilegal, como um ponteiro com alinhamento ilegal para o que está sendo referenciado.

O problema fica ainda pior quando você considera arquiteturas baseadas em segmento / deslocamento e outras implementações de ponteiro ímpar. Esse tipo de coisa normalmente é escondido do desenvolvedor por bons compiladores e uso criterioso de tipos, mas se você quiser romper o véu e tentar ser mais esperto do que os desenvolvedores de sistema operacional e compilador, você pode, mas não há uma maneira genérica para fazer isso irá lidar com todos os problemas que você pode encontrar.

A melhor coisa que você pode fazer é permitir o travamento e divulgar algumas informações de diagnóstico de qualidade.

John Grieggs
fonte
re: "divulgar algumas boas informações de diagnóstico", aí está o problema. Como você não pode verificar a validade do ponteiro, as informações sobre as quais precisa se preocupar são mínimas. "Uma exceção aconteceu aqui", pode ser tudo o que você terá. Toda a pilha de chamadas é boa, mas requer uma estrutura melhor do que a maioria das bibliotecas de tempo de execução C ++ fornecem.
Jesse Chisholm
1

Em geral, é impossível fazer. Aqui está um caso particularmente desagradável:

struct Point2d {
    int x;
    int y;
};

struct Point3d {
    int x;
    int y;
    int z;
};

void dump(Point3 *p)
{
    printf("[%d %d %d]\n", p->x, p->y, p->z);
}

Point2d points[2] = { {0, 1}, {2, 3} };
Point3d *p3 = reinterpret_cast<Point3d *>(&points[0]);
dump(p3);

Em muitas plataformas, isso imprimirá:

[0 1 2]

Você está forçando o sistema de tempo de execução a interpretar incorretamente os bits de memória, mas, neste caso, não vai travar, porque todos os bits fazem sentido. Esta é parte do projeto da linguagem (olhada polimorfismo C-estilo com struct inaddr, inaddr_in, inaddr_in6), então você não pode proteger de forma confiável contra ela em qualquer plataforma.

Tom
fonte
1

É inacreditável a quantidade de informações enganosas que você pode ler nos artigos acima ...

E mesmo na documentação do microsoft msdn, o IsBadPtr foi banido. Muito bem - eu prefiro o aplicativo funcional em vez de travar. Mesmo que o prazo de trabalho possa estar funcionando incorretamente (desde que o usuário final possa continuar com o aplicativo).

Ao pesquisar no Google, não encontrei nenhum exemplo útil para janelas - encontrei uma solução para aplicativos de 32 bits,

http://www.codeproject.com/script/Content/ViewAssociatedFile.aspx?rzp=%2FKB%2Fsystem%2Fdetect-driver%2F%2FDetectDriverSrc.zip&zep=DetectDriverSrc%2FDetectDriver=%2Fsrrcppp&robtid.cc%2Fsrrcpp2FDetectDriver=%2Fsrttppp2Fdetc_pps2Fdetc%2Fdetc%2Fsrpps2Fdetc%2Fsrtt2Fdetc_2 = 2

mas também preciso oferecer suporte a aplicativos de 64 bits, portanto, esta solução não funcionou para mim.

Mas eu colhi os códigos-fonte do wine e consegui preparar um tipo de código semelhante que funcionaria para aplicativos de 64 bits também - anexando o código aqui:

#include <typeinfo.h>   

typedef void (*v_table_ptr)();   

typedef struct _cpp_object   
{   
    v_table_ptr*    vtable;   
} cpp_object;   



#ifndef _WIN64
typedef struct _rtti_object_locator
{
    unsigned int signature;
    int base_class_offset;
    unsigned int flags;
    const type_info *type_descriptor;
    //const rtti_object_hierarchy *type_hierarchy;
} rtti_object_locator;
#else

typedef struct
{
    unsigned int signature;
    int base_class_offset;
    unsigned int flags;
    unsigned int type_descriptor;
    unsigned int type_hierarchy;
    unsigned int object_locator;
} rtti_object_locator;  

#endif

/* Get type info from an object (internal) */  
static const rtti_object_locator* RTTI_GetObjectLocator(void* inptr)  
{   
    cpp_object* cppobj = (cpp_object*) inptr;  
    const rtti_object_locator* obj_locator = 0;   

    if (!IsBadReadPtr(cppobj, sizeof(void*)) &&   
        !IsBadReadPtr(cppobj->vtable - 1, sizeof(void*)) &&   
        !IsBadReadPtr((void*)cppobj->vtable[-1], sizeof(rtti_object_locator)))  
    {  
        obj_locator = (rtti_object_locator*) cppobj->vtable[-1];  
    }  

    return obj_locator;  
}  

E o código a seguir pode detectar se o ponteiro é válido ou não, você provavelmente precisará adicionar alguma verificação NULL:

    CTest* t = new CTest();
    //t = (CTest*) 0;
    //t = (CTest*) 0x12345678;

    const rtti_object_locator* ptr = RTTI_GetObjectLocator(t);  

#ifdef _WIN64
    char *base = ptr->signature == 0 ? (char*)RtlPcToFileHeader((void*)ptr, (void**)&base) : (char*)ptr - ptr->object_locator;
    const type_info *td = (const type_info*)(base + ptr->type_descriptor);
#else
    const type_info *td = ptr->type_descriptor;
#endif
    const char* n =td->name();

Isso obtém o nome da classe do ponteiro - acho que deve ser o suficiente para as suas necessidades.

Uma coisa que ainda temo é o desempenho da verificação de ponteiro - no fragmento de código acima, já existem 3-4 chamadas de API sendo feitas - pode ser um exagero para aplicativos de tempo crítico.

Seria bom se alguém pudesse medir a sobrecarga da verificação de ponteiro em comparação, por exemplo, com chamadas C # / c ++ gerenciado.

TarmoPikaro
fonte
1

De fato, algo poderia ser feito em uma ocasião específica: por exemplo, se você deseja verificar se uma string de ponteiro de string é válida, usando write (fd, buf, szie) syscall pode ajudá-lo a fazer a mágica: deixe fd ser um descritor de arquivo temporário arquivo que você cria para teste, e buf apontando para a string que você está testando, se o ponteiro for inválido, write () retornaria -1 e errno definido como EFAULT, indicando que buf está fora de seu espaço de endereço acessível.

Wenlin.Wu
fonte
1

A seguir funciona no Windows (alguém sugeriu antes):

 static void copy(void * target, const void* source, int size)
 {
     __try
     {
         CopyMemory(target, source, size);
     }
     __except(EXCEPTION_EXECUTE_HANDLER)
     {
         doSomething(--whatever--);
     }
 }

A função deve ser um método estático, autônomo ou estático de alguma classe. Para testar em somente leitura, copie os dados no buffer local. Para testar a escrita sem modificar o conteúdo, escreva-os. Você pode testar apenas o primeiro / último endereço. Se o ponteiro for inválido, o controle será passado para 'doSomething' e, em seguida, fora dos colchetes. Só não use nada que exija destruidores, como o CString.

Andrei Kalantarian
fonte
1

No Windows, uso este código:

void * G_pPointer = NULL;
const char * G_szPointerName = NULL;
void CheckPointerIternal()
{
    char cTest = *((char *)G_pPointer);
}
bool CheckPointerIternalExt()
{
    bool bRet = false;

    __try
    {
        CheckPointerIternal();
        bRet = true;
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
    }

    return  bRet;
}
void CheckPointer(void * A_pPointer, const char * A_szPointerName)
{
    G_pPointer = A_pPointer;
    G_szPointerName = A_szPointerName;
    if (!CheckPointerIternalExt())
        throw std::runtime_error("Invalid pointer " + std::string(G_szPointerName) + "!");
}

Uso:

unsigned long * pTest = (unsigned long *) 0x12345;
CheckPointer(pTest, "pTest"); //throws exception
Artkov
fonte
0

IsBadReadPtr (), IsBadWritePtr (), IsBadCodePtr (), IsBadStringPtr () para Windows.
Isso leva tempo proporcional ao comprimento do bloco, portanto, para verificação de integridade, apenas verifico o endereço inicial.

pngaz
fonte
3
você deve evitar esses métodos porque eles não funcionam. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx
JaredPar
Às vezes, pode ser uma
solução alternativa
0

Já vi várias bibliotecas usarem algum método para verificar a existência de memória não referenciada e assim por diante. Eu acredito que eles simplesmente "substituem" os métodos de alocação e desalocação de memória (malloc / free), que tem alguma lógica que mantém o controle dos ponteiros. Suponho que isso seja um exagero para o seu caso de uso, mas seria uma maneira de fazer isso.

sebnow
fonte
Isso não ajuda para objetos alocados na pilha, infelizmente.
Tom
0

Tecnicamente, você pode substituir o operador novo (e excluir ) e coletar informações sobre toda a memória alocada, para que possa ter um método para verificar se a memória heap é válida. mas:

  1. você ainda precisa de uma maneira de verificar se o ponteiro está alocado na pilha ()

  2. você precisará definir o que é um ponteiro 'válido':

a) a memória nesse endereço é alocada

b) a memória nesse endereço é o endereço inicial do objeto (por exemplo, o endereço não está no meio de uma grande matriz)

c) a memória nesse endereço é o endereço inicial do objeto do tipo esperado

Resumindo : a abordagem em questão não é a maneira C ++, você precisa definir algumas regras que garantam que a função receba ponteiros válidos.


fonte
0

Adendo à (s) resposta (s) aceita (s):

Suponha que seu ponteiro pode conter apenas três valores - 0, 1 e -1 onde 1 significa um ponteiro válido, -1 um inválido e 0 outro inválido. Qual é a probabilidade de seu ponteiro ser NULL, sendo todos os valores igualmente prováveis? 1/3. Agora, retire o caso válido, portanto, para cada caso inválido, você terá uma proporção de 50:50 para capturar todos os erros. Parece bom certo? Dimensione isso para um ponteiro de 4 bytes. Existem 2 ^ 32 ou 4294967294 valores possíveis. Destes, apenas UM valor está correto, um é NULL e você ainda fica com 4294967292 outros casos inválidos. Recalcular: você tem um teste para 1 de (4294967292+ 1) casos inválidos. Uma probabilidade de 2.xe-10 ou 0 para a maioria dos propósitos práticos. Essa é a futilidade da verificação NULL.

diretamente
fonte
0

Você sabe, um novo driver (pelo menos no Linux) que é capaz disso provavelmente não seria tão difícil de escrever.

Por outro lado, seria tolice construir seus programas dessa forma. A menos que você tenha algum uso realmente específico e único para tal coisa, eu não o recomendaria. Se você construir um grande aplicativo carregado com verificações constantes de validade de ponteiro, provavelmente será terrivelmente lento.

dicroce
fonte
0

você deve evitar esses métodos porque eles não funcionam. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx - JaredPar 15 de fevereiro '09 às 16:02

Se eles não funcionarem - a próxima atualização do Windows irá consertar? Se eles não funcionarem no nível de conceito - a função provavelmente será removida completamente da API do Windows.

A documentação do MSDN afirma que eles estão proibidos, e a razão para isso é provavelmente uma falha de design posterior do aplicativo (por exemplo, geralmente você não deve comer ponteiros inválidos silenciosamente - se você for responsável pelo design de todo o aplicativo, é claro) e desempenho / tempo de verificação de ponteiro.

Mas você não deve alegar que eles não funcionam por causa de algum blog. Em meu aplicativo de teste, verifiquei se eles funcionam.

TarmoPikaro
fonte