Quy trình biên dịch
Sau bài này bạn sẽ làm được
- Kể tên bốn giai đoạn, công cụ và đuôi file tương ứng
- Chạy được từng giai đoạn riêng bằng gcc -E, -S, -c
- Đọc được bảng ký hiệu và bảng tái định vị trong file object
- Phân biệt được lỗi undefined reference và multiple definition
Khi bạn gõ gcc main.c, thực ra bốn chương trình khác nhau lần lượt chạy. Hiểu rõ bốn bước này là thứ giúp bạn tự chẩn đoán được phần lớn lỗi khó hiểu về sau, đặc biệt là lỗi liên kết. Đây là bài quan trọng nhất của Phần 0.
#Bốn giai đoạn
| Giai đoạn | Công cụ | Đầu vào | Đầu ra |
|---|---|---|---|
| Tiền xử lý | cpp | main.c | main.i |
| Biên dịch | cc1 | main.i | main.s |
| Hợp dịch | as | main.s | main.o |
| Liên kết | ld | main.o và các thư viện | main |
#1. Tiền xử lý
Bước đầu tiên do cpp đảm nhiệm. Điều quan trọng nhất cần nhớ: preprocessor không hiểu C. Nó chỉ thao tác trên văn bản, giống như thao tác tìm và thay thế trong trình soạn thảo.
Nó làm đúng bốn việc:
- Dán nguyên nội dung file được
#includevào vị trí đó - Thay thế mọi macro do
#definetạo ra - Xử lý biên dịch có điều kiện:
#if,#ifdef,#ifndef - Xóa toàn bộ chú thích
gcc -E main.c -o main.iHãy thử với một chương trình Hello World. Kết quả có thể làm bạn bất ngờ:
812
Toàn bộ 800 dòng thừa đến từ #include <stdio.h>. Preprocessor đã dán vào đó mọi khai báo hàm của thư viện nhập xuất chuẩn. Chương trình của bạn nằm ở cuối cùng.
#2. Biên dịch
Đây là bước phức tạp nhất. Trình biên dịch nhận file C thuần và sinh ra mã assembly cho kiến trúc đích. Bên trong nó là một dây chuyền nhiều tầng:
Phân tích từ vựng
Cắt mã nguồn thành các token:
int,main,(,void,)và tiếp tục.Phân tích cú pháp
Dựng cây cú pháp trừu tượng. Lỗi thiếu dấu chấm phẩy hay thiếu ngoặc bị bắt ở đây.
Phân tích ngữ nghĩa
Kiểm tra kiểu, phân giải tên biến và hàm. Phần lớn cảnh báo mà bạn thấy sinh ra ở bước này.
Sinh mã trung gian và tối ưu
Gấp hằng số, loại bỏ mã chết, nội tuyến hàm, mở vòng lặp, vector hóa. Đây là nơi cờ
-O2phát huy tác dụng.Sinh mã đích
Chuyển mã trung gian thành assembly của CPU cụ thể.
gcc -S main.c -o main.s
gcc -S -masm=intel main.c -o main.s # cú pháp Intel dễ đọc hơn#3. Hợp dịch
Trình hợp dịch as dịch từng dòng assembly thành mã máy nhị phân, tạo ra file object.
gcc -c main.c -o main.oHãy nhìn vào bên trong file object bằng ba lệnh sau:
0000000000000000 T main
U printfRelocation section '.rela.text': Offset Type Symbol 00001a R_X86_64_PLT32 printf - 4
Dòng U printf chính là điểm mấu chốt. Trình hợp dịch biết bạn gọi printf, nhưng hoàn toàn không biết hàm đó nằm ở địa chỉ nào. Nó để lại một chỗ trống và ghi vào bảng tái định vị rằng chỗ này cần địa chỉ của printf. Việc điền vào là của bước sau.
#4. Liên kết
Trình liên kết làm đúng ba việc:
- Gộp các phần. Ghép mọi vùng mã của các file object thành một vùng mã duy nhất.
- Phân giải ký hiệu. Mỗi ký hiệu
Uphải tìm được đúng một định nghĩa, không nhiều hơn và không ít hơn. - Tái định vị. Điền địa chỉ thật vào mọi chỗ trống đã đánh dấu.
Nó cũng nối thêm mã khởi động của thư viện chuẩn, thứ sẽ gọi hàm main của bạn. Chi tiết về mã khởi động này nằm ở bài tiếp theo.
gcc main.o -o main
ldd main # xem chương trình phụ thuộc thư viện động nào#Hai lỗi kinh điển
Gần như mọi lập trình viên C đều gặp hai lỗi này trong tuần đầu tiên. Chúng trông giống nhau nhưng nguyên nhân ngược nhau, và phân biệt được chúng sẽ tiết kiệm cho bạn rất nhiều giờ.
Trình liên kết không tìm thấy định nghĩa. Nguyên nhân thường gặp:
- Quên biên dịch kèm file .c chứa hàm đó
- Quên thư viện: dùng
sqrt()mà thiếu-lm - Gõ sai tên hàm
- Sai thứ tự:
gcc -lm main.cthất bại, phải viếtgcc main.c -lm
Trình liên kết tìm thấy nhiều hơn một định nghĩa. Nguyên nhân thường gặp:
- Định nghĩa biến trong file
.hrồi include ở nhiều nơi - Quên
externkhi khai báo biến toàn cục trong header - Hai file cùng định nghĩa một hàm trùng tên
#Chạy tay từng bước
Bài thực hành này nên làm ngay, không nên chỉ đọc. Nó biến bốn giai đoạn từ lý thuyết thành thứ bạn nhìn thấy tận mắt.
#include <stdio.h>
#define TEN "The gioi"
int main(void)
{
printf("Xin chao, %s!\n", TEN);
return 0;
}812 demo.i
0000000000000000 T main
U printfXin chao, The gioi!
Tự làm thử
- Tạo hai file:
a.cchứamain, vàb.cchứa hàmint nhan_doi(int). Biên dịch riêng thành hai file object rồi liên kết. Sau đó cố tình quênb.okhi liên kết và đọc kỹ thông báo lỗi. - Viết chương trình dùng
sqrt()từ<math.h>. Biên dịch không có-lm, đọc lỗi. Thêm-lmvào sai vị trí (trước tên file), đọc lỗi. Rồi đặt đúng vị trí. - So sánh kích thước của
gcc demo.c -o d1vàgcc -static demo.c -o d2. Chạylddcho cả hai. Giải thích vì sao chênh lệch lớn tới vậy. - Dùng
gcc -Etrên một file có#definevà#ifdef, quan sát preprocessor xử lý chúng ra sao.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Bốn giai đoạn: tiền xử lý (cpp), biên dịch (cc1), hợp dịch (as), liên kết (ld).
- Chạy riêng từng bước bằng
gcc -E,-S,-c, và giữ tất cả bằng-save-temps. - Preprocessor không hiểu C, nó chỉ thao tác trên văn bản.
- File object có mã máy nhưng địa chỉ chưa chốt. Ký hiệu
Unghĩa là chưa biết nằm ở đâu. - undefined reference nghĩa là thiếu định nghĩa, multiple definition nghĩa là thừa định nghĩa.
- Thư viện luôn đặt sau các file cần nó trên dòng lệnh.