Concepto detrás de estas cuatro líneas de complicado código C
¿Por qué este código da el resultado C++Sucks
? ¿Cuál es el concepto detrás de esto?
#include <stdio.h>
double m[] = {7709179928849219.0, 771};
int main() {
m[1]--?m[0]*=2,main():printf((char*)m);
}
Pruébalo aquí .
El número 7709179928849219.0
tiene la siguiente representación binaria como 64 bits double
:
01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------
+
muestra la posición del letrero; ^
del exponente y -
de la mantisa (es decir, el valor sin el exponente).
Dado que la representación utiliza exponente binario y mantisa, duplicar el número incrementa el exponente en uno. Su programa lo hace precisamente 771 veces, por lo que el exponente que comenzó en 1075 (representación decimal de 10000110011
) se convierte en 1075 + 771 = 1846 al final; La representación binaria de 1846 es 11100110110
. El patrón resultante se ve así:
01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
-------- -------- -------- -------- -------- -------- -------- --------
0x73 's' 0x6B 'k' 0x63 'c' 0x75 'u' 0x53 'S' 0x2B '+' 0x2B '+' 0x43 'C'
Este patrón corresponde al hilo que ves impreso, solo que al revés. Al mismo tiempo, el segundo elemento de la matriz se vuelve cero, proporcionando un terminador nulo, lo que hace que la cadena sea adecuada para pasar a printf()
.
Versión más legible:
double m[2] = {7709179928849219.0, 771};
// m[0] = 7709179928849219.0;
// m[1] = 771;
int main()
{
if (m[1]-- != 0)
{
m[0] *= 2;
main();
}
else
{
printf((char*) m);
}
}
Llama recursivamente main()
771 veces.
Al principio , m[0] = 7709179928849219.0
que significa . C++Suc;C
En cada llamada, m[0]
se duplica, para "reparar" las dos últimas letras. En la última llamada, m[0]
contiene representación de caracteres ASCII C++Sucks
y m[1]
solo contiene ceros, por lo que tiene un terminador nulo para C++Sucks
la cadena. Todo bajo el supuesto de que m[0]
se almacena en 8 bytes, por lo que cada carácter ocupa 1 byte.
Sin recursividad ni main()
llamadas ilegales, se verá así:
double m[] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i++)
{
m[0] *= 2;
}
printf((char*) m);
Descargo de responsabilidad: esta respuesta se publicó en la forma original de la pregunta, que mencionaba solo C++ e incluía un encabezado de C++. La conversión de la pregunta a C puro fue realizada por la comunidad, sin la participación del autor de la pregunta original.
Hablando formalmente, es imposible razonar sobre este programa porque está mal formado (es decir, no es C++ legal). Viola C++11[basic.start.main]p3:
La función main no se utilizará dentro de un programa.
Aparte de esto, se basa en el hecho de que en una computadora de consumo típica, a double
tiene 8 bytes de longitud y utiliza una determinada representación interna bien conocida. Los valores iniciales del array se calculan de forma que cuando se realice el "algoritmo", el valor final del primero double
será tal que la representación interna (8 bytes) serán los códigos ASCII de los 8 caracteres C++Sucks
. El segundo elemento de la matriz es entonces 0.0
, cuyo primer byte está 0
en la representación interna, lo que la convierte en una cadena de estilo C válida. Luego se envía a la salida usando printf()
.
Ejecutar esto en HW donde algo de lo anterior no se cumple daría como resultado texto basura (o tal vez incluso un acceso fuera de límites).
Quizás la forma más sencilla de entender el código sea trabajar a la inversa. Comenzaremos con una cadena para imprimir; para mantener el equilibrio, usaremos "C++Rocks". Punto crucial: al igual que el original, tiene exactamente ocho caracteres. Dado que vamos a hacer (más o menos) lo mismo que el original y lo imprimiremos en orden inverso, comenzaremos poniéndolo en orden inverso. Para nuestro primer paso, simplemente veremos ese patrón de bits como double
e imprimiremos el resultado:
#include <stdio.h>
char string[] = "skcoR++C";
int main(){
printf("%f\n", *(double*)string);
}
Esto produce 3823728713643449.5
. Entonces, queremos manipular eso de alguna manera que no sea obvia, pero que sea fácil de revertir. Elegiré de forma semiarbitraria la multiplicación por 256, lo que nos da 978874550692723072
. Ahora, solo necesitamos escribir un código ofuscado para dividir por 256, luego imprimir los bytes individuales en orden inverso:
#include <stdio.h>
double x [] = { 978874550692723072, 8 };
char *y = (char *)x;
int main(int argc, char **argv){
if (x[1]) {
x[0] /= 2;
main(--x[1], (char **)++y);
}
putchar(*--y);
}
Ahora tenemos muchos castings, pasando argumentos a (recursivo) main
que se ignoran por completo (pero la evaluación para obtener el incremento y el decremento son absolutamente cruciales) y, por supuesto, ese número que parece completamente arbitrario para encubrir el hecho de que lo que estamos haciendo es realmente bastante sencillo.
Por supuesto, dado que el punto es la ofuscación, si nos apetece, también podemos dar más pasos. Solo por ejemplo, podemos aprovechar la evaluación de cortocircuito para convertir nuestra if
declaración en una sola expresión, de modo que el cuerpo de main se vea así:
x[1] && (x[0] /= 2, main(--x[1], (char **)++y));
putchar(*--y);
Para cualquiera que no esté acostumbrado al código ofuscado (y/o al golf de código), esto empieza a parecer bastante extraño: calcular y descartar la lógica and
de algún número de punto flotante sin sentido y el valor de retorno de main
, que ni siquiera devuelve un valor. Peor aún, sin darnos cuenta (ni pensar en) cómo funciona la evaluación de cortocircuito, puede que ni siquiera sea inmediatamente obvio cómo evita la recursión infinita.
Nuestro siguiente paso probablemente sería separar la impresión de cada carácter de la búsqueda de ese carácter. Podemos hacerlo con bastante facilidad generando el carácter correcto como valor de retorno main
e imprimiendo lo que main
devuelve:
x[1] && (x[0] /= 2, putchar(main(--x[1], (char **)++y)));
return *--y;
Al menos a mí eso me parece bastante confuso, así que lo dejaré así.
Simplemente está creando una matriz doble (16 bytes) que, si se interpreta como una matriz de caracteres, genera los códigos ASCII para la cadena "C++Sucks".
Sin embargo, el código no funciona en todos los sistemas, se basa en algunos de los siguientes hechos no definidos:
- doble tiene exactamente 8 bytes
- endianidad