Concetto dietro queste quattro righe di codice C complicato


384

Perché questo codice fornisce l'output C++Sucks? Qual è il concetto alla base?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

int main() {
    m[1]--?m[0]*=2,main():printf((char*)m);    
}

Provalo qui .


1
Tecnicamente @BoBTFish, sì, ma funziona lo stesso in C99: ideone.com/IZOkql
nijansen

12
@nurettin Avevo pensieri simili. Ma non è colpa di OP, sono le persone che votano per questa conoscenza inutile. Ammesso, questa roba di offuscamento del codice può essere interessante ma digita "offuscamento" in Google e otterrai tonnellate di risultati in ogni linguaggio formale che ti viene in mente. Non fraintendetemi, trovo OK fare una domanda del genere qui. È solo una domanda sopravvalutata perché non molto utile.
TobiMcNamobi,

6
@ detonator123 "Devi essere nuovo qui" - se guardi il motivo della chiusura, puoi scoprire che non è così. La comprensione minima richiesta manca chiaramente dalla tua domanda: "Non capisco questo, spiegalo" non è una cosa gradita su StackTranslate.it. Se si è tentato qualcosa di te stesso prima, sarebbe la questione non sono stati chiusi. È banale per google "doppia rappresentazione C" o simili.

42
La mia macchina PowerPC big endian stampa skcuS++C.
Adam Rosenfield,

27
La mia parola, odio domande inventate come questa. È un po 'di modello in memoria che sembra essere lo stesso di una stringa sciocca. Non serve a nessuno scopo utile, eppure guadagna centinaia di punti rep sia per l'interrogante che per chi risponde. Nel frattempo, le domande difficili che potrebbero essere utili alle persone guadagnano forse una manciata di punti, se ce ne sono. Questo è una specie di poster di ciò che non va nella SO.
Carey Gregory,

Risposte:


494

Il numero 7709179928849219.0ha la seguente rappresentazione binaria come 64 bit double:

01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------

+mostra la posizione del segno; ^dell'esponente e -della mantissa (ovvero il valore senza esponente).

Poiché la rappresentazione usa esponente binario e mantissa, il raddoppio del numero aumenta l'esponente di uno. Il tuo programma lo fa esattamente 771 volte, quindi l'esponente che è iniziato a 1075 (rappresentazione decimale di 10000110011) diventa 1075 + 771 = 1846 alla fine; la rappresentazione binaria del 1846 è 11100110110. Il modello risultante è simile al seguente:

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
-------- -------- -------- -------- -------- -------- -------- --------
0x73 's' 0x6B 'k' 0x63 'c' 0x75 'u' 0x53 'S' 0x2B '+' 0x2B '+' 0x43 'C'

Questo modello corrisponde alla stringa che vedi stampata, solo all'indietro. Allo stesso tempo, il secondo elemento dell'array diventa zero, fornendo un terminatore null, rendendo la stringa adatta al passaggio printf().


22
Perché la stringa è al contrario?
Derek,

95
@Derek x86 è little-endian
Angew non è più orgoglioso di SO

16
@Derek Ciò è dovuto alla endianness specifica della piattaforma : i byte della rappresentazione astratta IEEE 754 sono archiviati in memoria a indirizzi decrescenti, quindi la stringa viene stampata correttamente. Su hardware con grande endianness si dovrebbe iniziare con un numero diverso.
dasblinkenlight,

14
@AlvinWong Hai ragione, lo standard non richiede IEEE 754 o altri formati specifici. Questo programma è quasi non portatile, o molto vicino ad esso :-)
dasblinkenlight

10
@GrijeshChauhan Ho usato una calcolatrice IEEE754 a doppia precisione : ho incollato il 7709179928849219valore e recuperato la rappresentazione binaria.
dasblinkenlight,

223

Versione più leggibile:

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);
    }
}

Chiama in modo ricorsivo main()771 volte.

In principio, m[0] = 7709179928849219.0che si distingue per C++Suc;C. In ogni chiamata, m[0]viene raddoppiato, per "riparare" le ultime due lettere. Nell'ultima chiamata, m[0]contiene la rappresentazione in caratteri ASCII C++Suckse m[1]contiene solo zeri, quindi ha un terminatore null per C++Sucksstringa. Tutto sotto presupposto che m[0]è memorizzato su 8 byte, quindi ogni carattere richiede 1 byte.

Senza ricorsione e main()chiamata illegale sembrerà così:

double m[] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i++)
{
    m[0] *= 2;
}
printf((char*) m);

8
È un decremento postfisso. Quindi verrà chiamato 771 volte.
Jack Aidley,

106

Disclaimer: questa risposta è stata pubblicata nella forma originale della domanda, che menzionava solo C ++ e includeva un'intestazione C ++. La conversione della domanda in C pura è stata fatta dalla comunità, senza input da parte del richiedente originale.


Formalmente parlando, è impossibile ragionare su questo programma perché è mal formato (cioè non è C ++ legale). Viola C ++ 11 [basic.start.main] p3:

La funzione principale non deve essere utilizzata all'interno di un programma.

A parte questo, si basa sul fatto che su un tipico computer di consumo, a doubleè lungo 8 byte e utilizza una certa rappresentazione interna ben nota. I valori iniziali dell'array vengono calcolati in modo tale che quando viene eseguito "l'algoritmo", il valore finale del primo doublesarà tale che la rappresentazione interna (8 byte) sarà il codice ASCII degli 8 caratteri C++Sucks. Il secondo elemento dell'array è quindi 0.0, il cui primo byte si trova 0nella rappresentazione interna, rendendola una stringa in stile C valida. Questo viene quindi inviato all'output usando printf().

Eseguirlo su HW dove alcuni dei precedenti non reggerebbero si tradurrebbe invece in immondizia di testo (o forse anche un accesso fuori limite).


25
Devo aggiungere che questa non è un'invenzione di C ++ 11 - C ++ 03 aveva anche basic.start.main3.6.1 / 3 con la stessa formulazione.
sharptooth

1
Il punto di questo piccolo esempio è illustrare cosa si può fare con C ++. Campione magico usando trucchi UB o enormi pacchetti software di codice "classico".
SChepurin,

1
@sharptooth Grazie per averlo aggiunto. Non intendevo implicare altrimenti, ho solo citato lo standard che ho usato.
Angew non è più orgoglioso di SO

@Angew: Sì, lo capisco, volevo solo dire che la formulazione è piuttosto vecchia.
sharptooth,

1
@JimBalter Avviso Ho detto "formalmente parlando, è impossibile ragionare", non "è impossibile ragionare formalmente". Hai ragione sul fatto che è possibile ragionare sul programma, ma è necessario conoscere i dettagli del compilatore utilizzato per farlo. Sarebbe pienamente nei diritti di un compilatore semplicemente eliminare la chiamata main()o sostituirla con una chiamata API per formattare il disco rigido o qualsiasi altra cosa.
Angew non è più orgoglioso di SO

57

Forse il modo più semplice per capire il codice è quello di elaborare le cose al contrario. Inizieremo con una stringa da stampare, per bilancia useremo "C ++ Rocks". Punto cruciale: proprio come l'originale, è lungo esattamente otto caratteri. Dato che faremo (approssimativamente) come l'originale e lo stamperemo in ordine inverso, inizieremo inserendolo in ordine inverso. Per il nostro primo passo, vedremo solo quel modello di bit come a doublee stamperemo il risultato:

#include <stdio.h>

char string[] = "skcoR++C";

int main(){
    printf("%f\n", *(double*)string);
}

Questo produce 3823728713643449.5. Quindi, vogliamo manipolarlo in un modo che non è ovvio, ma è facile da invertire. Sceglierò semi-arbitrariamente la moltiplicazione per 256, il che ci dà 978874550692723072. Ora, abbiamo solo bisogno di scrivere del codice offuscato da dividere per 256, quindi stampare i singoli byte di quello in ordine 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);
}

Ora abbiamo un sacco di casting, passando argomenti (ricorsivi) mainche sono completamente ignorati (ma la valutazione per ottenere l'incremento e il decremento sono assolutamente cruciali), e ovviamente quel numero dall'aspetto completamente arbitrario per nascondere il fatto che ciò che stiamo facendo è davvero molto semplice.

Naturalmente, dato che l'intero punto è l'offuscamento, se ne abbiamo voglia possiamo fare anche più passi. Ad esempio, possiamo trarre vantaggio dalla valutazione del cortocircuito, per trasformare la nostra ifaffermazione in una singola espressione, quindi il corpo del principale appare così:

x[1] && (x[0] /= 2,  main(--x[1], (char **)++y));
putchar(*--y);

A chiunque non sia abituato al codice offuscato (e / o al golf del codice) questo inizia a sembrare piuttosto strano - calcolare e scartare la logica anddi un numero in virgola mobile insignificante e il valore di ritorno da main, che non sta nemmeno restituendo un valore. Peggio ancora, senza rendersi conto (e senza pensare) di come funziona la valutazione del corto circuito, potrebbe anche non essere immediatamente ovvio come evitare la ricorsione infinita.

Il nostro prossimo passo sarebbe probabilmente quello di separare la stampa di ogni personaggio dalla ricerca di quel personaggio. Possiamo farlo abbastanza facilmente generando il carattere giusto come valore di ritorno da maine stampando ciò che mainrestituisce:

x[1] && (x[0] /= 2,  putchar(main(--x[1], (char **)++y)));
return *--y;

Almeno per me, sembra abbastanza offuscato, quindi lo lascerò a quello.


1
Adoro l'approccio forense.
Rykerker,

24

Sta solo costruendo un doppio array (16 byte) che - se interpretato come un array di caratteri - crea i codici ASCII per la stringa "C ++ Sucks"

Tuttavia, il codice non funziona su ciascun sistema, ma si basa su alcuni dei seguenti fatti non definiti:


12

C++Suc;CViene stampato il seguente codice , quindi l'intera moltiplicazione è solo per le ultime due lettere

double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);

11

Gli altri hanno spiegato la domanda abbastanza a fondo, vorrei aggiungere una nota che questo è un comportamento indefinito secondo lo standard.

C ++ 11 3.6.1 / 3 Funzione principale

La funzione principale non deve essere utilizzata all'interno di un programma. Il collegamento (3.5) di main è definito dall'implementazione. Un programma che definisce main come cancellato o che dichiara main come inline, statico o constexpr è mal formato. Il nome principale non è altrimenti riservato. [Esempio: le funzioni membro, le classi e le enumerazioni possono essere chiamate main, così come le entità in altri spazi dei nomi. —Esempio]


1
Direi che è persino mal formato (come ho fatto nella mia risposta) - viola un "deve".
Angew non è più orgoglioso di SO

9

Il codice potrebbe essere riscritto in questo modo:

void f()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        f();
    } else {
          printf((char*)m);
    }
}

Quello che sta facendo è produrre una serie di byte doublenell'array mche corrispondono ai caratteri 'C ++ Sucks' seguiti da un null-terminator. Hanno offuscato il codice scegliendo un doppio valore che quando raddoppiato 771 volte produce, nella rappresentazione standard, quell'insieme di byte con il terminatore nullo fornito dal secondo membro dell'array.

Si noti che questo codice non funzionerebbe con una rappresentazione endian diversa. Inoltre, main()è severamente vietato chiamare .


3
Perché il tuo freso int?
leftaroundabout

1
Ehm, perché ero senza cervello a copiare il intritorno alla domanda. Lascia che lo risolva.
Jack Aidley,

1

Innanzitutto dovremmo ricordare che i numeri a doppia precisione sono memorizzati nella memoria in formato binario come segue:

(i) 1 bit per il segno

(ii) 11 bit per l'esponente

(iii) 52 bit per la grandezza

L'ordine dei bit diminuisce da (i) a (iii).

Innanzitutto il numero decimale frazionario viene convertito in numero binario frazionario equivalente e quindi viene espresso come ordine di grandezza in forma binaria.

Quindi diventa il numero 7709179928849219.0

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

Ora, considerando i bit di magnitudo 1. viene trascurato poiché tutto il metodo dell'ordine di magnitudine deve iniziare con 1.

Quindi la parte magnitudo diventa:

1011011000110111010101010011001010110010101101000011 

Ora la potenza di 2 è 52 , dobbiamo aggiungere un numero di polarizzazione come 2 ^ (bit per esponente -1) -1 cioè 2 ^ (11 -1) -1 = 1023 , quindi il nostro esponente diventa 52 + 1023 = 1075

Ora il nostro codice suddivide il numero con 2 , 771 volte, il che fa aumentare l'esponente di 771

Quindi il nostro esponente è (1075 + 771) = 1846 il cui equivalente binario è (11100110110)

Ora il nostro numero è positivo, quindi il nostro bit di segno è 0 .

Quindi il nostro numero modificato diventa:

segno bit + esponente + magnitudo (semplice concatenazione dei bit)

0111001101101011011000110111010101010011001010110010101101000011 

poiché m viene convertito in puntatore char, divideremo il modello di bit in blocchi di 8 dall'LSD

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(il cui equivalente esadecimale è :)

 0x73 0x6B 0x63 0x75 0x53 0x2B 0x2B 0x43 

DIAGRAMMA ASCII Quale dalla mappa dei caratteri come mostrato è:

s   k   c   u      S      +   +   C 

Ora una volta che questo è stato fatto m [1] è 0 che significa un carattere NULL

Ora supponendo che si esegua questo programma su una macchina little-endian (il bit di ordine inferiore è memorizzato nell'indirizzo inferiore) quindi puntatore m puntatore al bit dell'indirizzo più basso e quindi procede prendendo i bit in mandrini di 8 (come tipo cast per char * ) e printf () si interrompe quando viene rilevato 00000000 nell'ultimo blocco ...

Questo codice non è tuttavia portatile.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.