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 .
c
deobfuscation
Codelayer1
źródło
źródło
skcuS++C
.Odpowiedzi:
Liczba
7709179928849219.0
ma następującą reprezentację binarną jako wersję 64-bitowądouble
:+
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 jest11100110110
. Powstały wzór wygląda następująco: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()
.źródło
7709179928849219
wartość, a dostał reprezentacji dwójkowej plecy.Bardziej czytelna wersja:
Wzywa rekurencyjnie
main()
771 razy.Na początku
m[0] = 7709179928849219.0
, który stoi zaC++Suc;C
. W każdym połączenium[0]
podwaja się, aby „naprawić” ostatnie dwie litery. W ostatnim wywołanium[0]
zawiera znak ASCII reprezentującyC++Sucks
im[1]
zawiera tylko zera, więc ma zerowy terminator dlaC++Sucks
łańcucha. Wszystko przy założeniu, żem[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:źródło
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:
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ść pierwszegodouble
będzie taka, że wewnętrzna reprezentacja (8 bajtów) będzie kodami ASCII 8 znakówC++Sucks
. Drugi element w tablicy jest wtedy0.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).
źródło
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: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 nam978874550692723072
. Teraz wystarczy napisać zaciemniony kod, aby podzielić przez 256, a następnie wydrukować poszczególne jego bajty w odwrotnej kolejności: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: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 odmain
, 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, comain
zwraca:Przynajmniej dla mnie to wydaje się dość zaciemnione, więc zostawię to przy tym.
źródło
Po prostu buduje podwójną tablicę (16 bajtów), która - interpretowana jako tablica char - buduje kody ASCII dla ciągu „C ++ Sucks”
Jednak kod nie działa w każdym systemie, opiera się na niektórych z następujących niezdefiniowanych faktów:
źródło
Zostanie wydrukowany następujący kod
C++Suc;C
, więc całe mnożenie dotyczy tylko dwóch ostatnich literźródło
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
źródło
Kod można przepisać w następujący sposób:
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.źródło
f
zwrotint
?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
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ę:
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)
ponieważ m jest konwertowane na wskaźnik char, podzielimy wzór bitowy na 8 części z LSD
(którego ekwiwalent szesnastkowy to :)
Który z mapy postaci, jak pokazano, to:
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.
źródło