Để thực hiện được các lời gọi này thì một chương trình trình Windows *.EXE luôn chứa một tham chiếu đến thư viện liên kết động khác mà nó cần dùng.. Khi đó, một chương trình Windows được
Trang 1Tìm hiểu về MessageBox
Trong bài viết này, chúng ta sẽ tạo ra một MessageBox với nội dung
“Win32 assembly is great”
Tổng quan về lập trình trên Windows
Hệ điều hành (HĐH) cung cấp cho các lập trình viên nguồn tài nguyên phong phú
để họ lập trình các ứng dụng chạy trên nền tảng Wins Đáng kể nhất là phải nói đến là
Windows API (Application Programming Interface) Nó được xem như là tập hợp hầu hết
các hàm đặc trưng thường dùng, và được các ứng dụng Win32 sử dụng Những hàm này
được chứa trong các thư viện liên kết động (DLLs) như: kernel32.dll, user32.dll và
gdi32.dll
Kernel32.dll chứa các hàm và thủ tục mà một hệ điều hành truyền thống quản lý
như: quản lý bộ nhớ, xuất nhập tập tin và quản lý tiến trình (là quá trình thực hiện một
chương trình từ khi khởi động đến khi kế thúc)
User32.dll quản lý giao diện người dùng (the user interface aspects) , cài đặt tất
cả khung cửa sổ ở mức luận lý chương trình của bạn
Gdi32.dll cung cấp toàn bộ giao diện thiết bị đồ họa (Graphics Device Interface)
cho phép chương trình ứng dụng hiển thị văn bản và đồ họa trên các thiết bị xuất phần
cứng như màn hình và máy in
Ngoài 3 thư viện chính trên, HĐH còn cung cấp những thư viện khác mà chương
trình của bạn có thể dùng, miễn là bạn có đầy đủ các thông tin về các “yêu cầu” cho hàm
API
Trong một chương trình Windows, có sự khác biệt khi ta gọi một hàm của thư viện
ngôn ngữ (chẳng hạn C, ASM, …) và một hàm của HĐH Windows (các hàm API) hay
thư viện liên kết động cung cấp Đó là khi biên dịch thành mã máy (machine code), các
hàm thư viện ngôn ngữ sẽ được liên kết thành mã chương trình (source code) Trong khi
các hàm Windows sẽ được gọi khi chương trình cần dùng đến chứ không liên kết vào
chương trình Để thực hiện được các lời gọi này thì một chương trình trình Windows
*.EXE luôn chứa một tham chiếu đến thư viện liên kết động khác mà nó cần dùng Khi
đó, một chương trình Windows được nạp vào bộ nhớ sẽ tạo thành con trỏ tham chiếu đến
những hàm thư viện DLLs mà chương trình dùng, nếu thư viện này chưa được nạp vào bộ
nhớ trứơc đó thì bây giờ sẽ nạp
Chương trình liên kết động với các DLLs này, nghĩa là code của hàm API sẽ
không được include vào trong file EXE của chương trình Để chương trình của bạn biết
tìm các hàm API mà nó đang cần “ở đâu” trong lúc chương trình đang thực thi, thì bạn
phải gắn thông tin nhận biết nó vào trong file EXE Thông tin này nằm trong “thư viện
nhập” (import libraries) đặc biệt được cung cấp bởi môi trường lập trình Bạn phải liên
kết chương trình với thư viện được import một cách chính xác, nếu không, thì nó sẽ
không thể tìm được các hàm API mà chương trình cần sử dụng
Khi một chương trình được nạp vào bộ nhớ, HĐH sẽ đọc các thông tin của
chương trình Các thông tin này chính là tên của các hàm (function) được sử dụng trong
chương trình và các thư viện DLLs mà các hàm sử dụng chứa trong nó Khi HĐH tìm
thấy các thông tin như thế trong chương trình, nó sẽ load các DLLs và thực hiện việcliên
kết địa chỉ của các hàm trong DLLs vào trong chương trình của bạn, vì vậy các lời gọi
Trang 2hàm được xem như sẽ chuyển quyền điều khiển đến đúng hàm API mà chương trình sử dụng khi thực thi
Có hai loại hàm API: một cho hệ thống ký tự ASCII (là các hàm dành cho các HĐH từ Wins 98 trở về trước, không hỗ trợ Unicode) và một cho UNICODE (đây là do
thuật ngữ dân lập trình gọi tắt, nhưng bạn phải hiểu là nó vẫn là chuẩn ANSI; nó chỉ khác
ở chỗ là có bổ sung các hàm hỗ trợ Unicode và chuẩn này có được hỗ trợ từ HĐH Wins
2000/WinNT trở về sau này) Tên của hàm cho ký tự ASCII được gắn hậu tố “A” như MessageBoxA Còn tên hàm cho UNICODE có hậu tố là “W” (viết tắt của wide character ký tự mở rộng)
Chúng ta thường quen dùng những chuỗi (string) ANSI, chúng là một mảng các ký
tự kết thúc bằng ký tự NULL (\0) Các ký tự ASCII có kích thước 1 byte Chúng ta thấy
rằng, với độ dài 8-bit của mã ANSI chỉ đủ biểu diễn đủ cho ngôn ngữ các nước thuộc Châu Âu, còn với các ngôn ngữ khác thì không, với hàng triệu các ký tự riêng biệt Đó chính là khái niệm sơ khởi của Unicode UNICODE là một hệ thống 16-bit đồng nhất,
cho phép có kích thước 2 byte, cho phép biểu diễn đến 65536 ký tự Điều này đủ cho tất
cả các ký tự và chữ tượng hình trong tất cả các ngôn ngữ viết của thế giới, bao gồm nhiều
ký hiệu toán học, biểu tượng, … Sau này, UNICODE được phát triển thành hệ thống 32-bit
Đa phần bạn sẽ dùng một file include (file có phần mở rộng INC) mà nó có thể
xác định và chọn lựa các hàm API thích hợp cho platform của bạn Nó chỉ tham chiếu đến các tên hàm mà không có các hậu tố nói trên
Cấu trúc của một chương trình ASM 32-bit
Tôi sẽ trình bày cấu trúc thường thấy của một chương trình ASM Sau đó, chúng ta
sẽ tìm hiểu từng từ khóa (keyword) một cách chi tiết hơn :
Chương trình sẽ thực thi bắt đầu từ chỉ thị sau nhãn <label> được chỉ định (nhãn
start) cho đến chỉ thị end <label> (end start) Trong cấu trúc trên, quá trình thực thi sẽ bắt đầu từ chỉ thị đầu tiên đứng sau nhãn start Quá trình thực thi sẽ thực thi từ chỉ thị này đến chỉ thị kế cho đến khi gặp chỉ thị điều khiển như jmp, je, ret … Các chỉ thị điều khiển này sẽ làm rẽ nhánh luồng chỉ thị này sang luồng chỉ thị khác Khi chương trình cần thoát để về lại Window, nó sẽ gọi API ExitProcess
Trang 3Dòng lệnh trên đây được gọi là một prototype (khai báo tên hàm) của hàm Khi khai báo
một prototype, thì định nghĩa các thuộc tính của hàm cho trình hợp dịch và trình liên kết biết để nó có thể thực hiện việc kiểm tra cú pháp giúp bạn Cú pháp khai báo một prototype của hàm như sau:
Tên_hàm PROTO Tên_tham_số_1 : <Kiểu dữ liệu> , Tên_tham_số_2 : <Kiểu dữ liệu>
Tóm lại, tên hàm đứng trước từ khóa PROTO và sau đó là danh sách các kiểu dữ liệu của các tham số, cách nhau bởi dấu phẩy Trong hàm ExitProcess như ví dụ trên, nó định nghĩa hàm ExitProcess như một hàm chỉ có duy nhất một tham số có kiểu là DWORD Các prototype của hàm rất hay dùng khi bạn sử dụng cú pháp gọi hàm cấp cao là Invoke
(thực thi hàm) Bạn có thể nghĩ rằng Invoke như là một lời gọi hàm đơn giản, nó được gọi để kiểm tra lỗi cú pháp Ví dụ, bạn muốn gọi gọi hàm ExitProcess để thoát, bạn sẽ viết như sau : Call ExitProcess hoặc Invoke ExitProcess
Khi không đẩy một tham số kiểu DWORD vào trong ngăn xếp, thì trình hợp dịch và trình liên kết sẽ không thể bắt lỗi cho bạn Bạn chỉ nhận biết lỗi này khi chương trình của bạn
bị treo Nhưng nếu bạn dùng INVOKE ExitProcess thì trình liên kết sẽ thông báo cho
bạn biết rằng “Bạn ơi, bạn quên đẩy một tham số có kiểu là DWORD vào trong stack rồi kìa!”, như thế sẽ ngăn được lỗi này Tôi khuyên bạn nên dùng Invoke thay cho một
lời gọi hàm đơn giản Cú pháp của INVOKE như sau:
Invoke Biểu thức [,Các đối số]
Biểu thức có thể là tên của một hàm, hay nó có thể là một con trỏ hàm Các tham
số hàm ngăn cách bởi dấu phẩy Hầu hết các prototype của hàm API được khai báo trong trong file include (file có phần mở rộng là INC) Nếu bạn dùng hutch’s MASM32, chúng
sẽ nằm trong thư mục MASM32/include
Các file include có phần mở rộng là INC và các prototype cho các hàm trong một DLL được chứa trong file INC với cái tên giống như tên file DLL
Ví dụ, hàm ExitProcess được export bởi kernel32.lib vì vậy các prototype cho hàm
ExitProcess được chứa trong kernel32.inc
Bạn cũng có thể cài đặt prototype của hàm cho chính các hàm do chính bạn lập
trình Trong các ví dụ của tôi, tôi sẽ dùng hutch’s window.inc mà bạn có thể download tại
http://win32asm.cjb.net Bây giờ trở lại với hàm ExitProcess, tham số uExitCode là giá trị mà bạn muốn chương trình trả về cho Windows sau khi chương trình kết thúc Bạn có thể gọi ExitProcess như sau: Invoke ExitProcess , 0
Nếu bạn đặt dòng lệnh này (Invoke ExitProcess , 0 ) ngay dưới nhãn start, bạn sẽ
có một chương trình Win32 ASM chỉ có chức năng thoát về Windows, nhưng dù sao nó cũng là một chương trình hoàn toàn hợp lệ và không bị crash
Trang 4Source code của một chương trình ASM Chương trình này đơn giản là sau khi được load
lên bộ, ngay lập tức sẽ được thoát ra, trả quyền kiểm soát về cho HĐH :
Giải thích:
option casemap:none: nói cho MASM biết rằng tên các nhãn trong chương trình phân
biệt chữ hoa, chữ thường, vì vậy ExitProcess sẽ khác với exitprocess
include : chỉ thị này được theo sau là tên file (nói chính xác là tên của thư viện nhập) mà
bạn muốn liên kết vào, để khi chương trình được thực thi, thì nó sẽ tìm các hàm API nào
được sử dụng trong các “thư viện nhập”
Chú ý: Tên file được chứa trong đường dẫn tương đối hoặc tuyệt đối, nghĩa là nếu
bạn đang muốn liên kết với các file thư viện của trình biên dịch (cụ thể thông qua tên của
thư viện nhập) cho chương trình bạn sử dụng khi thực thi, và bạn đã chỉ rõ trong lúc cấu
hình rằng môi trường lập trình (IDE) sử dụng trình biên dịch này để làm trình biên dịch
chính của IDE (nếu như trên máy của bạn có nhiều trình biên dịch) thì chỉ cần ghi đường
dẫn tương đối mà thôi
Để khai báo chỉ thị include theo đường dẫn tương đối đối include
windows.inc
Ngược lại, phải ghi đường dẫn tuyệt đối để cho IDE giao nhiệm vụ tìm thư viện mà
chương trình cần cho trình biên dịch, vì thế phải ghi rõ chính xác rằng trình biên dịch cần
phải tìm nó (thư viện nhập) nằm ở đâu, trong thư mục nào, nếu như thư viện nhập đó
được sử dụng cho nhiều template khác nhau (template ở đây hiểu theo nghĩa là có nhiều
dạng ứng dụng trong ứng dụng chạy trên nền Wins, thí dụ đối với ngôn ngữ C for Wins,
cũng cùng là ứng dụng chạy trên nền Windows, nhưng ứng dụng được code theo template
Win32 Application thì khác với ứng dụng được code theo template MFC Application)
Trang 5Trong ví dụ trên, khi MASM thực thi dòng include \masm32\include\windows.inc, nó
sẽ tìm file windows.inc trong thư mục \masm32\include\ và thực thi nội dụng của file windows.inc cũng như bạn copy nội dung của window.inc vào thay thế cho câu lệnh Thư viện windows.inc chứa hầu hết (không phải là bao hàm tất cả) các định nghĩa của
hằng số và cấu trúc mà bạn cần trong chương trình Win32 ASM Nó không chứa bất kỳ prototype hàm nào Có rất nhiều hằng số và cấu trúc, mà trong windows.inc chưa được định nghĩa Nó sẽ được cập nhật một cách thường xuyên
Từ window.inc, chương trình có được các hằng số và các định nghĩa cấu trúc Đối với
các prototypes của hàm, bạn cần phải include các file include khác Chúng được đặt trong
thư mục \masm32\include Trong ví dụ trên, chúng ta gọi một hàm được export bởi kernel32.dll, vì vậy chúng ta cần phải include prototypes hàm từ kernel32.dll File đó là kernel32.inc Nếu bạn open nó bằng một chương trình sọan thảo editor bạn sẽ thấy rằng
nó chứa đầy các prototyes của hàm trong kernel32.dll Nếu bạn không include kernel32.inc, bạn vẫn có thể gọi hàm ExitProcess nhưng chỉ với lời gọi hàm là Call ExitProcess Bạn không thể invoke hàm được
Điểm cần chú ý ở đây là để invoke một hàm, bạn phải đặt prototyes của chính hàm ở đâu đó trong mã nguồn chương trình
Nếu bạn không include kernel32.inc, bạn có thể định nghĩa prototype của hàm ở bất kỳ đâu trong mã nguồn trên, lệnh invoke và nó sẽ làm việc File include rất tiện dụng,
sẽ giúp bạn sử dụng các prototypes của hàm do bạn tạo ra bất cứ khi nào khi bạn có nhu cầu tái sử dụng
Bây giờ chúng ta sẽ tìm hiểu với một chỉ thị mới, đó là includelib includelib không làm việc giống như include Nó báo cho trình hợp dịch biết thư viện nhập nào mà chương trình của bạn cần sử dụng Khi trình hợp dịch thấy chỉ thị includelib, nó sẽ đặt một lệnh linker vào trong file object, để trình liên kết sẽ biết được thư viện nhập nào mà chương trình bạn cần để liên kết Tuy vậy, không bắt buộc phải sử dụng chỉ thị includelib Bạn có thể chỉ tên của thư viện import trong dòng lệnh của linker nhưng hãy tin tôi đi, nó rất mệt mõi, đồng thời dòng lệnh chỉ có thể tối đa là 128 ký tự mà thôi Bạn nên sử dụng chỉ thị includelib thì tốt hơn
Trang 6Bây giờ hãy save ví dụ với tên là msgbox.asm Giả sử file ml.exe trong đường dẫn, biên dịch file msgbox.asm như sau:
option /c : nói cho MASM biết chỉ cần dịch chương trình ra mã máy (machine code)
mà thôi Không invoke link.exe Hầu như là bạn không muốn gọi link.exe một cách tự động khi bạn cần phải thi hành một vài nhiệm vụ ưu tiên khác trước khi gọi link.exe.
option /coff : nói cho MASM biết tạo ra file có phần mở rộng obj theo định dạng COFF (Common Object File Format) MASM dùng sự khác nhau của COFF, được sử
dụng dưới hệ điều hành Unix như định dạng file object và executable của chúng
otpion /Cp : nói cho MASM vẫn tôn trọng các định danh do người dùng tự đặt ra Nếu bạn sử dụng hutch’s MASM32 package, bạn có thể đặt “option casemap:none” dưới chỉ thị model
Sau khi dịch file ASM sang mã máy thành công, bạn sẽ có file msgbox.obj (file object)
Từ file này, chúng ta chỉ cần làm một thao tác nữa là có thể tạo ra file EXE (executable)
Nó chứa các chỉ thị và dữ liệu ở dạng nhị phân Nó còn thiếu, cần phải điều chỉnh lại vài địa chỉ bằng linker Chúng ta sẽ làm tiếp bước cuối cùng là liên kết tạo ra EXE như sau:
option /SUBSYSTEM:WINDOWS cho trình liên kết biết loại của file exe của chương trình
là gì (ý nói file EXE chạy trên nền platform là gì, ở đây là HĐH Windows)
option /LIBPATH: <path to import library> : nói cho trình liên kết biết các thư viện
Trang 7Trình liên kết đọc trong file object và điều chỉnh các địa chỉ từ thư viện nhập Khi tiến
trình này hoàn tất, bạn sẽ có file msgbox.exe
Bây giờ bạn có file msgbox.exe, run nó đi bạn Bạn sẽ thấy nó không vì ngay từ đầu bạn không viết để cho chương trình thực thi gì Nhưng nó thật sự là một chương trình chạy trên nền HĐH Windows Kích thước của file chỉ có vài bytes Kế đến chúng ta khai báo Prototype của một MassageBox:
MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
hwnd là handle của cửa sổ cha (handle được xem như là ID của cửa sổ, đó là một số nguyên không dấu 32bit do HĐH tạo ra để làm định danh cho mỗi đối tượng, dùng để phân biệt cửa sổ này với cửa sổ khác trong hệ thống, sở dĩ phải cần có thông số này vì trong hệ thống có rất nhiều cửa sổ tồn tại với cửa sổ bạn đang muốn tham chiếu tới nó, nếu không dùng handle để quản lý, thì rất có thể chương trình bạn coding sẽ bị lỗi) Giá trị của nó là bao nhiêu không quan trọng Bạn chỉ nhớ rằng nó miêu tả cho một window
(cửa sổ, không có s phía sao, bạn nhớ phân biệt Windows là HĐH, còn window chính là
cửa sổ chương trình) Khi bạn muốn làm bất cứ điều gì với một cửa sổ, bạn phải tham chiếu đến nó qua handle của nó
lpText là con trỏ trỏ đến text mà bạn muốn hiển thị trong vùng client của MessageBox Một con trỏ thực ra là một địa chỉ nằm trong bộ nhớ Một con trỏ trỏ tới chuỗi Text chính
là địa chỉ của chuỗi đó
lpCaption là con trỏ trỏ tới tiêu đề của MessageBox
uType chỉ ra icon và number và lọai của button trên hộp thọai
Thay đổi file msgbox.asm, thêm vào MessageBox như sau:
Trang 8Biên dịch và chạy chương trình Bạn sẽ thấy một hộp thoại được hiển thị với dòng text
“Win32 Assembly is Great!” như sau:
Hãy nhìn lại source code:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.386
.model flat, stdcall
option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
include kernel32.inc
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data
MsgBoxCaption db "Iczelion Tutorial No.2",0
MsgBoxText db "Win32 Assembly is Great!",0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
start:
invoke MessageBox,NULL, addr MsgBoxText,
addr MsgBoxCaption, MB_OK
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
Chúng ta định nghĩa 2 string kết thúc bằng zero trong section data Hãy nhớ rằng, tất cả
các string ANSI trong Windows phải được kết thúc bởi giá trị NULL (0 hexa)
Chúng ta dùng 2 hằng số NULL và MB_OK Những hằng số này có trong file
windows.inc Vì vậy bạn có thể tham chiếu đến chúng bằng tên thay cho giá trị Sự cải
cách này có thể đọc được trong mã chương trình của bạn
Toán tử addr được dùng để chuyển địa chỉ một nhãn label đến 1 hàm Nó chỉ có giá trị khi bạn dùng chỉ thị invoke Bạn không thể dùng nó để gán địa chỉ của một label cho thanh ghi hay biến Bạn có thể dùng offset thay vì addr trong ví dụ trên Tuy nhiên có vài
sự khác nhau giữa hai toán tử này:
Trang 9Toán tử addr Toán tử offset
_ Không được quản lý trước khi tham
chiếu
Ví dụ, nếu bạn sử dụng lệnh invoke trước
khi khai báo một label thì addr sẽ không
làm việc
.code
start:
invoke MessageBox,NULL,
addr MsgBoxText,
addr MsgBoxCaption, MB_OK
MsgBoxCaption
db "Iczelion Tutorial No.2",0
MsgBoxText
db "Win32 Assembly is
Great!",0
invoke ExitProcess,NULL
end start
MASM sẽ báo lỗi như sau :
Test.asm(16) : error A2114: INVOKE
argument type mismatch : argument
: 2
Make error(s) occured
Total compile time 468 ms
_ Có thể xử lý biến cục bộ
_ Được quản lý trước khi tham chiếu
Nếu bạn dùng offset thay vì addr trong đọan code trên, MASM sẽ assemble nó một cách trơn tru
.code start:
invoke MessageBox,NULL,
offset MsgBoxText,
offset MsgBoxCaption, MB_OK
MsgBoxCaption
db "Iczelion Tutorial No.2",0 MsgBoxText
db "Win32 Assembly is Great!",0 invoke ExitProcess,NULL end start
MASM sẽ thông báo biên dịch thành công:
"C:\RadASM\Masm\Projects\Test\Test.exe"
Make finished
Total compile time 125 ms
_ Không thể xử lý biến cục bộ
Biến cục bộ là một vùng nhớ được dành riêng trong ngăn xếp Bạn chỉ chú ý tới địa chỉ của nó
trong thời gian thực thi chương trình
_ addr có thể xử lý biến cục bộ bởi vì
trong thực tế trình biên dịch kiểm tra biến
này được tham chiếu bởi addr là một biến
toàn cục hay cục bộ Nếu nó là một biến
toàn cục, nó sẽ đặt địa chỉ của biến vào
trong file object Trong trường hợp này, nó
làm việc như offset Nếu nó là một biến
cục bộ, nó sẽ sinh ra một chuỗi chỉ thị
giống như sau, trước khi nó gọi hàm:
lea eax, LocalVar
push eax
Khi đó, lệnh lea có thể xác định được địa
chỉ của label lúc thực thi
_ offset được dịch trong suốt thời gian biên dịch bởi trình biên dịch Vì vậy là điều tất nhiên khi
offset không làm việc cho biến cục bộ