Đối với Windows vấn đề này được giải quyết một cách trọn vẹn : HĐH đưa ra cơ chế thông điệp message cùng với tập hợp các cấu trúc dữ liệu và các hàm API hỗ trợ ứng dụng trong việc giao
Trang 1[Programming] Ứng dụng cơ chế Hook - Phần 1
Ứng dụng cơ chế Hook để xây dựng chương trình hỗ trợ gõ Tiếng Việt trên Hệ Điều Hành Windows 32 Bit - Phần 1
I.Sự kiện và thông điệp trên HĐH Windows
I.1.Giới thiệu
Ngày nay, Windows có lẽ không còn xa lạ với người sử dụng PC Nhắc đến
Windows, người ta thường nghĩ về nó như một Hệ Điều Hành (HĐH) dễ sử dụng, ở
đó, tương tác giữa người dùng với các ứng dụng cũng như với các thành phần tiện ích của Windows thông qua giao diện đồ họa (GUI), bằng các thao tác trên keyboard, mouse vô cùng đơn giản
Một câu hỏi được đặt ra là : “Các ứng dụng làm thế nào để phân loại, lưu giữ cũng
như đáp lại những tương tác đó cho người dùng ?” Đối với Windows vấn đề này được giải quyết một cách trọn vẹn : HĐH đưa ra cơ chế thông điệp (message) cùng
với tập hợp các cấu trúc dữ liệu và các hàm API hỗ trợ ứng dụng trong việc giao tiếp với người dùng
I.2.Hai loại hàng đợi thông điệp trên Windows
Trước hết, ta cần phải biết làm thế nào HĐH Windows lưu trữ các thông điệp Hai loại hàng đợi trong Windows phục vụ cho mục đích này :
+ Hàng đợi hệ thống (The System Queue)
+ Hàng đợi thông điệp của ứng dụng (Application Queue)
Hàng đợi hệ thống (The System Queue)
Windows có các trình điều khiển thiết bị (Drivers) chịu trách nhiệm cho các dịch vụ
ngắt từ thiết bị phần cứng mouse và keyboard.Tại mỗi thời điểm ngắt phát sinh, các
Driver này gọi một điểm vào (hàm) đặc biệt trong USER.EXE để chỉ ra rằng một sự
kiện vừa xảy ra
Tất cả các sự kiện mouse, keyboard được lưu trong một hàng đợi được gọi là hàng đợi
hệ thống (The System Queue) Đây là hàng đợi dùng chung cho toàn hệ thống , mọi tiến trình đang chạy đều chia xẻ hàng đợi này Nhiệm vụ độc nhất của nó là ghi nhận lại những sự kiện phần cứng (mouse actions, keystrokes) khi chúng xảy ra.
Trang 2Hàng đợi thông điệp của ứng dụng (Application Queue)
Khi một tiến trình được khởi tạo, một hàng đợi đại diện cho nó được tạo ra, hàng đợi này (đôi khi được gọi là hàng đợi tác vụ) được dùng để chứa những thông điệp sẽ được gởi cho các cửa sổ của ứng dụng Những thông điệp này là những thông điệp được gởi một cách tường minh bằng một trong hai hàm sau :
+ PostMessage + PostAppMessage (Lưu ý : Hàm PostQuitMessage không gởi thông điệp vào hàng đợi ứng dụng
Ứng dụng có thể dùng hai Primitive là GetMessage và PeekMessage doWindows
cung cấp để khảo sát hàng đợi của mình Hai hàm này cho phép ứng dụng “Lấy một message ra khỏi hàng đợi” để từ đó phân loại và có những “Trả lời” thích hợp với người dùng
I.3.Bàn thêm về GetMessage và PeekMessage
Bên trong HĐH Windows, GetMessage và PeekMessage thi hành cùng mã lệnh.
Điểm khác biệt chính giữa 2 hàm là trong trường hợp không có message nào được trả
về cho ứng dụng Trong trường hợp này, GetMessage đặt ứng dụng vào trạng thái
“Sleep” trong khi PeekMessage trả về ứng dụng giá trị NULL.
Ngay trước khi GetMessage và PeekMessage trả message về cho ứng dụng, hệ thống
sẽ kiểm tra xem có sự tồn tại của hook WH_GETMESSAGE hay không Nếu có một
“Filter Function” (thực chất là 1 hàm) ứng với hook trên được cài đặt, hàm này sẽ
được gọi “Filter Function” không được gọi nếu PeekMessage không tìm thấy
message và trả về NULL
PeekMessage với tùy chọn PM_NOREMOVE
Theo mặc định, cả PeekMessage và GetMessage sẽ lấy message ra khỏi hàng đợi khi
mỗi message được trả về cho ứng dụng Tuy nhiên, có đôi lúc, ứng dụng có thể chỉ muốn vào hàng đợi để kiểm tra sự tồn tại của một messsage mà không muốn gỡ bỏ
nó Để làm được điều này, chương trình của ta sẽ gọi PeekMessage với tùy chọn
PM_NOREMOVE, lúc này PeekMessage vẫn trả về message như thường lệ nhưng
sẽ không lấy nó ra khỏi hàng đợi.(Chi tiết về GetMessage và PeekMessage có thể
tham khảo tại Article “GetMessage and PeekMessage Internals” trong thư viện
MSDN)
Trang 3II.Sự kiện bàn phím
II.1.Mô hình chung
Mỗi phím trên bàn phím được hệ thống gắn với một giá trị duy nhất gọi là mã quét (Scan Code), đây là một giá trị phụ thuộc thiết bị, tức là giá trị này sẽ lệ thuộc vào
từng loại bàn phím cụ thể Khi người dùng gõ một phím, sẽ có 2 mã quét được sinh ra : một khi phím được nhấn và một khi phím được thả
Trình điều khiển bàn phím (Keyboard Device Driver)sẽ thông dịch mã quét và
chuyển nó thành mã phím ảo (vitual-key code), một giá trị độc lập thiết bị, được
định nghĩa bởi hệ thống Sau đó, trình điều khiển tạo một thông điệp bao gồm
scancode, virtual-key code cùng một số thông tin khác (sự lặp phím, trạng thái các phím Alt, Ctrl ) , đặt vào hàng đợi hệ thống Hệ thống lấy thông điệp này ra khỏi hàng đợi hệ thống và gởi đến hàng đợi thông điệp của ứng dụng.Cuối cùng, vòng lặp
thông điệp (Message Loop) sẽ lấy thông điệp ra khỏi hàng đợi ứng dụng và chuyển
nó đến hàm xử lý message thích hợp để xử lý Quá trình trên có thể minh hoạ qua hình vẽ sau :
Rõ ràng là nếu ta có một cách nào đó để có được message trước khi nó đến hàm xử lý thông điệp của cửa sổ, ta có thể thực hiện được một vài thao tác thú vị Và trên HĐH
Windows, điều này hoàn toàn có thể thực hiện được nhờ cơ chế hook như ta sẽ thấy ở
phần sau
II.2.Các thông điệp liên quan bàn phím
Khi ta nhấn một phím, một thông điệp WM_KEYDOWN sẽ được đặt vào hàng đợi của tiểu trình ứng với cửa sổ đang được focus Và khi ta nhả phím, hàng đợi sẽ có một thông điệp WM_KEYUP Hai thông điệp này thường xuất hiện thành cặp Tuy nhiên,
nếu ta nhấn phím và giữ phím đủ lâu làm đặc tính “Automatic Repeat” của bàn phím
hoạt động, hệ thống sẽ sinh nhiều thông điệp WM_KEYDOWN liên tiếp nhau.Và lưu ý một điều là khi chúng ta nhả phím, chỉ có một thông điệp WM_KEYUP được phát sinh mà thôi
wParam của cả 2 thông điệp trên sẽ cho ta mã phím ảo (virtual-key code) của phím
được nhấn
lParamdùng 32 bit của mình để mô tả thông tin thêm về sự kiện gõ phím đã tạo ra message
Trang 4Thông tin này bao gồm : sự lặp phím, mã quét (scan code), trạn thái phím ALT, trạng thái phím trước, Sơ đồ của 32 bit này nhưsau :
Repeat Count : Hệ thống tăng bộ đếm này lên khi bàn phím phát sinh thông điệp
WM_KEYDOWN nhanh hơn khả năng xử lý message của ứng dụng Điều này thường xảy ra khi người dùng giữ phím đủ lâu làm cho đặc tính “Automatic Repeat” của bàn phím hoạt động Lúc này, thay vì đặt vào hàng đợi hệ thống nhiều thông điệp WM_KEYDOWN liên tiếp, hệ thống sẽ kết hợp các message này thành một message duy nhất và tăng giá trị Repeat Count Tác vụ nhả phím không làm cho đặc tính
“Automatic Repeat” được kích hoạt, do đó giá trị Repeat Count của thông điệp WM_KEYUP luôn luôn là 1
Scan Code : Giá trị phụ thuộc thiết bị được bàn phím phát sinh khi một phím bị nhấn
hay nhả Đây không phải là mã ký tự đại diện cho phím được nhấn.Ứng dụng thường không quan tâm đến giá trị này, thay vì vậy, nó sử dụng giá trị độc lập thiết bị là mã phím ảo (virtual-key code) để xử lý message
Extended-Key Flag Bit cờ hiệu cho biết một phím có phải là phím mở rộng hay
không Bit này được bật khi phím bị nhấn là phím mở rộng như : INS, DEL, HOME, END,
Context Code Bit cờ hiệu cho biết tại thời điểm thông điệp phát sinh, phím ALT có
bị nhấn hay không Bit này bật khi phím ALT bị nhấn, tắt khi phím ALT được nhả
Previous Key-State Flag Bit cờ hiệu cho biết trạng thái của phím trước khi gởi
message là “up” hay “down” Bit này là 1 nếu phím là “down”, 0 nếu phím là “up”
Transition-State Flag Bit này là 0 đối với WM_KEYDOWN, là 1 đối với
WM_KEYUP.Thông điệp WM_KEYDOWN, WM_KEYUP cung cấp cho ta khá nhiều thông tin về sự kiện gõ phím, tuy nhiên, nó không cho ta mã ký tự của phím
được nhấn Để có được thông tin này, ứng dụng phải thêm hàm TranslateMessage
vào vòng lặp thông điệp của mình.Hàm này chuyển thông điệp WM_KEYDOWN thành thông điệp WM_CHAR chứa mã ký tự ứng với phím được gõ, sau khi đã kiểm tra trạng thái của phím SHIFT và CAPS LOCK
II.3.Giả lập gõ phím
Đôi khi chúng ta muốn có một sự kiện gõ phím mà không có sự tác động về mặt vật
lý đối với bàn phím.Hàm keybd_event giúp ta có được khả năng này, chức năng của
nó là giả lập một thao tác đối với bàn phím (nhấn hay nhả) Khai báo của hàm này như sau :
Trang 5VOID keybd_event(
BYTE bVk, // virtual-key code
BYTE bScan, // hardware scan code
DWORD dwFlags, // function options
ULONG_PTRdwExtraInfo // additional keystroke data
);
Ý nghĩa của từng tham số :
bVk : mã phím ảo của phím cần giả lập
bScan :tham số này không dùng
dwFlags : xác định loại thao tác cần giả lập, tham số này là một trong các giá
trị sau :
KEYEVENTF_EXTENDEDKEYScan Code được đi trước bởi giá trị 224
dwExtraInfo : thông tin thêm về sự kiện ta giả lập
III.Cơ chế Hook trên HĐH Windows
III.1.Giới thiệu
Trên HĐH Windows, một hook là cơ chế mà nhờ đó một hàm có thể chặn
các sự kiện (message, mouse actions, keystrokes) trước khi chúng đến
ứng dụng Hàm này có thể thực hiện một số thao tác trên sự kiện, và
trong một vài trường hợp có thể định nghĩa lại hoặc hủy bỏ sự kiện mà
nó bắt được Một đặc điểm quan trọng cần lưu ý là các hàm này được
gọi bởi bản thân HĐH Windows chứ không phải bởi ứng dụng.
Các hàm thuộc loại vừa nêu thường được gọi là Filter Function, chúng được phân
loại theo loại sự kiện mà chúng can thiệp.Ví dụ, một Filter Function hứng tất cả các
sự kiện mouse sẽ khác với một Filter Function can thiệp các sự kiện keyboard …
Một Filter Funtion muốn được Windows gọi thì trước tiên nó phải được gắn với một
Windows Hook, công việc này thường được gọi là “Cài đặt một hook” Windows
Hook thật ra là các loại hook khác nhau được HĐH định nghĩa, nó giúp người sử
Trang 6dụng có thể cài đặt Filter Function của mình đúng với mục đích mà họ mong muốn Khi cài đặt hook, ta xác định loại hook thông qua các hằng số có dạng WH_XXX, ví
dụ WH_KEYBOARD xác định loại hook dùng để can thiệp các sự kiện thuộc về bàn phím
Một Windows Hook có thể có nhiều Filter Function gắn với nó, trong tình huống này,
Windows sẽ duy trì một chuỗi các Filter Function Hàm được cài đặt gần nhất sẽ ở
đầu chuỗi, hàm được cài đặt lâu nhất sẽ ở cuối chuỗi Lúc này, nếu có một sự kiện xảy
ra và sự kiện này tương ứng với loại hook ta đã cài đặt, Windows sẽ gọi hàm đầu tiên
trong chuỗi các Filter Function ứng với hook đó
III.2.Khả năng và ứng dụng của cơ chế Hook
Hook cung cấp các khả năng mạnh cho các ứng dụng chạy trên nền Windows, các
ứng dụng này có thể dùng hook để :
· Xử lý hoặc định nghĩa tất cả các thông điệp cho dialog box, message box, scroll bar, hoặc menu của một ứng dụng (Sử dụng hook WH_MSGFILTER)
· Xử lý hoặc định nghĩa tất cả các thông điệp cho dialog box, message box, scroll bar, hoặc menu của hệ thống (Sử dụng hook WH_SYSMSGFILTER)
· Xử lý hoặc định nghĩa tất cả các thông điệp (bất chấp là thông điệp gì) của hệ thống
mỗi khi GetMessage hoặc PeekMessage được gọi (Sử dụng hook
WH_GETMESSAGE)
· Xử lý hoặc định nghĩa tất cả các thông điệp (bất chấp là thông điệp gì) của hệ thống
mỗi khi SendMessage được gọi (Sử dụng hook WH_CALLWNDPROC).
· Thu (Record) và phát lại (Playback) các sự kiện keyboard và mouse (Sử dụng hook WH_JOURNALRECORD, WH_JOURNALPLAYBACK)
· Xử lý , định nghĩa hoặc hủy bỏ tất cả các sự kiện bàn phím.(Sử dụng hook
WH_KEYBOARD)
· Xử lý , định nghĩa hoặc hủy bỏ tất cả các sự kiện chuột (Sử dụng hook
WH_MOUSE)
Tận dụng các khả năng trên, các ứng dụng có thể sử dụng hook để :
· Cung cấp phím trợ giúp F1 hỗ trợ menu, dialog box và message box (Sử dụng hook WH_MSGFILTER)
· Cung cấp các tính năng thu và phát các sự kiện mouse và keyboard, thường được gọi
là các macro (Sử dụng hook WH_JOURNALRECORD,
WH_JOURNALPLAYBACK)
· Theo dõi các thông điệp để biết được thông điệp nào được gởi đến cửa sổ nào cũng như các hành động nào sẽ làm phát sinh thông điệp tương ứng (Sử dụng hook
WH_GETMESSAGE & WH_CALLWNDPROC) Chương trình Spy trong bộ
Trang 7Win32™ SDK của Windows NT đã rất thành công trong việc sử dụng hook để thực hiện tác vụ này
· Giả lặp các tác vụ input của keyboard và mouse (Sử dụng hook
WH_JOURNALPLAYBACK) Chỉ có hook mới có thể cho ta một phương pháp chắc chắn và tin cậy để thực hiện điều này Nếu ta tiếp cận theo cách khác, gởi message
chẳng hạn, Windows sẽ không cập nhật trạng thái của mouse cũng như keyboard, và
điều này sẽ dẫn đến những hành động không mong muốn Còn nếu như hook được sử dụng, các sự kiện sẽ được xử lý y hệt như sự kiện vật lý
III.3.Quản lý chuỗi các Filter Function
Giao diện lập trình ứng dụng (API) của Windows cung cấp 3 hàm để thao tác với
hook :
· SetWindowsHookEx
· UnhookWindowsHookEx
· CallNextHookEx
Cài đặt một Filter Function vào chuỗi các Filter Function của một hook
Tác vụ này được thực hiện thông qua hàm SetWindowsHookEx, khai báo của hàm này như sau :
HHOOK SetWindowsHookEx(
int idHook, // hook type
HOOKPROC lpfn, // hook procedure
HINSTANCE hMod, // handle to application instance
DWORDdwThreadId // thread identifier
);
Ý nghĩa của từng tham số :
idHook : Xác định loại hook mà ta muốn cài đặt, tham số này có thể là một trong các
giá trị sau :
· WH_CALLWNDPROC
· WH_CALLWNDPROCRET
· WH_CBT
· WH_DEBUG
Trang 8· WH_FOREGROUNDIDLE
· WH_GETMESSAGE
· WH_JOURNALPLAYBACK
· WH_JOURNALRECORD
· WH_KEYBOARD
· WH_MOUSE
· WH_MSGFILTER
· WH_SYSMSGFILTER
Mỗi giá trị trên xác định một loại hook mà ta muốn cài đặt, mỗi loại hook có một ý nghĩa và tình huống sử
dụng khác nhau, chi tiết về từng loại hook sẽ được đề cập sau
lpfn : Địa chỉ của Filter Function mà ta muốn gắn với hook, hàm này phải được
“export” bằng macro thích
hợp khi ta cài đặt
hMod : Handle của module chứa Filter Function Nếu ta cài đặt một hook cục bộ
(nghĩa là sự thực thi của
Filter Function chỉ ảnh hưởng đối với tiến trình cài đặt hook), tham số này phải là NULL Còn nếu ta muốn
có một hook với phạm vi toàn hệ thống (tức là mọi tiến trình đang hiện hữu đều chịu ảnh hưởng bởi Filter
Function của ta), tham số này sẽ là Handle của DLL chứa Filter Function
dwThreadID : Định danh của thread ứng với hook đang được cài đặt Nếu tham số
này là một số khác 0,
Filter Function được gắn với hook chỉ được gọi trong ngữ cảnh của một thread xác định Còn nếu
dwThreadID = 0, Filter Function sẽ có phạm vi toàn hệ thống, và dĩ nhiên, nó sẽ được gọi trong ngữ cảnh
của bất kỳ thread nào đang tồn tại trên HĐH Có thể sử dụng hàm
GetCurrentThreadId để lấy được
handle của thread muốn cài đặt hook
Trang 9Một hook có thể được sử dụng ở mức hệ thống, ở mức cục bộ, hoặc ở cả hai mức vừa nêu Bảng sau mô
tả loại hook cùng tầm ảnh hưởng của nó :
Với một loại hook xác định, hook cục bộ sẽ được gọi trước, sau đó là hook toàn cục
SetWindowsHookEx trả về handle của hook được cài đặt (là 1 giá trị có kiểu
HHOOK) Giá trị này cần được
lưu lại để dùng trong hàm UnhookWindowsHookEx khi ta muốn gỡ bỏ hook Nó sẽ
trả về NULL nếu không
thể gắn Filter Function vào hook, trong trường hợp này, hãy gọi hàm GetLastError
để biết lý do cài đặt hook
thất bại, giá trị mã lỗi là một trong các giá trị sau :
ERROR_INVALID_HOOK_FILTER : hằng số xác định loại hook sai.
ERROR_INVALID_FILTER_PROC : địa chỉ Filter Function không chính
xác
ERROR_HOOK_NEEDS_HMOD : tham số hMod của hàm
SetWindowsHookEx đang là NULL trong khi ứng dụng muốncài đặt một hook toàn cục
ERROR_GLOBAL_ONLY_HOOK : Ứng dụng đang cố cài đặt một hook
cục bộ trong khi bản chất của loại hook này là hook toàn cục
ERROR_INVALID_PARAMETER : giá trị của tham số dwThreadID
không chính xác
ERROR_JOURNAL_HOOK_SET : Đã có một hook loại “Journal”
(WH_JOURNALPLAYBACK hoặc WH_JOURNALRECORD) đang hiện
hữu trong hệ thống Vì lý do an toàn, Windows chỉ cho phép ta cài đặt một và
chỉ một hook loại “Journal” tại một thời điểm
ERROR_MOD_NOT_FOUND : Ứng dụng không thể định vị handle của
DLL trong khi muốn cài đặt một hook toàn cục
Các giá trị khác : Vấn đề an toàn của HĐH không cho phép hook này được
cài đặt, hoặc hệ thống đã hết bộ nhớ
Trang 10Gỡ bỏ một Filter Function ra khỏi chuỗi các Filter Function của một hook Windows cung cấp hàm UnhookWindowsHookEx giúp ta thực hiện việc
này.Khai báo của nó như sau :
BOOL UnhookWindowsHookEx(
HHOOK hhk // handle to hook procedure
);
Tham số duy nhất của hàm này là handle của hook cần được gỡ bỏ Giá trị này là giá
trị được trả về từ hàm SetWindowsHookEx khi ta cài đặt hook
Chi tiết về Filter Function
Đây là thời điểm thích hợp để bàn về Filter Function Nhắc lại rằng Filter Function là một hàm được gắn với loại hook mà ta muốn cài đặt Hàm này được gọi bởi HĐH
Windows chứ không được gọi bởi ứng dụng, đây cũng là lý do mà đôi khi người ta
thường gọi nó là “Callback Function” Tuy nhiên , để thống nhất về mặt thuật ngữ,
từ nay về sau ta vẫn gọi nó là Filter Function
Tất cả các Filter Function đều có dạng sau :
LRESULT CALLBACK FilterFunc(int nCode, WPARAM wParam, LPARAM lParam);
Lưu ý : “FilterFunc” chỉ là tên hàm tượng trưng, khi cài đặt, Filter Function sẽ có tên
bất kỳ theo ý của lập trình viên
· Ý nghĩa của từng tham số truyền cho hàm.
nCode : tham số này thường được gọi là “hook code”, Filter Function sử dụng giá trị
này để quyết định cách thức xử lý đối với sự kiện
(Lưu ý là điều này hoàn toàn tuỳ thuộc vào lập trình viên Có thể hình dung hook code đem lại những thông tin nào đó, và từ những thông tin này, lập trình viên sẽ quyết định xử lý sự kiện bắt được như thế nào theo hướng riêng của anh ta)
Giá trị của hook code tùy thuộc vào từng loại hook cụ thể, và mỗi loại hook sẽ có tập hợp những giá trị hook code đặc trưng của riêng mình Có một quy luật mà dường
như các Filter Function của mọi loại hook cần tuân thủ : Khi Windows truyền cho
hàm giá trị hook code âm, Filter Function không được xử lý sự kiện mà phải gọi hàm
CallNextHookEx với chính những tham số mà HĐH truyền cho nó Sau đó, nó phải
trả về giá trị được trả về bởi hàm CallNextHookEx wParam, lParam : Đây là
những thông tin cần thiết cho Filter Function trong quá trình xử lý sự kiện Các giá trị này sẽ có ý nghĩa khác nhau tuỳ thuộc vào từng loại hook Ví dụ, Filter Function gắn với hook WH_KEYBOARD sẽ nhận mã phím ảo (Virtual-Key Code) từ wParam, đồng thời có được từ lParam thông tin mô tả trạng thái của bàn phím khi sự kiện gõ phím xảy ra