Dlaczego ten kod daje wynik C++Sucks
? Jaka jest za tym koncepcja?
#include <stdio.h>
double m[] = {7709179928849219.0, 771};
int main() {
m[1]--?m[0]*=2,main():printf((char*)m);
}
Sprawdź to tutaj .
skcuS++C
.
Dlaczego ten kod daje wynik C++Sucks
? Jaka jest za tym koncepcja?
#include <stdio.h>
double m[] = {7709179928849219.0, 771};
int main() {
m[1]--?m[0]*=2,main():printf((char*)m);
}
Sprawdź to tutaj .
skcuS++C
.
Odpowiedzi:
Liczba 7709179928849219.0
ma następującą reprezentację binarną jako wersję 64-bitową double
:
01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------
+
pokazuje pozycję znaku; ^
wykładnika i -
mantysy (tj. wartości bez wykładnika).
Ponieważ reprezentacja wykorzystuje wykładnik binarny i mantysę, podwojenie liczby zwiększa wykładnik o jeden. Twój program robi to dokładnie 771 razy, więc wykładnik, który zaczął się od 1075 (dziesiętna reprezentacja 10000110011
), staje się na końcu 1075 + 771 = 1846; binarna reprezentacja 1846 jest 11100110110
. Powstały wzór wygląda następująco:
01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
-------- -------- -------- -------- -------- -------- -------- --------
0x73 's' 0x6B 'k' 0x63 'c' 0x75 'u' 0x53 'S' 0x2B '+' 0x2B '+' 0x43 'C'
Ten wzór odpowiada drukowanemu ciągowi znaków, tylko do tyłu. W tym samym czasie drugi element tablicy staje się zerowy, co oznacza zerowy terminator, dzięki czemu ciąg jest odpowiedni do przekazania printf()
.
7709179928849219
wartość, a dostał reprezentacji dwójkowej plecy.
Bardziej czytelna wersja:
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);
}
}
Wzywa rekurencyjnie main()
771 razy.
Na początku m[0] = 7709179928849219.0
, który stoi za C++Suc;C
. W każdym połączeniu m[0]
podwaja się, aby „naprawić” ostatnie dwie litery. W ostatnim wywołaniu m[0]
zawiera znak ASCII reprezentujący C++Sucks
i m[1]
zawiera tylko zera, więc ma zerowy terminator dla C++Sucks
łańcucha. Wszystko przy założeniu, że m[0]
jest przechowywane na 8 bajtach, więc każdy znak zajmuje 1 bajt.
Bez rekurencji i nielegalnych main()
połączeń będzie to wyglądać następująco:
double m[] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i++)
{
m[0] *= 2;
}
printf((char*) m);
Zastrzeżenie: Ta odpowiedź została zamieszczona w oryginalnej formie pytania, w której wspomniano tylko o C ++ i zawierała nagłówek C ++. Społeczność dokonała konwersji pytania na czyste C bez udziału pierwotnego pytającego.
Formalnie rzecz biorąc, nie można wnioskować o tym programie, ponieważ jest źle sformułowany (tzn. Nie jest legalnym C ++). Narusza C ++ 11 [basic.start.main] p3:
Funkcja main nie powinna być używana w programie.
Poza tym opiera się na fakcie, że na typowym komputerze konsumenckim a double
ma 8 bajtów i wykorzystuje pewną dobrze znaną reprezentację wewnętrzną. Początkowe wartości tablicy są obliczane tak, że gdy wykonywany jest „algorytm”, końcowa wartość pierwszego double
będzie taka, że wewnętrzna reprezentacja (8 bajtów) będzie kodami ASCII 8 znaków C++Sucks
. Drugi element w tablicy jest wtedy 0.0
, którego pierwszy bajt znajduje się 0
w wewnętrznej reprezentacji, co czyni go prawidłowym łańcuchem w stylu C. Jest on następnie wysyłany do wyjścia za pomocą printf()
.
Uruchomienie tego na HW, gdzie niektóre z powyższych nie ma miejsca, spowoduje zamiast tego tekst śmieci (lub nawet dostęp poza granicami).
basic.start.main
3.6.1 / 3 o tym samym brzmieniu.
main()
lub zastąpić je wywołaniem API, aby sformatować dysk twardy lub cokolwiek innego.
Być może najłatwiejszym sposobem na zrozumienie kodu jest praca w odwrotnej kolejności. Zaczniemy od łańcucha do wydrukowania - dla równowagi użyjemy „C ++ Rocks”. Kluczowy punkt: dokładnie tak jak oryginał, ma dokładnie osiem znaków. Ponieważ zrobimy (z grubsza) jak oryginał i wydrukujemy go w odwrotnej kolejności, zaczniemy od umieszczenia go w odwrotnej kolejności. W naszym pierwszym kroku po prostu zobaczymy ten wzór bitowy jako double
i wydrukujemy wynik:
#include <stdio.h>
char string[] = "skcoR++C";
int main(){
printf("%f\n", *(double*)string);
}
To produkuje 3823728713643449.5
. Chcemy więc manipulować tym w sposób, który nie jest oczywisty, ale łatwo go odwrócić. Pół arbitralnie wybiorę mnożenie przez 256, co daje nam 978874550692723072
. Teraz wystarczy napisać zaciemniony kod, aby podzielić przez 256, a następnie wydrukować poszczególne jego bajty w odwrotnej kolejności:
#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);
}
Teraz mamy wiele rzutowania, przekazywania argumentów do (rekurencyjnego), main
które są całkowicie ignorowane (ale ocena, aby uzyskać przyrost i spadek są absolutnie kluczowe), i oczywiście ta całkowicie arbitralnie wyglądająca liczba, aby ukryć fakt, że to, co robimy jest naprawdę bardzo proste.
Oczywiście, ponieważ cała sprawa dotyczy zaciemnienia, jeśli mamy na to ochotę, możemy również podjąć więcej kroków. Na przykład możemy skorzystać z oceny zwarć, aby zamienić nasze if
stwierdzenie w jedno wyrażenie, więc główna część wygląda tak:
x[1] && (x[0] /= 2, main(--x[1], (char **)++y));
putchar(*--y);
Dla każdego, kto nie jest przyzwyczajony do zaciemniania kodu (i / lub kodu golfa), zaczyna to naprawdę wyglądać dość dziwnie - obliczanie i odrzucanie logiki and
jakiejś bezsensownej liczby zmiennoprzecinkowej i wartości zwracanej od main
, która nawet nie zwraca wartość. Co gorsza, bez uświadomienia sobie (i zastanowienia się), jak działa ocena zwarcia, może nawet nie być od razu oczywiste, w jaki sposób unika nieskończonej rekurencji.
Naszym następnym krokiem byłoby prawdopodobnie oddzielne wydrukowanie każdej postaci od jej znalezienia. Możemy to zrobić dość łatwo, generując odpowiedni znak jako wartość zwrotną main
i drukując to, co main
zwraca:
x[1] && (x[0] /= 2, putchar(main(--x[1], (char **)++y)));
return *--y;
Przynajmniej dla mnie to wydaje się dość zaciemnione, więc zostawię to przy tym.
Zostanie wydrukowany następujący kod C++Suc;C
, więc całe mnożenie dotyczy tylko dwóch ostatnich liter
double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);
Inni wyjaśnili pytanie dość dokładnie, chciałbym dodać uwagę, że jest to zachowanie niezdefiniowane zgodnie ze standardem.
C ++ 11 3.6.1 / 3 Funkcja główna
Funkcja main nie powinna być używana w programie. Połączenie (3.5) main jest zdefiniowane w implementacji. Program, który definiuje main jako usunięty lub deklaruje main jako wbudowany, statyczny lub constexpr, jest źle sformułowany. Nazwa main nie jest inaczej zastrzeżona. [Przykład: funkcje składowe, klasy i wyliczenia można nazwać main, podobnie jak encje w innych przestrzeniach nazw. —Przykład]
Kod można przepisać w następujący sposób:
void f()
{
if (m[1]-- != 0)
{
m[0] *= 2;
f();
} else {
printf((char*)m);
}
}
To, co robi, tworzy zestaw bajtów w double
tablicy, m
które przypadkowo odpowiadają znakom „C ++ Sucks”, po których następuje null-terminator. Ukryli kod, wybierając podwójną wartość, która podwojona 771 razy daje w standardowej reprezentacji ten zestaw bajtów z terminatorem zerowym dostarczonym przez drugi element tablicy.
Zauważ, że ten kod nie działałby pod inną reprezentacją endian. Również dzwonienie main()
nie jest ściśle dozwolone.
f
zwrot int
?
int
zwrot z pytania. Pozwól mi to naprawić.
Najpierw powinniśmy przypomnieć, że liczby podwójnej precyzji są przechowywane w pamięci w formacie binarnym w następujący sposób:
(i) 1 bit dla znaku
(ii) 11 bitów dla wykładnika
(iii) 52 bity dla wielkości
Kolejność bitów zmniejsza się z (i) do (iii).
Najpierw dziesiętna liczba ułamkowa jest konwertowana na równoważną ułamkową liczbę binarną, a następnie jest wyrażana jako postać wielkości w formacie binarnym.
Tak więc staje się liczba 7709179928849219.0
(11011011000110111010101010011001010110010101101000011)base 2
=1.1011011000110111010101010011001010110010101101000011 * 2^52
Teraz rozważając bity jasności 1. jest zaniedbywany, ponieważ cała metoda rzędu wielkości powinna zaczynać się od 1.
Więc część wielkości staje się:
1011011000110111010101010011001010110010101101000011
Teraz potęga 2 wynosi 52 , musimy dodać do niej liczbę odchylenia jako 2 ^ (bity dla wykładnika -1) -1, tj. 2 ^ (11 -1) -1 = 1023 , więc nasz wykładnik staje się 52 + 1023 = 1075
Teraz nasz kod mnoży liczbę 2 , 771 razy, co powoduje wzrost wykładnika o 771
Zatem naszym wykładnikiem jest (1075 + 771) = 1846, którego ekwiwalent binarny to (11100110110)
Teraz nasza liczba jest dodatnia, więc nasz bit znaku ma wartość 0 .
Nasz zmodyfikowany numer staje się:
znak bitu + wykładnik + wielkość (prosta konkatenacja bitów)
0111001101101011011000110111010101010011001010110010101101000011
ponieważ m jest konwertowane na wskaźnik char, podzielimy wzór bitowy na 8 części z LSD
01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
(którego ekwiwalent szesnastkowy to :)
0x73 0x6B 0x63 0x75 0x53 0x2B 0x2B 0x43
Który z mapy postaci, jak pokazano, to:
s k c u S + + C
Teraz, gdy to zostanie zrobione, m [1] wynosi 0, co oznacza znak NULL
Teraz zakładając, że uruchamiasz ten program na maszynie little-endian (bit niższego rzędu jest przechowywany w dolnym adresie), więc wskaźnik m wskazuje na najniższy bit adresu, a następnie kontynuuje przyjmowanie bitów w uchwytach po 8 (jak typ rzutowany na char * ), a printf () zatrzymuje się, gdy napotka 00000000 w ostatnim chunck ...
Ten kod nie jest jednak przenośny.