3.2. Mã hóa tập lệnh và các tập lệnh
3.2.1. Mã hóa tập lệnh
Một trong những chức năng quan trọng nhất của kiến trúc tập lệnh trong vi xử lý là mô tả cấu trúc và mã hóa các lệnh. Vấn đề mã hóa tập lệnh của vi xử lý ARM được tổng hợp trong Hình 3.2 dưới đây [2].
66
Hình 3.2: Tổng hợp về mã hóa tập lệnh
Hình 3.2 đã chỉ ra phương pháp mã hóa tập lệnh và khung nhìn tổng thể về cấu trúc các nhóm lệnh của vi xử lý ARM. Các câu lệnh ở mức thấp khi được biên dịch ra sẽ thành chuỗi 32-bit, các bit ở các vị trí khác nhau sẽ có ý nghĩa khác nhau.
Các bit có dấu x là các bit không xác định. Các tính chất trong ngoặc vuông có thể được kể đến như sau:
Trường cond không được phép nhận giá trị 1111. Các trường hợp khác cho phép bit [31:28] của câu lệnh là 1111.
Nếu trường mã thao tác (opcode) có dạng 10xx và trường S bằng 0, các dòng sau sẽ được thực thi.
Nếu trường cond là 1111, câu lệnh này là không biết trước kết quả với các chip trước ARMv5.
Trong đa số các trường hợp, chương trình sẽ được viết bằng ngôn ngữ C hay các ngôn ngữ bậc cao khác do đó các nhà phát triển phần mềm không cần thiết phải
67
biết các chi tiết của tập lệnh. Tuy nhiên, có một cái nhìn tổng quan về các lệnh và cấu trúc hợp ngữ sẽ rất có lợi khi gỡ lỗi chương trình. Các công cụ dịch hợp ngữ từ các nhà sản xuất khác nhau (ARM hay GNU) có cú pháp khác nhau. Trong các trường hợp, mã gợi nhớ (mnemonics) của các câu lệnh đều như nhau, điểm khác biệt ở đây là các chỉ dẫn (directives), định nghĩa (definitions), nhãn hay cú pháp chú thích.
Với trình biên dịch hợp ngữ của ARM, định dạng câu lệnh sau được sử dụng:
label
Mã gợi nhớ toán hạng 1, toán hạng 2,... ;chú thích label ở đây được sử dụng như giá trị tham chiếu để trỏ đến địa chỉ. Một số lệnh có thể có nhãn ở trước nó, và máy tính có thể lấy được địa chỉ của câu lệnh thông qua nhãn đó. Các câu lệnh trong tập lệnh ARM có định dạng 32 bit và có các trường khác nhau:
Mã gợi nhớ (mnemonic) (ví dụ ADD, SUB): Chỉ dẫn cho CPU biết thao tác câu lệnh này muốn thực hiện
Toán hạng nguồn (Source Operand): Các câu lệnh thường sẽ xử lý dữ liệu từ một nguồn. Nó có thể là địa chỉ chứa dữ liệu, địa chỉ thanh ghi hay giá trị tức thời.
Toán hạng đích (Destination Register): Chỉ ra đích đến để lưu trữ kết quả Tập lệnh chia ra làm rất nhiều nhóm lệnh như nhóm lệnh thao tác số học, nhóm lệnh thao tác lôgic, nhóm lệnh xử lý ngắt, v.v. Đối với mỗi nhóm lệnh, định dạng của các câu lệnh trong nhóm lệnh đó sẽ có sự khác biệt nhất định. Một câu lệnh ở dạng đơn giản nhất sẽ có định dạng như sau:
Ví dụ 3.1 – Minh họa câu lệnh hợp ngữ ARM đơn giản
Mã C: A = B + C
Mã hợp ngữ ARM: ADD R0, R1, R2
Chú ý rằng các mã gợi nhớ có thể sử dụng với các loại toán hạng khác nhau, và nó có thể đưa đến các kết quả mã hóa câu lệnh khác nhau. Ví dụ, lệnh MOV có thể được sử dụng để truyền dữ liệu giữa hai thanh ghi, hay nó có thể được sử dụng để truyền giá trị trực tiếp vào trong thanh ghi.
Số lượng các toán hạng của một câu lệnh cũng phụ thuộc vào loại câu lệnh đấy là gì, và cú pháp đối với các toán hạng cũng khác nhau trong mỗi trường hợp.
Ví dụ, giá trị trực tiếp thường có kí hiệu “#” đi phía trước:
68
MOVS R0, #0x12 ; R0 = 0x12 (mã hex)
MOVS R1, #’A’ ; R1 = kí tự A trong bảng mã ASCII Nội dung đứng sau dấu chấm phẩy “;” là một chú thích. Chú thích không gây ảnh hưởng đến chương trình, nhưng nó giúp cho người đọc hiểu hơn về chương trình đó.
Công cụ biên dịch hợp ngữ cần phải định nghĩa được các hằng. Việc đó khiến chương trình dễ đọc hơn và dễ bảo trì hơn. Với trình biên dịch của ARM, các hằng được định nghĩa như trong ví dụ dưới đây.
Ví dụ 3.2 – Định nghĩa các hằng cho vectơ ngắt NVIC_IRQ_SETEN EQU 0xE000E100
NVIC_IRQ0_ENABLE EQU 0x1
…
LDR R0, =NVIC_IRQ_SETEN ; thiết lập R0 = 0xE000E100
MOVS R1, #NVIC_IRQ0_ENABLE; ghi giá trị trực tiếp (0x01) lên thanh ghi R1
STR R1, [R0] ; lưu giá trị 0x1 vào 0xE0001000, cho phép ngắt ngoài IRQ#0
Trong đoạn mã lệnh ở trên, địa chỉ của thanh ghi NVIC được nạp vào thanh ghi R0 sử dụng câu lệnh LDR. Trình biên dịch sẽ đặt hằng số vào một vùng bên trong mã chương trình, và chèn một câu lệnh đọc bộ nhớ để đọc giá trị đó vào R0.
Việc sử dụng mã giả là cần thiết vì giá trị địa chỉ là quá lớn đối với một lệnh MOV cho giá trị trực tiếp. Khi sử dụng câu lệnh LDR để nạp địa chỉ vào thanh ghi, giá trị đó cần tiền tố “=”. Ở các trường hợp khác, khi giá trị trực tiếp được nạp vào thanh ghi, giá trị có tiền tố “#”.
Ta cũng có thể định nghĩa dữ liệu ở bên trong chương trình. Ví dụ, ta có thể định nghĩa dữ liệu ở một vị trí xác định bên trong bộ nhớ chương trình và truy cập vào nó thông qua lệnh đọc bộ nhớ.
Ví dụ 3.3 – Minh họa định nghĩa và truy cập vùng nhớ dữ liệu
LDR R3, =MY_NUMBER ; Lấy địa chỉ bộ nhớ của MY_NUMBER LDR R4, [R3] ; Đọc giá trị 0x12345678 vào R4 LDR R0, =HELLO_TEXT ; Nạp địa chỉ ban đầu của HELLO_TEXT BL PrintText ; Gọi hàm PrintText để hiển thị chuỗi
…
ALIGN 4
MY_NUMBER DCD 0x12345678
HELLO_TEXT DCB “Hello\n”, 0 ; Kết thúc chuỗi
69
Ở ví dụ trên, “DCD” được dùng để khai báo một phần tử dữ liệu có kích thước word vào trong bộ nhớ, còn “DCB” khai báo phần tử dữ liệu có kích thước 1 byte vào bộ nhớ. Khi chèn dữ liệu kích thước word vào chương trình, ta cần sử dụng chỉ dẫn “ALIGN” ở trước dữ liệu. Con số đằng sau chỉ dẫn ALIGN quyết định kích thước từng phần tử dữ liệu bên trong bộ nhớ. Trong trường hợp này, mỗi phần tử dữ liệu, dù là word hay byte cũng đều chiếm kích thước word trong bộ nhớ. Bằng cách đó, chương trình có thể truy cập vào dữ liệu chỉ bằng một lần truy cập và truyền dữ liệu qua bus. Một số chỉ dẫn thường được sử dụng để khai báo dữ liệu có thể kể đến như:
Bảng 3.1. Các chỉ dẫn khai báo biến của trình biên dịch hợp ngữ
Kiểu dữ liệu ARM Assembler GNU Assembler
Byte DCB
Ví du: DCB 0x12
.byte
Ví dụ: .byte 0x012 Half-Word DCW
Ví dụ: DCD 0x1234
.hword
Ví dụ: .hword 0x01234
Word DCD
Ví dụ: DCD 0x12345678
.word
Ví dụ: .word 0x01234567 Double-Word DCQ
Ví dụ: DCQ
0x12345678FF0055AA
.quad/.octa Ví dụ: .quad
0x12345678FF0055AA Floating-
point (single – precision)
DCFS
Ví dụ: DCFS 1E3
.float
Ví dụ: .float 1E3
Floating – point (double – precision)
DCFD
Ví dụ: DCFD 3.141519
.double
Ví dụ: .double 3f141519
String DCB
Ví dụ: DCB “Hello\n”, 0
.ascii/.asciz (với kết thúc chuỗi)
Ví dụ: .ascii “Hello\n”
.byte 0 (thêm 0 để kết thúc)
Instruction DCI
Ví dụ: DCI 0xBE00 ; Breakpoint (BKPT 0)
.word / .hword
Ví dụ: .hword 0xBE00
Trong đa số trường hợp, ta có thể thêm một nhãn ở phía trước chỉ dẫn, qua đó địa chỉ dữ liệu có thể được xác định thông qua nhãn đó. Các chỉ dẫn hỗ trợ cho trình biên dịch trong quá trình biên dịch. Một số chức năng của chỉ dẫn có thể kể đến như:
70
Khai báo vùng mã lệnh và và vùng dữ liệu
Khai báo vùng nhớ cho các biến chưa khởi tạo
Khởi tạo vùng nhớ
Hỗ trợ biên dịch các khối điều kiện
Định nghĩa các biến toàn cục
Chỉ ra các thư viện liên kết với chương trình chính.
Có một số chỉ dẫn hữu ích thương được sử dụng khi lập trình hợp ngữ. Ví dụ, một số chỉ dẫn của trình biên dịch ARM có thể kể đến như mô tả trong Bảng 3.2.
Bảng 3.2. Một số chỉ dẫn của trình biên dịch ARM
Chỉ dẫn Mô tả
THUMB (.thumb) Chỉ ra mã lệnh sử dụng thuộc tập lệnh Thumb theo chuẩn UAL.
CODE16 (.code 16) Chỉ ra mã hợp ngữ là lệnh Thumb theo cú pháp trước UAL.
AREA <section_name> {, <attr>}
{, attr}…
Chỉ dẫn cho trình biên dịch tạo một vùng nhớ dành cho mã lệnh hay cho dữ liệu.
Các vùng nhớ độc lập, được đặt tên và không thể được chia nhỏ.
SPACE <số_bytes>
(.zero <số_bytes>)
Xác lập sẵn một vùng nhớ và điền vào đó giá trị 0.
FILL <số_bytes>{, <giá trị>}{,
<kích thước giá trị>}
(.fill <số_bytes>{, <giá
trị>}{, <kích thước giá trị>})
Xác lập một vùng nhớ và điền vào đó các giá trị cho trước. Giá trị có thể là byte, half-word, word, chỉ ra bởi <kích thước giá trị> (1/2/4)
ALIGN {<expr>{,<offset>{,
<pad>{, <padsize>}}}}
(.align <alignment>{, <fill>{,
<max>}})
Sắp đặt các giá trị vào các ô nhớ có độ rộng ngang nhau bằng cách thêm 0 hoặc lệnh NOP.
Ví dụ: ALIGN 8 cho phép mỗi câu lệnh hay phần tử dữ liệu chiếm 8 bytes.
EXPORT <symbol>
(.global <symbol>)
Khai báo một nhãn có thể được sử dụng bởi linker để tham chiếu đến từ một hàm hay thư viện khác.
IMPORT <symbol> Khai báo một nhãn tham chiếu từ một đối tượng hay thư viện khác được xử lý bởi linker
LTORG (.pool) Xác lập một vùng nhớ tức thời. Nó chứa các dữ liệu là hằng hay các giá trị cho câu lệnh LDR.
Đối với tập lệnh ARM, có một yếu tố phải nhắc đến là mật độ mã lệnh (code density). Ở đây ta hiểu mật độ mã lệnh là số lệnh cần để thực hiện một công việc
71
yêu cầu, và không gian bộ nhớ mỗi lệnh chiếm. Mỗi lệnh cần càng ít không gian bộ nhớ và khối lượng công việc mỗi lệnh thực hiện được càng nhiều thì mật độ mã lệnh của tập lệnh đó càng lớn như minh họa trong Ví dụ 3.4 về việc sao chép một khối dữ liệu từ một vùng nhớ sang một vùng nhớ khác.
Ví dụ 3.4 – Minh họa mã hợp ngữ khác nhau với các trình hợp dịch khác nhau
Dùng ngôn ngữ bậc cao, chương trình sẽ có dạng như sau:
void memcpy (void *dest, void *source, int count_bytes) {
char *s, *d;
s = source; d = dest
while (count_bytes--) {*d++ = *s++;}
}
Giả sử chương trình được biên dịch thông qua trình biên dịch X, mã hợp ngữ có thể sẽ như sau:
MOV R0, count_bytes MOV R1, s
MOV R2, d Loop:
Ldrb R3, [R1]
Strb [R2], R3 MOV R3, 1 ADD R1, R3 ADD R2, R3 SUB R0, R3 CMP R0, 0 BNE Loop
Xem xét trình biên dịch của ARM, kết quả có thể như sau:
MOV R0, count_bytes MOV R1, s
MOV R2, d Loop
LDRB R3, [R1++]
STRB [R2++], R3 SUBS R0, R0, 1 BNE Loop
72
Như chúng ta có thể thấy, ở trường hợp đầu tiên, có đến 8 lệnh trong vòng lặp còn với trình biên dịch ARM, chúng ta chỉ cần thực hiện 4 lệnh trong vòng lặp.
Vòng lặp dài thì chương trình sẽ cần nhiều bộ nhớ đệm cho câu lệnh hơn và cần nhiều thời gian thực hiện hơn. Các câu lệnh của ARM đều có điều kiện đi kèm, do đó mỗi lệnh câu lệnh có thể thực hiện được nhiều việc hơn. Ở đây ta có thể nói mật độ mã lệnh của tập lệnh ARM cao hơn của trình biên dịch X.