2010-07-31 28 views
6

Tenga en cuenta que no quiero para resolver cualquier problema con mi pregunta - Estaba pensando acerca de las probabilidades de que las cosas sucedan y por lo tanto se preguntaba sobre algo:¿Qué sucede exactamente si elimina un objeto? (GCC) (Cuando doble eliminar los accidentes?)

¿Qué sucede exactamente si eliminas el objeto y usas gcc como compilador?

La semana pasada estaba investigando un bloqueo, donde una condición de carrera condujo a una eliminación doble de un objeto.

El bloqueo se produjo al llamar al destructor virtual del objeto, porque el puntero a la tabla de funciones virtuales ya se sobrescribió.

¿El puntero de función virtual se sobrescribe con la primera eliminación?

En caso negativo, ¿es seguro el segundo borrado entonces, siempre que no se realice una nueva asignación de memoria mientras tanto?

Me pregunto por qué el problema que tenía no se reconoció antes y la única explicación es que la tabla de funciones virtuales se sobrescribe inmediatamente durante la primera eliminación o la segunda eliminación no se bloquea.

(La primera significa que el bloqueo siempre ocurre en el mismo lugar si ocurre la "carrera" -el segundo, que normalmente no sucede cuando la carrera ocurre- y solo si un tercer hilo sobrescribe el objeto eliminado en el Mientras tanto, el problema se produce)


Editar/actualización:.

hice una prueba, el siguiente código se bloquea con un error de segmentación (gcc 4.4, i686 y AMD64):

class M 
{ 
private: 
    int* ptr; 
public: 
    M() { 
    ptr = new int[1]; 
    } 
    virtual ~M() {delete ptr;} 
}; 

int main(int argc, char** argv) 
{ 
    M* ptr = new M(); 
    delete ptr; 
    delete ptr; 
} 

Si elimino el 'virtual' del controlador, el programa se cancela con glibc porque detecta el doble libre. Con 'virtual', el bloqueo se produce cuando se realiza la llamada de función indirecta al destructor, porque el puntero a la tabla de funciones virtuales no es válido.

En ambos amd64 e i686 el puntero apunta a una región de memoria válida (pila), pero el valor no es válido (¿contador? Es muy bajo, por ejemplo 0x11 o 0x21) por lo que la 'llamada' (o 'jmp 'cuando el compilador realizó una optimización de retorno) salta a una región no válida.

Programa

recibido SIGSEGV señal,

Fallo de segmentación. 0x0000000000000021

in ??() (gdb)

# 0 0x0000000000000021 in ??()

# 1 0x000000000040083e en main()

Así que con las condiciones antes mencionadas, el puntero a la tabla de función virtual está siempre sobrescrito por el primer eliminar, por lo que la próxima borrado saltará al nirvana si la clase tiene un destructor virtual.

+0

Suena como que necesita para invertir en algún mutex o secciones críticas demasiado –

+0

0A0D: Este es mi solución prelimary (solución). De hecho, hubo una falla de diseño, porque se pensó que hay dos hilos que pueden eliminar el objeto. – IanH

Respuesta

6

Es muy dependientes de la aplicación del propio asignador de memoria, por no hablar de los fallos dependientes de aplicación como overwritting tabla v de algún objeto. Existen numerosos esquemas de asignación de memoria que difieren en capacidades y resistencia a double free() pero todos comparten una propiedad común: su aplicación se bloqueará en algún momento después del segundo free().

La razón de la caída es por lo general que se dedica asignador de memoria pequeña cantidad de memoria antes (encabezado) y después (pie de página) de cada porción de memoria reservada de la memoria para almacenar algunos detalles específicos de aplicación. El encabezado generalmente define el tamaño del fragmento y la dirección del siguiente fragmento. El pie de página es generalmente un puntero al encabezado del fragmento. Por lo general, eliminar dos veces al menos implica verificar si los trozos adyacentes son gratuitos. Por lo tanto el programa se bloqueará si:

1) apuntador al siguiente fragmento ha sido sobrescrito y el segundo libre() provoca violación de segmento cuando se trata de acceder a la siguiente parte.

2) el pie de página de la porción anterior ha sido modificado y el acceso a la cabecera de la porción anterior provoca violación de segmento.

Si la aplicación sobrevive, significa que free() ha dañado la memoria en varias ubicaciones o agregará una porción libre que se superpone a una de las partes que ya están libres, lo que puede dañar los datos en el futuro. Finalmente, su programa segfault en uno de los siguientes free() o malloc() que involucran las áreas de memoria dañadas.

+0

¿Qué sucede con el elemento eliminado, si no hay más malloc después de la eliminación? – IanH

+0

Hice algunas pruebas con gcc 4.4: Parece que la primera eliminación sobrescribe la tabla de funciones virtuales, por lo que el bloqueo se produce cuando se llama al destructor virtual por segunda vez. Con un destructor no virtual, el glibc detecta el doble libre y aborta el programa. – IanH

+0

Agregué la prueba que hice y su resultado a mi pregunta. – IanH

6

Eliminación de algo dos veces es un comportamiento indefinido - no necesita ninguna explicación más allá de eso, y es generalmente infructuoso buscar uno. Puede hacer que un programa falle, es posible que no, pero siempre es algo malo y el programa siempre estará en un estado desconocido después de haberlo hecho.

+2

Conocimiento sobre lo que puede suceder con los objetos eliminados y lo que no ayuda en el análisis de volcado de núcleo. – IanH

+0

@Ian Prefiero no tener volcados de memoria en primer lugar. Además, realmente no se puede saber qué sucederá a menos que tenga un conocimiento profundo del sistema de asignación de memoria, que pocas personas hacen, y que bien puede cambiar de un lanzamiento a otro del compilador. –

+0

Prefiero no tener errores en absoluto, pero se producen, y también lo hacen los bloqueos. Entonces es muy útil saber qué puede pasar (y cuál puede ser el motivo de lo que sucedió) y qué no. En proyectos de larga ejecución, generalmente no se cambia el compilador a menudo. Y es de esperar que el compilador y su libc no cambien el asignador de memoria sin darse cuenta. – IanH

1

Al ejecutar delete dos veces (o incluso free), es posible que la memoria ya se haya reasignado y ejecutando delete nuevamente puede dañar la memoria. El tamaño del bloque de memoria asignado se suele mantener justo antes del bloque de memoria.

Si usted tiene una clase derivada, no llame a borrarse de la clase derivada (niño). Si no se declara virtual, solo se llama al destructor ~BaseClass() dejando cualquier memoria asignada del DerivedClass para que persista y se escape. Esto supone que el DerivedClass tiene memoria adicional asignada por encima y más allá de BaseClass que debe liberarse.

decir

BaseClass* obj_ptr = new DerivedClass; // Allowed due to polymorphism. 
... 
delete obj_ptr; // this will call the destructor ~Parent() and NOT ~Child() 
+0

Ver mi actualización anterior: Al menos con un nuevo gcc, un doble-eliminar se bloquea inmediatamente, incluso si no hay nuevo/malloc mientras tanto. Y sé la razón por la cual los destructores casi siempre tienen que ser virtuales. – IanH

Cuestiones relacionadas