THẾ NÀO LÀ NGÔN NGỮ LẬP TRÌNH BẬC THẤP, NGÔN NGỮ LẬP TRÌNH BẬC CAO?
THẾ NÀO LÀ NGÔN NGỮ LẬP
TRÌNH BẬC THẤP?
THẾ NÀO LÀ NGÔN NGỮ LẬP TRÌNH BẬC CAO?
Ngôn ngữ
lập trình là gì thì chắc ở đây ai cũng biết rồi, vậy nhưng tại sao người ta còn
có bậc thấp và bậc cao?
Để ngắn
gọn, ta có thể nhớ luôn Hợp ngữ (Assembly language) và ngôn ngữ máy là ngôn ngữ
cấp thấp, còn tất cả các ngôn ngữ khác đều là bậc cao.
1. NGÔN
NGỮ MÁY
Máy tính
vốn không hiểu tiếng người, bộ nhớ của nó chỉ chứa duy nhất các bit 0 và 1,
được gom lại thành từng byte. Việc đọc hay ghi luôn được thực hiện theo đơn vị
byte, cũng như việc đánh địa chỉ cũng theo byte. Bạn không thể yêu cầu CPU hay
các thiết bị ngoại vi: "Hãy lấy cho tôi 1 bit ở vị trí xyz nào đó".
Muốn làm điều đó bạn phải tính toán xem bit đó thuộc byte nào (cứ chia cho 8 là
được), đọc byte đó, rồi xem bit đó tương ứng với vị trí thứ mấy trong byte,
dùng một toán tử bit nào đó (AND chẳng hạn) để kiểm tra xem nó bằng 1 hay bằng
0.
Mã máy
cũng không ngoại lệ, chúng cũng chỉ là những con số, được ghi theo một quy tắc
do CPU quy định. Ví dụ nếu nó quy định số 1 là phép toán cộng 2 biến a và b
trong bộ nhớ, sau đó gán vào biến c, vì phép toán cộng (mã máy 1) này sẽ cần 3
tham số, nên 3 byte kế tiếp sẽ thuộc vào lệnh này luôn, ta sẽ có 4 byte trong
bộ nhớ như sau (được viết dưới dạng hex):
0x01
0x20 0x21 0x40
(giả sử
biến a nằm ở địa chỉ 0x20, biến b nằm ở 0x21 và c tại 0x40)
CPU biết
rằng lệnh 0x01 luôn chiếm 4 byte, nên nó sẽ đọc và thực thi mã lệnh kế tiếp sau
4 byte này. Trong thực tế các lệnh sẽ phức tạp hơn, ví dụ địa chỉ sẽ lớn hơn
(vì 1 byte chỉ có thể định vị được đến địa chỉ 255), cho phép làm việc với
thanh ghi... tuy nhiên phương thức làm việc thì hoàn toàn giống như ví dụ trên.
Có thể
mã máy nghe rất phức tạp, nhưng thực sự khi đã biết cách nó làm việc thì lại
không quá khó hiểu. Tôi không rõ bây giờ các bạn học như thế nào, nhưng xưa tôi
học là dùng các phần mềm hiển thị các file .exe dạng hex, sau đó in ra giấy
(yes! in ra giấy) và ngồi dịch ngược lại bằng tay để xem nó làm gì. Tất nhiên
các chương trình .exe này cũng là các chương trình nhỏ, đơn giản được tạo ra
với mục đích học tập.
Nói qua
quy tắc thì có thể đơn giản, nhưng viết chương trình bằng ngôn ngữ máy có nhiều
nhược điểm:
- Rất
khó nhớ mã lệnh, một CPU thời cách đây vài chục năm cũng đã có hàng trăm lệnh
khác nhau, những tập lệnh được bổ sung sau đó cũng rất lớn, vậy nên việc nhớ
hết là hoàn toàn không khả thi (và dễ nhầm lẫn).
- Khi
bạn thay đổi một phần chương trình, bạn sẽ phải sửa lại rất nhiều thứ. Trong ví
dụ trên, nếu ta chèn thêm một biến vào trước c thì sao? Vậy c sẽ phải dịch ra
sau 1 byte, và địa chỉ của c sẽ là 0x41, câu lệnh c = a + b ở trên cũng phải
sửa lại, nếu không kết quả phép cộng sẽ được gán cho biến mới chứ không phải là
c nữa. Hay nếu như bạn muốn đổi kiểu dữ liệu của a, b và từ byte thành int (32
bit)? Vậy thì địa chỉ của b cũng phải đổi thành 0x23, và c sẽ là 0x 0x46. Bạn
sẽ phải sửa lại tất cả các câu lệnh liên quan đến a, b, c, và thậm chí cả những
câu lệnh liên quan đến các biến nằm phía sau chúng nữa.
Nói
chung không cần phải giải thích thì bạn cũng đã thấy việc viết một chương trình
bằng mã máy phức tạp và bất tiện thế nào, vì vậy người ta đã nghĩ ra Assembly
language.
ASSEMBLY
LANGUAGE
Để đơn
giản hóa việc ghi nhớ, người ta sẽ tạo ra một bảng ánh xạ 1-1 giữa từ khóa và
mã máy, ví dụ khi tôi ánh xạ 0x01 với add (lệnh cộng hai số), tôi có thể viết
câu lệnh trên như sau:
add
[0x20],[0x21],[0x40]
Trình
dịch sẽ tự động chuyển chữ add đó thành số 0x01, ta đỡ phải ghi nhớ, mà dễ đọc
hơn nhiều.
Đối với
các biến, assembly language cũng cho phép chúng ta khai báo và sử dụng thông
qua tên thay vì địa chỉ:
a db
0x99
b db
0x01
c db 0
db cho
phép khai báo một biến kiểu byte, trình dịch sẽ tự động tính toán để biết a, b,
c nằm ở địa chỉ nào, do vậy khi ta viết:
add
a,b,c
Trình
dịch sẽ tự động thay thế add với 0x01, vị trí a, b, c sẽ được thay thế bởi địa
chỉ của các biến tương ứng. Nếu ta thay đổi kiểu dữ liệu, trình dịch sẽ tự động
tính toán theo kiểu dữ liệu mới, ta không cần phải làm thêm gì nữa.
Assembly
có rất nhiều ưu điểm, bạn có thể nhìn vào một câu lệnh và biết ngay nó làm gì,
không còn phải dịch ngược từ những con số sang một cái gì có ý nghĩa để có thể
hiểu. Có thể nói hiện tại có lẽ không ai còn phải viết trực tiếp bằng ngôn ngữ
máy nữa, nhưng nó cũng có một số nhược điểm:
- Phụ
thuộc vào kiến trúc hệ thống: mỗi một CPU có một tập lệnh riêng, bạn không thể
đem chương trình viết cho CPU này sang một CPU khác.
- Chỉ
thể hiện được đúng những gì CPU làm, tức là theo cách suy nghĩ của CPU, không
phải của con người. Đoạn lệnh
if (a > b) {
c = 1;
}
else {
c = 0;
}
sẽ phải
được viết là:
mov eax,
DWORD PTR [a] ; gán biến a vào thanh ghi eax, CPU Intel không cho phép so sánh
trực tiếp 2 giá trị trong bộ nhớ
cmp eax,
DWORD PTR [b] ; so sánh eax với biến b
jle .L1
; jle = jump if less than or equal, nếu b < eax thì nhảy 'goto' đến nhãn L1
mov
DWORD PTR [c], 1 ; nếu không thì gán c = 1 rồi nhảy đến L2
jmp .L2
.L1:
mov
DWORD PTR [c], 0
.L2:
Vẫn khá
khó hiểu phải không? Vậy nên người ta phát minh ra ngôn ngữ lập trình bậc cao.
2. NGÔN
NGỮ LẬP TRÌNH BẬC CAO
Ngoài 2
ngôn ngữ trên, tất cả những ngôn ngữ bạn học đều là bậc cao: C, C++, Java,
JavaScript, PHP, C#... Ngôn ngữ bậc cao được tạo ra với những thành phần logic
mang tư duy con người hơn: các cấu trúc điều khiển, vòng lặp, các lớp, hàm, các
toán tử, biểu thức, các cấu trúc phức tạp hơn như async/await, virtual, class,
lock, using, try/catch... Tất cả những thứ này sẽ được trình dịch dịch thành mã
máy, một cấu trúc đơn giản ở bậc cao có thể được diễn dịch ra thành hàng chục,
hàng trăm lệnh máy.
Các ngôn
ngữ bậc cao có rất nhiều ưu điểm so với assembly language:
- Dễ
đọc, dễ học, dễ nhớ... nói thật là ai cũng có thể học được nếu muốn, nên mấy
bạn biết được vài ngôn ngữ, code được vài chương trình rồi lại nghĩ mình giỏi
ơi là giỏi là hem phải nhé .
- Cho
phép triển khai nhiều khái niệm trừu tượng, phức tạp mà nếu chỉ sử dụng
assembly thì không làm nổi (như OOP, async/await...).
- Cho
phép dịch chương trình sang nhiều kiến trúc khác nhau mà không cần sửa code.
Với
người lập trình ứng dụng, bạn chỉ cần thành thạo một ngôn ngữ nào đó là có thể
đi làm được, bạn không cần hiểu sâu về những gì diễn ra đằng sau, bạn không cần
biết kiến trúc máy tính thế nào (tất nhiên biết thì vẫn tốt hơn), chỉ cần viết,
dịch và chạy. Thay vì phải mất vài năm, giờ chỉ cần vài tháng là đã có thể
'hành nghề' được rồi. Tất cả là nhờ các ngôn ngữ bậc cao.
Ấy!
Nhưng sao nghe nói còn có ngôn ngữ bậc trung nữa???
Khái
niệm ngôn ngữ lập trình bậc trung tôi cũng không rõ do ai nghĩ ra, nhưng có lẽ
nó mang tính marketing nhiều hơn kỹ thuật. Nó được dùng để chỉ đến những ngôn
ngữ bậc cao nhưng cho phép chúng ta kiểm soát sâu hơn về code được sinh ra.
Ngoài C/C++, có lẽ chỉ có Rust (với nhiều tính năng undocumented) có thể được
coi là ngôn ngữ bậc trung (nếu bạn biết thêm cái nào thì hãy kể giúp tôi nhé).
Vì sao
ta cần kiểm soát sâu cách code được tạo ra? Với một ứng dụng bình thường, có lẽ
bạn không quan tâm lắm, một chương trình .exe có kích thước 100KB, nay tăng lên
thành 150KB cũng chẳng có vấn đề gì, tôi dám cá là 99% các bạn còn không quan
tâm sau khi sửa code và dịch lại thì nó tăng hay giảm bao nhiêu byte. Tuy
nhiên, khi làm việc ở cấp độ sâu hơn, hoặc viết chương trình cho các thiết bị
có tài nguyên vô cùng hạn chế: bộ nhớ 4KB, 8KB chẳng hạn, hoặc làm việc với các
cấu trúc dữ liệu của hệ điều hành, hoặc thậm chí khi viết các hệ điều hành, bạn
sẽ phải tính toán từng byte một, bạn sẽ phải kiểm soát chính xác kích thước và
vị trí của từng biến. Thậm chí có những câu lệnh mà chẳng ngôn ngữ lập trình
nào cung cấp, như thay đổi chế độ hoạt động của CPU, hoặc thay đổi nội dung của
một thanh ghi hệ thống... trong trường hợp đó bạn phải nhúng thêm một ít code
assembly.
Hầu hết
các ngôn ngữ không cho phép bạn làm điều đó, ngoại trừ một số rất ít kể ở trên.
Điều đó cũng hoàn toàn bình thường, vì số người viết driver, code nhúng hay hệ
điều hành ít hơn nhiều so với số người muốn viết các ứng dụng thông thường.
Nhưng
nói chung là ngôn ngữ lập trình bậc trung vẫn là ngôn ngữ bậc cao!
Đọc thêm loạt bài về OOP:
No comments