Một số tính năng của ngôn ngữ C bắt đầu như là các bản hack đã xảy ra.
Nhiều chữ ký cho danh sách đối số chính, cũng như danh sách đối số có độ dài thay đổi, là một trong những tính năng đó.
Các lập trình viên nhận thấy rằng họ có thể truyền các đối số bổ sung cho một hàm và không có gì xấu xảy ra với trình biên dịch đã cho của họ.
Đây là trường hợp nếu các quy ước gọi như vậy:
- Hàm gọi xóa các đối số.
- Các đối số ngoài cùng bên trái gần với đầu ngăn xếp hơn hoặc với cơ sở của khung ngăn xếp, để các đối số giả không làm mất hiệu lực của địa chỉ.
Một tập hợp các quy ước gọi tuân theo các quy tắc này là tham số dựa trên ngăn xếp truyền theo đó trình gọi bật các đối số và chúng được đẩy từ phải sang trái:
;; pseudo-assembly-language
;; main(argc, argv, envp); call
push envp ;; rightmost argument
push argv ;;
push argc ;; leftmost argument ends up on top of stack
call main
pop ;; caller cleans up
pop
pop
Trong các trình biên dịch mà loại quy ước gọi này là trường hợp, không cần thực hiện gì đặc biệt để hỗ trợ hai loại main
hoặc thậm chí là các loại bổ sung. main
có thể là một hàm không có đối số, trong trường hợp đó, nó không để ý đến các mục đã được đẩy lên ngăn xếp. Nếu đó là một hàm của hai đối số, thì nó sẽ tìm argc
và argv
là hai mục ngăn xếp trên cùng. Nếu đó là biến thể ba đối số dành riêng cho nền tảng với con trỏ môi trường (tiện ích mở rộng chung), thì điều đó cũng sẽ hoạt động: nó sẽ tìm đối số thứ ba đó là phần tử thứ ba từ đầu ngăn xếp.
Và do đó, một cuộc gọi cố định hoạt động cho mọi trường hợp, cho phép một mô-đun khởi động cố định duy nhất được liên kết với chương trình. Mô-đun đó có thể được viết bằng C, dưới dạng một hàm tương tự như sau:
/* I'm adding envp to show that even a popular platform-specific variant
can be handled. */
extern int main(int argc, char **argv, char **envp);
void __start(void)
{
/* This is the real startup function for the executable.
It performs a bunch of library initialization. */
/* ... */
/* And then: */
exit(main(argc_from_somewhere, argv_from_somewhere, envp_from_somewhere));
}
Nói cách khác, mô-đun bắt đầu này luôn luôn gọi một chính ba đối số. Nếu hàm main không có đối số hoặc chỉ int, char **
, nó sẽ hoạt động tốt, cũng như nếu nó không có đối số, do các quy ước gọi.
Nếu bạn làm điều này trong chương trình của mình, nó sẽ là hành vi không thể di chuyển và được coi là không xác định theo ISO C: khai báo và gọi một hàm theo một cách và định nghĩa nó theo cách khác. Nhưng thủ thuật khởi động của trình biên dịch không nhất thiết phải có tính di động; nó không được hướng dẫn bởi các quy tắc cho các chương trình di động.
Nhưng giả sử rằng các quy ước gọi là như vậy mà nó không thể hoạt động theo cách này. Trong trường hợp đó, trình biên dịch phải xử lý main
đặc biệt. Khi nó nhận thấy rằng nó đang biên dịch main
hàm, nó có thể tạo ra mã tương thích với, chẳng hạn như lệnh gọi ba đối số.
Có nghĩa là, bạn viết thế này:
int main(void)
{
/* ... */
}
Nhưng khi trình biên dịch nhìn thấy nó, về cơ bản nó thực hiện một chuyển đổi mã để hàm mà nó biên dịch trông giống như sau:
int main(int __argc_ignore, char **__argv_ignore, char **__envp_ignore)
{
/* ... */
}
ngoại trừ việc các tên __argc_ignore
không tồn tại theo nghĩa đen. Không có tên nào như vậy được đưa vào phạm vi của bạn và sẽ không có bất kỳ cảnh báo nào về các đối số không được sử dụng. Việc chuyển đổi mã khiến trình biên dịch phát ra mã với liên kết chính xác biết rằng nó phải xóa ba đối số.
Một chiến lược triển khai khác là để trình biên dịch hoặc có thể là trình liên kết tạo __start
hàm tùy chỉnh (hoặc bất cứ thứ gì nó được gọi), hoặc ít nhất chọn một từ một số lựa chọn thay thế được biên dịch trước. Thông tin có thể được lưu trữ trong tệp đối tượng về các biểu mẫu được hỗ trợ main
đang được sử dụng. Trình liên kết có thể xem thông tin này và chọn phiên bản chính xác của mô-đun khởi động có chứa lệnh gọi main
tương thích với định nghĩa của chương trình. Việc triển khai C thường chỉ có một số nhỏ các dạng được hỗ trợ main
vì vậy cách tiếp cận này là khả thi.
Các trình biên dịch cho ngôn ngữ C99 luôn phải xử lý main
đặc biệt, ở một mức độ nào đó, để hỗ trợ việc hack mà nếu hàm kết thúc mà không có return
câu lệnh, thì hành vi đó giống như return 0
được thực thi. Điều này, một lần nữa, có thể được xử lý bằng một phép chuyển đổi mã. Trình biên dịch thông báo rằng một hàm được gọi main
đang được biên dịch. Sau đó, nó kiểm tra xem phần cuối của cơ thể có thể tiếp cận được hay không. Nếu vậy, nó sẽ chèn mộtreturn 0;
main
phương thức trong một chương trình duy nhất trongC
(hoặc, thực sự, bằng khá nhiều ngôn ngữ có cấu trúc như vậy).