Language Processtor
1. Khái niệm
-
Language Processors (hay Language Translator) là một chương trình máy tính giúp chuyển mã nguồn từ một ngôn ngữ lập trình nhất định sang một loại ngôn ngữ khác hoặc ngôn ngữ máy (thường được biết đến như mã nguồn đối tượng -
object code). -
Sự giống nhau giữa các Translator: Đều là những phần mềm dùng để dịch mã nguồn được viết ở ngôn ngữ bậc cao thành ngôn ngữ bậc thấp, giúp máy tính có thể hiểu và thực thi được chương trình.
Tại sao cần dùng tới Language Translator?
- Vì ngôn ngữ máy được cấu tạo từ các bit 0 và 1 khiến cho việc đọc hiểu và giao tiếp với máy tính của con người trở nên cực kỳ khó khăn.
- Vì vậy, con người tạo ra Language Translators để dịch những ngôn ngữ bậc cao (ngôn ngữ thân thiện với con người) thành mã máy (ngôn ngữ máy tính có thể hiểu được), biến việc lập trình trở nên dễ dàng và hiệu quả hơn.
2. Các loại Language Processor
2.1. Compiler
Trình biên dịch (Compiler) là một phần mềm giúp chuyển đổi mã nguồn được viết ở các loại ngôn ngữ bậc cao hơn (như C++, Java) sang các loại ngôn ngữ có bậc thấp hơn mà máy tính có thể hiểu và thực thi được (mã máy). Đặc điểm của Compiler là nó dịch toàn bộ chương trình thành một tệp thực thi (executable file) thay vì dịch từng dòng như Trình thông dịch (Interpreter) (sẽ được tìm hiểu ở mục sau).
Cách Compiler hoạt động:
-
Phân tích từ vựng (Lexical Analysis)
- Đầu tiên, mã nguồn được đưa đến bộ phân tích từ vựng (lexer). Khối này có nhiệm vụ quét qua từng ký tự và chia đoạn code của bạn thành các đơn vị cơ bản nhất, gọi là các tokens. Một token có thể là từ khóa (như
int,return), tên biến/hàm (identifiers), toán tử (+,=,->), hoặc các dấu câu ({},;).
- Đầu tiên, mã nguồn được đưa đến bộ phân tích từ vựng (lexer). Khối này có nhiệm vụ quét qua từng ký tự và chia đoạn code của bạn thành các đơn vị cơ bản nhất, gọi là các tokens. Một token có thể là từ khóa (như
-
Phân tích cú pháp (Syntax Analysis)
- Tiếp theo, các tokens được chuyển tới bộ phân tích cú pháp (parser). Tại đây, Compiler sẽ kiểm tra xem tổ hợp các tokens này có tuân thủ đúng bộ quy tắc cú pháp của C++ hay không. Kết quả của bước này là việc xây dựng một Cây cú pháp trừu tượng (Abstract Syntax Tree - AST), giúp thể hiện toàn bộ cấu trúc phân cấp và logic của đoạn code. Các lỗi kinh điển như thiếu dấu chấm phẩy hoặc viết sai cấu trúc vòng lặp sẽ bị hệ thống báo lỗi tại đây.
-
Phân tích ngữ nghĩa (Semantic Analysis)
- Mặc dù mã nguồn có thể đúng cú pháp nhưng nó cần có ý nghĩa hợp lý. Ở bước này, Compiler sẽ kiểm tra tính hợp lệ về mặt ngữ nghĩa trên cây AST chẳng hạn như: lỗi không khớp kiểu dữ liệu (ví dụ bạn cố gắng gán một chuỗi string vào một biến int), gọi các biến chưa được khai báo, hoặc sử dụng sai công năng của toán tử.
-
Tối ưu hóa mã (Optimization)
- Đây là một giai đoạn tùy chọn (optional). Trình biên dịch sẽ áp dụng kỹ thuật để cải thiện hiệu suất của đoạn code đầu ra. Nó có thể tự động loại bỏ các đoạn mã không bao giờ được chạy tới, gỡ cuộn vòng lặp, hoặc sắp xếp lại chuỗi lệnh để CPU thực thi nhanh nhất có thể.
-
Sinh mã (Code Generation)
- Bước cuối cùng là dịch cây AST (hoặc dạng mã trung gian đã được tối ưu) thành ngôn ngữ máy. Bộ sinh mã thường sẽ tạo ra mã Hợp ngữ (Assembly code) trước. Sau đó, đoạn mã này tiếp tục được dịch thành mã nhị phân (binary / object code) gồm các chuỗi 0 và 1 – ngôn ngữ duy nhất mà phần cứng máy tính thực sự hiểu và có thể thực thi.
Sơ đồ luồng hoạt động của Compiler:
2.2. Interpreter
Trình thông dịch (Interpreter) có chức năng tương tự như Compiler: đều chuyển mã nguồn bậc cao thành dạng mà máy có thể thực thi. Tuy nhiên, thay vì dịch toàn bộ chương trình trước rồi mới chạy, Interpreter thường phân tích và thực thi từng câu lệnh theo thứ tự trong lúc chương trình đang chạy.
Cách Interpreter hoạt động:
-
Nạp mã nguồn và lấy lệnh kế tiếp (Fetch)
- Interpreter đọc mã nguồn và lấy từng câu lệnh theo thứ tự thực thi.
-
Phân tích từ vựng + cú pháp cho câu lệnh hiện tại (Lexical & Syntax Analysis)
- Câu lệnh được tách thành token, sau đó kiểm tra cấu trúc cú pháp trước khi chạy.
-
Kiểm tra ngữ nghĩa và ngữ cảnh thực thi (Semantic Check)
- Interpreter kiểm tra biến đã tồn tại chưa, kiểu dữ liệu có hợp lệ không, phạm vi biến có đúng không,...
-
Thực thi và cập nhật trạng thái
- Nếu câu lệnh hợp lệ, nó sẽ được dịch ra mã máy và chuyển cho CPU chạy ngay, sau đó mới cập nhật trạng thái bộ nhớ/chương trình.
-
Lặp lại đến hết chương trình hoặc gặp lỗi
- Quá trình trên lặp lại cho đến khi hết chương trình.
- Nếu lỗi phát sinh ở câu lệnh nào, chương trình dừng tại đó và báo lỗi tương ứng.
Sơ đồ luồng hoạt động của Interpreter:
Vì sao Compiler có thể dịch nhanh hơn Interpreter
Có 3 lý do chính:
-
Gánh nặng ở thời gian chạy
-
Với Compiler: Mọi khâu (như phân tích từ vựng, kiểm tra cú pháp, kiểm tra kiểu dữ liệu) đều đã được giải quyết xong tại thời điểm biên dịch (Compile-time). Đến thời điểm chạy (Run-time), file thực thi (executable file) chỉ chứa các lệnh mã máy (0 và 1). CPU chỉ việc nạp vào bộ nhớ và thực thi.
-
Với Interpreter: Quá trình dịch và chạy diễn ra đồng thời. Mỗi khi chương trình chạy, Interpreter phải đọc từng dòng lệnh, phân tích cú pháp của dòng đó, kiểm tra lỗi, dịch ra mã máy, rồi mới cho CPU chạy. Tất cả những thao tác dư thừa này tạo ra một độ trễ rất lớn (overhead) đè nặng lên Run-time.
-
-
Không có khâu tối ưu hóa
- Như đã đề cập ở quy trình dịch của Compiler, nó có thể sắp xếp lại thứ tự lệnh, loại bỏ các biến không dùng đến, hoặc thay thế các phép toán phức tạp bằng các lệnh máy đơn giản hơn (ví dụ: biến phép nhân thành phép dịch bit bitwise shift).
- Trình thông dịch thường chỉ nhìn thấy từng dòng (hoặc từng khối lệnh nhỏ) tại một thời điểm, nên nó không thể thực hiện các bước tối ưu hóa như vậy.
-
Vòng lặp
- Giả sử bạn có một vòng lặp for chạy 10,000 lần.
- Compiler: Nó chỉ dịch khối lệnh trong vòng lặp đúng 1 lần duy nhất lúc build chương trình. CPU cứ thế chạy khối mã máy đó 10,000 lần.
- Interpreter: Nó sẽ lặp lại chu trình Đọc -> Phân tích -> Dịch -> Chạy cho cùng một khối lệnh đó... 10,000 lần. Việc dịch đi dịch lại một đoạn code không thay đổi làm lãng phí một lượng khổng lồ tài nguyên của CPU.
2.3. Assembler
Trình dịch hợp ngữ (Assembler) là phần mềm chuyên dụng làm nhiệm vụ chuyển đổi mã nguồn Assembly (hợp ngữ) thành mã máy nhị phân (object code). Điểm khác biệt là mã Assembly đã nằm rất sát với lớp phần cứng vật lý, do đó mỗi câu lệnh Assembly thường ánh xạ trực tiếp sang một mã lệnh (opcode) tương ứng của CPU.
Cách Assembler hoạt động:
-
Đọc và chuẩn hóa mã hợp ngữ (Preprocessing/Scanning)
- Khối quét (Scanner) sẽ duyệt qua file mã nguồn để tách từng dòng lệnh, loại bỏ các dòng chú thích (comments), nhận diện các nhãn (
label), chỉ thị (directive) và các lệnh hợp ngữ cơ bản.
- Khối quét (Scanner) sẽ duyệt qua file mã nguồn để tách từng dòng lệnh, loại bỏ các dòng chú thích (comments), nhận diện các nhãn (
-
Lượt 1 - Xây dựng Bảng ký hiệu (Pass 1: Symbol Table)
- Assembler quét qua toàn bộ chương trình lần đầu để tính toán địa chỉ tương đối của từng câu lệnh và vùng dữ liệu. Từ đó, nó xây dựng một Bảng ký hiệu (Symbol Table) – nơi lưu trữ địa chỉ bộ nhớ tương ứng của tất cả các nhãn (labels) có trong code.
-
Lượt 2 - Sinh mã máy (Pass 2: Code Generation)
- Ở lượt quét thứ hai, Assembler tiến hành dịch các từ gợi nhớ (Mnemonic - ví dụ:
MOV,ADD,JMP) thành các mã opcode nhị phân của CPU. Đồng thời, nó tra cứu Bảng ký hiệu đã tạo ở Pass 1 để thay thế các nhãn trong câu lệnh bằng địa chỉ bộ nhớ thực tế.
- Ở lượt quét thứ hai, Assembler tiến hành dịch các từ gợi nhớ (Mnemonic - ví dụ:
-
Tạo file đối tượng (Object File)
- Kết quả là file
.obj/.ochứa mã máy, thông tin symbol và relocation (nếu có).
- Kết quả là file
-
(Tùy chọn) Chuyển sang bước liên kết
- Các tệp đối tượng này sẽ được đưa cho Linker để gom nối với các thư viện khác, tạo thành file thực thi cuối cùng (Executable File).
Sơ đồ luồng hoạt động của Assembler:
2.4. Bảng tóm tắt và Phân biệt các loại Language Processor
| Tiêu chí | Compiler (Trình biên dịch) | Interpreter (Trình thông dịch) | Assembler (Trình dịch hợp ngữ) |
|---|---|---|---|
| Cách hoạt động | Dịch toàn bộ mã nguồn cùng một lúc thành mã máy trước khi chạy. | Dịch và thực thi từng dòng lệnh một trong quá trình chạy. | Dịch ngôn ngữ Assembly (hợp ngữ) sang ngôn ngữ máy. |
| File đầu ra | Tạo ra file trung gian (object file) hoặc file thực thi độc lập (.exe). | Không tạo ra file thực thi trung gian. | Tạo ra object file. |
| Thời gian | Quá trình dịch (build) chậm, nhưng tốc độ chạy (run) rất nhanh. | Tốc độ chạy chậm hơn vì phải dịch liên tục trong lúc chạy. | Tốc độ dịch và chạy đều rất nhanh (vì gần với ngôn ngữ máy). |
| Ví dụ | C++, C, Java | Python, JavaScript | Assembly |
Lưu ý: Quá trình dịch (Compile-time) và Quá trình chạy (Runtime) được đề cập ở trên là khác nhau
Để hiểu hơn thì mời bạn đọc 3. Quá trình dịch của C++ (Compiler, Linker & Libraries)
3. Quá trình dịch của C++ (Compiler, Linker & Libraries)
Đối với các dự án C++, quá trình để biến các file chứa mã nguồn (.cpp) thành một chương trình chạy được (Executable file) không chỉ do mình Compiler đảm nhận, mà là một chuỗi 3 giai đoạn, bao gồm:
- Tiền xử lý (Preprocessing)
Trình tiền xử lý sẽ quét qua mã nguồn để xử lý các chỉ thị bắt đầu bằng dấu
#(như#include,#define).
Ví dụ: Khi thấy #include <iostream>, nó sẽ copy toàn bộ nội dung của file header iostream và dán trực tiếp vào đầu file code của bạn. Nó cũng sẽ xóa bỏ toàn bộ các dòng chú thích (comments) để làm gọn code.
-
Biên dịch và Dịch hợp ngữ:
- Kiểm tra cú pháp & ngữ nghĩa: Compiler phân tích từ vựng (Lexer), dựng cây cú pháp (Parser) và kiểm tra tính hợp lệ của mã (Semantic).
Lưu ý: Nếu phát hiện sai sót (thiếu dấu
;, viết sai tên biến), quá trình sẽ dừng lại và báo lỗi biên dịch (Compile-time error). Bạn phải sửa lỗi (debug) thì mới đi tiếp được.-
Tối ưu & Sinh mã: Nếu code hợp lệ, nó sẽ được tối ưu hóa, dịch sang mã Assembly và cuối cùng nhờ Assembler chuyển thành mã máy.
-
Kết quả: Mã máy này được lưu vào các file trung gian gọi là Tệp đối tượng (Object file -
.objhoặc.o).
Ví dụ về Compile-time Error:
Loading...Stdin:
Stdout:
-
Liên kết (Linking) và Nạp thêm thư viện:
-
Một dự án C++ thực tế thường bao gồm hàng chục file
.cpp. Ở bước 2, Compiler chỉ dịch chúng thành hàng chục file.objrời rạc, chưa hề biết đến nhau. Đây là lúc Trình liên kết (Linker) tham gia:-
Linker sẽ gom và kết nối tất cả các object files này lại thành một khối.
-
Nó tiếp tục tìm kiếm và liên kết mã của bạn với các Thư viện C++ (Libraries). (Ví dụ: Nó sẽ tìm đoạn mã máy thực thi chức năng in ra màn hình của
std::couttrong thư viện chuẩn để đính kèm vào chương trình). -
Kết quả: Tạo ra một file thực thi duy nhất (ví dụ:
main.exetrên Windows). Lúc này chương trình mới có thể chạy!
-
-
Quá trình biên dịch (Compile-time): Là toàn bộ chu trình 3 bước phía trên (Từ Tiền xử lý đến Liên kết). Các lỗi phát hiện ở đây (như sai cú pháp, gọi hàm không tồn tại, thiếu thư viện) gọi chung là lỗi Compile-time. Bạn không thể chạy chương trình nếu chưa sửa hết các lỗi này.
Quá trình thực thi (Runtime): Là lúc máy tính (CPU) chính thức chạy file .exe vừa được tạo ra. Nếu chương trình đang chạy mà bị dừng đột ngột (crash) do các tác vụ vật lý (ví dụ: chia một số cho 0, mở một file không tồn tại, truy cập mảng quá giới hạn), thì đó gọi là lỗi Runtime error.
Tài liệu tham khảo: