TUTORIAL 2: MESSAGEBOX
Trong tut này, chúng ta sẽ cài đặt một chương trình window hiển thị một hộp thọai nói rằng “Win32 assembly is great”
Download file ví dụ :
LÝ THUYẾT
Windows chuẩn bị một nguồn tài nguyên rất dồi dào dành cho các chương trình Windows. Phần chính của nguồn tài nguyên này là các hàm API (Application Programming Interface). Windows API là một bộ sưu tập rất đồ sộ các hàm thường dùng được chứa trong chính hệ điều hành Windows, nó sẳn sàng chia sẽ cho các chương trình chạy dưới HĐH Windows 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 xử lý bộ nhớ và điều khiển tiến trình thực thi. User32.dll điều khiển giao diện người dùng (the user interface aspects) chương trình của bạn. Còn gdi.dll chịu trách nhiệm các họat động đồ họa. Những thư viện nói ở trên là 3 thư viện chính, có những thư viện DLLs 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.
Những chương trình Windows liên kết động với các DLLs này, nghĩa là code của hàm API sẽ ko include (gắn chứa) vào trong file .exe của chương trình Windows (chương trình chạy dưới HĐH Windows).Để chương trình của bạn biết “ở đâu” để tìm hàm API lúc chương trình 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 là thư viện IMPORT. Bạn phải liên kết chương trình của bạn với thư viện IMPORT một cách chính xác, nếu ko nó sẽ ko thể định vị được các hàm API.
Khi một chương trình Windows được tải vào trong bộ nhớ,Windows (HĐH) sẽ đọc các thông tin được cài đặt trong chương trình. Các thông tin này bao hàm các “tên hàm” được sử dụng trong chương trình và các thư viện DLLs mà các hàm chương trình cần dùng chứa trong nó. Khi Windows tìm thấy các thông tin như thế trong chương trình, nó sẽ tải DLLs và thi hành gắn các địa chỉ của hàm trong DLLs vào trong chương trình của bạn , vì vậy các hàm Calls 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 lọai hàm API: Một cho ANSI và một cho UNICODE. Tên của hàm cho ANSI được gắn hậu tố “A” như MessageBoxA. Còn tên hàm cho UNICODE có hậu tố là “W” ( Cho Wide Char, tôi nghĩ thế ). Win95 thì hổ trợ ANSI, còn WinNT thì UNICODE.
Chúng ta thường quen dùng những chuổi (string) ANSI, chúng là một dãy các ký tự kết thúc bằng NULL. Các ký tự ANSI có kích thước 1 byte. Các code ANSI đủ khả năng cho các ngôn ngữ châu Âu, nhưng lại ko đủ dùng cho các ngôn ngữ châu Á mà chúng có hàng triệu các ký tự riêng biệt. Vì vậy tại sao UNICODE được sinh ra là thế. Một ký tự UNICODE có kích thước 2 byte, vì vậy nó có thể chứa đến 65536 các ký tự riêng biệt trong các chuổi string.
Nhưng nhiều lúc, bạn sẽ dùng 1 “include file”(file có đôi .inc) mà nó có thể xác định và chọn lựa các hàm API thích hợp của plalform của bạn. Nó chỉ tham chiếu đến các tên hàm mà ko có các hậu tố nói trên.
VÍ DỤ :
Tôi xin đưa ra đây một “nhân” (skeleton) chương trình rỗng , chúng ta sẽ thịt nó sau:
.386
.model flat, stdcall
.data
.code
start:
end start
Chương trình sẽ thực thi bắt đầu từ chỉ thị sau nhãn
ExitProcess proto uExitCode:DWORD
Dòng trên đây được gọi là một prototype (nguyên mẫu) của hàm. Một prototype hàm, định nghĩa các thuộc tính của một hàm cho assembler/linker (bộ biên dịch và liên kết asm) ,vì vậy nó có thể là 1 lọai kiểm tra Type (lọai) (tyoe-checking) giúp bạn. Định dạng một prototype của hàm như sau:
FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,...
Tóm tắt: Tên hàm đứng trước từ khóa PROTO và sau đó là list (danh sách) các lọai dữ liệu của 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ố là DWORD. Các prototype của hàm rất hay dùng khi bạn sử dụng cú pháp (syntax) gọi hàm cấp cao là INVOKE (nghĩa tiếng Anh là “hiện ra”). Bạn có thể nghĩ rằng INVOKE như là một hàm đơn giản được gọi để kiểm tra lọai dữ liệu (type-checking). Như ví dụ sau, nếu bạn gọi hàm như vầy:
Call ExitProcess
Mà ko push một dword vào trong stack, thì assembler/linker(trình biên dịch asm và trình liên kết linker) sẽ ko thể nắm được lỗi của bạn là gì. Bạn chỉ nhận biết được lỗi này khi chương trình của bạn bị crash( phá vỡ). Nhưng nếu bạn dùng:
Invoke ExitProcess
Thì Linker sẽ khai báo cho bạn biết bạn quên push một dword vào trong stack, như thế sẽ ngăn ngừa được lỗi này. Ở đây tui giới thiệu bạn dùng Invoke thay vì một hàm call đơn giản. Cú pháp của Invoke như sau:
INVOKE expression [,arguments]
expression: biểu thức
argument: đối số
Biểu thức có thể là tên của một hàm, hay nó có thể là một pointer (con trỏ )của hàm. Các tham số hàm ngăn cách bởi dấu phẩy.
Phần lớn các prototype của hàm API được cất giữ trong file include (file có đuôi là .inc). Nếu bạn dùng hutch’s MASM32, chúng sẽ chứa trong thư mục MASM32/include. Các file include có phần đuôi 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 (xuất khẩu) 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 của bạn.
Suốt 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 đây: 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ề (return) cho Windows sau khi chương trình kết thúc. Bạn có thể gọi ExitProcess giống như vầy:
invoke ExitProcess, 0
Đặt dòng trên ngay dưới nhãn Start, bạn sẽ có một chương trình win32 chỉ có chức năng exit về Windows, nhưng dù sao nó cũng là một chương trình có “giá trị” (ko bị crash, hòan tòan hợp lệ).
.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.data
.code
start:
invoke ExitProcess,0
end start
Dòng option casemap:none nói cho MASM biết nên chú ý đến chử hoa và thường trong các tên nhãn , vì vậy ExitProcess sẽ khác exitprocess. Chú ý đây là một chỉ thị mới thêm vào. Chỉ thị này được theo sau bởi tên của một file mà bạn muốn chèn vào nơi chỉ thị đó đứng. Trong ví dụ trên, khi MASM thực thi dòng include \masm32\include\windows.inc, nó sẽ mở windows.inc ra trong thư mục \masm32\include\ và thi hành nội dụng của file windows.inc khi bạn dán nội dung của window.inc ở đó. Hutch’s windows.inc (file windows.inc của Hutch) chứa các định nghĩa, các hằng số và cấu trúc mà bạn cần trong chương trình win32. Nó ko chứa bất kỳ prototype nào . windows.inc ko có nghĩa là bao hàm tất cả. Hutch và tôi thử cho vào rất nhiều hằng số và cấu trúc trong nó bất cứ khi nào có thể. Nhưng vẫn có nhiều thiếu sót đã include (đính, tích hợp) vào. Nó sẽ được cập nhật một cách thường xuyên. Bạn hảy kiểm tra homepages của tôi hay hutch’s cho việc update (cập nhật).
Từ window.inc, chương trình có được các constant (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 folder: \masm32\include .
Trong ví dụ trên, chúng ta gọi 1 hàm được export (xuất khẩu) 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 1 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 ko include kernel32.inc, bạn vẫn có thể gọi hàm ExitProcess nhưng chỉ với một cú pháp đơn (là Call ExitProcess). Bạn ko 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 nguồn code. Trong ví dụ trên, nếu bạn ko include kernel32.inc, bạn có thể định nghĩa prototype của hàm ở bất kỳ đâu trong nguồn code 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 bạn cần dùng lại.
Bây giờ chúng ta sẽ chạm trán với 1 chỉ thị mới, đó là includelib . includelib ko làm việc giống như include. Nó chỉ nói cho assembler biết thư viện import nào mà chương trình của bạn sử dụng. Khi assembler thấy chỉ thị includelib , nó sẽ đặt 1 lệnh linker vào trong file object , do đó linker sẽ biết được thư viện import nào mà chương trình bạn cần để liên kết . Tuy vậy bạn ko bị ép buộc 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ể là 128 ký tự mà thôi. Bạn nên sử dụng chỉ thị includelib thì tốt hơn.
Bây giờ hảy lưu (save) ví dụ với 1 cái tên là msgbox.asm . Giả sử file ml.exe trong path (đường dẫn) của bạn, biên dịch assemble file msgbox.asm như sau:
ml /c /coff /Cp msgbox.asm
/c nói cho MASM biết chỉ assemble (dịch chươgn trình viết bằng MASM ra mã máy) mà thôi. Ko invoke link.exe . Phần lớn bạn sẽ ko 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
/coff nói cho MASM biết cài đặt file .obj theo định dạng COFF . MASM dùng sự thay đổi của COFF (Common Object File Format) được sử dụng dưới hệ điều hành Unix như định dạng file object và executable của chúng.
/Cp nói cho MASM để tuân thủ trường hợp viết hoa do người dùng chỉ định. Nếu bạn sử dụng hutch’s MASM32 package, bạn có thể đặt “option casemap:none” tại head của nguồn code của bạn, dưới chỉ thị .model để hòan thành tác dụng giống như vậy.
Sau khi bạn hòan thành assemble masgbox.asm , bạn sẽ có file msgbox.obj . File này là 1 file object. Một file object chỉ là một bước để làm ra file .exe (executable). Nó chứa các instrutions/data (chỉ thị và dữ liệu) trong dạng nhị phân binary. Nó còn thiếu , cần phải fixup (sửa chửa lại) vài addresses bằng linker.
Chúng ta sẽ làm tiếp link như sau:
link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib msgbox.obj
/SUBSYSTEM:WINDOWS cho Link biết .exe của chương trình này là gì
/LIBPATH:
Link đọc trong file object và fix nó với các địa chỉ addr từ trong thư viện import . Khi tiến trình này hòan thành, 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ó ko có gì hết. Tốt, chúng ta ko có put bất cứ gì quan trọng trong nó lúc này. Nhưng nó thật sự là một chương trình Windows chính cống. Và hảy nhìn kích thước của nó. Trong máy PC của tui là 1.536 bytes.
Kế đến chúng ta sẽ put một massage box (hộp hội thọai). Prototyte của hàm này là :
MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
hwnd là handle của window cha. Bạn có thể nghĩ một handle như là một số miêu tả cho một window (cửa sổ chương trình) mà bạn đang tham khảo đến nó. Giá trị của nó là bao nhiêu ko quan trọng. Bạn chỉ nhớ rằng nó miêu tả cho một window (cửa sổ window, ko 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 window, bạn phải tham chiếu đến nó qua handle của nó.
lpText là con trỏ đến text mà bạn muốn hiển thị trong vùng client của message box. Một con trỏ thực ra là một addr của một cái gì đó. Một con trỏ pointer cho text string == addr của string đó.
lpCaption là pointer (con trỏ) của caption (tiêu đề) hộp thọai meassage box.
uType chỉ ra icon và number và lọai của button trên hộp thọai
Hảy thay đổi file msgbox.asm , include(đính) vào message box như sau:
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
.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
invoke ExitProcess, NULL
end start
Assemble (biên dịch ra mã máy từ file asm) và chạy nó đi bạn. Bạn sẽ thấy một hộp thọai được hiển thị với dòng text “Win32 Assembly is Great!”.
Hảy nhìn lại source code:
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 “ghi chú” trong file windows.inc. Vì vậy bạn có thể tham chiếu đến chúng bằng tên thay cho values(giá trị). Sự cải cách này có thể đọc được trong nguồn code của bạn.
Tóan tử addr được dùng để chuyển địa chỉ một nhãn label đến 1 hàm. Nó chỉ có giá trị trong “hòan cảnh” dùng chỉ thị invoke. Bạn ko thể dùng nó để chỉ định một địa chỉ của một label đến một thanh ghi register hay một biến variable . 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ữ hai tóan tử này:
1. addr ko thể xử lý trước khi tham chiếu, trong khi offset là có thể. Ví dụ, nếu một label được định nghĩa ở đâu đó cách xa dòng invoke trong nguồn code, addr sẽ ko làm việc
invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK
......
MsgBoxCaption db "Iczelion Tutorial No.2",0
MsgBoxText db "Win32 Assembly is Great!",0
MASM sẽ thông báo lỗi. 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
2. addr có thể xử lý biến cục bộ trong khi offset ko thể. Một biến cục bộ (local variable) chỉ là một vài khỏang trống dành riêng trong stack. Bạn chỉ biết địa chỉ của nó trong suốt lần runtime (thời gian chương trình chạy). offset được dịch trong suốt lần biên dịch assembly bởi chương trình biên dịch assembler. Vì vậy là điều tất nhiên khi offset ko làm việc cho biến cục bộ. addr có thể xử lý biến cục bộ bởi vì trong thực tế assembler kiểm tra trước tiên biến được tham chiếu bởi addr là một biến global hay local có đúng thế ko. Nếu nó là một biến global, nó sẽ put address của biến vào trong file object. Trong case này, nó làm việc như offset. Nếu nó là một biến local, nó sẽ sinh ra một chuổi chỉ thị giống như sau đây trước khi nó gọi hàm:
lea eax, LocalVar
lea có thể xác định một address của label lúc runtime, lệnh này làm việc rất tốt.
---------------The End Tut -----------------
Benina
Update 20/11/2005
(Không đồng ý bất kỳ ai sử dụng tài liệu này cho mục đích thương mại nếu ko được phép của người dịch)