까다로운 C 코드의이 네 줄에 대한 개념


384

이 코드는 왜 출력을 제공 C++Sucks합니까? 그 뒤에 개념은 무엇입니까?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

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

여기에서 테스트 하십시오 .


1
네, 기술적으로 @BoBTFish하지만 모든 C99에서 같은 실행 : ideone.com/IZOkql을
nijansen

12
@ nurettin 나는 비슷한 생각을했습니다. 그러나 이것은 OP의 잘못이 아니며, 쓸모없는 지식에 투표하는 사람들입니다. 물론이 코드 난독 화는 흥미로울 수 있지만 Google에서는 '난독 화'를 입력하면 생각할 수있는 모든 공식 언어로 수많은 결과를 얻을 수 있습니다. 나를 잘못 이해하지 마라. 여기서 그런 질문을해도 괜찮습니다. 유용한 질문이 아니기 때문에 과대 평가되었습니다.
TobiMcNamobi

6
@ detonator123 "당신은 여기에 새로 와야합니다"-폐쇄 이유를 보면, 그것이 사실이 아니라는 것을 알 수 있습니다. 필요한 최소한의 이해가 귀하의 질문에서 분명히 누락되었습니다. "이것을 이해하지 못하고 설명하십시오"는 스택 오버플로에서 환영받는 것이 아닙니다. 당신이 뭔가를 시도해야 자신 첫째, 문제는 폐쇄되지 않은 것입니다. 구글 "이중 표현 C"등은 사소한 일입니다.

42
내 빅 엔디안 PowerPC 머신이 출력됩니다 skcuS++C.
Adam Rosenfield 3

27
내 말은, 나는 이런 생각을 제기 질문이 싫어. 그것은 어리석은 문자열과 같은 메모리의 비트 패턴입니다. 그것은 누구에게도 유용한 목적을 제공하지는 않지만 질문자와 응답자 모두에게 수백 개의 담당자를 얻습니다. 한편, 사람들에게 유용한 어려운 질문은 몇 가지 포인트가있을 수 있습니다. 이것은 SO에 문제가있는 포스터 아이의 일종입니다.
Carey Gregory

답변:


494

숫자 7709179928849219.0는 64 비트로 다음 이진 표현을 갖습니다 double.

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

+기호의 위치를 ​​보여줍니다. ^지수와 -가수의 (즉 지수가없는 값).

이 표현은 이진 지수와 가수를 사용하므로 숫자를 두 배로 늘리면 지수가 1 씩 증가합니다. 프로그램은 정확히 771 번 수행하므로 1075 (10 진수로 표시 10000110011) 에서 시작된 지수 는 1075 + 771 = 1846이됩니다. 1846의 이진 표현은입니다 11100110110. 결과 패턴은 다음과 같습니다.

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

이 패턴은 인쇄 된 문자열에 해당하며 뒤로 만 나타납니다. 동시에 배열의 두 번째 요소는 0이되어 null 종결자를 제공하여 문자열을에 전달하기에 적합합니다 printf().


22
왜 문자열이 거꾸로 되나요?
Derek

95
@Derek x86은 리틀 엔디안입니다
Angew는 더 이상 SO

16
@Derek 플랫폼 고유의 엔디안 (endianness ) 때문입니다. 추상 IEEE 754 표현의 바이트는 감소하는 주소의 메모리에 저장되므로 문자열이 올바르게 인쇄됩니다. 엔디안이 큰 하드웨어에서는 다른 숫자로 시작해야합니다.
dasblinkenlight

14
@AlvinWong 맞습니다. 표준에는 IEEE 754 또는 기타 특정 형식이 필요하지 않습니다. 이 :-) 여기에 매우 근접하는, 또는으로이 프로그램에 대한 비 이식
dasblinkenlight

10
@GrijeshChauhan 배정 밀도 IEEE754 계산기를 사용했습니다 . 7709179928849219값을 붙여넣고 이진 표현을 다시 얻었습니다.
dasblinkenlight

223

더 읽기 쉬운 버전 :

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

재귀 적으로 main()771 번 호출합니다 .

처음에는 m[0] = 7709179928849219.0의미 합니다 C++Suc;C. 모든 통화에서 m[0]마지막 두 글자를 "수리"하기 위해 두 배가됩니다. 마지막 호출에서 m[0]의 아스키 문자 표현을 포함 C++Sucks하고 m[1]그것이 가지고, 그래서 0 만 포함 널 종결 에 대한 C++Sucks문자열입니다. m[0]8 바이트에 저장된 모든 가정하에 각 문자는 1 바이트를 사용합니다.

재귀와 불법 main()전화가 없으면 다음과 같이 보일 것입니다.

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

8
접미사 감소입니다. 따라서 771 번이라고합니다.
잭 에이 들리

106

면책 조항 : 이 답변은 C ++ 만 언급하고 C ++ 헤더를 포함하는 원래 형식의 질문에 게시되었습니다. 질문을 순수 C로 변환 한 것은 원래 질문의 입력없이 커뮤니티에서 수행했습니다.


공식적으로 말하면,이 프로그램은 형식이 잘못되어 (즉, 합법적 인 C ++이 아니기 때문에)이 프로그램에 대해 추론 할 수 없습니다. C ++ 11 [basic.start.main] p3을 위반합니다 :

프로그램 내에서 기능 메인을 사용해서는 안됩니다.

이를 제외하고는 일반적인 소비자 컴퓨터에서 double길이가 8 바이트이며 잘 알려진 특정 내부 표현을 사용 한다는 사실에 의존합니다 . 배열의 초기 값은 "알고리즘"이 수행 double될 때 첫 번째의 최종 값이 내부 표현 (8 바이트)이 8 자의 ASCII 코드가되도록 계산됩니다 C++Sucks. 그런 다음 배열의 두 번째 요소는 0.0입니다. 첫 번째 바이트는 0내부 표현에 있으므로 유효한 C 스타일 문자열이됩니다. 그런 다음을 사용하여 출력으로 전송됩니다 printf().

위의 일부가 유지되지 않는 HW에서 이것을 실행하면 가비지 텍스트 (또는 경계를 벗어난 액세스)가 대신 발생합니다.


25
나는 이것이 C ++ 11의 발명품이 아니라는 것을 덧붙여 야한다. C ++ 03도 basic.start.main같은 단어로 3.6.1 / 3을 가졌다 .
sharptooth

1
이 작은 예제의 요점은 C ++로 수행 할 수있는 작업을 설명하는 것입니다. UB 트릭이나 "고전적인"코드의 거대한 소프트웨어 패키지를 사용하는 매직 샘플.
SChepurin

1
@sharptooth 추가해 주셔서 감사합니다. 나는 달리 암시한다는 의미는 아니며, 내가 사용한 표준을 인용했다.
Angew는 더 이상 SO

@Angew : 그래, 나는 그 말이 꽤 오래되었다고 말하고 싶었다.
sharptooth

1
@JimBalter 공지 "공식적으로 추론하는 것은 불가능합니다."가 아니라 "공식적으로 말하면 추론 하기가 불가능합니다." 프로그램에 대해 추론하는 것이 가능하지만, 그렇게하는 데 사용 된 컴파일러의 세부 사항을 알아야합니다. 에 대한 호출을 간단히 제거 하거나 하드 드라이브 또는 기타 형식을 지정하기 위해 API 호출로 대체하는 것은 컴파일러의 권리main()속합니다.
문헌 : Angew는 더 이상 자랑 SO의없는

57

코드를 이해하는 가장 쉬운 방법은 반대로 작업하는 것입니다. 인쇄 할 문자열로 시작하겠습니다. 균형을 위해 "C ++ Rocks"를 사용합니다. 중요한 점 : 원본과 마찬가지로 정확히 8 자입니다. 원본과 거의 비슷하게 인쇄하고 역순으로 인쇄하기 때문에 역순으로 시작합니다. 첫 번째 단계에서는 비트 패턴을로보고 double결과를 인쇄합니다.

#include <stdio.h>

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

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

이 생성합니다 3823728713643449.5. 따라서 우리는 명확하지 않지만 반전하기 쉬운 방식으로 조작하고 싶습니다. 256으로 곱셈을 임의로 임의로 선택하겠습니다 978874550692723072. 이제 난독 화 코드를 작성하여 256으로 나눈 다음 해당 바이트의 개별 바이트를 역순으로 인쇄하면됩니다.

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

이제 우리 main는 완전히 무시 되는 (재귀 적) 인수를 전달하는 많은 캐스팅을 가지고 있지만 (증가 및 감소를 얻는 평가는 완전히 중요합니다) 물론 우리가하고있는 사실을 커버하기 위해 완전히 임의의 숫자 정말 간단합니다.

물론 요점은 난독 화이기 때문에 느낌이 들면 더 많은 조치를 취할 수 있습니다. 예를 들어 단락 평가를 활용하여 if명령문을 단일 표현식으로 변환 할 수 있으므로 기본 본문은 다음과 같습니다.

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

난독 처리 된 코드 (및 / 또는 코드 골프)에 익숙하지 않은 사람에게는 이것이 실제로 이상한 것처럼 보이기 시작합니다- and의미없는 부동 소수점 수 의 논리 와main . 값. 더 나쁜 것은 단락 평가가 어떻게 작동 하는지를 생각하지 않고 생각하지 않고서도 무한 재귀를 피하는 방법을 즉시 알지 못할 수도 있습니다.

다음 단계는 각 문자를 인쇄하는 것과 해당 문자를 찾는 것을 분리하는 것입니다. 에서 올바른 문자를 반환 값으로 생성하고 반환 main되는 내용을 인쇄하여 쉽게 수행 할 수 있습니다 main.

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

적어도 나에게는 그것이 난독 화 된 것처럼 보이므로 그것을 그대로 두겠습니다.


1
법의학 접근법을 사랑하십시오.
ryyker

24

문자 배열로 해석되면 문자열 "C ++ Sucks"에 대한 ASCII 코드를 작성하는 이중 배열 (16 바이트)을 작성하는 것입니다.

그러나 코드는 각 시스템에서 작동하지 않으며 다음과 같은 정의되지 않은 사실 중 일부에 의존합니다.

  • double은 정확히 8 바이트입니다
  • 엔디안

12

다음 코드는 인쇄 C++Suc;C되므로 전체 곱셈은 마지막 두 글자에만 해당됩니다.

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

11

다른 사람들은 질문을 아주 철저하게 설명했습니다 . 표준에 따라 정의되지 않은 동작 이라는 점을 추가하고 싶습니다 .

C ++ 11 3.6.1 / 3 주요 기능

프로그램 내에서 기능 메인을 사용해서는 안됩니다. main의 연결 (3.5)은 구현에 따라 정의됩니다. main을 삭제 된 것으로 정의하거나 main을 인라인, 정적 또는 constexpr로 선언하는 프로그램은 형식이 잘못되었습니다. main이라는 이름은 예약되어 있지 않습니다. [예 : 다른 네임 스페이스의 엔터티와 마찬가지로 멤버 함수, 클래스 및 열거를 main이라고 할 수 있습니다. — 끝 예제]


1
나는 그것이 (내 대답에서했던 것처럼) 잘못 형성되었다고 말하고 싶습니다.
문헌 : Angew는 더 이상 자랑 SO의없는

9

코드는 다음과 같이 다시 작성할 수 있습니다.

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

그것이하고 있는 일은 double배열 에서 바이트 m'C ++ Sucks'에 해당하는 null 집합이 오는 바이트 세트를 생성하는 것입니다. 그들은 771 배가 두 배가 될 때 표준 표현에서 배열의 두 번째 멤버가 제공하는 null 종결자가있는 바이트 세트를 생성하는 double 값을 선택하여 코드를 난독 화했습니다.

이 코드는 다른 엔디안 표현에서는 작동하지 않습니다. 또한 전화 main()는 엄격하게 허용되지 않습니다.


3
왜 당신의 f반환은 int?
leftaroundabout

1
어, 'cos 나는 int그 질문에 대한 답을 머리없이 복사했습니다 . 그 문제를 해결하겠습니다.
Jack Aidley

1

먼저 배정도 숫자는 다음과 같이 이진 형식으로 메모리에 저장됩니다.

(i) 부호를위한 1 비트

(ii) 지수에 대한 11 비트

(iii) 크기에 대한 52 비트

비트 순서는 (i)에서 (iii)으로 감소합니다.

먼저 소수 소수는 등가 소수 이진수로 변환 된 다음 이진수로 크기 순서로 표시됩니다.

따라서 숫자 7709179928849219.0

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

이제 모든 비트 순서 방법이 1로 시작 하므로 크기 비트 1 을 고려하는 동안 무시됩니다 .

따라서 크기 부분은 다음과 같습니다.

1011011000110111010101010011001010110010101101000011 

이제 2 의 거듭 제곱 은 52 이므로 2 ^ (지수 -1의 비트) -12 ^ (11 -1) -1 = 1023 으로 바이어 싱 수를 추가해야합니다 . 따라서 지수는 52 + 1023 = 1075가됩니다.

이제 우리의 코드는 2 , 771 배로 숫자를 곱 하면 지수가 771 만큼 증가합니다.

따라서 지수는 (1075 + 771) = 1846 이고 이의 등가는 (11100110110)

이제 우리의 숫자는 양수이므로 부호 비트는 0 입니다.

수정 된 숫자는 다음과 같습니다.

부호 비트 + 지수 + 크기 (비트의 간단한 연결)

0111001101101011011000110111010101010011001010110010101101000011 

m은 char 포인터로 변환되기 때문에 비트 패턴을 LSD에서 8의 청크로 나눕니다.

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(16 진수에 해당하는 것은 :)

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

ASCII 차트 다음과 같이 문자표에서 다음 중 하나입니다.

s   k   c   u      S      +   +   C 

이것이 만들어지면 m [1]은 0입니다. 이것은 NULL 문자를 의미합니다

이제 리틀 엔디안 머신 에서이 프로그램을 실행한다고 가정하면 (낮은 순서 비트는 낮은 주소에 저장 됨) 포인터 m은 가장 낮은 주소 비트를 가리키고 8의 척에서 비트를 차지하여 진행됩니다 (char로 캐스팅 된 유형으로 * ) 및 마지막 청크에서 00000000이 발생하면 printf ()가 중지됩니다 ...

그러나이 코드는 이식성이 없습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.