¿Por qué este código da la salida 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í .
skcuS++C
.
¿Por qué este código da la salida 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í .
skcuS++C
.
Respuestas:
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 signo; ^
del exponente y -
de la mantisa (es decir, el valor sin exponente).
Como la representación usa exponente binario y mantisa, duplicar el número incrementa el exponente en uno. Su programa lo hace con precisión 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 a la cadena que ves impresa, solo al revés. Al mismo tiempo, el segundo elemento de la matriz se convierte en cero, proporcionando un terminador nulo, haciendo que la cadena sea adecuada para pasar a printf()
.
7709179928849219
valor y recuperé la representación binaria.
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.
En el principio, m[0] = 7709179928849219.0
que se destaca por C++Suc;C
. En cada llamada, m[0]
se duplica, para "reparar" las dos últimas letras. En la última llamada, m[0]
contiene la representación de caracteres ASCII C++Sucks
y m[1]
solo contiene ceros, por lo que tiene un terminador nulo para la C++Sucks
cadena. Todo bajo el supuesto de que m[0]
se almacena en 8 bytes, por lo que cada carácter toma 1 byte.
Sin recurrencia y 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 solo mencionaba C ++ e incluía un encabezado de C ++. La conversión de la pregunta a C pura fue realizada por la comunidad, sin aportes del autor de la pregunta original.
Hablando formalmente, es imposible razonar sobre este programa porque está mal formado (es decir, no es legal C ++). 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 una longitud de 8 bytes y utiliza una cierta representación interna bien conocida. Los valores iniciales de la matriz se calculan de modo que cuando se realiza 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 en la matriz es entonces 0.0
, cuyo primer byte está 0
en la representación interna, lo que lo convierte en una cadena de estilo C válida. Esto se envía a la salida usando printf()
.
Ejecutar esto en HW donde algo de lo anterior no se cumple daría como resultado un texto basura (o tal vez incluso un acceso fuera de los límites).
basic.start.main
3.6.1 / 3 con la misma redacción.
main()
, o reemplazarla con una llamada API para formatear el disco duro, o lo que sea.
Quizás la forma más fácil de entender el código es trabajar a la inversa. Comenzaremos con una cadena para imprimir; para equilibrar, usaremos "C ++ Rocks". Punto crucial: al igual que el original, tiene exactamente ocho caracteres de longitud. Como vamos a hacer (aproximadamente) como el original, e imprimirlo en orden inverso, comenzaremos poniéndolo en orden inverso. Para nuestro primer paso, solo veremos ese patrón de bits como a 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é semi-arbitrariamente la multiplicación por 256, lo que nos da 978874550692723072
. Ahora, solo necesitamos escribir un código ofuscado para dividirlo entre 256, luego imprimir los bytes individuales de eso 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 lanzamientos, pasando argumentos a (recursivos) main
que se ignoran por completo (pero la evaluación para obtener el incremento y la disminución son absolutamente cruciales) y, por supuesto, ese número 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 lo deseamos, también podemos tomar más medidas. 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 código de golf), esto comienza a parecer bastante extraño: computar y descartar la lógica and
de algún número de punto flotante sin sentido y el valor de retorno main
, que ni siquiera devuelve un valor. Peor aún, sin darse cuenta (y pensar) cómo funciona la evaluación de cortocircuito, puede que ni siquiera sea inmediatamente obvio cómo evita la recursión infinita.
Nuestro próximo paso probablemente sería separar la impresión de cada personaje de encontrar ese personaje. Podemos hacerlo fácilmente 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 para mí, eso parece lo suficientemente ofuscado, así que lo dejaré así.
Simplemente está creando una matriz doble (16 bytes) que, si se interpreta como una matriz de caracteres, crea los códigos ASCII para la cadena "C ++ Sucks"
Sin embargo, el código no funciona en cada sistema, se basa en algunos de los siguientes hechos no definidos:
Se imprime el siguiente código C++Suc;C
, por lo que toda la multiplicación es solo para las dos últimas letras
double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);
Los demás han explicado la pregunta bastante a fondo, me gustaría agregar una nota de que este es un comportamiento indefinido de acuerdo con el estándar.
C ++ 11 3.6.1 / 3 Función principal
La función main no se utilizará dentro de un programa. El enlace (3.5) de main está definido por la implementación. Un programa que define main como eliminado o que declara main como inline, static o constexpr está mal formado. El nombre principal no está reservado de otra manera. [Ejemplo: las funciones miembro, las clases y las enumeraciones se pueden llamar main, al igual que las entidades en otros espacios de nombres. —Ejemplo]
El código podría reescribirse así:
void f()
{
if (m[1]-- != 0)
{
m[0] *= 2;
f();
} else {
printf((char*)m);
}
}
Lo que está haciendo es producir un conjunto de bytes en la double
matriz m
que corresponde a los caracteres 'C ++ Sucks' seguido de un terminador nulo. Han ofuscado el código eligiendo un valor doble que cuando se duplica 771 veces produce, en la representación estándar, ese conjunto de bytes con el terminador nulo proporcionado por el segundo miembro de la matriz.
Tenga en cuenta que este código no funcionaría bajo una representación endian diferente. Además, las llamadas main()
no están estrictamente permitidas.
f
devolución int
?
int
la respuesta en la pregunta. Déjame arreglar eso.
Primero debemos recordar que los números de doble precisión se almacenan en la memoria en formato binario de la siguiente manera:
(i) 1 bit para el signo
(ii) 11 bits para el exponente
(iii) 52 bits para la magnitud
El orden de los bits disminuye de (i) a (iii).
Primero, el número fraccionario decimal se convierte en un número binario fraccionario equivalente y luego se expresa en forma de orden de magnitud en binario.
Entonces el número 7709179928849219.0 se convierte en
(11011011000110111010101010011001010110010101101000011)base 2
=1.1011011000110111010101010011001010110010101101000011 * 2^52
Ahora, considerando los bits de magnitud 1. se descuida, ya que todo el método de orden de magnitud comenzará con 1.
Entonces la parte de magnitud se convierte en:
1011011000110111010101010011001010110010101101000011
Ahora la potencia de 2 es 52 , debemos agregarle un número de polarización como 2 ^ (bits para el exponente -1) -1, es decir, 2 ^ (11 -1) -1 = 1023 , por lo que nuestro exponente se convierte en 52 + 1023 = 1075
Ahora nuestro código multiplica el número con 2 , 771 veces, lo que hace que el exponente aumente en 771
Entonces nuestro exponente es (1075 + 771) = 1846 cuyo equivalente binario es (11100110110)
Ahora nuestro número es positivo, por lo que nuestro bit de signo es 0 .
Entonces nuestro número modificado se convierte en:
bit de signo + exponente + magnitud (concatenación simple de los bits)
0111001101101011011000110111010101010011001010110010101101000011
como m se convierte en puntero de caracteres, dividiremos el patrón de bits en fragmentos de 8 del LSD
01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
(cuyo equivalente hexadecimal es :)
0x73 0x6B 0x63 0x75 0x53 0x2B 0x2B 0x43
Cuál del mapa de caracteres como se muestra es:
s k c u S + + C
Ahora, una vez hecho esto, m [1] es 0, lo que significa un carácter NULO
Ahora, suponiendo que ejecute este programa en una máquina little-endian (el bit de orden inferior se almacena en la dirección inferior), de modo que el puntero m apunte al bit de dirección más bajo y luego continúe tomando bits en mandriles de 8 (como tipo insertado en char * ) y printf () se detiene cuando se encuentra 00000000 en el último fragmento ...
Sin embargo, este código no es portátil.