Proyecto de fin de semana: un reto divertido en lenguaje C
(lcamtuf.substack.com)- C es simple y expresivo, pero solo con la sentencia
switchy las reglas de las etiquetas se puede escribir código capaz de desconcertar incluso a desarrolladores experimentados - La ofuscación al estilo IOCCC oculta el código con macros y formateo, pero incluso con la propia sintaxis de C se puede lograr código legible pero extraño
switch (...)no requiere llaves obligatoriamente y, como salta a la etiquetacaseque coincida, puede comportarse distinto al flujo normal de inicialización de bloques- Las etiquetas
caseno tienen que estar solo en el nivel superior del bloqueswitch, así que también compila unswitchsin llaves que combinaif (0) case...con una cadena deelse if - La extensión
&&de dirección de etiqueta en GNU C permite incluso hacer unswitchmanual o bucles basados en etiquetas, pero algunos ejemplos son exclusivos de GCC y pueden no ser seguros frente a comportamiento indefinido
Código extraño creado por la sintaxis de C
- C tiene muchos defectos, pero gracias a su sintaxis simple y su expresividad se usa como un lenguaje con el que se puede escribir software grande como sistemas operativos
- Su sintaxis concisa también influyó en la estructura del código de muchos lenguajes posteriores de uso masivo, desde Java hasta Go
- Un caso representativo de ofuscación de código es IOCCC
- Las obras ganadoras del IOCCC suelen estar compuestas por macros del preprocesador, formateo poco convencional, nombres de variables nada útiles y expresiones aritméticas enrevesadas
- Ese tipo de código puede ser admirable, pero como hay que hacer ingeniería inversa hasta llevarlo a código normal, puede resultar menos divertido para aprender
Los rincones profundos de C que revelan switch, case y goto
switch (...)puede escribirse sin llaves, igual queif (...)ofor (...)switch (i) case 1: puts("i = 1");compila sin problema- Si no hay llaves, solo una sentencia queda asociada al
switch, por lo que elcase 2:que siga ya no está dentro delswitchy provoca un error
- En esencia,
switches una estructura más cercana agotoque salta a la etiquetacasecoincidente- Aunque dentro del bloque
switchhayaint a = 123;yputs(...), si salta adefault:, el código de inicialización anterior no se ejecuta - En ese caso,
ano se inicializa con 123 y, técnicamente, eso es comportamiento indefinido - El ejemplo puede verse en Godbolt
- Aunque dentro del bloque
- Las etiquetas
caseno tienen que estar solo en el nivel superior del bloqueswitchasociado- También funciona una forma como
if (0) case 0: puts("i = 0"); - Como el
switchsalta directamente a esecase, elif (0)anterior se omite - Después de ejecutar ese
puts(...), otras sentencias de salida posteriores siguen bloqueadas por la condiciónif (0), así que se puede evitar el fallthrough incluso sinbreak - El ejemplo puede verse en Godbolt
- También funciona una forma como
- Como una cadena
if ... elsefunciona sintácticamente como una sola sentencia de nivel superior, puede combinarse conswitchpara crear unswitchrarísimo sin llaves- El ejemplo incluye el
casede rangocase 1 ... 10y tambiéndefault - El ejemplo puede verse en Godbolt
- El ejemplo incluye el
- El operador
&&, una extensión de GNU, permite obtener la dirección de una etiqueta y hacergotoa esa dirección- Con eso puede implementarse un
switchmanual usando algo comogoto *(void*[]){ &&case_0, &&case_1, &&case_2 }[i]; - El ejemplo puede verse en Godbolt
- Con eso puede implementarse un
- Usando la misma extensión de GNU, también se puede implementar un bucle basado en etiquetas dentro de una declaración de variables, incluso sin
for (...)- El ejemplo construye un bucle que imprime desde
i = 0hastai = 5usando etiquetas ygoto *&&_ - El ejemplo puede verse en Godbolt
- Este último fragmento es exclusivo de GCC y podría no ser seguro frente a comportamiento indefinido
- El ejemplo construye un bucle que imprime desde
- En C, incluso sin ofuscar el código intencionalmente, se pueden crear formas completamente extrañas y confusas
1 comentarios
Opiniones en Hacker News
El ejemplo anterior dice que imprime el valor de
a, pero que no se inicializa en 123; sin embargo, en C en realidad podría pasar eso.Usar una variable no inicializada no significa leer “el valor que por casualidad quedó en esa memoria”, sino que es comportamiento indefinido, así que el compilador puede hacer lo que quiera.
Por ejemplo, podría inicializar esa memoria siempre en 123, o considerar que todo el fragmento tiene comportamiento indefinido y eliminar todas las instrucciones para que no imprima nada. Más aún, también puede optimizar eliminando el
returnposterior o incluso instrucciones anteriores, de modo que el comportamiento indefinido puede parecer que afecta “hacia atrás en el tiempo”.Puede afectar instrucciones anteriores, pero la reordenación de código y las transformaciones complejas ocurren incluso sin comportamiento indefinido.
Los objetos estáticos siempre se inicializan, así que esa situación no se da.
Lo que queda son objetos dinámicos, como miembros no inicializados de una estructura asignada con
malloc; leer memoria dinámica no inicializada no es comportamiento indefinido en C, sino que se obtiene el valor que representen los bits no inicializados. Si ese tipo no tiene representaciones trampa, no puede fallar.También es posible una sintaxis de este estilo:
switch(k) { if (0) case 0: x = 1; if (0) case 1: x = 2; if (0) default: x = 3; }Se puede armar algo parecido a un
switchen el que no haga falta ponerbreakal final de cada cláusula, e incluso usar una macro como#define brkcase if (0) case. Al compilador probablemente no le guste el flujo de control, pero en general creo que lo eliminará bien.#define brkcase break;casefuncionaría de forma parecida, pero entonces se pierde un poco el propósito de la macro.gotoque abusar de unswitchal estilo Duff's device.casees de una sola línea o está envuelto entre llaves.Hace tiempo usé esta estructura con el sentido de “saltar solo la primera línea de la siguiente etiqueta
casey hacer fallthrough con el resto tal cual”.Si se ve una etiqueta
casesimplemente como una etiqueta, y no como un separador entre sentencias, todo tiene sentido.Esta técnica puede usarse para implementar corrutinas en C: https://stackoverflow.com/questions/24202890/switch-based-co...
No sé por qué no sabía que
case 1 ... 10:era C válido.Usé C durante años, y me da curiosidad de qué estándar salió esto.
No encontré la historia de la extensión y, hasta donde sé, no existe en C estándar. No estoy seguro sobre
clang.Hace tiempo, por diversión, escribí un código raro en C que hacía una cuenta regresiva de 10 a 1.
Las versiones en C, Python y shell pueden hacerse en una sola línea usando
&&y llamadas recursivas.sys.stdout.write(f"{n}\n")puede reemplazarse porprint(n).El código actual, salvo por el
f-string, parece estilo Python 2, cuandoprintera una sentencia. En Python 3,printes una función normal y devuelveNone, que se evalúa como falso, así que el primerandtambién tendría que cambiarse poror.Otra cosa sorprendente es que
4[arr]es lo mismo quearr[4].*(array_label+offset), y aquí pasa a ser*(offset+array_label).Es decir,
*(arr+4)y*(4+arr)son lo mismo.arr[i][j]yj[i[arr]]también son exactamente lo mismo.Solo hay que saber que
a[x][y]es lo mismo que(a[x])[y], y quea[x]es lo mismo quex[a].arr[i][j]→(arr[i])[j]→(i[arr])[j]→j[i[arr]]El último fragmento de código ofuscado del artículo muestra otra extensión de GCC: https://stackoverflow.com/questions/34559705/ternary-conditi...
Vi primero en Twitter estas técnicas juguetonas del autor del blog.
También se pueden hacer bucles con sentencias
switch: https://twitter.com/lcamtuf/status/1807129116980007037Creo que estas travesuras con
switchson una parte importante de Duff's device.switchcon un bucle, en la elección de que loscasehagan fallthrough si no se usabreakexplícitamente, y en que en C un bucle puede saltar de vuelta hacia dentro de unswitch.Duff intentaba optimizar E/S mapeada en memoria (MMIO), y hoy no se haría así ni siquiera en C. Ahora la MMIO no es tan rápida como las instrucciones de CPU, y si hay aunque sea un poco más de datos se puede usar DMA.
Un lenguaje moderno tampoco trataría la MMIO como una simple indirección de puntero, pero C ha seguido agregando mecanismos de escape al sistema de tipos para mantener eso.
Personalmente, creo que lo que continúa el “Device” de Tom Duff es el mecanismo de iterate loops de WUFFS. Es una forma de especificar cómo desenrollar parcialmente N pasos de un bucle, prometiendo el mismo resultado que ejecutar el cuerpo principal del bucle N veces, pero potencialmente más rápido. La vectorización puede entender mejor la intención, y los casos límite molestos como
M % N != 0quedan a cargo de una herramienta, no de una persona, para manejarlos correctamente.