Source code C++ được biên dịch như thế nào ?

Khi bạn phát triển một chương trình bằng ngôn ngữ C++, sau khi code xong thì bước tiếp theo là compile (biên dịch) chương trình. Quá trình biên dịch là quá trình chuyển đổi chương trình được viết bằng ngôn ngữ bậc cao (để con người có thể đọc được) như C/C++, Java… thành mã máy mà CPU có thể hiểu được.

Ví dụ: nếu bạn có tệp mã nguồn C++ có tên prog1.cpp thì bạn có thể thực thi lệnh biên dịch như sau →

Quá trình biên dịch từ source code đến file thư viện hoặc file chạy (executable file) của chương trình bao gồm 4 giai đoạn chính.

  1. Bộ tiền xử lý của C++ lấy tệp mã nguồn C++ và xử lý các chỉ thị tiền xử lý như #include, #define, #if, #ifdef, #ifndef, #endif,… và các chỉ thị tiền xử lý khác
  2. Tệp mã nguồn C++ mở rộng (được tạo bởi bộ tiền xử lý C++) được biên dịch thành mã code ngôn ngữ assembly (*.s hoặc *.asm)
  3. Assembly code được tạo bởi trình biên dịch được assembler convert tiếp thành object code files (*.o hoặc *.obj)
  4. Các object code files được tạo bởi assembler được linker link (liên kết) với nhau và link với object code files của các hàm trong các thư viện khác (nếu trong source code đang được biên dịch có call đến các hàm thư viện) để tạo ra file thư viện hoặc file thực thi.

Preprocessing (tiền xử lý)

Bộ tiền xử lý có nhiệm vụ xử lý các chỉ thị tiền xử lý, như #include, #define, #if, #ifdef, #ifndef, #endif,… Nó làm việc trên một file source C++ tại một thời điểm bằng cách thay thế các chỉ thị #include bằng nội dung của các file tương ứng (thường chỉ chứa khai báo), thay thế các macro #define và chọn các phần khác nhau trong source code để biên dịch tùy thuộc vào các chỉ thị #if, #ifdef, #ifndef.

Chúng ta có thể bắt lỗi ngay ở giai đoạn này với việc sử dụng một cách hợp lý các chỉ thị #if#error. Bằng cách sử dụng option -E của trình biên dịch như bên dưới, chúng ta có thể dừng quá trình biên dịch ngay ở giai đoạn tiền xử lý nếu có lỗi ở giai đoạn này.

Compilation (biên dịch)

Bước biên dịch được thực hiện trên mỗi output của bộ tiền xử lý. Trình biên dịch phân tích mã nguồn C++ thuần túy (bây giờ không có bất kỳ chỉ thị tiền xử lý nào) và chuyển đổi nó assembly code. Ở giai đoạn này, trình biên dịch sẽ bắt các lỗi về kiểu dữ liệu, cú pháp đồng thời có thể thực hiện tối ưu tự động source code để chương trình hoạt động hiệu quả hơn. Nếu có lỗi xảy ra trình biên dịch sẽ thông báo và chúng ta phải sửa lại code để xử lý các lỗi đó và thực hiện biên dịch lại.

Sau khi biên dịch ngon lành ở bước này, compiler sẽ gọi đến một cái back-end bên dưới gọi là assembler để convert tiếp assembly code thành các object code files chứa machine code (mã máy).

Assembling

Ở bước này thì assembler sẽ chuyển đổi các assembly code trong các assembler files thành mã máy (machine code – là mã mà CPU có thể hiểu được) trong các object code files. Lưu ý assembler phụ thuộc vào kiến trúc của CPU (x86, PowerPC, ARM,…) nên các kiến trúc CPU khác nhau thì sẽ có các assembler khác nhau. Trên hệ thống UNIX, các object code files có hậu tố .o (.OBJ trên Windows). Các object code files chứa code đã được biên dịch (ở dạng nhị phân) của các symbols (có hiểu đơn giản symbols ở đây là các hàm, các biến) định nghĩa trong file source đầu vào. Symbols trong các object code files được tham chiếu đến bằng tên.

Để dừng quá trình biên dịch ngay sau bước này chúng ta có thể sử dụng tùy chọn -c như sau

Linking

Linker (còn gọi là trình liên kết) là cái sẽ tạo ra output cuối cùng của quá trình biên dịch từ các object code filesassembler đã tạo ra ở bước trước đó. Đầu ra này có thể là thư viện shared (or dynamic) library hoặc file chaỵ (executable file).

Nó liên kết tất cả các object code files bằng cách thay thế các tham chiếu đến các symbols bằng các địa chỉ chính xác. Mỗi symbol này có thể được định nghĩa trong các object code files khác hoặc trong các thư viện. Nếu chúng được định nghĩa trong các thư viện khác với thư viện chuẩn, bạn cần phải khai báo rõ với linker về chúng (điều này được thực hiện thông qua các config build).

Ở giai đoạn này, các lỗi phổ biến nhất là thiếu định nghĩa hoặc định nghĩa trùng lặp.

  • Thiếu định nghĩa : Điều này có nghĩa là các định nghĩa không tồn tại (các class, các hàm không được implement) hoặc object code files hoặc thư viện nơi chúng cư trú không được cung cấp cho trình linker biết.
  • Trùng lặp: cùng một symbol được định nghĩa trong hai hoặc nhiều object code files hoặc thư viện khác nhau.

— Phạm Minh Tuấn (Shun) —