Hầu hết các hệ điều hành máy tính hiện nay, đặc biệt là các hệ điều hành của Microsoft, đều hỗ trợ hai dạng cấu trúc tập tin thực thi có thể hoạt động trên nó, đó là tập tin cấu trúc dạng COM và tập tin cấu trúc dạng EXE. Có nhiều điểm khác nhau giữa hai cấu trúc chương trình này, nhưng điểm khác biệt lớn nhất là: Các chương trình cấu trúc dạng EXE gồm 3 đoạn: Mã lệnh (Code), dữ liệu (Data) và Ngăn xếp (Stack). Khi hoạt động, 3 đoạn này sẽ được nạp vào 3 đoạn (Segment) bộ nhớ tách biệt trên bộ nhớ;
Các chương trình dạng COM thì ngược lại, nó chỉ có 1 đoạn mã lệnh, trong đó chứa cả mã lệnh và ngăn xếp. Vì thế, khi được nạp vào bộ nhớ để hoạt động nó chỉ được cấp phát một đoạn bộ nhớ. Rõ ràng kích thước của một chương trình dạng COM không thể vượt quá giới hạn của một đoạn bộ nhớ (với Intel 8088/80286 và MSDOS, 1 Segment bộ nhớ = 64KB).
Trong khi đó một chương trình dạng EXE có thể lớn hơn 3 Segment bộ nhớ. Do đó, khi thiết kế các chương trình lớn, với chức năng phức tạp, trong đó có liên kết giữa các modun chương trình khác nhau thì ta phải thiết kế theo cấu trúc chương trình dạng EXE.
Hợp ngữ hỗ trợ thiết kế cả hai dạng cấu trúc chương trình EXE và COM, mỗi dạng phù hợp với một nhóm trình biên dịch nào đó. Muốn biên dịch một chương trình hợp ngữ sang dạng EXE thì ngoài việc nó phải được viết theo cấu trúc dạng EXE ta còn cần phải sử dụng một trình biên dịch phù hợp. Điều này cũng tương tự với việc muốn có một chương trình thực thi dạng COM.
Văn bản của một chương trình hợp ngữ dạng EXE cũng cho thấy rõ nó gồm 3 đoạn: Code, Data và Stack. Tương tự, văn bản của chương trình hợp ngữ dạng COM cho thấy nó chỉ có 1 đoạn: Code, cả Data và Stack (không tường minh) đều nằm ở đây.
Một chương trình hợp ngữ gồm hai thành phần chính: phần lệnh hợp ngữ và phần chỉ dẫn biên dịch. Chỉ có các lệnh là được biên dịch thành ngôn ngữ máy. Phần hướng dẫn biên dịch không được dịch sang ngôn ngữ máy, nó chỉ có tác dụng với các trình biên dịch. Thông thường mỗi chương trình biên dịch có một nhóm hướng dẫn biên dịch phù hợp với nó, những với các hướng dẫn biên dịch cơ bản và đơn giản thì nó phù hợp với hầu hết các trình biên dịch hợp ngữ hiện nay. Trong tài liệu này chúng tôi sử dụng các hướng dẫn biên dịch phù hợp với trình biên dịch Microsoft Macro Assembler (MASM).
Cấu trúc chương trình được giới thiệu sau đây sử dụng các hướng dẫn biên dịch định nghĩa đoạn đơn giản (.Model, .Code, .Stack, .Data) phù hợp với MASM, TASM (Turbo Macro Assembler), A86. Việc sử dụng định nghĩa đoạn đơn giản sẽ làm cho văn bản chương trình sáng sủa và dễ đọc hơn. Với các định nghĩa đoạn đơn giản ta cũng có thể xây dựng được các chương trình từ đơn giản đến phức tạp.
Cấu trúc chương trình dạng COM:
.Model <Chế độ bộ nhớ>
.Code
ORG 100h
<Nhãn chính>:
JMP <Thủ tục chính>
<Khai báo dữ liệu đặt tại đây>
<Thủ tục chính> PROC
<Các lệnh của chương trình đặt tại đây>
<Thủ tục chính> Endp
<Các thủ tục khác đặt tại đây>
End <Nhãn chính>
Trong cấu trúc chương trình trên các từ khóa Model, Code, ORG, Proc, Endp, End là các hướng dẫn biên dịch. <Nhãn chính> là nhãn của lệnh Jmp.
Cấu trúc này cho thấy rõ, một chương trình hợp ngữ dạng COM chỉ có 1 đoạn, đó chính là đoạn Code (đoạn mã lệnh), trong này bao gồm cả phần khai báo dữ liệu. Các khai báo dữ liệu trong chương trình dạng COM có thể đặt ở đầu hoặc ở cuối chương trình, nhưng với việc sử dụng định nghĩa đoạn đơn giản các khai báo dữ liệu phải đặt ở đầu chương trình.
Chỉ dẫn ORG 100h và lệnh JMP <Thủ tục chính> sẽ được đề cập trở lại ở các phần sau đây của tài liệu này.
Cấu trúc chương trình dạng EXE:
.Model <Chế độ bộ nhớ>
.Stack 100h
.Data
<Khai báo dữ liệu đặt tại đây>
.Code
<Thủ tục chính> PROC
<Các lệnh của chương trình đặt tại đây>
<Thủ tục chính> Endp
<Các thủ tục khác đặt tại đây>
END
Trong cấu trúc chương trình trên các từ khóa Model, Code, Data, Stack, Proc, Endp, End là các hướng dẫn biên dịch.
Cấu trúc này cho thấy rõ, một chương trình hợp ngữ dạng gồm 3 đoạn: đoạn Code, chứa toàn bộ mã lệnh của chương trình. Đoạn Data, chứa phần khai báo dữ liệu của chương trình. Đoạn Stack, nơi chứa stack (ngăn xếp) của chương trình khi chương trình được nạp vào bộ nhớ để hoạt động.
Chỉ dẫn .Stackđặt ở đầu chương trình với mục đích khai báo kích thước của Stack dùng cho chương trình sau này. Kích thước thường được chọn là 100h (256) byte.
Chỉ dẫn .Model được đặt ở đầu cả cấu trúc chương trình dạng COM và EXE với mục đích khai báo chế độ bộ nhớ mà chương trình sử dụng.
Ví dụ: Sau đây là hai chương trình hợp ngữ đơn giản, dạng COM và dạng EXE, cùng thực hiện nhiệm vụ in ra màn hình 3 dòng văn bản như sau :
Nguyen Kim Le Tuan
Nguyen Le Tram Thanh
Nguyen Le Tram Uyen
Hai chương trình dưới đây chỉ có tác dụng minh họa cho việc sử dụng các hướng dẫn biên dịch định nghĩa đoạn đơn giản và giúp các bạn thấy được những điểm giống nhau, khác nhau giữa hai dạng cấu trúc chương trình dạng COM và EXE, vì vậy, ở đây các bạn chưa cần quan tâm đến ý nghĩa của các lệnh và các hàm/ngắt trong nó. Phần lệnh hợp ngữ và các hàm/ngắt sẽ được trình bày ngay sau đây.
Chương trình viết theo cấu trúc dạng COM:
.Model Small
.Code
ORG 100h
Start:
Jmp Main
MyChildren DB ‘Nguyen Kim Le Tuan’,0Ah,0Dh
DB ‘Nguyen Le Tram Thanh’,0Ah,0Dh
DB ‘Nguyen Le Tram Uyen’,’$’
Main PROC
;------- in ra mot xau voi ham 09/21h -------
Mov Ah, 09h
Lea Dx, MyChildren
Int 21h
;------- ket thuc chuong trinh -------
Int 20h
Main Endp
End Start
Chương trình này chọn chế độ bộ nhớ Small. Tên thủ tục chính là Main (tên thủ tục chính là tùy ý). Nhãn chính của chương trình là Start (tên thủ tục chính là tùy ý), đó chính là nhãn của lệnh Jmp. Phần khai báo dữ liệu chỉ khai báo một biến, đó là MyChildren.
Chương trình này gọi hàm 4Ch của ngắt 21h để kết thúc chương trình. Có thể gọi ngắt 20h để kết thúc các chương trình dạng COM.
Chương trình viết theo cấu trúc dạng EXE:
.Model Small
.Stack 100h
.Data
MyChildren DB ‘Nguyen Kim Le Tuan’,0Ah,0Dh
DB ‘Nguyen Le Tram Thanh’,0Ah,0Dh
DB ‘Nguyen Le Tram Uyen’,’$’
.Code
Main PROC
;------- khởi tạo DS -------
Mov Ax, @Data
Mov DS, Ax
;------- in ra mot xau voi ham 09/21h -------
Mov Ah, 09h
Lea Dx, MyChildren
Int 21h
;------- ket thuc chuong trinh -------
Mov Ah, 4Ch
Int 21h
Main Endp
END Main
Chương trình này chọn chế độ bộ nhớ Small. Khai báo kích thước Stack là 100h byte. Phần khai báo dữ liệu được đặt trong đoạn Data, ở đây chỉ khai báo một biến, đó là MyChildren. Tên thủ tục chính là Main (tên thủ tục chính là tùy ý).
Thao tác đầu tiên của chương trình là trỏ thanh ghi đoạn DS về đầu đoạn Data, hay còn gọi là khởi tạo thanh ghi đoạn DS:
Mov Ax, @Data
Mov DS, Ax
thao tác này được xem như là bắt buộc đối với cấu trúc chương trình dạng EXE sử dụng định nghĩa đoạn đơn giản. Các chương trình viết theo cấu trúc dạng EXE phải gọi hàm 4Ch của ngắt 21h để kết thúc.
Có thể thấy, cấu trúc chương trình dạng COM và cấu trúc chương trình dạng EXE chỉ khác phần hướng dẫn biên dịch, phần khai báo biến và phần lệnh thao tác chính hoàn toàn giống nhau. Hai chương trình đơn giản ở trên hoàn toàn giống nhau ở biến là MyChildren và các lệnh gọi hàm 09h của ngắt 21h để in ra màn hình một xâu kí tự (xâu này chính là giá trị khởi tạo của biến MyChildren).
Chú ý 1: Trình biên dịch hợp ngữ (Macro Assembler) cho phép các chương trình được dịch bởi nóc họn sử dụng một trong các chế độ bộ nhớ sau:
- Small: Đoạn mã lệnh (Code) và đoạn dữ liệu (Data) của chương trình đều chỉ có thể chứa trong một đoạn (segment) bộ nhớ. Tức là, kích thước của chương trình chỉ có thể tối đa là hai đoạn bộ nhớ. Tuy vậy chế độ bộ nhớ này đủ dùng cho hầu hết các chương trình hợp ngữ.
- Medium: Đoạn Code của chương trình có thể chiếm nhiều hơn một đoạn bộ nhớ. Trong khi đó, đoạn Data chỉ có thể chiếm 1 đoạn bộ nhớ.
- Compact: Đoạn Data của chương trình có thể chiếm nhiều hơn một đoạn bộ nhớ. Trong khi đó, đoạn Code chỉ có thể chiếm 1 đoạn bộ nhớ.
- Large: Đoạn Code và đoan Data của chương trình đều có thể chiếm nhiều hơn một đoạn bộ nhớ. Nhưng trong trường hợp này không thể định nghĩa một mảng dữ liệu có kích thước lớn hơn 64 Kbyte.
- Huge: Tương tự như Large, nhưng trong trường hợp này có thể định nghĩa một mảng dữ liệu có kích thước lớn hơn 64 Kbyte.
Chế độ bộ nhớ Small là đơn giản nhất, được hầu hết các chương trình lựa chọn.
Chú ý 2: Với các chương trình hợp ngữ sử dụng định nghĩa đoạn đơn giản: Khi được nạp vào bộ nhớ để hoạt động thì các thanh ghi đoạn sẽ tự động trỏ về các đoạn chương trình tương ứng. Cụ thể: Thanh ghi đoạn CS chứa địa chỉ segment của đoạn bộ nhớ chứa đoạn Code của chương trình. Thanh ghi đoạn DS (và có thể cả ES) chứa địa chỉ segment của đoạn bộ nhớ chứa đoạn Data của chương trình. Thanh ghi đoạn SS chứa địa chỉ segment của đoạn bộ nhớ chứa đoạn Stack của chương trình.
Tuy nhiên, trong thực tế, khi nạp chương trình EXE vào bộ nhớ DOS luôn dành ra 256 byte đầu tiên của vùng nhớ, mà DOS cấp phát cho chương trình, để chứa PSP (Program Segment Prefix) của chương trình. PSP chứa các thông tin cần thiết mà trình biên dịch chuyển đến cho DOS để hỗ trợ DOS trong việc thực hiện chương trình này, đặc biệt, chương trình cũng có thể truy xuất vùng nhớ PSP. Do đó, DOS phải đưa địa chỉ segment của vùng nhớ chứa PSP vào cả DS và ES trước khi chương trình được thực hiện. Tức là, ngay khi chương trình được nạp vào bộ nhớ DS không phải chứa địa chỉ segment của đoạn Data của chương trình mà chứa địa chỉ segment của PSP.
Vì vậy, để trỏ DS về lại đoạn Data chương trình chúng ta phải đặt ngay hai lệnh sau đây ở đầu chương trình viết theo cấu trúc EXE:
Mov Ax, @Data
Mov DS, Ax
Với việc khởi tạo thanh ghi đoạn DS ở trên, địa chỉ segment của tất cả các biến khai báo trong đoạn Data đều được chứa trong thanh ghi DS, do đó, trong các thao tác xử lý biến sau này chương trình không cần quan tâm đến địa chỉ segment của nó nữa.
Chú ý 3: Hợp ngữ còn cho phép các chương trình sử dụng các hướng dẫn biên dịch định nghĩa đoạn toàn phần, các định nghĩa này phù hợp với hầu hết các trình biên dịch hợp ngữ hiện nay. Định nghĩa đoạn toàn phần giúp cho việc viết chương trình hợp ngữ trở nên mềm dẻo và linh hoạt hơn, nó giúp người lập trình có thể điều khiển thứ tự các đoạn chương trình, kết hợp các đoạn chương trình, liên kết các đoạn chương trình trong bộ nhớ,... , ngay trong khi lập trình.
Chi tiết về cách sử dụng và mục đích sử dụng của các hướng dẫn biên dịch nói chung và các định nghĩa đoạn toàn phần nói riêng dễ dàng tìm thấy trong rất nhiều tài liệu về lập trình hợp ngữ [1], [2]. Ở đây chúng tôi chỉ giới thiệu sơ lược về nó thông qua ví dụ dưới đây.
Ví dụ: Sau đây là một chương trình dạng EXE sử dụng các hướng dẫn biên dịch định nghĩa đoạn toàn phần (phù hợp với Macro Assembler):
S_Seg Segment Stack
DB 100h DUP (?)
S_Seg Ends
D_Seg Segmet
MyChildren DB ‘Nguyen Kim Le Tuan’,0Ah,0Dh
DB ‘Nguyen Le Tram Thanh’,0Ah,0Dh
DB ‘Nguyen Le Tram Uyen’,’$’
D_Seg Ends
C_Seg Segment
ASSUME CS:C_Seg, SS:S_Seg, DS:D_Seg
Main PROC
;------- khởi tạo DS -------
Mov Ax, D_Seg
Mov DS, Ax
Mov Ah, 09h
Lea Dx, MyChildren ; địa chỉ offset của biến MyChildren
Int 21h
Mov Ah, 4Ch
Int 21h
Main Endp
C_Seg Ends
END Main
Điều dễ nhận thấy đầu tiên là phần khai báo biến và phần lệnh chính trong chương trình này hoàn toàn giống như trong chương trình sử dụng định nghĩa đoạn đơn giản (hai chương trình ví dụ ở trên).
Chương trình này sử dụng hướng dẫn biên dịch định nghĩa đoạn toàn phần Segment ... Ends để định nghĩa 3 đoạn chương trình với tên lần lượt là: S_Seg (đoạn stack), D_Seg (đoạn Data), C_Seg (đoạn Code). Tên của các đoạn được định nghĩa ở đây là tùy ý.
Hướng dẫn biên dịch Assume được sử dụng để báo cho trình biên dịch biết chương trình muốn chứa địa chỉ segment của các đoạn chương trình trong các thanh ghi đoạn nào (trỏ thanh ghi đoạn về đoạn chương trình). Cụ thể ở đây là: Thanh ghi đoạn CS chứa địa chỉ segment của đoạn Code (CS:C_Seg). Thanh ghi đoạn SS chứa địa chỉ segment của đoạn Stack (SS:S_Seg). Thanh ghi đoạn DS chứa địa chỉ segment của đoạn Data (DS:C_Seg). Tuy nhiên, trong thực tế Assume DS:D_Seg không tự động nạp địa chỉ segment của D_Seg vào DS, do đó chương trình phải nạp trực tiếp bằng các lệnh:
Mov Ax, D_Seg
Mov DS, Ax
Nên nhớ, hướng dẫn biên dịch Segment ... Ends chỉ có tác dụng định nghĩa đoạn, nó không thể báo cho trình biên dịch biết đoạn được định nghĩa thuộc loại đoạn chương trình nào (Code, Data, Stack, Extra). Chỉ có định nghĩa Segment Stack ... Ends là báo cho trình biên dịch biết đoạn được định nghĩa là đoạn Stack, nhờ đó, khi chương trình được nạp vào bộ nhớ thanh ghi đoạn SS sẽ được trỏ về đoạn này.
Các chương trình dạng COM thì ngược lại, nó chỉ có 1 đoạn mã lệnh, trong đó chứa cả mã lệnh và ngăn xếp. Vì thế, khi được nạp vào bộ nhớ để hoạt động nó chỉ được cấp phát một đoạn bộ nhớ. Rõ ràng kích thước của một chương trình dạng COM không thể vượt quá giới hạn của một đoạn bộ nhớ (với Intel 8088/80286 và MSDOS, 1 Segment bộ nhớ = 64KB).
Trong khi đó một chương trình dạng EXE có thể lớn hơn 3 Segment bộ nhớ. Do đó, khi thiết kế các chương trình lớn, với chức năng phức tạp, trong đó có liên kết giữa các modun chương trình khác nhau thì ta phải thiết kế theo cấu trúc chương trình dạng EXE.
Hợp ngữ hỗ trợ thiết kế cả hai dạng cấu trúc chương trình EXE và COM, mỗi dạng phù hợp với một nhóm trình biên dịch nào đó. Muốn biên dịch một chương trình hợp ngữ sang dạng EXE thì ngoài việc nó phải được viết theo cấu trúc dạng EXE ta còn cần phải sử dụng một trình biên dịch phù hợp. Điều này cũng tương tự với việc muốn có một chương trình thực thi dạng COM.
Văn bản của một chương trình hợp ngữ dạng EXE cũng cho thấy rõ nó gồm 3 đoạn: Code, Data và Stack. Tương tự, văn bản của chương trình hợp ngữ dạng COM cho thấy nó chỉ có 1 đoạn: Code, cả Data và Stack (không tường minh) đều nằm ở đây.
Một chương trình hợp ngữ gồm hai thành phần chính: phần lệnh hợp ngữ và phần chỉ dẫn biên dịch. Chỉ có các lệnh là được biên dịch thành ngôn ngữ máy. Phần hướng dẫn biên dịch không được dịch sang ngôn ngữ máy, nó chỉ có tác dụng với các trình biên dịch. Thông thường mỗi chương trình biên dịch có một nhóm hướng dẫn biên dịch phù hợp với nó, những với các hướng dẫn biên dịch cơ bản và đơn giản thì nó phù hợp với hầu hết các trình biên dịch hợp ngữ hiện nay. Trong tài liệu này chúng tôi sử dụng các hướng dẫn biên dịch phù hợp với trình biên dịch Microsoft Macro Assembler (MASM).
Cấu trúc chương trình được giới thiệu sau đây sử dụng các hướng dẫn biên dịch định nghĩa đoạn đơn giản (.Model, .Code, .Stack, .Data) phù hợp với MASM, TASM (Turbo Macro Assembler), A86. Việc sử dụng định nghĩa đoạn đơn giản sẽ làm cho văn bản chương trình sáng sủa và dễ đọc hơn. Với các định nghĩa đoạn đơn giản ta cũng có thể xây dựng được các chương trình từ đơn giản đến phức tạp.
Cấu trúc chương trình dạng COM:
.Model <Chế độ bộ nhớ>
.Code
ORG 100h
<Nhãn chính>:
JMP <Thủ tục chính>
<Khai báo dữ liệu đặt tại đây>
<Thủ tục chính> PROC
<Các lệnh của chương trình đặt tại đây>
<Thủ tục chính> Endp
<Các thủ tục khác đặt tại đây>
End <Nhãn chính>
Trong cấu trúc chương trình trên các từ khóa Model, Code, ORG, Proc, Endp, End là các hướng dẫn biên dịch. <Nhãn chính> là nhãn của lệnh Jmp.
Cấu trúc này cho thấy rõ, một chương trình hợp ngữ dạng COM chỉ có 1 đoạn, đó chính là đoạn Code (đoạn mã lệnh), trong này bao gồm cả phần khai báo dữ liệu. Các khai báo dữ liệu trong chương trình dạng COM có thể đặt ở đầu hoặc ở cuối chương trình, nhưng với việc sử dụng định nghĩa đoạn đơn giản các khai báo dữ liệu phải đặt ở đầu chương trình.
Chỉ dẫn ORG 100h và lệnh JMP <Thủ tục chính> sẽ được đề cập trở lại ở các phần sau đây của tài liệu này.
Cấu trúc chương trình dạng EXE:
.Model <Chế độ bộ nhớ>
.Stack 100h
.Data
<Khai báo dữ liệu đặt tại đây>
.Code
<Thủ tục chính> PROC
<Các lệnh của chương trình đặt tại đây>
<Thủ tục chính> Endp
<Các thủ tục khác đặt tại đây>
END
Trong cấu trúc chương trình trên các từ khóa Model, Code, Data, Stack, Proc, Endp, End là các hướng dẫn biên dịch.
Cấu trúc này cho thấy rõ, một chương trình hợp ngữ dạng gồm 3 đoạn: đoạn Code, chứa toàn bộ mã lệnh của chương trình. Đoạn Data, chứa phần khai báo dữ liệu của chương trình. Đoạn Stack, nơi chứa stack (ngăn xếp) của chương trình khi chương trình được nạp vào bộ nhớ để hoạt động.
Chỉ dẫn .Stackđặt ở đầu chương trình với mục đích khai báo kích thước của Stack dùng cho chương trình sau này. Kích thước thường được chọn là 100h (256) byte.
Chỉ dẫn .Model được đặt ở đầu cả cấu trúc chương trình dạng COM và EXE với mục đích khai báo chế độ bộ nhớ mà chương trình sử dụng.
Ví dụ: Sau đây là hai chương trình hợp ngữ đơn giản, dạng COM và dạng EXE, cùng thực hiện nhiệm vụ in ra màn hình 3 dòng văn bản như sau :
Nguyen Kim Le Tuan
Nguyen Le Tram Thanh
Nguyen Le Tram Uyen
Hai chương trình dưới đây chỉ có tác dụng minh họa cho việc sử dụng các hướng dẫn biên dịch định nghĩa đoạn đơn giản và giúp các bạn thấy được những điểm giống nhau, khác nhau giữa hai dạng cấu trúc chương trình dạng COM và EXE, vì vậy, ở đây các bạn chưa cần quan tâm đến ý nghĩa của các lệnh và các hàm/ngắt trong nó. Phần lệnh hợp ngữ và các hàm/ngắt sẽ được trình bày ngay sau đây.
Chương trình viết theo cấu trúc dạng COM:
.Model Small
.Code
ORG 100h
Start:
Jmp Main
MyChildren DB ‘Nguyen Kim Le Tuan’,0Ah,0Dh
DB ‘Nguyen Le Tram Thanh’,0Ah,0Dh
DB ‘Nguyen Le Tram Uyen’,’$’
Main PROC
;------- in ra mot xau voi ham 09/21h -------
Mov Ah, 09h
Lea Dx, MyChildren
Int 21h
;------- ket thuc chuong trinh -------
Int 20h
Main Endp
End Start
Chương trình này chọn chế độ bộ nhớ Small. Tên thủ tục chính là Main (tên thủ tục chính là tùy ý). Nhãn chính của chương trình là Start (tên thủ tục chính là tùy ý), đó chính là nhãn của lệnh Jmp. Phần khai báo dữ liệu chỉ khai báo một biến, đó là MyChildren.
Chương trình này gọi hàm 4Ch của ngắt 21h để kết thúc chương trình. Có thể gọi ngắt 20h để kết thúc các chương trình dạng COM.
Chương trình viết theo cấu trúc dạng EXE:
.Model Small
.Stack 100h
.Data
MyChildren DB ‘Nguyen Kim Le Tuan’,0Ah,0Dh
DB ‘Nguyen Le Tram Thanh’,0Ah,0Dh
DB ‘Nguyen Le Tram Uyen’,’$’
.Code
Main PROC
;------- khởi tạo DS -------
Mov Ax, @Data
Mov DS, Ax
;------- in ra mot xau voi ham 09/21h -------
Mov Ah, 09h
Lea Dx, MyChildren
Int 21h
;------- ket thuc chuong trinh -------
Mov Ah, 4Ch
Int 21h
Main Endp
END Main
Chương trình này chọn chế độ bộ nhớ Small. Khai báo kích thước Stack là 100h byte. Phần khai báo dữ liệu được đặt trong đoạn Data, ở đây chỉ khai báo một biến, đó là MyChildren. Tên thủ tục chính là Main (tên thủ tục chính là tùy ý).
Thao tác đầu tiên của chương trình là trỏ thanh ghi đoạn DS về đầu đoạn Data, hay còn gọi là khởi tạo thanh ghi đoạn DS:
Mov Ax, @Data
Mov DS, Ax
thao tác này được xem như là bắt buộc đối với cấu trúc chương trình dạng EXE sử dụng định nghĩa đoạn đơn giản. Các chương trình viết theo cấu trúc dạng EXE phải gọi hàm 4Ch của ngắt 21h để kết thúc.
Có thể thấy, cấu trúc chương trình dạng COM và cấu trúc chương trình dạng EXE chỉ khác phần hướng dẫn biên dịch, phần khai báo biến và phần lệnh thao tác chính hoàn toàn giống nhau. Hai chương trình đơn giản ở trên hoàn toàn giống nhau ở biến là MyChildren và các lệnh gọi hàm 09h của ngắt 21h để in ra màn hình một xâu kí tự (xâu này chính là giá trị khởi tạo của biến MyChildren).
Chú ý 1: Trình biên dịch hợp ngữ (Macro Assembler) cho phép các chương trình được dịch bởi nóc họn sử dụng một trong các chế độ bộ nhớ sau:
- Small: Đoạn mã lệnh (Code) và đoạn dữ liệu (Data) của chương trình đều chỉ có thể chứa trong một đoạn (segment) bộ nhớ. Tức là, kích thước của chương trình chỉ có thể tối đa là hai đoạn bộ nhớ. Tuy vậy chế độ bộ nhớ này đủ dùng cho hầu hết các chương trình hợp ngữ.
- Medium: Đoạn Code của chương trình có thể chiếm nhiều hơn một đoạn bộ nhớ. Trong khi đó, đoạn Data chỉ có thể chiếm 1 đoạn bộ nhớ.
- Compact: Đoạn Data của chương trình có thể chiếm nhiều hơn một đoạn bộ nhớ. Trong khi đó, đoạn Code chỉ có thể chiếm 1 đoạn bộ nhớ.
- Large: Đoạn Code và đoan Data của chương trình đều có thể chiếm nhiều hơn một đoạn bộ nhớ. Nhưng trong trường hợp này không thể định nghĩa một mảng dữ liệu có kích thước lớn hơn 64 Kbyte.
- Huge: Tương tự như Large, nhưng trong trường hợp này có thể định nghĩa một mảng dữ liệu có kích thước lớn hơn 64 Kbyte.
Chế độ bộ nhớ Small là đơn giản nhất, được hầu hết các chương trình lựa chọn.
Chú ý 2: Với các chương trình hợp ngữ sử dụng định nghĩa đoạn đơn giản: Khi được nạp vào bộ nhớ để hoạt động thì các thanh ghi đoạn sẽ tự động trỏ về các đoạn chương trình tương ứng. Cụ thể: Thanh ghi đoạn CS chứa địa chỉ segment của đoạn bộ nhớ chứa đoạn Code của chương trình. Thanh ghi đoạn DS (và có thể cả ES) chứa địa chỉ segment của đoạn bộ nhớ chứa đoạn Data của chương trình. Thanh ghi đoạn SS chứa địa chỉ segment của đoạn bộ nhớ chứa đoạn Stack của chương trình.
Tuy nhiên, trong thực tế, khi nạp chương trình EXE vào bộ nhớ DOS luôn dành ra 256 byte đầu tiên của vùng nhớ, mà DOS cấp phát cho chương trình, để chứa PSP (Program Segment Prefix) của chương trình. PSP chứa các thông tin cần thiết mà trình biên dịch chuyển đến cho DOS để hỗ trợ DOS trong việc thực hiện chương trình này, đặc biệt, chương trình cũng có thể truy xuất vùng nhớ PSP. Do đó, DOS phải đưa địa chỉ segment của vùng nhớ chứa PSP vào cả DS và ES trước khi chương trình được thực hiện. Tức là, ngay khi chương trình được nạp vào bộ nhớ DS không phải chứa địa chỉ segment của đoạn Data của chương trình mà chứa địa chỉ segment của PSP.
Vì vậy, để trỏ DS về lại đoạn Data chương trình chúng ta phải đặt ngay hai lệnh sau đây ở đầu chương trình viết theo cấu trúc EXE:
Mov Ax, @Data
Mov DS, Ax
Với việc khởi tạo thanh ghi đoạn DS ở trên, địa chỉ segment của tất cả các biến khai báo trong đoạn Data đều được chứa trong thanh ghi DS, do đó, trong các thao tác xử lý biến sau này chương trình không cần quan tâm đến địa chỉ segment của nó nữa.
Chú ý 3: Hợp ngữ còn cho phép các chương trình sử dụng các hướng dẫn biên dịch định nghĩa đoạn toàn phần, các định nghĩa này phù hợp với hầu hết các trình biên dịch hợp ngữ hiện nay. Định nghĩa đoạn toàn phần giúp cho việc viết chương trình hợp ngữ trở nên mềm dẻo và linh hoạt hơn, nó giúp người lập trình có thể điều khiển thứ tự các đoạn chương trình, kết hợp các đoạn chương trình, liên kết các đoạn chương trình trong bộ nhớ,... , ngay trong khi lập trình.
Chi tiết về cách sử dụng và mục đích sử dụng của các hướng dẫn biên dịch nói chung và các định nghĩa đoạn toàn phần nói riêng dễ dàng tìm thấy trong rất nhiều tài liệu về lập trình hợp ngữ [1], [2]. Ở đây chúng tôi chỉ giới thiệu sơ lược về nó thông qua ví dụ dưới đây.
Ví dụ: Sau đây là một chương trình dạng EXE sử dụng các hướng dẫn biên dịch định nghĩa đoạn toàn phần (phù hợp với Macro Assembler):
S_Seg Segment Stack
DB 100h DUP (?)
S_Seg Ends
D_Seg Segmet
MyChildren DB ‘Nguyen Kim Le Tuan’,0Ah,0Dh
DB ‘Nguyen Le Tram Thanh’,0Ah,0Dh
DB ‘Nguyen Le Tram Uyen’,’$’
D_Seg Ends
C_Seg Segment
ASSUME CS:C_Seg, SS:S_Seg, DS:D_Seg
Main PROC
;------- khởi tạo DS -------
Mov Ax, D_Seg
Mov DS, Ax
Mov Ah, 09h
Lea Dx, MyChildren ; địa chỉ offset của biến MyChildren
Int 21h
Mov Ah, 4Ch
Int 21h
Main Endp
C_Seg Ends
END Main
Điều dễ nhận thấy đầu tiên là phần khai báo biến và phần lệnh chính trong chương trình này hoàn toàn giống như trong chương trình sử dụng định nghĩa đoạn đơn giản (hai chương trình ví dụ ở trên).
Chương trình này sử dụng hướng dẫn biên dịch định nghĩa đoạn toàn phần Segment ... Ends để định nghĩa 3 đoạn chương trình với tên lần lượt là: S_Seg (đoạn stack), D_Seg (đoạn Data), C_Seg (đoạn Code). Tên của các đoạn được định nghĩa ở đây là tùy ý.
Hướng dẫn biên dịch Assume được sử dụng để báo cho trình biên dịch biết chương trình muốn chứa địa chỉ segment của các đoạn chương trình trong các thanh ghi đoạn nào (trỏ thanh ghi đoạn về đoạn chương trình). Cụ thể ở đây là: Thanh ghi đoạn CS chứa địa chỉ segment của đoạn Code (CS:C_Seg). Thanh ghi đoạn SS chứa địa chỉ segment của đoạn Stack (SS:S_Seg). Thanh ghi đoạn DS chứa địa chỉ segment của đoạn Data (DS:C_Seg). Tuy nhiên, trong thực tế Assume DS:D_Seg không tự động nạp địa chỉ segment của D_Seg vào DS, do đó chương trình phải nạp trực tiếp bằng các lệnh:
Mov Ax, D_Seg
Mov DS, Ax
Nên nhớ, hướng dẫn biên dịch Segment ... Ends chỉ có tác dụng định nghĩa đoạn, nó không thể báo cho trình biên dịch biết đoạn được định nghĩa thuộc loại đoạn chương trình nào (Code, Data, Stack, Extra). Chỉ có định nghĩa Segment Stack ... Ends là báo cho trình biên dịch biết đoạn được định nghĩa là đoạn Stack, nhờ đó, khi chương trình được nạp vào bộ nhớ thanh ghi đoạn SS sẽ được trỏ về đoạn này.
Có thể bạn quan tâm: Beginning x64 Assembly Programming: From Novice to AVX Professional 1st ed. Edition