Prueba de punteros para la validez (C / C ++)

91

¿Hay alguna forma de determinar (programáticamente, por supuesto) si un puntero dado es "válido"? Verificar NULL es fácil, pero ¿qué pasa con cosas como 0x00001234? Cuando se intenta eliminar la referencia a este tipo de puntero, se produce una excepción / bloqueo.

Se prefiere un método multiplataforma, pero la plataforma específica (para Windows y Linux) también está bien.

Actualización para aclarar: el problema no es con punteros obsoletos / liberados / no inicializados; en su lugar, estoy implementando una API que toma punteros de la persona que llama (como un puntero a una cadena, un identificador de archivo, etc.). La persona que llama puede enviar (a propósito o por error) un valor no válido como puntero. ¿Cómo evito un accidente?

noamtm
fuente
Consulte también stackoverflow.com/questions/496034/…
ChrisW
Creo que la mejor respuesta positiva para Linux la da George Carrette. Si eso no es suficiente, considere construir la tabla de símbolos de función en la biblioteca, o incluso otro nivel de tabla de bibliotecas disponibles con sus propias tablas de funciones. Luego verifique esas tablas exactas. Por supuesto, esas respuestas negativas también son correctas: no puede estar 100% seguro de si un puntero de función es válido o no a menos que ponga muchas restricciones adicionales a la aplicación de usuario.
minghua
¿Especifica realmente la especificación de API tal obligación a cumplir por la implementación? Por cierto, pretendo no haber asumido que eres tanto el desarrollador como el diseñador. Mi punto es que no creo que una API especifique algo como "En caso de que se pase un puntero no válido como argumento, la función debe manejar el problema y devolver NULL". Una API asume la obligación de proporcionar un servicio en las condiciones de uso adecuadas, no mediante piratería. Sin embargo, no hace ningún daño ser un poco estúpido. El uso de una referencia hace que estos casos causen menos estragos. :)
Poniros

Respuestas:

75

Actualización para aclarar: el problema no es con punteros obsoletos, liberados o no inicializados; en su lugar, estoy implementando una API que toma punteros de la persona que llama (como un puntero a una cadena, un identificador de archivo, etc.). La persona que llama puede enviar (a propósito o por error) un valor no válido como puntero. ¿Cómo evito un accidente?

No puedes hacer ese cheque. Simplemente no hay forma de comprobar si un puntero es "válido". Tienes que confiar en que cuando las personas usan una función que toma un puntero, esas personas saben lo que están haciendo. Si le pasan 0x4211 como valor de puntero, entonces debe confiar en que apunta a la dirección 0x4211. Y si chocan "accidentalmente" con un objeto, incluso si usaras alguna función aterradora del sistema operativo (IsValidPtr o lo que sea), seguirías cometiendo un error y no fallarías rápidamente.

Comience a usar punteros nulos para señalar este tipo de cosas y dígale al usuario de su biblioteca que no deben usar punteros si tienden a pasar accidentalmente punteros inválidos, en serio :)

Johannes Schaub - litb
fuente
Esta es probablemente la respuesta correcta, pero creo que una función simple que verifique las ubicaciones comunes de memoria hexadecimal sería útil para la depuración general ... Ahora mismo tengo un puntero que a veces apunta a 0xfeeefeee y si tuviera una función simple, podría use to pepper afirma alrededor Sería mucho más fácil encontrar al culpable ... EDITAR: Aunque no sería difícil escribir uno usted mismo, supongo ..
Quant
@quant, el problema es que algunos códigos de C y C ++ podrían hacer aritmética de punteros en una dirección no válida sin verificar (según el principio de entrada y salida de basura) y, por lo tanto, pasará un puntero "modificado aritméticamente" de uno de esos pozos -direcciones no válidas conocidas. Los casos comunes son buscar un método de un vtable inexistente basado en una dirección de objeto no válida o uno de un tipo incorrecto, o simplemente leer campos de un puntero a una estructura que no apunta a uno.
rwong
Básicamente, esto significa que solo puede tomar índices de matriz del mundo exterior. Una API que debe defenderse de la persona que llama simplemente no puede tener punteros en la interfaz. Sin embargo, aún sería bueno tener macros para usar en afirmaciones sobre la validez de los punteros (que seguramente tendrá internamente). Si se garantiza que un puntero apunte dentro de una matriz cuyo punto de inicio y longitud se conocen, eso se puede verificar explícitamente. Es mejor morir por una infracción de aserción (error documentado) que por un deref (error no documentado).
Rob
34

Aquí hay tres formas sencillas para que un programa en C bajo Linux se vuelva introspectivo sobre el estado de la memoria en la que se está ejecutando y por qué la pregunta tiene respuestas sofisticadas apropiadas en algunos contextos.

  1. Después de llamar a getpagesize () y redondear el puntero a un límite de página, puede llamar a mincore () para averiguar si una página es válida y si pasa a ser parte del conjunto de trabajo del proceso. Tenga en cuenta que esto requiere algunos recursos del kernel, por lo que debe compararlo y determinar si llamar a esta función es realmente apropiado en su api. Si su api va a manejar interrupciones, o leer desde puertos seriales a la memoria, es apropiado llamar a esto para evitar comportamientos impredecibles.
  2. Después de llamar a stat () para determinar si hay un directorio / proc / self disponible, puede abrir y leer / proc / self / maps para encontrar información sobre la región en la que reside un puntero. Estudie la página de manual de proc, el pseudo sistema de archivos de información de proceso. Obviamente, esto es relativamente caro, pero es posible que pueda salirse con la suya almacenando en caché el resultado del análisis en una matriz que puede buscar de manera eficiente mediante una búsqueda binaria. También considere el / proc / self / smaps. Si su api es para computación de alto rendimiento, entonces el programa querrá saber acerca de / proc / self / numa que está documentado en la página de manual de numa, la arquitectura de memoria no uniforme.
  3. La llamada get_mempolicy (MPOL_F_ADDR) es apropiada para el trabajo de API de computación de alto rendimiento donde hay múltiples subprocesos de ejecución y usted está administrando su trabajo para tener afinidad por la memoria no uniforme en lo que se refiere a los núcleos de la CPU y los recursos del socket. Por supuesto, dicha API también le dirá si un puntero es válido.

En Microsoft Windows existe la función QueryWorkingSetEx que está documentada en la API de estado del proceso (también en la API de NUMA). Como corolario de la programación sofisticada de la API de NUMA, esta función también le permitirá realizar un trabajo simple de "punteros de prueba para la validez (C / C ++)", por lo que es poco probable que esté obsoleto durante al menos 15 años.

George Carrette
fuente
13
Primera respuesta que no trata de ser moral sobre la pregunta en sí y en realidad la responde perfectamente. La gente a veces no se da cuenta de que realmente se necesita este tipo de enfoque de depuración para encontrar errores, por ejemplo, en bibliotecas de terceros o en código heredado porque incluso valgrind solo encuentra punteros salvajes cuando realmente accede a ellos, no, por ejemplo, si desea verificar regularmente la validez de los punteros. en una tabla de caché que se ha sobrescrito desde algún otro lugar en su código ...
lumpidu
Esta debería ser la respuesta aceptada. Lo hice de manera similar en una plataforma que no es Linux. Fundamentalmente, está exponiendo la información del proceso al proceso en sí. Con este aspecto, parece que Windows hace un mejor trabajo que Linux al exponer la información más significativa a través de la API de estado del proceso.
minghua
31

Evitar un bloqueo causado por la persona que llama enviando un puntero no válido es una buena manera de crear errores silenciosos que son difíciles de encontrar.

¿No es mejor para el programador que usa su API obtener un mensaje claro de que su código es falso al bloquearlo en lugar de ocultarlo?

Fabricante de clavos
fuente
8
Sin embargo, en algunos casos, comprobar si hay un puntero incorrecto inmediatamente cuando se llama a la API es la forma en que falla al principio. Por ejemplo, ¿qué pasa si la API almacena el puntero en una estructura de datos de la que solo se deferencia más adelante? Luego, pasar a la API un puntero incorrecto provocará un bloqueo en algún momento posterior aleatorio. En ese caso, sería mejor fallar antes, en la llamada API donde se introdujo originalmente el valor incorrecto.
peterflynn
28

En Win32 / 64 hay una forma de hacer esto. Intente leer el puntero y detectar la excepción SEH resultante que se lanzará en caso de error. Si no lanza, entonces es un puntero válido.

Sin embargo, el problema con este método es que solo devuelve si puede leer o no los datos del puntero. No ofrece ninguna garantía sobre la seguridad del tipo o cualquier otro invariante. En general, este método sirve para poco más que decir "sí, puedo leer ese lugar en particular en la memoria en un momento que ya pasó".

En resumen, no hagas esto;)

Raymond Chen tiene una publicación de blog sobre este tema: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx

JaredPar
fuente
3
@Tim, no hay forma de hacer eso en C ++.
JaredPar
6
Es solo la "respuesta correcta" si define "puntero válido" como "no causa una infracción de acceso / segfault". Preferiría definirlo como "puntos a datos significativos asignados para el propósito que va a utilizar". Yo diría que esa es una mejor definición de validez de puntero ...;)
jalf
Incluso si el puntero es válido, no se puede verificar de esta manera. Piense en thread1 () {.. if (IsValidPtr (p)) * p = 7; ...} hilo2 () {dormir (1); eliminar p; ...}
Christopher
2
@Christopher, muy cierto. Debería haber dicho "Puedo leer ese lugar en particular en la memoria en un momento que ya pasó"
JaredPar
@JaredPar: Una sugerencia realmente mala. Puede activar una página de protección, por lo que la pila no se expandirá más tarde o algo igualmente agradable.
Deduplicador
16

AFAIK no hay manera. Debe intentar evitar esta situación estableciendo siempre los punteros en NULL después de liberar memoria.

Ferdinand Beyer
fuente
4
Establecer un puntero en nulo no le proporciona nada, excepto tal vez una falsa sensación de seguridad.
Eso no es verdad. Especialmente en C ++, puede determinar si desea eliminar los objetos miembros comprobando nulos. También tenga en cuenta que, en C ++, es válido eliminar punteros nulos, por lo que la eliminación incondicional de objetos en destructores es popular.
Ferdinand Beyer
4
int * p = nuevo int (0); int * p2 = p; eliminar p; p = NULO; eliminar p2; // crash
1
zabzonk, y ?? lo que dijo es que puede eliminar un puntero nulo. p2 no es un puntero nulo, pero es un puntero no válido. tienes que establecerlo en nulo antes.
Johannes Schaub - litb
2
Si tiene alias para la memoria apuntada, solo uno de ellos se establecerá en NULL, otros alias están colgando.
jdehaan
7

Eche un vistazo a esta y esta pregunta. También eche un vistazo a los punteros inteligentes .

tunnuz
fuente
7

Con respecto a la respuesta un poco arriba en este hilo:

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

Mi consejo es mantenerse alejado de ellos, alguien ya ha publicado este: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx

Otra publicación sobre el mismo tema y del mismo autor (creo) es esta: http://blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx ("IsBadXxxPtr realmente debería llamarse CrashProgramRandomly ").

Si los usuarios de su API envían datos incorrectos, deje que se bloquee. Si el problema es que los datos pasados ​​no se utilizan hasta más tarde (y eso hace que sea más difícil encontrar la causa), agregue un modo de depuración en el que las cadenas, etc., se registren en la entrada. Si son malos, será obvio (y probablemente fallará). Si está sucediendo con demasiada frecuencia, podría valer la pena sacar su API del proceso y dejar que bloqueen el proceso de la API en lugar del proceso principal.

Fredrik
fuente
Probablemente otra forma es usar _CrtIsValidHeapPointer . Esta función devolverá VERDADERO si el puntero es válido y lanzará una excepción cuando se libere el puntero. Como se documenta, esta función solo está disponible en debug CRT.
Crend King
6

En primer lugar, no veo ningún sentido en tratar de protegerse de la persona que llama que intenta deliberadamente provocar un accidente. Podrían hacer esto fácilmente intentando acceder a través de un puntero inválido ellos mismos. Hay muchas otras formas: podrían simplemente sobrescribir su memoria o la pila. Si necesita protegerse contra este tipo de cosas, debe ejecutar un proceso separado utilizando sockets o algún otro IPC para la comunicación.

Escribimos una gran cantidad de software que permite a los socios / clientes / usuarios ampliar la funcionalidad. Inevitablemente, se nos informa primero de cualquier error, por lo que es útil poder mostrar fácilmente que el problema está en el código del complemento. Además, existen problemas de seguridad y algunos usuarios son más confiables que otros.

Utilizamos varios métodos diferentes según los requisitos de rendimiento / rendimiento y la confiabilidad. De los más preferidos:

  • procesos separados usando sockets (a menudo pasando datos como texto).

  • Procesos separados usando memoria compartida (si se pasan grandes cantidades de datos).

  • El mismo proceso separa hilos a través de la cola de mensajes (si son mensajes cortos frecuentes).

  • El mismo proceso separa los hilos de todos los datos pasados ​​asignados desde un grupo de memoria.

  • mismo proceso a través de una llamada a procedimiento directo: todos los datos pasados ​​asignados desde un grupo de memoria.

Tratamos de no recurrir nunca a lo que está tratando de hacer cuando se trata de software de terceros, especialmente cuando nos dan los complementos / biblioteca como código binario en lugar de código fuente.

El uso de un grupo de memoria es bastante fácil en la mayoría de las circunstancias y no tiene por qué ser ineficaz. Si USTED asigna los datos en primer lugar, entonces es trivial comparar los indicadores con los valores que asignó. También puede almacenar la longitud asignada y agregar valores "mágicos" antes y después de los datos para verificar el tipo de datos válido y los desbordamientos de datos.

Varilla graduada
fuente
5

En Unix, debería poder utilizar una llamada al sistema del kernel que compruebe el puntero y devuelva EFAULT, como por ejemplo:

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

regresando:

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

Probablemente haya una mejor llamada al sistema para usar que open () [quizás acceso], ya que existe la posibilidad de que esto conduzca a una ruta de código de creación de archivos real y un requisito de cierre posterior.

Peeter Joot
fuente
1
Este es un truco brillante. Me encantaría ver consejos sobre diferentes llamadas al sistema para validar los rangos de memoria, especialmente si se puede garantizar que no tengan efectos secundarios. Puede mantener abierto un descriptor de archivo para escribir en / dev / null para probar si los búferes están en la memoria legible, pero probablemente haya soluciones más simples. Lo mejor que puedo encontrar es el enlace simbólico (ptr, "") que establecerá errno en 14 en una dirección incorrecta o 2 en una buena dirección, pero los cambios del kernel podrían cambiar el orden de verificación.
Preston
1
@Preston En DB2, creo que solíamos usar unistd.h's access (). Usé open () arriba porque es un poco menos oscuro, pero probablemente tengas razón en que hay muchas llamadas al sistema posibles para usar. Windows solía tener una API de verificación de puntero explícita, pero resultó que no era seguro para subprocesos (creo que usó SEH para intentar escribir y luego restaurar los límites del rango de memoria).
Peeter Joot
4

Siento mucha simpatía por su pregunta, ya que yo mismo estoy en una posición casi idéntica. Aprecio lo que dicen muchas de las respuestas, y son correctas: la rutina que proporciona el puntero debería proporcionar un puntero válido. En mi caso, es casi inconcebible que pudieran haber corrompido el puntero, pero si lo hubieran logrado, sería MI software el que falla y YO el que tendría la culpa :-(

Mi requisito no es que continúe después de una falla de segmentación, eso sería peligroso, solo quiero informarle al cliente lo que sucedió antes de terminar para que pueda corregir su código en lugar de culparme.

Así es como encontré hacerlo (en Windows): http://www.cplusplus.com/reference/clibrary/csignal/signal/

Para dar una sinopsis:

#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);
}

Ahora parece que esto se comporta como espero (imprime el mensaje de error y luego finaliza el programa), pero si alguien puede detectar una falla, ¡hágamelo saber!

Mike Sadler
fuente
No lo use exit(), elude RAII y por lo tanto puede causar fugas de recursos.
Sebastian Mach
Interesante, ¿hay otra forma de terminar ordenadamente en esta situación? ¿Y es la declaración de salida el único problema al hacerlo así? Me doy cuenta de que he adquirido un "-1", ¿es solo por la 'salida'?
Mike Sadler
Vaya, me doy cuenta de que se trata de una situación bastante excepcional. Acabo de ver exit()y mi campana de alarma portátil C ++ comenzó a sonar. Debería estar bien en esta situación específica de Linux, donde su programa saldría de todos modos, perdón por el ruido.
Sebastian Mach
1
signal (2) no es portátil. Utilice sigaction (2). man 2 signalen Linux tiene un párrafo que explica por qué.
rptb1
1
En esta situación, normalmente llamaría abortar (3) en lugar de salir (3) porque es más probable que produzca algún tipo de seguimiento de depuración que pueda utilizar para diagnosticar el problema post-mortem. En la mayoría de Unixen, abort (3) volcará el núcleo (si se permiten volcados de núcleo) y en Windows ofrecerá iniciar un depurador si está instalado.
rptb1
2

No hay disposiciones en C ++ para probar la validez de un puntero como caso general. Obviamente, se puede suponer que NULL (0x00000000) es malo, y a varios compiladores y bibliotecas les gusta usar "valores especiales" aquí y allá para facilitar la depuración (por ejemplo, si alguna vez veo que un puntero aparece como 0xCECECECE en Visual Studio, sé Hice algo mal) pero la verdad es que dado que un puntero es solo un índice en la memoria, es casi imposible saber con solo mirar el puntero si es el índice "correcto".

Hay varios trucos que puede hacer con dynamic_cast y RTTI para asegurarse de que el objeto al que se apunta sea del tipo que desea, pero todos requieren que apunte a algo válido en primer lugar.

Si desea asegurarse de que su programa puede detectar punteros "no válidos", mi consejo es el siguiente: configure cada puntero que declare en NULL o en una dirección válida inmediatamente después de la creación y configúrelo en NULL inmediatamente después de liberar la memoria a la que apunta. Si es diligente con esta práctica, entonces todo lo que necesita es verificar NULL.

Toji
fuente
Una constante de puntero nulo en C ++ (o C, para el caso), está representada por un cero integral constante. Muchas implementaciones usan ceros binarios para representarlo, pero no es algo con lo que contar.
David Thornley
2

No hay ninguna forma portátil de hacer esto, y hacerlo para plataformas específicas puede ser entre difícil e imposible. En cualquier caso, nunca debe escribir código que dependa de dicha verificación; no permita que los punteros adopten valores no válidos en primer lugar.


fuente
2

Establecer el puntero en NULL antes y después de usarlo es una buena técnica. Esto es fácil de hacer en C ++ si administra punteros dentro de una clase, por ejemplo (una cadena):

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 anillo
fuente
La pregunta actualizada hace que mi respuesta sea incorrecta, por supuesto (o al menos una respuesta a otra pregunta). Estoy de acuerdo con las respuestas que básicamente dicen que se bloqueen si abusan de th api. No se puede evitar que la gente se golpee el pulgar con un martillo ...
Tim Ring
2

No es una buena política aceptar punteros arbitrarios como parámetros de entrada en una API pública. Es mejor tener tipos de "datos sin formato" como un entero, una cadena o una estructura (me refiero a una estructura clásica con datos sin formato dentro, por supuesto; oficialmente, cualquier cosa puede ser una estructura).

¿Por qué? Bueno, porque, como dicen otros, no existe una forma estándar de saber si le han dado un puntero válido o uno que apunta a basura.

Pero a veces no tienes la opción: tu API debe aceptar un puntero.

En estos casos, es deber de la persona que llama pasar un buen puntero. NULL puede aceptarse como valor, pero no como puntero a basura.

¿Puede verificarlo de alguna manera? Bueno, lo que hice en un caso como ese fue definir un invariante para el tipo al que apunta el puntero y llamarlo cuando lo obtenga (en modo de depuración). Al menos si el invariante falla (o falla), sabrá que se le pasó un valor incorrecto.

// 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
fuente
re: "pasar un tipo de datos plano ... como una cadena" Pero en C ++, las cadenas se pasan con mayor frecuencia como punteros a caracteres, (char *) o (const char *), por lo que está de vuelta a pasar punteros. Y su ejemplo pasa in_person como referencia, no como puntero, por lo que la comparación (in_person! = NULL) implica que hay algunas comparaciones de objeto / puntero definidas en la clase Person.
Jesse Chisholm
@JesseChisholm Por cadena me refiero a una cadena, es decir, una std :: cadena. De ninguna manera recomiendo usar char * como una forma de almacenar cadenas o pasarlas. No hagas eso.
Daniel Daranas
@JesseChisholm Por alguna razón, cometí un error cuando respondí esta pregunta hace cinco años. Claramente, no tiene sentido verificar si una Persona & es NULL. Eso ni siquiera se compilaría. Quería usar punteros, no referencias. Lo arreglé ahora.
Daniel Daranas
1

Como han dicho otros, no puede detectar de manera confiable un puntero no válido. Considere algunas de las formas que podría adoptar un puntero no válido:

Podría tener un puntero nulo. Ese es uno que podría verificar fácilmente y hacer algo al respecto.

Podría tener un puntero a algún lugar fuera de la memoria válida. Lo que constituye una memoria válida varía según cómo el entorno de tiempo de ejecución de su sistema configure el espacio de direcciones. En los sistemas Unix, generalmente es un espacio de direcciones virtuales que comienza en 0 y llega a una gran cantidad de megabytes. En los sistemas integrados, podría ser bastante pequeño. En cualquier caso, es posible que no comience en 0. Si su aplicación se ejecuta en modo supervisor o equivalente, entonces su puntero puede hacer referencia a una dirección real, que puede o no tener una copia de seguridad con memoria real.

Podría tener un puntero a algún lugar dentro de su memoria válida, incluso dentro de su segmento de datos, bss, pila o montón, pero sin apuntar a un objeto válido. Una variante de esto es un puntero que solía apuntar a un objeto válido, antes de que le sucediera algo malo. Las cosas malas en este contexto incluyen la desasignación, la corrupción de la memoria o la corrupción del puntero.

Podría tener un puntero ilegal plano, como un puntero con alineación ilegal para la cosa a la que se hace referencia.

El problema empeora aún más cuando se consideran arquitecturas basadas en segmentos / desplazamientos y otras implementaciones de punteros extraños. Este tipo de cosas normalmente se ocultan al desarrollador por los buenos compiladores y el uso juicioso de los tipos, pero si quieres traspasar el velo e intentar burlar a los desarrolladores del sistema operativo y del compilador, bueno, puedes, pero no hay una forma genérica. para hacerlo que manejará todos los problemas que pueda encontrar.

Lo mejor que puede hacer es permitir el bloqueo y publicar buena información de diagnóstico.

John Grieggs
fuente
re: "publicar buena información de diagnóstico", ahí está el problema. Dado que no puede verificar la validez del puntero, la información por la que debe preocuparse es mínima. "Aquí ocurrió una excepción", puede que sea todo lo que obtenga. Toda la pila de llamadas es buena, pero requiere un marco mejor que el que ofrecen la mayoría de las bibliotecas de tiempo de ejecución de C ++.
Jesse Chisholm
1

En general, es imposible de hacer. Aquí hay un caso particularmente desagradable:

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);

En muchas plataformas, esto se imprimirá:

[0 1 2]

Estás obligando al sistema de tiempo de ejecución a interpretar incorrectamente los bits de memoria, pero en este caso no se bloqueará, porque todos los bits tienen sentido. Esto es parte del diseño de la lengua (ver polimorfismo C-estilo con struct inaddr, inaddr_in, inaddr_in6), por lo que no puede proteger de forma fiable contra ella en cualquier plataforma.

Tom
fuente
1

Es increíble la cantidad de información engañosa que puede leer en los artículos anteriores ...

E incluso en la documentación de microsoft msdn, se afirma que IsBadPtr está prohibido. Bueno, prefiero la aplicación en funcionamiento en lugar de fallar. Incluso si el trabajo temporal puede estar funcionando incorrectamente (siempre que el usuario final pueda continuar con la aplicación).

Al buscar en Google, no encontré ningún ejemplo útil para Windows: encontré una solución para aplicaciones de 32 bits,

http://www.codeproject.com/script/Content/ViewAssociatedFile.aspx?rzp=%2FKB%2Fsystem%2Fdetect-driver%2F%2FDetectDriverSrc.zip&zep=DetectDriverSrc%2FDetectDriver%2Fsrc%t2FDetectDriver%2Fsrcpp%t2FDbD88 = 2

pero también necesito admitir aplicaciones de 64 bits, por lo que esta solución no funcionó para mí.

Pero he cosechado los códigos fuente de Wine y me las arreglé para cocinar un tipo de código similar que también funcionaría para aplicaciones de 64 bits, adjuntando el código aquí:

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

Y el siguiente código puede detectar si el puntero es válido o no, probablemente necesite agregar alguna verificación 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();

Esto obtiene el nombre de la clase del puntero; creo que debería ser suficiente para sus necesidades.

Una cosa que todavía me temo es que el rendimiento de la comprobación del puntero (en el fragmento de código anterior ya se están realizando 3-4 llamadas a la API) podría ser excesivo para las aplicaciones de tiempo crítico.

Sería bueno si alguien pudiera medir la sobrecarga de la comprobación del puntero en comparación, por ejemplo, con las llamadas C # / administradas en C ++.

TarmoPikaro
fuente
1

De hecho, se podría hacer algo en una ocasión específica: por ejemplo, si desea verificar si una cadena de puntero de cadena es válida, usar write (fd, buf, szie) syscall puede ayudarlo a hacer la magia: deje que fd sea un descriptor de archivo de temporal archivo que crea para la prueba, y buf apuntando a la cadena que está probando, si el puntero no es válido, write () devolvería -1 y errno establecido en EFAULT, lo que indica que buf está fuera de su espacio de direcciones accesible.

Wenlin.Wu
fuente
1

Lo siguiente funciona en Windows (alguien lo sugirió antes):

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

La función tiene que ser un método estático, independiente o estático de alguna clase. Para probar en solo lectura, copie los datos en el búfer local. Para probar la escritura sin modificar los contenidos, escríbalos. Solo puede probar la primera / última dirección. Si el puntero no es válido, el control se pasará a 'hacer algo' y luego fuera de los corchetes. Simplemente no use nada que requiera destructores, como CString.

Andrei Kalantarian
fuente
1

En 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
fuente
0

He visto que varias bibliotecas usan algún método para verificar la memoria sin referencia y tal. Creo que simplemente "anulan" los métodos de asignación de memoria y desasignación (malloc / free), que tiene alguna lógica que realiza un seguimiento de los punteros. Supongo que esto es excesivo para su caso de uso, pero sería una forma de hacerlo.

sebnow
fuente
Eso no ayuda para los objetos asignados a la pila, desafortunadamente.
Tom
0

Técnicamente, puede anular el operador nuevo (y eliminar ) y recopilar información sobre toda la memoria asignada, por lo que puede tener un método para verificar si la memoria del montón es válida. pero:

  1. todavía necesita una forma de verificar si el puntero está asignado en la pila ()

  2. deberá definir qué es el puntero 'válido':

a) se asigna memoria en esa dirección

b) la memoria en esa dirección es la dirección de inicio del objeto (por ejemplo, la dirección no está en el medio de una gran matriz)

c) la memoria en esa dirección es la dirección de inicio del objeto del tipo esperado

En pocas palabras: el enfoque en cuestión no es C ++, necesita definir algunas reglas que aseguren que la función reciba punteros válidos.


fuente
0

Anexo a la respuesta o respuestas aceptadas:

Suponga que su puntero solo puede contener tres valores: 0, 1 y -1 donde 1 significa un puntero válido, -1 uno no válido y 0 otro no válido. ¿Cuál es la probabilidad de que su puntero sea NULL, si todos los valores son igualmente probables? 1/3. Ahora, elimine el caso válido, de modo que para cada caso no válido, tenga una proporción de 50:50 para detectar todos los errores. Se ve bien, ¿verdad? Escale esto para un puntero de 4 bytes. Hay 2 ^ 32 o 4294967294 valores posibles. De estos, solo UN valor es correcto, uno es NULL y todavía le quedan 4294967292 otros casos no válidos. Recalcular: tiene una prueba para 1 de (4294967292+ 1) casos no válidos. Una probabilidad de 2.xe-10 o 0 para la mayoría de los propósitos prácticos. Tal es la inutilidad de la verificación NULL.

dirkgently
fuente
0

Ya sabes, un nuevo controlador (al menos en Linux) que sea capaz de esto probablemente no sería tan difícil de escribir.

Por otro lado, sería una locura construir sus programas de esta manera. A menos que tenga un uso realmente específico y único para tal cosa, no lo recomendaría. Si creó una aplicación grande cargada con constantes comprobaciones de validez de puntero, probablemente sería terriblemente lenta.

dicroce
fuente
0

debe evitar estos métodos porque no funcionan. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx - JaredPar 15 de febrero de 2009 a las 16:02

Si no funcionan, ¿la próxima actualización de Windows lo solucionará? Si no funcionan a nivel de concepto, la función probablemente se eliminará de la API de Windows por completo.

La documentación de MSDN afirma que están prohibidos, y la razón de esto probablemente sea una falla en el diseño posterior de la aplicación (por ejemplo, generalmente no debe comer punteros no válidos en silencio, si está a cargo del diseño de toda la aplicación, por supuesto) y el rendimiento / tiempo de comprobación de puntero.

Pero no debes afirmar que no funcionan por culpa de algún blog. En mi aplicación de prueba he verificado que funcionan.

TarmoPikaro
fuente