A principios de 2018, nuestro blog se complementó con una serie de artículos sobre la sexta verificación del código fuente del proyecto Chromium. La serie incluye 8 artículos sobre errores y recomendaciones para su prevención. Dos artículos provocaron una acalorada discusión, y todavía ocasionalmente recibo comentarios por correo sobre los temas tratados en ellos. Tal vez, debería dar explicaciones adicionales y, como dicen, dejar las cosas claras.
Ha pasado un año desde que escribió una serie de artículos sobre una verificación regular del código fuente del proyecto Chromium:
- Cromo: el sexto control del proyecto y 250 errores
- Bonito cromo y torpe memoria
- ruptura y caída
- Cromo: pérdidas de memoria
- Cromo: errores tipográficos
- Cromo: uso de datos no confiables
- Por qué es importante verificar qué devolvió la función malloc
- Cromo: otros errores
Los artículos dedicados a
memset y
malloc han causado y continúan causando debates, lo que me parece extraño. Aparentemente, hubo cierta confusión debido al hecho de que no había sido lo suficientemente preciso al verbalizar mis pensamientos. Decidí volver a esos artículos y hacer algunas aclaraciones.
memset
Comencemos con un artículo sobre
memset , porque aquí todo es simple. Aparecieron algunos argumentos sobre la mejor manera de inicializar estructuras. Muchos programadores escribieron que sería mejor dar la recomendación de no escribir:
HDHITTESTINFO hhti = {};
pero para escribir de la siguiente manera:
HDHITTESTINFO hhti = { 0 };
Razones:
- La construcción {0} es más fácil de notar al leer el código, que {}.
- La construcción {0} es más intuitivamente comprensible que {}. Lo que significa que 0 sugiere que la estructura está llena de ceros.
En consecuencia, los lectores me sugieren cambiar este ejemplo de inicialización en el artículo. No estoy de acuerdo con los argumentos y no planeo hacer ninguna edición en el artículo. Ahora voy a explicar mi opinión y dar algunas razones.
En cuanto a la visibilidad, creo que es cuestión de gustos y hábitos. No creo que la presencia de 0 entre paréntesis cambie fundamentalmente la situación.
En cuanto al segundo argumento, estoy totalmente en desacuerdo con él. El registro del tipo {0} da una razón para percibir incorrectamente el código. Por ejemplo, puede suponer que si reemplaza 0 con 1, todos los campos se inicializarán con unos. Por lo tanto, es más probable que dicho estilo de escritura sea perjudicial en lugar de útil.
El analizador PVS-Studio incluso tiene un diagnóstico relacionado
V1009 ,
cuya descripción se cita a continuación.
V1009. Verifique la inicialización de la matriz. Solo el primer elemento se inicializa explícitamente.El analizador ha detectado un posible error relacionado con el hecho de que al declarar una matriz, el valor se especifica solo para un elemento. Por lo tanto, los elementos restantes se inicializarán implícitamente por cero o por un constructor predeterminado.
Consideremos el ejemplo de código sospechoso:
int arr[3] = {1};
Quizás el programador esperaba que
arr consistiría completamente en unos, pero no lo es. La matriz constará de los valores 1, 0, 0.
Código correcto:
int arr[3] = {1, 1, 1};
Tal confusión puede ocurrir debido a la similitud con la construcción
arr = {0} , que inicializa toda la matriz con ceros.
Si tales construcciones se utilizan activamente en su proyecto, puede deshabilitar este diagnóstico.
También recomendamos no descuidar la claridad de su código.
Por ejemplo, el código para codificar valores de un color se registra de la siguiente manera:
int White[3] = { 0xff, 0xff, 0xff }; int Black[3] = { 0x00 }; int Green[3] = { 0x00, 0xff };
Gracias a la inicialización implícita, todos los colores se especifican correctamente, pero es mejor reescribir el código más claramente:
int White[3] = { 0xff, 0xff, 0xff }; int Black[3] = { 0x00, 0x00, 0x00 }; int Green[3] = { 0x00, 0xff, 0x00 };
malloc
Antes de seguir leyendo, recuerde el contenido del artículo "
Por qué es importante verificar qué devolvió la función malloc ". Este artículo ha dado lugar a mucho debate y crítica. Estas son algunas de las discusiones:
reddit.com/r/cpp ,
reddit.com/r/C_Programming ,
habr.com (en). Ocasionalmente, los lectores todavía me envían correos electrónicos sobre este artículo.
El artículo es criticado por los lectores por los siguientes puntos:
1. Si malloc devolvió NULL , entonces es mejor terminar inmediatamente el programa, que escribir un montón de if -s e intentar manejar de alguna manera la memoria, debido a lo cual la ejecución del programa es frecuentemente imposible de todos modos.No he presionado para luchar hasta el final con las consecuencias de la pérdida de memoria, pasando el error cada vez más alto. Si se permite que su aplicación termine su trabajo sin previo aviso, entonces que así sea. Para este propósito, incluso una sola comprobación justo después de
malloc o usando
xmalloc es suficiente (vea el siguiente punto).
Me opuse y advertí sobre la falta de controles debido a que el programa continúa funcionando como si nada hubiera pasado. Es un caso completamente diferente. Es peligroso, ya que conduce a un comportamiento indefinido, corrupción de datos, etc.
2. No existe una descripción de una solución que se base en escribir funciones de envoltura para asignar memoria con una verificación a continuación o usar funciones ya existentes, como xmalloc .De acuerdo, perdí este punto. Al escribir el artículo simplemente no estaba pensando en la forma de remediar la situación. Para mí era más importante transmitirle al lector el peligro de la ausencia de cheques. Cómo solucionar un error es una cuestión de gusto y detalles de implementación.
La función
xmalloc no forma parte de la biblioteca C estándar (consulte "
¿Cuál es la diferencia entre xmalloc y malloc? "). Sin embargo, esta función puede declararse en otras bibliotecas, por ejemplo, en la biblioteca de
utilidades de GNU (
libiberty de GNU ).
El punto principal de la función es que el programa se bloquea cuando no puede asignar memoria. La implementación de esta función podría tener el siguiente aspecto:
void* xmalloc(size_t s) { void* p = malloc(s); if (!p) { fprintf (stderr, "fatal: out of memory (xmalloc(%zu)).\n", s); exit(EXIT_FAILURE); } return p; }
En consecuencia, al llamar a una función
xmalloc en lugar de
malloc cada vez, puede estar seguro de que no se producirá un comportamiento indefinido en el programa debido al uso de un puntero nulo.
Desafortunadamente,
xmalloc tampoco es una panacea. Uno debe recordar que el uso de
xmalloc es inaceptable cuando se trata de escribir código de bibliotecas. Hablaré de eso más tarde.
3. La mayoría de los comentarios fueron los siguientes: "en la práctica, malloc nunca devuelve NULL ".Afortunadamente, no soy el único que entiende que este es el enfoque equivocado. Realmente me gustó este
comentario en mi soporte:
Según mi experiencia en la discusión de este tema, tengo la sensación de que hay dos sectas en Internet. Los partidarios del primero creen firmemente que malloc nunca devuelve NULL en Linux. Los partidarios del segundo afirman de todo corazón que si no se puede asignar memoria en su programa, no se puede hacer nada, solo puede fallar. No hay forma de persuadirlos demasiado. Especialmente cuando estas dos sectas se cruzan. Solo puedes tomarlo como un hecho. E incluso no es importante sobre qué recurso especializado tiene lugar una discusión.Pensé por un momento y decidí seguir los consejos, así que no intentaré persuadir a nadie :). Con suerte, estos grupos de desarrolladores escriben solo programas no fatales. Si, por ejemplo, algunos datos en el juego se corrompen, no hay nada crucial en ellos.
Lo único que importa es que los desarrolladores de bibliotecas y bases de datos no deben hacer esto.
Apelar a los desarrolladores de código y bibliotecas altamente dependientes
Si está desarrollando una biblioteca u otro código altamente dependiente, siempre verifique el valor del puntero devuelto por la función
malloc / realloc y devuelva un código de error si la memoria no se puede asignar.
En las bibliotecas, no puede llamar a la función de
salida , si falla la asignación de memoria. Por la misma razón, no puedes usar
xmalloc . Para muchas aplicaciones, es inaceptable simplemente abortarlas. Debido a esto, por ejemplo, una base de datos puede estar dañada. Se pueden perder datos que se evaluaron durante muchas horas. Debido a esto, el programa puede alcanzar vulnerabilidades de "denegación de servicio" cuando, en lugar de manejar correctamente la creciente carga de trabajo, una aplicación multiproceso simplemente finaliza.
No se puede suponer, de qué maneras y en qué proyectos se utilizará la biblioteca. Por lo tanto, se debe suponer que la aplicación puede resolver tareas muy críticas. Es por eso que matarlo llamando a
exit no es bueno. Lo más probable es que dicho programa esté escrito teniendo en cuenta la posibilidad de falta de memoria y puede hacer algo en este caso. Por ejemplo, un sistema CAD no puede asignar un búfer de memoria apropiado que sea suficiente para la operación regular debido a la fuerte fragmentación de la memoria. En este caso, no es la razón por la que se aplasta en el modo de emergencia con pérdida de datos. El programa puede proporcionar una oportunidad para guardar el proyecto y reiniciarse normalmente.
En ningún caso es imposible confiar en
malloc que siempre podrá asignar memoria. No se sabe en qué plataforma y cómo se utilizará la biblioteca. Si la situación de poca memoria en una plataforma es exótica, puede ser una situación bastante común en la otra.
No podemos esperar que si
malloc devuelve
NULL , el programa se bloqueará. Cualquier cosa puede pasar. Como describí en el
artículo , el programa puede escribir datos no por la dirección nula. Como resultado, algunos datos pueden estar dañados, lo que lleva a consecuencias impredecibles. Incluso
memset es peligroso. Si el relleno con datos va en orden inverso, primero algunos datos se corrompen y luego el programa se bloqueará. Pero el choque puede ocurrir demasiado tarde. Si los datos contaminados se utilizan en subprocesos paralelos mientras la función
memset funciona, las consecuencias pueden ser fatales. Puede obtener una transacción corrupta en una base de datos o enviar comandos para eliminar archivos "innecesarios". Cualquier cosa tiene la posibilidad de suceder. Sugiero a un lector que sueñe con lo que podría suceder debido al uso de basura en la memoria.
Por lo tanto, la biblioteca solo tiene una forma correcta de trabajar con las funciones
malloc . Debe comprobar INMEDIATAMENTE que la función regresó y, si es NULL, devolver un estado de error.
Enlaces adicionales
- Manejo de OOM
- Diversión con punteros NULL: parte 1 , parte 2
- Lo que todo programador de C debe saber sobre el comportamiento indefinido: parte 1 , parte 2 , parte 3