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 .
skcuS++C
.
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 .
skcuS++C
.
Câu trả lời:
Số 7709179928849219.0
có 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()
.
7709179928849219
giá trị và nhận lại biểu diễn nhị phân.
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++Sucks
và m[1]
chỉ chứa các số 0, do đó, nó có một bộ kết thúc null cho C++Sucks
chuỗ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);
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 double
dà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 double
sẽ 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 0
trong 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).
basic.start.main
3.6.1 / 3 với cách diễn đạt tương tự.
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ì.
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 double
và 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 if
câ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ừ main
và in ra những gì main
trả 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ó ở đó.
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:
Đ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);
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ụ
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 double
mả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.
f
trở lại một int
?
int
trở lại trong câu hỏi. Hãy để tôi sửa nó.
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 2 là 52 , 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
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.