¿Cómo funcionan el trabajo libre y malloc en C?

Estoy tratando de averiguar qué pasaría si trato de liberar un puntero “desde el medio”, por ejemplo, mira el siguiente código:

char *ptr = (char*)malloc(10*sizeof(char)); for (char i=0 ; i<10 ; ++i) { ptr[i] = i+10; } ++ptr; ++ptr; ++ptr; ++ptr; free(ptr); 

Obtengo un locking con un mensaje de error de excepción no controlada. Quiero entender por qué y cómo funciona el servicio gratuito para que sepa no solo cómo usarlo, sino también para poder entender errores extraños y excepciones y depurar mejor mi código ץ

Muchas gracias

Cuando malloc un bloque, realmente asigna un poco más de memoria de la que pediste. Esta memoria extra se usa para almacenar información, como el tamaño del bloque asignado, y un enlace al siguiente bloque libre / usado en una cadena de bloques, y algunas veces algunos “datos de guardia” que ayudan al sistema a detectar si se escribe pasado el final de tu bloque asignado Además, la mayoría de los asignadores redondearán el tamaño total y / o el inicio de su parte de la memoria a un múltiplo de bytes (por ejemplo, en un sistema de 64 bits puede alinear los datos a un múltiplo de 64 bits (8 bytes) como acceder a datos desde direcciones no alineadas puede ser más difícil e ineficiente para el procesador / bus), por lo que también puede terminar con algunos “rellenos” (bytes no utilizados).

Cuando libera su puntero, usa esa dirección para encontrar la información especial que agregó al principio (generalmente) de su bloque asignado. Si transfiere una dirección diferente, accederá a la memoria que contiene basura y, por lo tanto, su comportamiento no está definido (pero con mayor frecuencia provocará un locking).

Más tarde, si libera () el bloque pero no “olvida” su puntero, puede tratar accidentalmente de acceder a los datos a través de ese puntero en el futuro, y el comportamiento no está definido. Cualquiera de las siguientes situaciones podría ocurrir:

  • la memoria puede colocarse en una lista de bloques libres, por lo que cuando accede a ella, todavía contiene los datos que dejó allí y su código se ejecuta normalmente.
  • el asignador de memoria puede haberle dado (parte de) la memoria a otra parte de tu progtwig, y ​​es de suponer que habrá sobrescrito (algunos de) tus datos anteriores, así que cuando lo leas, obtendrás basura que podría causar un comportamiento inesperado o se bloquea de tu código. O escribirá sobre los otros datos, haciendo que la otra parte de su progtwig se comporte de manera extraña en algún momento en el futuro.
  • la memoria podría haber sido devuelta al sistema operativo (una “página” de memoria que ya no está utilizando puede ser eliminada de su espacio de direcciones, por lo que ya no hay memoria disponible en esa dirección, esencialmente un “agujero” no utilizado) en la memoria de tu aplicación). Cuando su aplicación intente acceder a los datos, se producirá un error de memoria dura y matará su proceso.

Esta es la razón por la que es importante asegurarse de no utilizar un puntero después de liberar la memoria a la que apunta: la mejor práctica para esto es establecer el puntero a NULL después de liberar la memoria, ya que puede probar fácilmente NULL, y intentar acceder a la memoria a través de un puntero NULL causará un comportamiento malo pero consistente , que es mucho más fácil de depurar.

Probablemente sepas que debes pasar exactamente el puntero que recibiste.

Debido a que free () no sabe al principio qué tan grande es su bloque, necesita información auxiliar para identificar el bloque original desde su dirección y luego devolverlo a una lista libre. También intentará fusionar pequeños bloques liberados con los vecinos para producir un bloque libre grande más valioso.

En última instancia, el asignador debe tener metadatos sobre su bloque, como mínimo tendrá que haber almacenado la longitud en algún lugar.

Describiré tres formas de hacer esto.

  • Un lugar obvio sería almacenarlo justo antes del puntero devuelto. Podría asignar un bloque que es unos pocos bytes más grande que el solicitado, almacenar el tamaño en la primera palabra, luego devolver un puntero a la segunda palabra.

  • Otra forma sería mantener un mapa separado que describa al menos la longitud de los bloques asignados, usando la dirección como clave.

  • Una implementación podría derivar cierta información de la dirección y parte de un mapa. El asignador de kernel 4.3BSD (llamado, creo, el “asignador McKusick-Karel” ) hace asignaciones de potencia de dos para objetos de tamaño inferior al de página y conserva solo un tamaño por página, haciendo todas las asignaciones de una página determinada de un solo tamaño.

Sería posible con algunos tipos del segundo y probablemente cualquier tipo del tercer tipo de asignador detectar realmente que ha avanzado el puntero y DTRT , aunque dudo si alguna implementación quemará el tiempo de ejecución para hacerlo.

La mayoría de las implementaciones (si no todas) buscarán la cantidad de datos para liberar algunos bytes antes del puntero real que está manipulando. Hacer un salvaje free dará lugar a la corrupción del mapa de memoria.

Si su ejemplo, cuando asigna 10 bytes de memoria, el sistema realmente se reserva, digamos, 14. Los primeros 4 contienen la cantidad de datos que solicitó (10) y luego el valor de retorno del malloc es un puntero al primer byte de datos no utilizados en los 14 asignados.

Cuando llame free a este puntero, el sistema buscará 4 bytes hacia atrás para saber que originalmente asignó 14 bytes para que sepa cuánto liberar. Este sistema le impide proporcionar la cantidad de datos para liberar como un parámetro adicional para free .

Por supuesto, otra implementación de malloc / free puede elegir otra forma de lograr esto. Pero generalmente no admiten free en un puntero diferente que lo devuelto por malloc o función equivalente.

Desde http://opengroup.org/onlinepubs/007908775/xsh/free.html

La función free () hace que el espacio apuntado por ptr sea desasignado; es decir, disponible para una asignación adicional. Si ptr es un puntero nulo, no se produce ninguna acción. De lo contrario, si el argumento no coincide con un puntero devuelto anteriormente por la función calloc (), malloc (), realloc () o valloc (), o si el espacio es desasignado por una llamada a free () o realloc (), el el comportamiento no está definido Cualquier uso de un puntero que haga referencia al espacio liberado provoca un comportamiento indefinido.

Es un comportamiento indefinido, no lo hagas. Solo free() punteros free() obtenidos de malloc() , nunca los ajuste antes de eso.

El problema es free() debe ser muy rápido, por lo que no intenta encontrar la asignación a la que pertenece su dirección ajustada, sino que intenta devolver el bloque exactamente en la dirección ajustada al montón. Eso lleva a un comportamiento indefinido, generalmente un montón de corrupción o locking del progtwig.

Estás liberando la dirección incorrecta. Al cambiar el valor de ptr, cambia la dirección. free no tiene forma de saber que debería tratar de liberar un bloque comenzando con 4 bytes. Mantenga el puntero original intacto y libre en lugar del manipulado. Como otros señalaron, los resultados de hacer lo que estás haciendo son “indefinidos” … de ahí la excepción no controlada.

Nunca hagas esto

Estás liberando la dirección incorrecta. Al cambiar el valor de ptr, cambia la dirección. free no tiene forma de saber que debería tratar de liberar un bloque comenzando con 4 bytes. Mantenga el puntero original intacto y libre en lugar del manipulado. Como otros señalaron, los resultados de hacer lo que estás haciendo son “indefinidos” … de ahí la excepción no controlada

Tomado del libro: comprensión y uso de punteros C

Cuando se asigna memoria, la información adicional se almacena como parte de una estructura de datos mantenida por el administrador de stack. Esta información incluye, entre otras cosas, el tamaño del bloque y, por lo general, se coloca inmediatamente adyacente al bloque asignado.