Concepto detrás de estas cuatro líneas de código C complicado

384

¿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í .

codelayer1
fuente
1
@BoBTFish técnicamente, sí, pero funciona igual en C99: ideone.com/IZOkql
nijansen
12
@nurettin Tenía pensamientos similares. Pero no es culpa de OP, son las personas que votan por este conocimiento inútil. Admitido, este código de ofuscación puede ser interesante, pero escriba "ofuscación" en Google y obtendrá toneladas de resultados en todos los idiomas formales que se le ocurran. No me malinterpreten, me parece bien hacer esa pregunta aquí. Sin embargo, es una pregunta sobrevalorada porque no es muy útil.
TobiMcNamobi
66
@ detonator123 "Debe ser nuevo aquí": si observa el motivo del cierre, puede descubrir que ese no es el caso. La comprensión mínima requerida está claramente ausente en su pregunta: "No entiendo esto, explíquelo" no es algo que sea bienvenido en Stack Overflow. En caso de que haya intentado algo a sí mismo en primer lugar, la cuestión sería que no se han cerrado. Es trivial para google "doble representación C" o similar.
42
Mi máquina PowerPC big-endian se imprime skcuS++C.
Adam Rosenfield
27
Mi palabra, odio las preguntas artificiales como esta. Es un patrón de bits en la memoria que resulta ser el mismo que una cadena tonta. No tiene ningún propósito útil para nadie y, sin embargo, gana cientos de puntos de repetición tanto para el interrogador como para el que responde. Mientras tanto, las preguntas difíciles que podrían ser útiles para las personas ganan, tal vez, un puñado de puntos. Esto es una especie de cartel secundario de lo que está mal con SO.
Carey Gregory

Respuestas:

494

El número 7709179928849219.0tiene 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().

dasblinkenlight
fuente
22
¿Por qué la cuerda está al revés?
Derek
95
@Derek x86 es little-endian
Angew ya no está orgulloso de SO
16
@Derek Esto se debe a la plataforma específica endianness : los bytes de la representación abstracta IEEE 754 se almacenan en la memoria en direcciones decrecientes, por lo que las impresiones de cuerda correctamente. En hardware con gran endianness, uno debería comenzar con un número diferente.
dasblinkenlight
14
@AlvinWong Tiene razón, el estándar no requiere IEEE 754 ni ningún otro formato específico. Este programa es tan poco portátil como parece, o muy cercano a él :-)
dasblinkenlight
10
@GrijeshChauhan Usé una calculadora IEEE754 de doble precisión : pegué el 7709179928849219valor y recuperé la representación binaria.
dasblinkenlight
223

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.0que 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++Sucksy m[1]solo contiene ceros, por lo que tiene un terminador nulo para la C++Suckscadena. 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);
Adam Stelmaszczyk
fuente
8
Es decremento de postfix. Entonces se llamará 771 veces.
Jack Aidley
106

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 doubletiene 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 doubleserá 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á 0en 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).

Angew ya no está orgulloso de SO
fuente
25
Tengo que agregar que esto no es una invención de C ++ 11 - C ++ 03 también tenía basic.start.main3.6.1 / 3 con la misma redacción.
Sharptooth
1
El objetivo de este pequeño ejemplo es ilustrar lo que se puede hacer con C ++. Muestra mágica usando trucos UB o enormes paquetes de software de código "clásico".
SChepurin
1
@sharptooth Gracias por agregar esto. No quise dar a entender lo contrario, solo cité el estándar que usé.
Angew ya no está orgulloso de SO
@Angew: Yeap, entiendo eso, solo quería decir que la redacción es bastante antigua.
Sharptooth
1
Aviso de @JimBalter Dije "hablando formalmente, es imposible razonar", no "es imposible razonar formalmente". Tiene razón en que es posible razonar sobre el programa, pero necesita conocer los detalles del compilador utilizado para hacerlo. Estaría completamente dentro de los derechos de un compilador simplemente eliminar la llamada a main(), o reemplazarla con una llamada API para formatear el disco duro, o lo que sea.
Angew ya no está orgulloso de SO
57

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 doublee 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) mainque 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 ifdeclaració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 andde 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 maine imprimiendo lo que maindevuelve:

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í.

Jerry Coffin
fuente
1
Me encanta el enfoque forense.
ryyker
24

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:

DR
fuente
12

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);
Servir Laurijssen
fuente
11

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]

Yu Hao
fuente
1
Yo diría que incluso está mal formado (como lo hice en mi respuesta), viola un "deber".
Angew ya no está orgulloso de SO
9

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 doublematriz mque 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.

Jack Aidley
fuente
3
¿Por qué tu fdevolución int?
Leftaroundabout
1
Er, porque estaba sin cerebro copiando intla respuesta en la pregunta. Déjame arreglar eso.
Jack Aidley
1

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 

TABLA DE ASCII 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.

Abhishek Ghosh
fuente