Khái niệm đằng sau bốn dòng mã C phức tạp này


384

Tại sao mã này cho đầu ra C++Sucks? Khái niệm đằng sau nó là gì?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

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

Kiểm tra nó ở đây .


1
@BoBTFish về mặt kỹ thuật, vâng, nhưng nó chạy hoàn toàn giống nhau trong C99: ideone.com/IZOkql
nijansen

12
@nurettin Mình cũng có suy nghĩ tương tự. Nhưng đó không phải là lỗi của OP, đó là những người bỏ phiếu cho kiến ​​thức vô dụng này. Phải thừa nhận rằng, công cụ mã hóa này có thể rất thú vị nhưng hãy gõ "obfuscation" trong Google và bạn nhận được vô số kết quả bằng mọi ngôn ngữ chính thức mà bạn có thể nghĩ tới. Đừng hiểu sai ý tôi, tôi thấy ổn khi đặt câu hỏi như vậy ở đây. Nó chỉ là một đánh giá cao bởi vì câu hỏi không hữu ích mặc dù.
TobiMcNamobi

6
@ Detonator123 "Bạn phải là người mới ở đây" - nếu bạn nhìn vào lý do đóng cửa, bạn có thể phát hiện ra rằng đó không phải là trường hợp. Sự hiểu biết tối thiểu cần thiết rõ ràng bị thiếu trong câu hỏi của bạn - "Tôi không hiểu điều này, giải thích nó" không phải là điều được hoan nghênh trên Stack Overflow. Nên bạn đã cố gắng một cái gì đó cho mình đầu tiên, sẽ là câu hỏi chưa được đóng lại. Thật tầm thường khi google "đại diện kép C" hoặc tương tự.

42
Máy PowerPC lớn của tôi in ra skcuS++C.
Adam Rosenfield

27
Từ của tôi, tôi ghét những câu hỏi như thế này. Đó là một mô hình bit trong bộ nhớ xảy ra giống như một chuỗi ngớ ngẩn. Nó không phục vụ mục đích hữu ích cho bất kỳ ai, và nó kiếm được hàng trăm điểm đại diện cho cả người hỏi và người trả lời. Trong khi đó, những câu hỏi khó có thể hữu ích cho mọi người kiếm được có thể là một số điểm, nếu có. Đây là một loại con của những gì sai với SO.
Carey Gregory

Câu trả lời:


494

Số 7709179928849219.0có biểu diễn nhị phân sau dưới dạng 64 bit double:

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

+cho thấy vị trí của dấu hiệu; ^của số mũ và -của mantissa (tức là giá trị không có số mũ).

Vì biểu diễn sử dụng số mũ nhị phân và mantissa, nhân đôi số tăng số mũ lên một. Chương trình của bạn thực hiện chính xác 771 lần, do đó, số mũ bắt đầu từ 1075 (biểu diễn thập phân của 10000110011) trở thành 1075 + 771 = 1846 ở cuối; đại diện nhị phân của năm 1846 là 11100110110. Mẫu kết quả trông như thế này:

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

Mẫu này tương ứng với chuỗi mà bạn thấy được in, chỉ ngược lại. Đồng thời, phần tử thứ hai của mảng trở thành 0, cung cấp bộ kết thúc null, làm cho chuỗi phù hợp để chuyển đếnprintf() .


22
Tại sao chuỗi ngược?
Derek

95
@Derek x86 là một endian nhỏ
Angew không còn tự hào về SO

16
@ Derek Điều này là do trong những nền tảng cụ thể endianness : các byte của trừu tượng IEEE 754 đại diện được lưu trữ trong bộ nhớ tại địa chỉ giảm, vì vậy các bản in chuỗi một cách chính xác. Trên phần cứng có độ bền lớn, người ta sẽ cần bắt đầu với một số khác.
dasblinkenlight

14
@AlvinWong Bạn đã đúng, tiêu chuẩn không yêu cầu IEEE 754 hoặc bất kỳ định dạng cụ thể nào khác. Chương trình này không phải là di động như nó nhận được, hoặc rất gần với nó :-)
dasblinkenlight

10
@GrijeshChauhan Tôi đã sử dụng máy tính IEEE754 có độ chính xác kép : Tôi đã dán 7709179928849219giá trị và nhận lại biểu diễn nhị phân.
dasblinkenlight

223

Phiên bản dễ đọc hơn:

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

Nó gọi đệ quy main() 771 lần.

Trong đầu m[0] = 7709179928849219.0, đó là viết tắt của C++Suc;C. Trong mỗi cuộc gọi, m[0]được nhân đôi, để "sửa chữa" hai chữ cái cuối cùng. Trong cuộc gọi cuối cùng, m[0]chứa biểu diễn char của ASCII C++Sucksm[1]chỉ chứa các số 0, do đó, nó có một bộ kết thúc null cho C++Suckschuỗi. Tất cả theo giả định rằngm[0] được lưu trữ trên 8 byte, vì vậy mỗi char mất 1 byte.

Nếu không có đệ quy và main()gọi bất hợp pháp, nó sẽ trông như thế này:

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

8
Đó là sự suy giảm postfix. Vì vậy, nó sẽ được gọi là 771 lần.
Jack Aidley

106

Tuyên bố miễn trừ trách nhiệm: Câu trả lời này đã được đăng lên dạng ban đầu của câu hỏi, chỉ đề cập đến C ++ và bao gồm một tiêu đề C ++. Chuyển đổi câu hỏi sang C thuần túy được thực hiện bởi cộng đồng, không có đầu vào từ người hỏi ban đầu.


Nói một cách chính thức, không thể lập luận về chương trình này vì nó không đúng định dạng (tức là nó không hợp pháp C ++). Nó vi phạm C ++ 11 [basic.start.main] p3:

Chức năng chính sẽ không được sử dụng trong một chương trình.

Điều này sang một bên, nó dựa trên thực tế là trên một máy tính tiêu dùng thông thường, a doubledài 8 byte và sử dụng một biểu diễn bên trong nổi tiếng nhất định. Các giá trị ban đầu của mảng được tính toán để khi "thuật toán" được thực hiện, giá trị cuối cùng của giá trị đầu tiên doublesẽ sao cho biểu diễn bên trong (8 byte) sẽ là mã ASCII của 8 ký tự C++Sucks. Phần tử thứ hai trong mảng sau đó 0.0, có byte đầu tiên nằm 0trong biểu diễn bên trong, biến đây thành chuỗi kiểu C hợp lệ. Điều này sau đó được gửi đến đầu ra bằng cách sử dụng printf().

Chạy cái này trên CTNH, nơi một số thứ ở trên không giữ được sẽ dẫn đến văn bản rác (hoặc thậm chí có thể truy cập ngoài giới hạn).


25
Tôi phải nói thêm rằng đây không phải là một phát minh của C ++ 11 - C ++ 03 cũng có basic.start.main3.6.1 / 3 với cách diễn đạt tương tự.
sharptooth

1
Điểm của ví dụ nhỏ này là để minh họa những gì có thể được thực hiện với C ++. Mẫu ma thuật sử dụng thủ thuật UB hoặc gói phần mềm khổng lồ của mã "cổ điển".
SChepurin

1
@sharptooth Cảm ơn bạn đã thêm điều này. Tôi không có ý ám chỉ khác, tôi chỉ trích dẫn tiêu chuẩn tôi đã sử dụng.
Angew không còn tự hào về SO

@Angew: Yeap, tôi hiểu điều đó, chỉ muốn nói rằng từ ngữ này khá cũ.
sharptooth

1
@JimBalter Lưu ý Tôi đã nói "chính thức nói, không thể lý luận", không phải "không thể chính thức lý do." Bạn đúng rằng có thể suy luận về chương trình, nhưng bạn cần biết chi tiết về trình biên dịch được sử dụng để làm điều đó. Sẽ hoàn toàn nằm trong quyền của nhà soạn nhạc khi chỉ cần loại bỏ lệnh gọi main()hoặc thay thế nó bằng lệnh gọi API để định dạng ổ cứng hoặc bất cứ thứ gì.
Angew không còn tự hào về SO

57

Có lẽ cách dễ nhất để hiểu mã là làm việc thông qua những thứ ngược lại. Chúng tôi sẽ bắt đầu với một chuỗi để in ra - để cân bằng, chúng tôi sẽ sử dụng "Đá C ++". Điểm quan trọng: giống như bản gốc, nó dài chính xác tám ký tự. Vì chúng tôi sẽ làm (đại khái) như bản gốc và in nó ra theo thứ tự ngược lại, chúng tôi sẽ bắt đầu bằng cách đặt nó theo thứ tự ngược lại. Đối với bước đầu tiên của chúng tôi, chúng tôi sẽ chỉ xem mẫu bit đó như một doublevà in ra kết quả:

#include <stdio.h>

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

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

Điều này tạo ra 3823728713643449.5. Vì vậy, chúng tôi muốn thao túng điều đó theo một cách nào đó không rõ ràng, nhưng lại dễ dàng đảo ngược. Tôi sẽ tùy ý chọn phép nhân với 256, cung cấp cho chúng tôi 978874550692723072. Bây giờ, chúng ta chỉ cần viết một số mã bị xáo trộn để chia cho 256, sau đó in ra các byte riêng lẻ theo thứ tự ngược lại:

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

Bây giờ chúng ta có rất nhiều lần truyền, chuyển các đối số sang (đệ quy) main hoàn toàn bị bỏ qua (nhưng đánh giá để có được mức tăng và giảm là cực kỳ quan trọng), và tất nhiên là con số hoàn toàn tùy ý để che đậy sự thật rằng chúng ta đang làm gì là thực sự khá đơn giản.

Tất nhiên, vì toàn bộ vấn đề là obfuscation, nếu chúng ta cảm thấy như vậy, chúng ta có thể thực hiện nhiều bước nữa. Ví dụ, chúng ta có thể tận dụng đánh giá ngắn mạch, để biến ifcâu lệnh của chúng ta thành một biểu thức duy nhất, vì vậy phần chính của nó trông như thế này:

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

Đối với bất kỳ ai không quen với mã bị xáo trộn (và / hoặc mã golf), điều này thực sự bắt đầu trông khá kỳ lạ - tính toán và loại bỏ logic and của một số số dấu phẩy động vô nghĩa và giá trị trả về từmain đó thậm chí không trả về giá trị. Tồi tệ hơn, mà không nhận ra (và suy nghĩ về) cách thức đánh giá ngắn mạch hoạt động, thậm chí có thể không rõ ràng ngay lập tức làm thế nào nó tránh được đệ quy vô hạn.

Bước tiếp theo của chúng tôi có lẽ là tách biệt việc in từng ký tự khỏi việc tìm kiếm ký tự đó. Chúng ta có thể làm điều đó khá dễ dàng bằng cách tạo đúng ký tự làm giá trị trả về từ mainvà in ra những gì maintrả về:

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

Ít nhất với tôi, điều đó dường như đủ xáo trộn, vì vậy tôi sẽ để nó ở đó.


1
Yêu cách tiếp cận pháp y.
ryyker

24

Nó chỉ xây dựng một mảng kép (16 byte) mà - nếu được hiểu là mảng char - xây dựng mã ASCII cho chuỗi "C ++ Sucks"

Tuy nhiên, mã không hoạt động trên mỗi hệ thống, nó dựa vào một số sự kiện không xác định sau:


12

Đoạn mã sau in C++Suc;C, vì vậy toàn bộ phép nhân chỉ dành cho hai chữ cái cuối

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

11

Những người khác đã giải thích câu hỏi khá kỹ lưỡng, tôi muốn thêm một lưu ý rằng đây là hành vi không xác định theo tiêu chuẩn.

C ++ 11 3.6.1 / 3 Chức năng chính

Chức năng chính sẽ không được sử dụng trong một chương trình. Liên kết (3.5) của chính được xác định theo thực hiện. Một chương trình xác định chính là đã xóa hoặc tuyên bố chính là nội tuyến, tĩnh hoặc constexpr không được định dạng. Tên chính không được bảo lưu. [Ví dụ: các hàm thành viên, các lớp và liệt kê có thể được gọi là chính, cũng như các thực thể trong các không gian tên khác. Ví dụ


1
Tôi muốn nói rằng nó thậm chí còn không đúng định dạng (như tôi đã làm trong câu trả lời của mình) - nó vi phạm "sẽ".
Angew không còn tự hào về SO

9

Mã này có thể được viết lại như thế này:

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

Những gì nó đang làm là tạo ra một tập hợp byte trong doublemảngm xảy ra tương ứng với các ký tự 'C ++ Sucks', theo sau là một bộ kết thúc null. Họ đã làm xáo trộn mã bằng cách chọn một giá trị kép mà khi nhân đôi 771 lần tạo ra, trong biểu diễn tiêu chuẩn, tập hợp byte đó với bộ kết thúc null được cung cấp bởi thành viên thứ hai của mảng.

Lưu ý rằng mã này sẽ không hoạt động dưới một đại diện endian khác. Ngoài ra, gọi điện thoại main()không được phép nghiêm ngặt.


3
Tại sao bạn ftrở lại một int?
leftaroundabout

1
Er, 'tôi đã vô thức sao chép sự inttrở lại trong câu hỏi. Hãy để tôi sửa nó.
Jack Aidley

1

Trước tiên, chúng ta nên nhớ rằng các số chính xác kép được lưu trữ trong bộ nhớ ở định dạng nhị phân như sau:

(i) 1 bit cho dấu hiệu

(ii) 11 bit cho số mũ

(iii) 52 bit cho độ lớn

Thứ tự của các bit giảm từ (i) xuống (iii).

Đầu tiên, số phân số thập phân được chuyển đổi thành số nhị phân phân số tương đương và sau đó nó được biểu thị dưới dạng thứ tự của độ lớn trong nhị phân.

Vì vậy, số 7709179928849219.0 trở thành

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

Bây giờ trong khi xem xét các bit cường độ 1. bị bỏ qua vì tất cả thứ tự của phương pháp cường độ sẽ bắt đầu bằng 1.

Vì vậy, phần cường độ trở thành:

1011011000110111010101010011001010110010101101000011 

Bây giờ sức mạnh của 252 , chúng ta cần thêm số thiên vị cho nó là 2 ^ (bit cho số mũ -1) -1 tức là 2 ^ (11 -1) -1 = 1023 , vì vậy số mũ của chúng tôi trở thành 52 + 1023 = 1075

Bây giờ mã của chúng tôi nhân đôi số với 2 , 771 lần, làm cho số mũ tăng thêm 771

Vậy số mũ của chúng tôi là (1075 + 771) = 1846 có số nhị phân tương đương là (11100110110)

Bây giờ số của chúng tôi là dương nên bit dấu của chúng tôi là 0 .

Vì vậy, số sửa đổi của chúng tôi trở thành:

dấu bit + số mũ + độ lớn (cách ghép đơn giản của các bit)

0111001101101011011000110111010101010011001010110010101101000011 

vì m được chuyển đổi thành con trỏ char, chúng ta sẽ chia mẫu bit thành các khối 8 từ LSD

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(có Hex tương đương :)

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

SƠ ĐỒ ASCII Mà từ bản đồ nhân vật như được hiển thị là:

s   k   c   u      S      +   +   C 

Bây giờ một khi điều này đã được thực hiện m [1] là 0 có nghĩa là một ký tự NULL

Bây giờ giả sử rằng bạn chạy chương trình này trên một máy cuối nhỏ (bit thứ tự thấp hơn được lưu trữ ở địa chỉ thấp hơn), do đó, con trỏ m đến bit địa chỉ thấp nhất và sau đó tiến hành bằng cách lấy các bit trong số 8 (như kiểu được đúc thành char * ) và printf () dừng lại khi gặp 00000000 trong chunck cuối cùng ...

Mã này tuy nhiên không phải là di động.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.