Bạn đã kết hợp với nhau một số câu hỏi khác nhau (nhưng có liên quan). Một vài trong số chúng không thực sự có chủ đề ở đây (ví dụ: tiêu chuẩn mã hóa), vì vậy tôi sẽ bỏ qua những điều đó.
Tôi sẽ bắt đầu với nếu kernel là "mã C không chính xác về mặt kỹ thuật". Tôi bắt đầu ở đây vì câu trả lời giải thích vị trí đặc biệt mà hạt nhân chiếm giữ, điều rất quan trọng để hiểu phần còn lại.
Là hạt nhân kỹ thuật mã C không chính xác?
Câu trả lời chắc chắn là "không chính xác".
Có một vài cách mà chương trình C có thể nói là không chính xác. Trước tiên, hãy lấy một vài cái đơn giản:
- Một chương trình không tuân theo cú pháp C (nghĩa là có lỗi cú pháp) là không chính xác. Nhân sử dụng các phần mở rộng GNU khác nhau cho cú pháp C. Đó là, theo như tiêu chuẩn C có liên quan, lỗi cú pháp. (Tất nhiên, với GCC, họ không có. Hãy thử biên dịch với
-std=c99 -pedantic
hoặc tương tự ...)
- Một chương trình không làm những gì nó được thiết kế để làm là không chính xác. Hạt nhân là một chương trình khổng lồ và, ngay cả việc kiểm tra nhanh các thay đổi của nó sẽ chứng minh, chắc chắn là không. Hoặc, như chúng ta thường nói, nó có lỗi.
Tối ưu hóa có nghĩa là gì trong C
[LƯU Ý: Phần này có phần rất hạn chế các quy tắc thực tế; để biết chi tiết, xem tiêu chuẩn và tìm kiếm Stack Overflow.]
Bây giờ cho một trong đó có nhiều lời giải thích. Tiêu chuẩn C nói rằng mã nhất định phải tạo ra hành vi nhất định. Nó cũng cho biết một số điều có giá trị cú pháp C có "hành vi không xác định"; một ví dụ (không may là phổ biến!) là truy cập bên ngoài phần cuối của một mảng (ví dụ: tràn bộ đệm).
Hành vi không xác định là mạnh mẽ như vậy. Nếu một chương trình chứa nó, dù chỉ một chút xíu, tiêu chuẩn C không còn quan tâm đến hành vi nào mà chương trình thể hiện hoặc đầu ra mà trình biên dịch tạo ra khi đối mặt với nó.
Nhưng ngay cả khi chương trình chỉ chứa hành vi được xác định, C vẫn cho phép trình biên dịch mất nhiều thời gian. Như một ví dụ tầm thường (lưu ý: đối với các ví dụ của tôi, tôi đang bỏ qua #include
các dòng, v.v., để cho ngắn gọn):
void f() {
int *i = malloc(sizeof(int));
*i = 3;
*i += 2;
printf("%i\n", *i);
free(i);
}
Điều đó, tất nhiên, in 5 theo sau là một dòng mới. Đó là những gì được yêu cầu bởi tiêu chuẩn C.
Nếu bạn biên dịch chương trình đó và tháo rời đầu ra, bạn sẽ mong đợi malloc được gọi để lấy một số bộ nhớ, con trỏ được trả về được lưu trữ ở đâu đó (có thể là một thanh ghi), giá trị 3 được lưu vào bộ nhớ đó, sau đó thêm 2 vào bộ nhớ đó (có thể thậm chí yêu cầu tải, thêm và lưu trữ), sau đó bộ nhớ được sao chép vào ngăn xếp và cũng là một chuỗi điểm "%i\n"
được đặt trên ngăn xếp, sau đó printf
gọi hàm. Một chút công bằng. Nhưng thay vào đó, những gì bạn có thể thấy là như thể bạn đã viết:
/* Note that isn't hypothetical; gcc 4.9 at -O1 or higher does this. */
void f() { printf("%i\n", 5) }
và đây là điều: tiêu chuẩn C cho phép điều đó. Tiêu chuẩn C chỉ quan tâm đến kết quả , không phải theo cách họ đạt được.
Đó là những gì tối ưu hóa trong C là về. Trình biên dịch đưa ra một cách thông minh hơn (thường là nhỏ hơn hoặc nhanh hơn, tùy thuộc vào các cờ) để đạt được kết quả theo yêu cầu của tiêu chuẩn C. Có một vài trường hợp ngoại lệ, chẳng hạn như -ffast-math
tùy chọn của GCC , nhưng nếu không thì mức tối ưu hóa không thay đổi hành vi của các chương trình chính xác về mặt kỹ thuật (nghĩa là các chương trình chỉ chứa hành vi được xác định).
Bạn có thể viết một kernel chỉ sử dụng hành vi được xác định không?
Hãy tiếp tục xem xét chương trình ví dụ của chúng tôi. Phiên bản chúng tôi đã viết, không phải những gì trình biên dịch biến nó thành. Điều đầu tiên chúng ta làm là gọi malloc
để có được một số bộ nhớ. Tiêu chuẩn C cho chúng ta biết những gì malloc
nó làm, nhưng không phải nó làm như thế nào.
Nếu chúng ta nhìn vào một triển khai malloc
nhằm mục đích rõ ràng (trái ngược với tốc độ), chúng ta sẽ thấy rằng nó tạo ra một số tòa nhà (chẳng hạn như mmap
với MAP_ANONYMOUS
) để có được một khối lớn bộ nhớ. Nó bên trong giữ một số cấu trúc dữ liệu cho nó biết phần nào của đoạn đó được sử dụng so với miễn phí. Nó tìm thấy một đoạn miễn phí ít nhất lớn bằng những gì bạn yêu cầu, khắc số tiền bạn yêu cầu và trả về một con trỏ cho nó. Nó cũng hoàn toàn được viết bằng C và chỉ chứa hành vi được xác định. Nếu chủ đề của nó an toàn, nó có thể chứa một số cuộc gọi pthread.
Bây giờ, cuối cùng, nếu chúng ta nhìn vào những gì mmap
không, chúng ta thấy tất cả các loại công cụ thú vị. Đầu tiên, nó thực hiện một số kiểm tra để xem hệ thống có đủ RAM miễn phí và / hoặc trao đổi để ánh xạ hay không. Tiếp theo, nó tìm thấy một số không gian địa chỉ miễn phí để đặt khối vào. Sau đó, nó chỉnh sửa cấu trúc dữ liệu được gọi là bảng trang và có thể thực hiện một loạt các cuộc gọi lắp ráp nội tuyến trên đường đi. Nó thực sự có thể tìm thấy một số trang bộ nhớ vật lý miễn phí (nghĩa là các bit thực tế trong các mô-đun DRAM thực tế) --- một quá trình có thể yêu cầu buộc bộ nhớ khác phải trao đổi ---. Nếu nó không làm điều đó cho toàn bộ khối được yêu cầu, thay vào đó, nó sẽ thiết lập mọi thứ để điều đó xảy ra khi bộ nhớ cho biết được truy cập lần đầu tiên. Phần lớn điều này được thực hiện với các bit lắp ráp nội tuyến, ghi vào các địa chỉ ma thuật khác nhau, v.v ... Lưu ý rằng nó cũng sử dụng các phần lớn của kernel, đặc biệt là nếu cần phải tráo đổi.
Việc lắp ráp nội tuyến, ghi vào các địa chỉ ma thuật, v.v ... đều nằm ngoài đặc tả C. Điều này không gây ngạc nhiên; C chạy trên nhiều kiến trúc máy khác nhau, bao gồm một bó mà hầu như không thể tưởng tượng được vào đầu những năm 1970 khi C được phát minh. Ẩn mã cụ thể của máy đó là một phần cốt lõi của hạt nhân (và ở một mức độ nào đó thư viện C) dành cho.
Tất nhiên, nếu bạn quay lại chương trình ví dụ, nó trở nên rõ ràng printf
phải tương tự. Thật rõ ràng làm thế nào để thực hiện tất cả các định dạng, vv trong tiêu chuẩn C; Nhưng thực sự có được nó trên màn hình? Hoặc dẫn đến một chương trình khác? Một lần nữa, rất nhiều phép thuật được thực hiện bởi kernel (và có thể là X11 hoặc Wayland).
Nếu bạn nghĩ về những thứ khác mà kernel làm, rất nhiều trong số chúng nằm ngoài C. Ví dụ, kernel đọc dữ liệu từ các đĩa (C không biết gì về đĩa, bus PCIe hoặc SATA) vào bộ nhớ vật lý (C chỉ biết về malloc, không phải của DIMM, MMU, v.v.), làm cho nó có thể thực thi được (C không biết gì về các bit thực thi của bộ xử lý), và sau đó gọi nó là các hàm (không chỉ bên ngoài C, rất không được phép).
Mối quan hệ giữa hạt nhân và trình biên dịch của nó
Nếu bạn nhớ từ trước, nếu một chương trình có hành vi không xác định, cho đến khi có liên quan đến tiêu chuẩn C, tất cả các cược đều bị tắt. Nhưng một hạt nhân thực sự phải chứa hành vi không xác định. Vì vậy, phải có một số mối quan hệ giữa kernel và trình biên dịch của nó, ít nhất là đủ để các nhà phát triển kernel có thể tự tin rằng kernel sẽ hoạt động mặc dù vi phạm tiêu chuẩn C. Ít nhất là trong trường hợp của Linux, điều này bao gồm kernel có một số kiến thức về cách GCC hoạt động bên trong.
Làm thế nào nó có khả năng để phá vỡ?
Các phiên bản GCC trong tương lai có thể sẽ phá vỡ kernel. Tôi có thể nói điều này khá tự tin như nó đã xảy ra vài lần trước đây. Tất nhiên, những thứ như tối ưu hóa răng cưa nghiêm ngặt trong GCC cũng đã phá vỡ rất nhiều thứ bên cạnh kernel.
Cũng lưu ý rằng nội tuyến mà hạt nhân Linux phụ thuộc vào không phải là nội tuyến tự động, đó là nội tuyến mà các nhà phát triển nhân đã chỉ định thủ công. Có nhiều người đã biên dịch kernel bằng -O0 và báo cáo về cơ bản nó hoạt động, sau khi sửa một vài vấn đề nhỏ. (Một là ngay cả trong chủ đề bạn liên kết đến). Hầu hết, đó là các nhà phát triển nhân thấy không có lý do để biên dịch -O0
và yêu cầu tối ưu hóa vì tác dụng phụ làm cho một số thủ thuật hoạt động và không ai kiểm tra -O0
, vì vậy nó không được hỗ trợ.
Ví dụ, điều này biên dịch và liên kết với -O1
hoặc cao hơn, nhưng không phải với -O0
:
void f();
int main() {
int x = 0, *y;
y = &x;
if (*y)
f();
return 0;
}
Với tối ưu hóa, gcc có thể tìm ra rằng f()
sẽ không bao giờ được gọi và bỏ qua nó. Không tối ưu hóa, gcc để lại cuộc gọi và trình liên kết không thành công vì không có định nghĩa f()
. Các nhà phát triển kernel dựa vào hành vi tương tự để làm cho mã kernel dễ đọc / ghi hơn.