elhacker.net cabecera Bienvenido(a), Visitante. Por favor Ingresar o Registrarse
¿Perdiste tu email de activación?.

 

 


Tema destacado: Rompecabezas de Bitcoin, Medio millón USD en premios


+  Foro de elhacker.net
|-+  Programación
| |-+  Programación C/C++ (Moderadores: Eternal Idol, Littlehorse, K-YreX)
| | |-+  campos de bits
0 Usuarios y 1 Visitante están viendo este tema.
Páginas: 1 [2] Ir Abajo Respuesta Imprimir
Autor Tema: campos de bits  (Leído 13,197 veces)
RayR

Desconectado Desconectado

Mensajes: 243


Ver Perfil
Re: campos de bits
« Respuesta #10 en: 3 Octubre 2023, 02:48 am »

Como aquí estamos poniendo sólo ejemplos ilustrativos, creo que es válido ver la representación interna de las variables de esa manera, pero reitero que lo mejor es usar un unsigned char* y recorrer con él los bytes de la estructura, ya que, en general, en C y C++ no es válido acceder a una variable mediante un puntero a un tipo distinto. Una excepción son los punteros a char (con o sin signo), los cuales sí se pueden usar con variables de otros tipos. Hay otras excepciones (por ejemplo, un puntero unsigned/signed puede acceder a datos signed/unsigned, siempre que sean del mismo tipo) pero son muy pocas.

Romper esa regla resulta en comportamiento indefinido o UB, y en ese caso los compiladores pueden hacer prácticamente lo que quieran. Pudiera parecer estos son sólo problemas teóricos, pero no es así, pues constantemente se encuentran problemas derivados de estos malos usos. Aquí pongo unos ejemplos:

Código
  1. #include <stdio.h>
  2.  
  3. typedef struct {
  4.    unsigned char codigo : 4;
  5.    unsigned short cantidad : 9;
  6.    unsigned char procedencia : 1;
  7.    unsigned char basura : 2;
  8. } producto_t;
  9.  
  10. producto_t* prod;
  11.  
  12. int main()
  13. {
  14.    unsigned base[2] = { 0 };
  15.    prod = (producto_t*)&base;
  16.  
  17.    prod->codigo = 5;
  18.    prod->procedencia = 1;
  19.    prod->cantidad = 4;
  20.    prod->basura = 3;
  21.  
  22.    printf("base     : %08x %08x\n", base[1], base[0]);
  23.  
  24. }

En GCC o MinGW, si se compila con -O2 o superior, la salida es:

Código:
base     : 00000000 00000000

Lo que pasó es que el compilador decidió optimizar el programa y dejar sólo esto:

Código
  1.    printf("base     : %08x %08x\n", 0, 0);

es decir, directamente imprime los valores con los que base fue inicializado. Puesto que *prod y base tienen tipos distintos e incompatibles, no es válido modificar base mediante ese puntero,  así que el compilador decide que base jamás fue modificado luego de su inicialización.

Otro ejemplo casi igual pero con un resultado todavía peor:

Código
  1. #include <stdio.h>
  2.  
  3. typedef struct {
  4.    unsigned char codigo : 4;
  5.    unsigned short cantidad : 9;
  6.    unsigned char procedencia : 1;
  7.    unsigned char basura : 2;
  8. } producto_t;
  9.  
  10.  
  11. producto_t* prod;
  12.  
  13. int main()
  14. {
  15.    unsigned base[2] = { 0 };
  16.    prod = (producto_t*)&base;
  17.  
  18.    prod->codigo = 5;
  19.    prod->procedencia = 1;
  20.    prod->cantidad = 4;
  21.    prod->basura = 3;
  22.  
  23.    base[0] = base[0];
  24.    base[1] = base[1];
  25.  
  26.    printf("base     : %08x %08x\n", base[1], base[0]);
  27.    printf("codigo: %u\n", prod->codigo);
  28.    printf("procedencia: %u\n", prod->procedencia);
  29.    printf("cantidad: %u\n", prod->cantidad);
  30.    printf("basura: %u\n", prod->basura);
  31. }

de nuevo, GCC y MinGW arrojan esta salida:

Código:
base     : 00000000 00000000
codigo: 0
procedencia: 0
cantidad: 0
basura: 0

por las mismas razones de antes.

Y esto no se limita a GCC, sino que cada compilador tiene sus "detalles". Por ejemplo, hace años alguien reportó un posible bug en clang. El ejemplo que puso era más o menos así:

Código
  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5.  int par[2] = { 1 };
  6.  int k = 0;
  7.  
  8.  for (int i = 0; i < 1; i++) {
  9.    if (par[1] > 0) {
  10.      *(short*)&par[0] = 5;
  11.    }
  12.    par[k++] = 0;
  13.  }
  14.  
  15.  printf("Esto deberia ser 0: %d\n", par[0]);
  16.  
  17. }

con -O1 o superior, en clang da esta salida:

Código:
Esto deberia ser 0: 1

Edit: corrijo números de línea
se supone que siempre debería ser 0 porque la última instrucción que se ejecuta antes del printf es la de la línea 12, pero el compilador hace lo que quiere, porque la línea 10 contiene UB (¡a pesar de que nunca se va a ejecutar, ya que la condición del if forzosamente será falsa!). Si se cambia el short* por char*, ahí la modificación ya es válida y el programa imprime 0, como se esperaba.

Estos ejemplos pueden parecer muy rebuscados o "artificiales", pero la realidad es que normalmente ese tipo de problemas se encuentran al estar trabajando en programas reales. Obviamente uno no va poner programas enteros en los foros o en reportes de bugs, así que se busca crear un ejemplo lo más básico y simplificado posible que muestre el problema, pero estas cosas ocurren todo el tiempo en programas de todo tipo y tamaño, y de hecho hay tanto debate al respecto, que hay muchas conferencias hablando sobre el tema y justificando (o no) lo que hacen los compiladores.

Como muchos otros programadores, no soy fan de optimizaciones tan agresivas como éstas, y para mí, el caso de clang debería considerarse un bug (si experimentamos un poco con el código, obtenemos resultados aún más raros), pero sus desarrolladores no piensan así, y nunca lo arreglaron. Técnicamente, el estándar de C (y C++) les da la razón. El caso es que esté o no uno de acuerdo, los compiladores hacen este tipo de cosas cuando encuentran código que viola el estándar, así que es mejor intentar apegarse a las reglas.


« Última modificación: 5 Octubre 2023, 19:24 pm por RayR » En línea

MAFUS


Desconectado Desconectado

Mensajes: 1.603



Ver Perfil
Re: campos de bits
« Respuesta #11 en: 4 Octubre 2023, 12:27 pm »

Es correcto todo lo que dices. Aunque también es cierto que si usas volatile te quitas de encima esas molestas optimizaciones del compilador. Y oye, que para eso está la volatile.

Y sí, lo he probado con el código que pasaste de ejemplo con -O2, con volatile y te muestra los resultados.


En línea

RayR

Desconectado Desconectado

Mensajes: 243


Ver Perfil
Re: campos de bits
« Respuesta #12 en: 5 Octubre 2023, 19:42 pm »

Sí, además de su función principal, volatile puede ser útil al depurar, y en casos como éste, también puede ser válido. Ojo, en ningún momento pretendo decir que todos los ejemplos de los primeros mensajes estén "mal". Se entiende que son cosas que uno normalmente no usaría para cosas más reales (aunque no está de más decir que, aún en códigos simples de ejemplo, violar las reglas nos puede traer más de una sorpresa desagradable).

Pero hay gente, incluso aquí en el foro, que a veces intenta ya no sólo experimentar sino implementar cosas de bajo nivel en sus programas. Por ejemplo, hace poco un usuario estaba haciendo algo similar a un editor hexadecimal. Mis recomendaciones, y lo que sigue, son sobre todo para quien quiera hacer cosas así, "reales".

Primero, para saber más sobre el tema, se puede googlear "strict aliasing" que es como se conoce a la regla de la que hablo en mi mensaje anterior. Y hay unas cosas que es fundamental entender: 1) Esto lo recalco de nuevo, cuando un programa hace cosas que el estándar cataloga como "undefined behaviour", el resultado, y las acciones del compilador, se vuelven impredecibles y éste puede hacer lo que quiera y a menudo hace cosas distintas en versiones distintas. Otra referencia más en FAQ de C: https://c-faq.com/ansi/undef.html . 2) Las optimizaciones que el compilador hace son totalmente dependientes del contexto. Dado un conjunto de líneas, su optimización puede ser totalmente distinta si las metemos en un for, o en dos for anidados, o si hay un if, o si las variables a las que acceden son globales o locales, o parámetros recibidos, o si la función es inline, etc., casi cualquier cosa lo altera. 3) Si el programa está escrito correctamente, un compilador jamás va a cambiar su comportamiento observable y su funcionamiento. Los ejemplos anteriores fallan únicamente porque violan el strict aliasing. Si se cambian de manera que respeten esta regla (usando unsigned char*, por ejemplo) se arreglan de forma permanente.

El calificador volatile no es realmente solución; en todo caso, es un parche temporal. De nuevo, si se googlea sobre el tema, van a salir muchos resultados de fuentes confiables, ninguno de los cuales sugiere siquiera a volatile como solución (los pocos que lo mencionan básicamente dicen: "no arregla el problema. Si te sirve es suerte"). El propio Linus Torvalds dijo lo siguiente cuando alguien supuso que volatile servía para arreglar estos problemas: "sí, volatile podría funcionar, pero es como matar una mosca con una bomba atómica (...) y no hay garantía de que el compilador no haga las optimizaciones, aún con acceso volatile, así que de todas formas es un punto irrelevante".

En términos prácticos, con volatile el compilador desactiva optimizaciones relacionadas con el acceso a las variables calificadas de esta manera (ya no las elimina, y para cada lectura, se traen sus valores de la memoria, etc.), pero eso no tiene nada que ver con el aliasing, lo que pasa es que en algunos contextos, como mis ejemplos, la única optimización que hace el compilador al encontrar UB coincide, de forma fortuita, con las que deshabilita volatile. Pero hay toda una gama de cosas que el compilador puede hacer (eliminar instrucciones que producen UB, reordenarlas, etc.) que vuelven a romper los programas a pesar de usar este calificador. No quiero que salga un mensaje aún más largo de lo que es, y no le veo sentido a poner más ejemplos (siempre se puede encontrar un "parche" que arregle de momento el programa, y luego poner un nuevo ejemplo con las instrucciones  reacomodadas/cambiadas de manera que el parche no funcione, y luego otro parche, y así nos pasamos la vida, porque no estamos arreglando el problema) pero nada de esto es hipotético, pasa en la realidad. Por no hablar de otros problemas, como la alineación incorrecta. De esto también hay ejemplos reales, incluso en x86-64 (donde algunas instrucciones producen errores de violación de acceso si los datos no tienen una alineación específica), y derivados de violar el strict aliasing.

La única solución general es seguir las reglas del lenguaje, en este caso, respetar el strict aliasing. Usar punteros char o memcpy (con ciertas precauciones) son maneras válidas. De esta manera nos quitamos de cualquier preocupación. Los programas van a funcionar siempre, en cualquier compilador que acepte la versión del lenguaje elegido, y con cualquier nivel de optimización. Si forzosamente necesitamos acceder a datos mediante punteros a tipos incompatibles (en unas pocas ocasiones puede ser conveniente), lo que se debe hacer es compilar con -fno-strict-aliasing o equivalente. Esto le dice al compilador que vamos a violar el strict aliasing, y así sabe exactamente que debe desactivar las optimizaciones específicas a esta regla (y no otras que podrían o no arreglar estos problemas en un contexto dado). Y todo esto sin los problemas de rendimiento que trae volatile.
« Última modificación: 6 Octubre 2023, 21:29 pm por RayR » En línea

Páginas: 1 [2] Ir Arriba Respuesta Imprimir 

Ir a:  

Mensajes similares
Asunto Iniciado por Respuestas Vistas Último mensaje
Tamaño de variables y campos de bits
PHP
morpheus747 1 4,778 Último mensaje 12 Enero 2009, 04:11 am
por Karman
Memoria en campos de bits
Programación C/C++
Shon 3 3,958 Último mensaje 31 Julio 2010, 21:01 pm
por do-while
tengo una notbook de 64 bits y necesito trabajar con un prog de 32 bits
Programación Visual Basic
twister69 2 3,349 Último mensaje 4 Septiembre 2012, 13:55 pm
por twister69
[Ubuntu] Instalar librerías 32 bits para SO a 64 bits (ia32-libs)
GNU/Linux
#!Mitsu 2 17,148 Último mensaje 29 Noviembre 2014, 23:43 pm
por #!Mitsu
campos de bits + punteros + macros
Programación C/C++
michael_753 1 2,596 Último mensaje 24 Octubre 2023, 14:12 pm
por MAFUS
WAP2 - Aviso Legal - Powered by SMF 1.1.21 | SMF © 2006-2008, Simple Machines