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 .
skcuS++C
.
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 .
skcuS++C
.
Risposte:
Il numero 7709179928849219.0
ha 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()
.
7709179928849219
valore e recuperato la rappresentazione binaria.
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.0
che 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++Sucks
e m[1]
contiene solo zeri, quindi ha un terminatore null per C++Sucks
stringa. 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);
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 double
sarà 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 0
nella 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).
basic.start.main
3.6.1 / 3 con la stessa formulazione.
main()
o sostituirla con una chiamata API per formattare il disco rigido o qualsiasi altra cosa.
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 double
e 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) main
che 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 if
affermazione 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 and
di 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 main
e stampando ciò che main
restituisce:
x[1] && (x[0] /= 2, putchar(main(--x[1], (char **)++y)));
return *--y;
Almeno per me, sembra abbastanza offuscato, quindi lo lascerò a quello.
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:
C++Suc;C
Viene stampato il seguente codice , quindi l'intera moltiplicazione è solo per le ultime due lettere
double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);
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]
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 double
nell'array m
che 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 .
f
reso int
?
int
ritorno alla domanda. Lascia che lo risolva.
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
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.