Wednesday, April 27, 2011

C/C++ Compiler hoạt động như thế nào?

Để tìm hiểu hoạt động của một compiler, bạn hãy thử đặt mình vào vị trí của một compiler và đang compiling một source file, nội dung main.c như sau:
       
#include "stdio.h"
#define PI 3.14
int main()
{
    printf("PI: %f \n\r",PI);
    return 0;
}
Source 1: Chương trình in ra số PI
Compiling là quá trình compiler làm việc để tạo ra file thực thi *.exe[1] từ source code[1] của bạn. Quá trình compiling có thể chia ra thành 3 giai đoạn chính: preprocessing, compilationlinking.

Giai đoạn 1:    Preprocessing
Trong mỗi compiler đều có một C preprocessor, nhiệm vụ của C preprocessor là làm thay đổi source code của bạn trước khi compilation. Vậy source code bị thay đổi như thế nào?

Thực tế, compiler không thể thấy tất cả các câu lệnh được đặt sau giấu #, có nghĩa là các câu lệnh #include<stdio.h>, #define PI 3.14 đối với compiler chỉ giống như những comment và bị nó bỏ qua.

Vậy nên nhiệm vụ của C processor là phải thay đổi source code sao cho compiler có thể thấy và hiểu được những câu lệnh đặt sau giấu #.


Tức là, C processor sẽ duyệt qua tất cả các source file, khi gặp câu lệnh #include<stdio.h> nó sẽ copy nội dung của file stdio.h để thay thế vào vị trí dòng lệnh #include<stdio.h> trong source file đó.
Tương tự, câu lệnh printf(“PI: %f \n”,PI); cũng sẽ bị chuyển về dạng printf(“PI: %f \n”,3.14); vì compiler không thể thấy được câu lệnh  #define PI 3.14.
#include "stdio.h"
int main()
{
    printf("PI: %f \n\r",3.14);
    return 0;
}
Source 2: Chương  trình in ra số PI sau bước Preprocessing

Như mô tả ở Source 2 ta thấy dòng lệnh  #define PI 3.14 bị bỏ đi, giá trị PI sẽ được thấy trực tiếp tại câu lệnh printf("PI: %f \n\r",3.14); .
Source 2 là tất cả những gì compiler thấy khi tương ứng với những gì lập trình viên thấy ở Source 1.

Vậy: Nhiệm vụ của bước Preprocessing là thay đổi source code để compiler có thể thấy được tất cả những gì lập trình viên muốn thể hiện trong source code.

Giai đoạn 2:    Compilation
Compilation là quá trình chuyển tất cả các file source code *.c thành các file *.o[1] tương ứng. Oh! Bạn thắc mắc vì sao phải làm như thế?

Vì máy tính không thể hiểu được các câu lệnh C bậc cao, nó chỉ có thể hiểu được các mã máy đơn giản [xem thêm về mã máy tại: Machine Language].
Một file *.o chính là kết quả của việc biên dịch một file *.c từ ngôn ngữ  C sang mã máy để máy tính có thể hiểu được.

Example 1: Sau khi compilation source file main.c bạn sẽ thu được object file main.o mà máy tính có thể hiểu được.

Vậy: Compilation là quá trình biên dịch ngôn ngữ C sang ngôn ngữ máy để máy tính có thể hiểu được, kết quả được lưu dưới dạng file *.o.

Máy tính đã có thể hiểu file *.o nhưng chúng ta vẫn chưa thể chạy chương trình được, cần phải chuyển các file *.o thành file *.exe.

Giai đoạn 3:    Linking
Linking là việc tạo ra file *.exe từ các file *.o và thư viện object.
Thư viện object là gì, nó ở đâu ra?

Bộ phận thực hiện chức năng linking gọi là linker, linker không chỉ liên kết các object file được tạo ra từ source code của bạn mà nó còn liên kết tới các object file nằm ngoài source code của bạn.

Example 2: Bạn vẫn có thể sử dụng hàm printf(“”);[2]
trong hàm main cho dù bạn không định nghĩa hay khai báo hàm printf(“”);. Thực tế, hàm printf(“”); được định nghĩa trong một file object có tên là stdio.o và khai báo trong file stdio.h, cả hai file này được cung cấp bởi compiler[3], vậy nên khi bạn thực hiện #include <stdio.h> tức là bạn đã cho phép linker liên kết đến file stdio.o để có thể sử dụng được hàm printf(“”);. 
Tương tự khi bạn #include “function.h” tức là cho phép linker liên kết đến object function.o[3].

Vậy quá trình Linking diễn ra như thế nào?

Quay lại Example 2, trong quá trình compilation, compiler sẽ biên dịch các file *.c thành các file mã máy *.o, khi biên dịch đến dòng lệnh sử dụng hàm printf(“”); compiler sẽ đánh giấu dòng lệnh đó và bỏ qua nó[4] để tiếp tục biên dịch các câu lệnh tiếp theo.

Phải đợi đến quá trình linking, khi đó linker sẽ kiểm tra tất cả các file object được tạo ra từ source code của bạn để tìm các dòng lệnh mà compiler đã đánh giấu trước đó và thay thế nó bằng các hàm tương ứng được định nghĩa trong các file object được liên kết đến, trong Example 2 thì linker sẽ lấy hàm printf(“”); trong object stdio.o và thay thế vào dòng lệnh được đánh giấu trong object main.o. Quá trình tìm kiếm và thay thế này được gọi là Fixup.

Sau khi hoàn thành linking tạo ra file *.exe chính là một chương trình có thể thực thi được (Executable program). Quá trình compiler kết thúc.

---------------------------------------------------------------------------------------------------------------------------------
[1]: *.exe là một định dạng file trong đó exe viết tắt của executable, tương tự *.o, o là viết tắt của object. Source code là tập hợp của nhiều source file.
[2]: printf(“\n”) là một hàm chứ không phải một câu lệnh trong ngôn ngữ C.
[3]: Các file object nào được cung cấp bởi compiler thì gọi là file thư viện, stdio.o là object thư viện nhưng function.o không phải là object thư viện.
[4]: Vì trong file *.c của bạn không định nghĩa hàm printf(“”); nên compiler không thể dịch được nó sang mã máy trong khi compilation.

No comments:

Post a Comment