Trong chương trình hợp ngữ, mỗi lệnh được đặt trên một dòng - dòng lệnh. Do đó, ta không cần phải ngăn cách mỗi lệnh bằng dấu ;
Lệnh có 2 dạng
Không phân biệt chữ hoa hay chữ thường trong các dòng lệnh hợp ngữ
Cú pháp của một lệnh Assembly thường có dạng sau:
[label] [mnemonic] [operands] ; [comment]
Trong đó
;
MAIN PROC
và MAIN ENDP
đánh dấu bắt đầu và kết thúc của một thủ tụcMAIN
trướcMAIN
là tên của thủ tục và PROC
là từ khóa để bắt đầu một thủ tục. Mọi lệnh nằm giữa MAIN PROC
và MAIN ENDP
sẽ thuộc về thủ tục MAIN
MAIN PROC
trong một chương trình Assembly:MAIN PROC
; Các lệnh của thủ tục main ở đây
MAIN ENDP
Trong đoạn mã trên, ta thấy rằng MAIN
chính là phần trường tên cụ thể là tên thủ tục. Và ở trường mã lệnh ta có hai lệnh giả PROC
và ENDP
PROC
và ENDP
theo mẫu sau:tên_ctc PROC
;Các lệnh của thân chương trình
RET
tên_ctc ENDP
JMP
hoặc các lệnh gọi hàm CALL
CALL
, bạn thực sự đang nhảy đến một nhãn được đặt trước đó, thực hiện mã trong phần đó, và sau đó có thể quay lại nơi gọi hàm thông qua lệnh RET
(return)0
ở trước để chương trình dịch có thể hiểu được đó là số hệ 16 chứ không phải là một tên hoặc một nhãnVí dụ các số viết đúng
0011B
: Số hệ 21234
: Số hệ 100ABBAH
: Số hệ 161EF1H
: Số hệ 16
'A'
hay "abcd"
. Chương trình sẽ dịch ký tự ra mã ASCII tương ứng của nóTrong ngôn ngữ lập trình Assembly, một biến là một vùng nhớ được đặt tên, nơi mà chúng ta có thể lưu trữ và truy xuất dữ liệu
Một biến phải được định kiểu dữ liệu trước khi sử dụng, nó có thể được khởi tạo hoặc không cần khởi tạo giá trị ban đầu. Để định nghĩa các kiểu dữ liệu ta dùng các lệnh sau:
DB
(define byte) (1 byte): định nghĩa biến kiểu byteDW
(define word) (2 bytes): định nghĩa biến kiểu từDD
(define double word) (4 bytes): định nghĩa biến kiểu từ képCú pháp khai báo biến:
[Tên] [Kiểu dữ liệu] [Giá trị khởi đầu]
Ví dụ các cách khai báo biến đúng
B1 DB 4
: Khai báo biến B1 với giá trị ban đầu là 4B2 DB ?
: Khai báo biến B2 không có giá trị ban đầuC1 DB 'A
C2 DB 34
Cú pháp khai báo hằng số:
[Tên] EQU [Giá trị]
Hằng có thể là kiểu số, kí tự, hoặc chuỗi kí tự
Một biến kiểu byte trong Assembly có thể chứa được cả số và ký tự. Biến byte có thể chứa bất kỳ giá trị nào có thể được biểu diễn trong 8 bit, bao gồm cả các số nguyên từ 0 đến 255 (hoặc từ -128 đến 127 nếu được xem là số nguyên có dấu) và các ký tự trong bảng mã ASCII.
Biến từ (word) trong Assembly dành 2 byte không gian bộ nhớ. Biến từ có thể chứa các giá trị từ 0 đến 65535 đối với số không dấu (hoặc từ -32768 đến 32767 đối với số có dấu). Nó cũng có thể chứa được tối đa 2 ký tự ASCII, vì mỗi ký tự ASCII chiếm 1 byte
M1 DB 4,5,6,7,8,9
M2 DB 100 DUP(0)
M2 DB 100 DUP(?)
M3 DB 4,3,2, 2 DUP(1, 2 DUP(5), 6)
4, 3, 2
khai báo ba byte đầu tiên của mảng với giá trị lần lượt là 4, 3 và 22 DUP(5)
tạo ra hai phần từ lặp lại là 5, 5DUP (1, 2 DUP(5), 6)
tương ứng với 1, 5, 5, 62 DUP(1, 2 DUP(5), 6)
tương ứng với 1, 5, 5, 6, 1, 5, 5, 6M3
với các giá trị là 4, 3, 2, 1, 5, 5, 6, 1, 5, 5, 6Biến kiểu xâu kí tự là một trường hợp đặc biệt của biến mảng, trong đó các phần tử của mảng là các kí tự, do đó khi khai báo kiểu xâu kí tự ta sẽ sử dụng kiểu dữ liệu DB
cho mỗi ký tự là đủ
Ví dụ: STR1 DB 'string'
(biến STR1 được cấp 6 byte trong bộ nhớ)
Một xâu kí tự cũng có thể được định nghĩa bằng các kí tự và mã ASCII của nó
Ví dụ: STR2 DB 73h, 74h, 'r', 'i', 6Eh, 67h
(Nếu chuyển các giá trị hex sang ký tự ASCII ta sẽ có chuỗi "string")
Để đảm bảo chuỗi được đọc / ghi đúng theo mong đợi thì bạn nên thêm ký tự kết thúc chuỗi $
vào cuối mỗi xâu ký tự
Trong các ngôn ngữ lập trình bậc thấp như Assembly, việc khai báo kích thước bộ nhớ là cần thiết để trình biên dịch biết cách quản lý và sử dụng bộ nhớ. Tuy nhiên, trong một số ngôn ngữ bậc cao như Python, JavaScript hoặc Ruby, việc quản lý bộ nhớ thường được thực hiện tự động thông qua các cơ chế như quản lý bộ nhớ tự động (garbage collection)
Các ngôn ngữ bậc thấp hơn thường yêu cầu lập trình viên phải có kiểm soát chặt chẽ hơn về cách sử dụng bộ nhớ, vì không có cơ chế tự động quản lý bộ nhớ. Trong các ngôn ngữ lập trình bậc cao, trình biên dịch thường có khả năng tự động xác định kích thước của dữ liệu và quản lý bộ nhớ cho bạn, giảm bớt việc phải làm điều này bằng tay. Tuy nhiên điều này có thể làm ảnh hưởng đến hiệu suất của chương trình, đặc biệt là trong các ứng dụng yêu cầu hiệu suất cao hoặc có hạn chế về tài nguyên như lập trình vi xử lý bằng Assembly[1]
Ta khai báo quy mô sử dụng bộ nhớ như sau:
.MODEL [Kiểu kích thước bộ nhớ]
Các kiểu kích thước bộ nhớ cho chương trình hợp ngữ[2]
Kiểu kích thước | Giới hạn |
---|---|
Tiny (Hẹp) | Sử dụng tối đa 64K bytes cho cả Code và Data (dữ liệu) |
Small (Nhỏ) | Sử dụng tối đa 64KB cho Code và 64KB cho Data (Code <= 64KB, Data <= 64KB) (Trong chương trình học của mình thì ta sẽ sử dụng kiểu khai báo này nhiều nhất) |
Medium (Trung bình) | Sử dụng tối đa 64KB cho Data và có thể nhiều hơn 64KB cho code (Code > 64KB, Data <= 64KB) |
Compact (Gọn) | Sử dụng tối đa 64KB cho Code và có thể nhiều hơn cho 64KB cho Data (Code <= 64KB, Data > 64KB) |
Large (Lớn) | Cả Code và Data đều có thể vượt quá dung lượng 64KB. Tuy nhiên, khi sử dụng mảng (array) ta không được sử dụng vượt quá 64KB (Code > 64KB, Data > 64KB) |
Huge (Đồ sộ) | Cả Code và Data đều có thể vượt quá dung lượng 64KB. Thêm nữa là ta cũng có thể sử dụng mảng vượt quá 64KB |
Cách khai báo đoạn ngăn xếp:
.STACK [Kích thước]
Stack Segment chủ yếu được sử dụng để lưu trữ dữ liệu tạm thời và thông tin về ngữ cảnh của chương trình, như giá trị trả về của các hàm và địa chỉ trở lại sau khi gọi hàm (giống như khi thực hiện quay lui hay đệ quy vậy)
Nếu không khai báo kích thước thì chương trình dịch sẽ tự động gán cho kích thước giá trị là 1KB
Tuy nhiên, đối với chương trình học của ta thì kích thước ngăn xếp này là quá lớn nên ta sẽ sử dụng 100-256 byte là đủ
Trong Assembly, Stack
được khai báo trên đầu của chương trình và là Stack
duy nhất được sử dụng xuyên suốt chương trình
Để sử dụng Stack
ta sử dụng hai lệnh là PUSH
và POP
.DATA
.DATA
MSG DB 'hello!$'
CR DB 13
LF EQU 10
.DATA
của chương trình Assembly, hệ thống sẽ cấp phát một vùng nhớ trong RAM (bộ nhớ chính) để lưu trữ giá trị của biến đó. Địa chỉ của vùng nhớ này sẽ được liên kết với tên biến mà bạn đã khai báo. Chính vì vậy mà sau khi khai báo dữ liệu xong ta cần phải tiếp tục khởi tạo cho thanh ghi DS
như sau:;khoi tao DS
MOV AX, @Data ;khoi dau thanh ghi DS
MOV DS, AX ;tro thanh ghi ds ve dau doan data
DS
trong Assembly không chứa dữ liệu trực tiếp mà chứa địa chỉ của đoạn dữ liệu. Khi bạn thực hiện một lệnh truy cập dữ liệu, DS
sẽ được sử dụng như một cơ sở để tính toán địa chỉ thực sự của dữ liệu cần truy cập. Đây là cách mà các lệnh trên thanh ghi có thể truy cập vào vùng nhớ rộng lớn hơn nhiều so với kích thước của thanh ghiĐể hiểu tại sao lại cần sử dụng 2 lệnh MOV để khởi tạo cho thanh ghi DS bạn hãy đọc nội dung phần #8.1
Trong hệ điều hành DOS, ngắt (interrupt) là một quá trình dừng chương trình chính đang chạy để ưu tiên thực hiện một chương trình khác
Khi một thiết bị phần cứng (như bàn phím hoặc chuột) cần sự chú ý của máy tính, nó sẽ gửi một "ngắt" đến bộ xử lý. Bộ xử lý sau đó sẽ dừng chương trình đang chạy để xử lý ngắt, và sau đó quay trở lại tiếp tục chương trình chính
Một lệnh ngắt trong DOS được sử dụng thường xuyên cho Emu8086 là ngắt INT 21H
. Ngắt này được sử dụng để giao tiếp với bàn phím và màn hình. Dưới đây là cách sử dụng một số hàm của lệnh ngắt INT 21H:
MOV AH, 1
INT 21H ; Ngắt để nhập 1 kí tự vào từ bàn phím
Thanh ghi AH đóng vai trò quan trọng trong việc thực hiện ngắt. Trong lệnh ngắt, giá trị được đặt trong thanh ghi AH sẽ giúp xác định hàm cụ thể sẽ được thực hiện
MOV AH, 2
MOV DL, MÃ ASCII CỦA KÍ TỰ
INT 21H ; Ngắt để hiển thị kí tự ra màn hình
Ví dụ: Hiển thị kí tự 'A'
MOV AH, 2
MOV DL, 'A'
INT 21H ; Ngắt để hiển thị kí tự ra màn hình
$
. Điều này có nghĩa là, nó sẽ in ra chuỗi ký tự cho đến khi gặp ký tự $
MOV AH, 9
LEA DX, message
INT 21H
...
message DB 'Hello, World!$'
MOV AH, 10
LEA DX, str
INT 21H
INT 21H
với AH = 10 được sử dụng để đọc một chuỗi từ bàn phím. Chuỗi này được lưu dưới dạng chuỗi DOS, trong đó byte đầu tiên chứa độ dài tối đa của chuỗi, byte thứ hai chứa độ dài thực sự của chuỗi, và các byte tiếp theo chứa các ký tự của chuỗistr[0]
chứa độ dài tối đa của chuỗi, str[1]
chứa độ dài thực sự của chuỗi. Khi thực hiện in chuỗi nhập vào từ bàn phím thì ta sẽ in ra bắt đầu từ str[2]
Ví dụ: chuỗi 'Hello' mà bạn nhập vào bàn phím thực chất là 256,5,H,e,l,l,o
MOVE AH, 4CH
INT 21H
Để hiểu cách IF THEN hoạt động ta đi vào một ví dụ sau:
Thực hiện gán giá trị tuyệt đối của AX cho BX
CMP AX, 0
JNL GAN
NEG AX
GAN: MOV BX, AX
Giải thích
CMP AX, 0
: So sánh giá trị trong thanh ghi AX với giá trị 0. Kết quả của phép so sánh này sẽ được lưu trong các cờ của bộ xử lýJNL GAN
: Đây là một lệnh nhảy có điều kiện. Trong trường hợp này JNL
là viết tắt của "Jump if Not Less" (nhảy nếu không nhỏ hơn). Nó kiểm tra cờ Zero (ZF) và cờ Sign (SF). Nếu ZF = 1 (giá trị = 0) hoặc SF = 0 (Giá trị dương), nghĩa là giá trị trong AX không nhỏ hơn 0, thì chương trình sẽ nhảy đến nhãn GAN
. Nếu không, chương trình sẽ tiếp tục thực thi lệnh tiếp theoNEG AX
: Nếu giá trị trong AX là số âm, lệnh này sẽ đảo dấu của nóGAN: MOV BX, AX
: Trong lệnh này GAN
là nhãn (label). Lệnh MOV BX, AX
sao chép giá trị trong thanh ghi AX vào thanh ghi BXCác lệnh rẽ nhánh tiêu biểu
Instruction | Description | Flags Test |
---|---|---|
JE/JZ | Jump Equal or Jump Zero | ZF |
JNE/JNZ | Jump Not Equal or Jump Not Zero | ZF |
JG/JNLE | Jump Greater or Jump Not Less/Equal | OF, SF, ZF |
JGE/JNL | Jump Greater/Equal or Jump Not Less | OF, SF |
JL/JNGE | Jump Less or Jump Not Greater/Equal | OF, SF |
JLE/JNG | Jump Less/Equal or Jump Not Greater | OF, SF, ZF |
for
như các ngôn ngữ cấp cao khác. Tuy nhiên, bạn vẫn có thể mô phỏng cấu trúc for bằng cách sử dụng các chỉ thị nhảy có điều kiện.MODEL SMALL
.STACK 100h
.DATA
count DB 9 ; Số lần lặp
.CODE
MAIN PROC
MOV AX, @DATA
MOV DS, AX
MOV CH, count ; Khởi tạo CH bằng giá trị của biến count
; Vòng lặp for
FOR_LOOP:
; Thực hiện các lệnh ở đây
; Ví dụ: in ra các giá trị của CX
MOV AH, 2
MOV DL, CH
ADD DL, '0'; Thực hiện ép kiểu số sang ký tự trước khi in ra
INT 21h
; Giảm CX đi 1
DEC CH
CMP CH, 0
; Kiểm tra điều kiện lặp, nếu CH bằng 0 thì dừng lại
JNE FOR_LOOP
; Kết thúc chương trình
MOV AH, 4Ch
INT 21h
MAIN ENDP
END MAIN
MOV Dest, Source
: dữ liệu được chép từ toán hạng nguồn (Source) tới toán hạng đích (Dest). Lệnh này không làm thay đổi toán hạng nguồn, chỉ làm thay đổi toán hạng đích. Chú ý một số trường hợp không được sử dụng lệnh này bao gồm:
LEA Dest, Source
(Load Effective Address): Lệnh gán địa chỉ của gốc vào đích
INC [Toán hạng đích]
: Làm tăng giá trị của Toán hạng đích lên 1 đơn vịDEC [Toán hạng đích]
: Làm giảm giá trị của Toán hạng đích xuống 1 đơn vịADD [Toán hạng đích], [Toán hạng nguồn]
: Lấy giá trị / nội dung của Toán hạng nguồn cộng vào giá trị / nội dung của Toán hạng đíchSUB [Toán hạng đích], [Toán hạng nguồn]
: Lấy giá trị / nội dung của Toán hạng đích trừ đi giá trị / nội dung của Toán hạng nguồnMUL [Toán hạng nguồn]
: Thực hiện phép nhân trên số không dấuMUL CX
có nghĩa là MUL AX, CX
DX:AX
DX
AX
IMUL [Toán hạng nguồn]
(Integer Multiply): Tương tự như MUL nhưng là số có dấuDIV [Toán hạng nguồn]
: Thực hiện phép chia trên số không dấu.IDIV [Toán hạng nguồn]
:Tương tự như DIV nhưng là số có dấuNOT [Toán hạng đích]
AND [Toán hạng đích], [Toán hạng nguồn]
OR [Toán hạng đích], [Toán hạng nguồn]
XOR [Toán hạng đích], [Toán hạng nguồn]
XOR
nhiều nhất để khởi tạo một thanh ghi với giá trị ban đầu là 0, hoặc có thể được sử dụng để khởi tạo bộ đếm CX về 0 trước khi sử dụng trong một vòng lặp bằng cách viết XOR CX, CX
LOOP [Nhãn đích]
Nhãn đích
đủ n lần, với n được đặt trước trong thanh ghi CX (Thanh ghi CX được sử dụng làm bộ đếm vòng lặp)Lap:
;các lệnh cần lặp được viết ở đây
LOOP Lap
lodsb
(viết tắt của "Load String Byte"): là lệnh di chuyển dữ liệu từ bộ nhớ vào thành ghi AX và tăng con trỏ bộ nhớ sau khi lệnh được thực thi. Cụ thể nó hoạt động như saulodsb
sẽ đọc một byte từ bộ nhớ tại địa chỉ mà con trỏ bộ nhớ (nằm trong thành ghi SI) trỏ tớilodsw
cũng giống như lodsb
nhưng nó đọc một từ (word) từ bộ nhớ và lưu trữ trong thanh ghi AX.lodsw
sẽ đọc hai byte từ bộ nhớ tại địa chỉ mà con trỏ bộ nhớ (nằm trong thành ghi SI) trỏ tớiCMP Dest, Source
CMP
(Compare) được sử dụng để so sánh giá trị / nội dung của Toán hạng đích so với Toán hạng nguồnCMP Dest, Source
là lệnh SUB Dest, Source
(thực hiện phép tính Dest - Source
, nhưng kết quả của phép tính không được lưu vào Des
như trong lệnh SUB
. Thay vào đó, kết quả của phép trừ được sử dụng để cập nhật trong thanh ghi cờJE
(Jump if Equal) để kiểm soát luồng thực thi của chương trình