Trong lần chạy cuối cùng của vòng lặp, bạn viết vào array[10]
, nhưng chỉ có 10 phần tử trong mảng, được đánh số từ 0 đến 9. Đặc tả ngôn ngữ C nói rằng đây là hành vi không xác định được. Điều này có nghĩa trong thực tế là chương trình của bạn sẽ cố ghi vào int
phần bộ nhớ có kích thước nằm ngay sau array
bộ nhớ. Điều gì xảy ra sau đó phụ thuộc vào những gì thực tế nằm ở đó và điều này không chỉ phụ thuộc vào hệ điều hành mà còn phụ thuộc vào trình biên dịch, vào các tùy chọn trình biên dịch (như cài đặt tối ưu hóa), vào kiến trúc bộ xử lý, vào mã xung quanh , v.v ... Nó thậm chí có thể thay đổi từ thực thi sang thực thi, ví dụ do ngẫu nhiên không gian địa chỉ (có thể không có trong ví dụ về đồ chơi này, nhưng nó xảy ra trong cuộc sống thực). Một số khả năng bao gồm:
- Vị trí không được sử dụng. Vòng lặp chấm dứt bình thường.
- Vị trí được sử dụng cho một cái gì đó có giá trị 0. Vòng lặp kết thúc bình thường.
- Vị trí chứa địa chỉ trả về của hàm. Vòng lặp kết thúc bình thường, nhưng sau đó chương trình gặp sự cố vì nó cố nhảy đến địa chỉ 0.
- Vị trí chứa biến
i
. Vòng lặp không bao giờ kết thúc vì i
khởi động lại ở 0.
- Vị trí chứa một số biến khác. Vòng lặp chấm dứt bình thường, nhưng sau đó, những điều thú vị trên mạng xảy ra.
- Vị trí là một địa chỉ bộ nhớ không hợp lệ, ví dụ: vì
array
ở ngay cuối trang bộ nhớ ảo và trang tiếp theo không được ánh xạ.
- Quỷ bay ra khỏi mũi của bạn . May mắn là hầu hết các máy tính thiếu phần cứng cần thiết.
Những gì bạn quan sát thấy trên Windows là trình biên dịch đã quyết định đặt biến i
ngay sau mảng trong bộ nhớ, vì vậy array[10] = 0
cuối cùng đã gán cho i
. Trên Ubuntu và CentOS, trình biên dịch không được đặt i
ở đó. Hầu như tất cả các cài đặt C thực hiện nhóm biến cục bộ trong bộ nhớ, trên ngăn xếp bộ nhớ , với một ngoại lệ chính: một số biến cục bộ có thể được đặt hoàn toàn trong các thanh ghi . Ngay cả khi biến nằm trên ngăn xếp, thứ tự các biến được xác định bởi trình biên dịch và nó có thể không chỉ phụ thuộc vào thứ tự trong tệp nguồn mà còn phụ thuộc vào các loại của chúng (để tránh lãng phí bộ nhớ cho các ràng buộc căn chỉnh sẽ để lại lỗ hổng) , trên tên của họ, trên một số giá trị băm được sử dụng trong cấu trúc dữ liệu nội bộ của trình biên dịch, v.v.
Nếu bạn muốn tìm hiểu xem trình biên dịch của bạn đã quyết định làm gì, bạn có thể yêu cầu nó hiển thị cho bạn mã trình biên dịch. Ồ, và học cách giải mã trình biên dịch chương trình (nó dễ hơn viết nó). Với GCC (và một số trình biên dịch khác, đặc biệt là trong thế giới Unix), hãy vượt qua tùy chọn -S
để tạo mã trình biên dịch thay vì nhị phân. Ví dụ: đây là đoạn mã trình biên dịch cho vòng lặp từ biên dịch với GCC trên amd64 với tùy chọn tối ưu hóa -O0
(không tối ưu hóa), với các nhận xét được thêm thủ công:
.L3:
movl -52(%rbp), %eax ; load i to register eax
cltq
movl $0, -48(%rbp,%rax,4) ; set array[i] to 0
movl $.LC0, %edi
call puts ; printf of a constant string was optimized to puts
addl $1, -52(%rbp) ; add 1 to i
.L2:
cmpl $10, -52(%rbp) ; compare i to 10
jle .L3
Ở đây biến i
là 52 byte bên dưới đỉnh của ngăn xếp, trong khi mảng bắt đầu 48 byte bên dưới đỉnh của ngăn xếp. Vì vậy, trình biên dịch này đã xảy ra i
ngay trước mảng; bạn sẽ ghi đè lên i
nếu bạn tình cờ viết thư cho array[-1]
. Nếu bạn thay đổi array[i]=0
thành array[9-i]=0
, bạn sẽ nhận được một vòng lặp vô hạn trên nền tảng cụ thể này với các tùy chọn trình biên dịch cụ thể này.
Bây giờ hãy biên dịch chương trình của bạn với gcc -O1
.
movl $11, %ebx
.L3:
movl $.LC0, %edi
call puts
subl $1, %ebx
jne .L3
Nó ngắn hơn! Trình biên dịch không chỉ từ chối phân bổ vị trí ngăn xếp cho i
- nó chỉ được lưu trong sổ đăng ký ebx
- nhưng nó không bận tâm phân bổ bất kỳ bộ nhớ nào cho array
hoặc tạo mã để đặt các phần tử của nó, bởi vì nó nhận thấy rằng không có phần tử nào được sử dụng
Để làm cho ví dụ này trở nên thú vị hơn, hãy đảm bảo rằng các phép gán mảng được thực hiện bằng cách cung cấp cho trình biên dịch một cái gì đó mà nó không thể tối ưu hóa. Một cách dễ dàng để làm điều đó là sử dụng mảng từ một tệp khác - do quá trình biên dịch riêng biệt, trình biên dịch không biết điều gì xảy ra trong một tệp khác (trừ khi nó tối ưu hóa tại thời điểm liên kết, điều này gcc -O0
hay gcc -O1
không). Tạo một tệp nguồn use_array.c
chứa
void use_array(int *array) {}
và thay đổi mã nguồn của bạn thành
#include <stdio.h>
void use_array(int *array);
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test \n");
}
printf("%zd \n", sizeof(array)/sizeof(int));
use_array(array);
return 0;
}
Biên dịch với
gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o
Lần này mã trình biên dịch mã trông như thế này:
movq %rsp, %rbx
leaq 44(%rsp), %rbp
.L3:
movl $0, (%rbx)
movl $.LC0, %edi
call puts
addq $4, %rbx
cmpq %rbp, %rbx
jne .L3
Bây giờ mảng nằm trên ngăn xếp, 44 byte từ đầu. Thế còn i
? Nó không xuất hiện ở bất cứ đâu! Nhưng bộ đếm vòng lặp được giữ trong thanh ghi rbx
. Nó không chính xác i
, nhưng địa chỉ của array[i]
. Trình biên dịch đã quyết định rằng vì giá trị của i
không bao giờ được sử dụng trực tiếp, nên không có điểm nào trong việc thực hiện số học để tính toán nơi lưu trữ 0 trong mỗi lần chạy vòng lặp. Thay vào đó, địa chỉ đó là biến vòng lặp và số học để xác định các ranh giới được thực hiện một phần tại thời gian biên dịch (nhân 11 lần lặp với 4 byte cho mỗi phần tử mảng để có 44) và một phần trong thời gian chạy nhưng một lần và mãi mãi trước khi vòng lặp bắt đầu ( thực hiện phép trừ để lấy giá trị ban đầu).
Ngay cả trong ví dụ rất đơn giản này, chúng ta đã thấy cách thay đổi tùy chọn trình biên dịch (bật tối ưu hóa) hoặc thay đổi thứ gì đó nhỏ ( array[i]
thành array[9-i]
) hoặc thậm chí thay đổi thứ gì đó dường như không liên quan (thêm lệnh gọi use_array
) có thể tạo ra sự khác biệt đáng kể với những gì chương trình thực thi được tạo bởi trình biên dịch nào. Tối ưu hóa trình biên dịch có thể làm rất nhiều thứ có vẻ không trực quan trên các chương trình gọi hành vi không xác định . Đó là lý do tại sao hành vi không xác định được hoàn toàn không xác định. Khi bạn đi chệch một chút so với các bài hát, trong các chương trình trong thế giới thực, có thể rất khó hiểu mối quan hệ giữa những gì mã làm và những gì nó nên làm, ngay cả đối với các lập trình viên có kinh nghiệm.