Mã C khó hiểu này tuyên bố chạy mà không có hàm main (), nhưng nó thực sự làm được gì?


84
#include <stdio.h>
#define decode(s,t,u,m,p,e,d) m##s##u##t
#define begin decode(a,n,i,m,a,t,e)

int begin()
{
    printf("Ha HA see how it is?? ");
}

Cái này có gọi là gián tiếp mainkhông? làm sao?


146
Các macro được xác định mở rộng bắt đầu nói "chính". Nó chỉ là một thủ thuật. Không có gì thú vị.
rghome

10
Toolchain của bạn nên có một tùy chọn để lại mã preprocessed xung quanh trong một tập tin - file thực tế được biên soạn - nơi bạn sẽ nhìn thấy nó, trên thực tế, có một main ()

@rghome Tại sao không đăng dưới dạng câu trả lời? Và nó rõ ràng là thú vị, với số lượng ủng hộ.
Matsemann

3
@Matsemann Chà! Tôi đã không nhận thấy các phiếu bầu. Tôi có thể thay đổi nó thành một câu trả lời, và nếu lượt bình chọn tăng lên là câu trả lời - thì đó sẽ là điểm số tốt nhất của tôi, nhưng đã có một câu trả lời chi tiết. Tôi nghĩ rằng điểm nhận xét của tôi là nó không thực sự thú vị và do đó nó hoạt động như một sự thay thế cho những người không muốn bỏ phiếu cho câu trả lời. Cảm ơn vì đã chỉ ra nó.
rghome

Các bạn, Trình liên kết là một công cụ của hệ điều hành để thiết lập điểm vào, chứ không phải bản thân ngôn ngữ. Bạn thậm chí có thể đặt điểm vào của riêng chúng tôi, và bạn có thể tạo một thư viện cũng có thể thực thi được! unix.stackexchange.com/a/223415/37799
Ho1

Câu trả lời:


193

Ngôn ngữ C định nghĩa môi trường thực thi theo hai loại: tự dođược lưu trữ . Trong cả hai môi trường thực thi, một hàm được gọi bởi môi trường khởi động chương trình.
Trong môi trường tự do, chức năng khởi động chương trình có thể được xác định trong khi trong môi trường được lưu trữ, nó phải như vậy main. Không chương trình nào trong C có thể chạy mà không có chức năng khởi động chương trình trên các môi trường đã xác định.

Trong trường hợp của bạn, mainbị ẩn bởi các định nghĩa tiền xử lý. begin()sẽ mở rộng đến decode(a,n,i,m,a,t,e)mà sẽ được mở rộng hơn nữa main.

int begin() -> int decode(a,n,i,m,a,t,e)() -> int m##a##i##n() -> int main() 

decode(s,t,u,m,p,e,d)là một macro được tham số hóa với 7 tham số. Danh sách thay thế cho macro này là m##s##u##t. m, s, ut4 thứ , 1 st , 3 thứ và 2 nd tham số được sử dụng trong danh sách thay thế.

s, t, u, m, p, e, d
1  2  3  4  5  6  7

Phần còn lại không có ích gì ( chỉ để làm rối loạn ). Đối số được chuyển tới decodelà " a , n , i , m , a, t, e" vì vậy, các định danh m, s, utđược thay thế bằng các đối số m, a, intương ứng.

 m --> m  
 s --> a 
 u --> i 
 t --> n

11
@GrijeshChauhan tất cả các trình biên dịch C xử lý macro, nó được yêu cầu bởi tất cả các tiêu chuẩn C kể từ C89.
jdarthenay

17
Điều đó rõ ràng là sai. Trên Linux tôi có thể sử dụng _start(). Hoặc cấp thấp hơn nữa, tôi có thể cố gắng căn chỉnh phần bắt đầu chương trình của mình với địa chỉ mà IP được đặt sau khi khởi động. main()thư viện C Standard . Bản thân C không áp đặt hạn chế về điều này.
ljrk

1
@haccks Thư viện chuẩn xác định một điểm vào. Ngôn ngữ riêng của mình không quan tâm
ljrk

3
Bạn có thể vui lòng giải thích làm thế nào decode(a,n,i,m,a,t,e)trở thành m##a##i##n? Nó có thay thế các ký tự không? Bạn có thể cung cấp liên kết đến tài liệu của decodehàm không? Cảm ơn.
AL

1
@AL Đầu tiên beginđược định nghĩa để được thay thế bởi decode(a,n,i,m,a,t,e)cái được định nghĩa trước đó. Hàm này nhận các đối số s,t,u,m,p,e,dvà nối chúng ở dạng này m##s##u##t( ##có nghĩa là nối). Tức là, nó bỏ qua các giá trị của p, e và d. Khi bạn "gọi" decodevới s = a, t = n, u = i, m = m, nó sẽ thay thế beginbằng main.
ljrk

71

Hãy thử sử dụng gcc -E source.c, đầu ra kết thúc bằng:

int main()
{
    printf("Ha HA see how it is?? ");
}

Vì vậy, một main()hàm thực sự được tạo ra bởi bộ tiền xử lý.


37

Chương trình được đề cập thực hiện cuộc gọi main()do mở rộng macro, nhưng giả định của bạn là thiếu sót - nó hoàn toàn không phải gọi main()!

Nói một cách chính xác, bạn có thể có một chương trình C và có thể biên dịch nó mà không cần mainký hiệu. mainlà thứ mà đối tượng c librarymong đợi sẽ nhảy vào, sau khi nó hoàn thành quá trình khởi tạo của chính nó. Thông thường bạn nhảy vào maintừ biểu tượng libc được gọi là _start. Luôn luôn có thể có một chương trình rất hợp lệ, chỉ đơn giản thực hiện hợp ngữ mà không cần có một chương trình chính. Hãy xem này:

/* This must be compiled with the flag -nostdlib because otherwise the
 * linker will complain about multiple definitions of the symbol _start
 * (one here and one in glibc) and a missing reference to symbol main
 * (that the libc expects to be linked against).
 */

void
_start ()
{
    /* calling the write system call, with the arguments in this order:
     * 1. the stdout file descriptor
     * 2. the buffer we want to print (Here it's just a string literal).
     * 3. the amount of bytes we want to write.
     */
    asm ("int $0x80"::"a"(4), "b"(1), "c"("Hello world!\n"), "d"(13));
    asm ("int $0x80"::"a"(1), "b"(0)); /* calling exit syscall, with the argument to be 0 */
}

Biên dịch ở trên với gcc -nostdlib without_main.cvà xem nó in Hello World!trên màn hình chỉ bằng cách đưa ra lệnh gọi hệ thống (ngắt) trong lắp ráp nội tuyến.

Để biết thêm thông tin về vấn đề cụ thể này, hãy xem blog ksplice

Một vấn đề thú vị khác là bạn cũng có thể có một chương trình biên dịch mà không cần mainký hiệu tương ứng với một hàm C. Ví dụ, bạn có thể có phần sau là một chương trình C rất hợp lệ, chương trình này chỉ làm cho trình biên dịch rên rỉ khi bạn tăng cấp Cảnh báo.

/* These values are extracted from the decimal representation of the instructions
 * of a hello world program written in asm, that gdb provides.
 */
const int main[] = {
    -443987883, 440, 113408, -1922629632,
    4149, 899584, 84869120, 15544,
    266023168, 1818576901, 1461743468, 1684828783,
    -1017312735
};

Các giá trị trong mảng là các byte tương ứng với các hướng dẫn cần thiết để in Hello World trên màn hình. Để có tài khoản chi tiết hơn về cách chương trình cụ thể này hoạt động, hãy xem bài đăng trên blog này , đây là nơi tôi cũng đọc nó đầu tiên.

Tôi muốn thông báo cuối cùng về các chương trình này. Tôi không biết liệu họ có đăng ký là các chương trình C hợp lệ theo đặc tả ngôn ngữ C hay không, nhưng việc biên dịch các chương trình này và chạy chúng chắc chắn là rất khả thi, ngay cả khi chúng vi phạm chính đặc tả.


1
Là tên của _startmột phần của tiêu chuẩn đã xác định, hay đó chỉ là việc thực hiện cụ thể? Chắc chắn "chính như một mảng" của bạn là kiến ​​trúc cụ thể. Cũng quan trọng, sẽ không vô lý nếu thủ thuật "chính dưới dạng mảng" của bạn không thành công trong thời gian chạy do các hạn chế về bảo mật (mặc dù điều đó sẽ có nhiều khả năng xảy ra hơn nếu bạn không sử dụng bộ định lượng constvà vẫn có nhiều hệ thống cho phép nó).
mah,

1
@mah: _startkhông có trong tiêu chuẩn ELF, mặc dù AMD64 psABI có chứa tham chiếu đến Khởi tạo quy trình_start tại 3.4 . Về mặt chính thức, ELF chỉ biết về địa chỉ e_entrytrong tiêu đề ELF, _startchỉ là một cái tên mà triển khai đã chọn.
ninjalj

1
@mah Cũng quan trọng, sẽ không vô lý nếu thủ thuật "chính dưới dạng mảng" của bạn không thành công trong thời gian chạy do các hạn chế bảo mật (mặc dù điều đó sẽ có nhiều khả năng xảy ra hơn nếu bạn không sử dụng bộ định lượng const và vẫn có nhiều hệ thống cho phép nó). Chỉ khi tệp thực thi cuối cùng theo một cách nào đó có thể phân biệt được như một thứ gì đó không an toàn - tệp thực thi nhị phân là tệp thực thi nhị phân bất kể nó đến đó bằng cách nào. Và constsẽ không quan trọng một chút nào - tên biểu tượng trong tệp thực thi nhị phân đó là main. Không nhiều không ít. constlà một cấu trúc C có nghĩa là không có gì tại thời điểm thực thi.
Andrew Henle

1
@Stewart: chắc chắn nó không thành công trên ARMv6l (lỗi phân đoạn). Nhưng nó sẽ hoạt động trên bất kỳ kiến ​​trúc x86-64 nào.
bùng binh trái

@AndrewHenle một tệp thực thi nhị phân là một tệp thực thi nhị phân bất kể nó đến đó bằng cách nào - không hoàn toàn đúng. Một tệp thực thi nhị phân không phải là một khối lệnh thực thi đơn lẻ, nó là một khối phân vùng được ánh xạ cẩn thận, một số trong số đó là hướng dẫn, một số là dữ liệu chỉ đọc và một số là dữ liệu được khởi tạo thành dữ liệu đọc-ghi. (Một số) MMU phần cứng bảo mật có thể ngăn việc thực thi từ các trang không được đánh dấu như vậy và đây là một tính năng tốt để ngăn chặn, ví dụ: tràn ngăn xếp dẫn đến việc thực thi mã trên ngăn xếp nhưng đáng buồn là điều đó đôi khi hợp pháp hoặc thường không được bật.
mah,

30

Ai đó đang cố gắng hành động như Magician. Anh ta nghĩ rằng anh ta có thể lừa chúng tôi. Nhưng chúng ta đều biết, việc thực thi chương trình c bắt đầu bằng main().

Các int begin()sẽ được thay thế bằng decode(a,n,i,m,a,t,e)bằng một đường chuyền của giai đoạn tiền xử lý. Sau đó, một lần nữa, decode(a,n,i,m,a,t,e)sẽ được thay thế bằng m ## a ## i ## n. Như bằng liên kết vị trí của lệnh gọi macro, ssẽ có giá trị là ký tự a. Tương tự như vậy, usẽ được thay thế bằng 'i' và tsẽ được thay thế bằng 'n'. Và, đó là cách, m##s##u##tsẽ trở thànhmain

Về, ##biểu tượng trong mở rộng macro, nó là toán tử tiền xử lý và nó thực hiện dán mã thông báo. Khi macro được mở rộng, hai mã thông báo ở hai bên của mỗi toán tử '##' được kết hợp thành một mã thông báo duy nhất, sau đó thay thế cho '##' và hai mã thông báo ban đầu trong mở rộng macro.

Nếu bạn không tin tôi, bạn có thể biên dịch mã của mình với -Ecờ. Nó sẽ dừng quá trình biên dịch sau khi xử lý trước và bạn có thể thấy kết quả của việc dán mã thông báo.

gcc -E FILENAME.c

11

decode(a,b,c,d,[...])xáo trộn bốn đối số đầu tiên và kết hợp chúng để nhận một số nhận dạng mới, theo thứ tự dacb. (Ba đối số còn lại bị bỏ qua.) Ví dụ: decode(a,n,i,m,[...])đưa ra số nhận dạng main. Lưu ý rằng đây là những gì beginmacro được định nghĩa.

Do đó, beginmacro được định nghĩa đơn giản là main.


2

Trong ví dụ của bạn, main()hàm thực sự có mặt, vì beginlà một macro mà trình biên dịch thay thế bằng decodemacro, lần lượt được thay thế bằng biểu thức m ## s ## u ## t. Sử dụng mở rộng macro ##, bạn sẽ đạt được maintừ decode. Đây là một dấu vết:

begin --> decode(a,n,i,m,a,t,e) --> m##parameter1##parameter3##parameter2 ---> main

Đó chỉ là một thủ thuật cần có main(), nhưng việc sử dụng tên main()cho hàm nhập của chương trình là không cần thiết trong ngôn ngữ lập trình C. Nó phụ thuộc vào hệ điều hành của bạn và trình liên kết là một trong những công cụ của nó.

Trong Windows, bạn không phải lúc nào cũng sử dụng main(), nhưng đúng hơn là WinMainhoặcwWinMain , mặc dù bạn có thể sử dụng main(), ngay cả với chuỗi công cụ của Microsoft . Trong Linux, người ta có thể sử dụng _start.

Nó phụ thuộc vào trình liên kết như một công cụ của hệ điều hành để thiết lập điểm nhập, chứ không phải bản thân ngôn ngữ. Bạn thậm chí có thể đặt điểm vào của riêng chúng tôi, và bạn có thể tạo một thư viện cũng có thể thực thi được !


@vaxquis Bạn nói đúng, nhưng đây là một phần câu trả lời tôi đã viết để khen ngợi / sửa câu trả lời đầu tiên liên kết main()hàm với ngôn ngữ lập trình C, câu này không đúng.
Ho1

@vaxquis Tôi đã giả định rằng việc giải thích "hàm main () không cần thiết trong các chương trình C" sẽ là một phần câu trả lời. Tôi đã thêm một đoạn văn để làm cho câu trả lời hoàn chỉnh. - Ho1 16 phút trước
Ho1 18/04
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.