在控制台中使用ReadConsoleInput函数读取键盘事件时,发现访问KeyEvent.uChar.AsciiChar得到的字符跟输入的总是不一致。比如从小键盘输入1,得到的是OO,输入2,得到的是PP。可以根据这个结果推断,程序能识别出键盘事件,但是在判断按键状态和取输入字符的时候出了问题。
下面的是为了说明这个问题而编写的MASM代码。
;MasmDemo.asm, 编译环境, MasmPlus
.386
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
include libc.inc
includelib msvcrt.lib
putchar proto C :DWORD
EVENT union
KeyEvent KEY_EVENT_RECORD <>
MouseEvent MOUSE_EVENT_RECORD <>
WindowBufferSizeEvent WINDOW_BUFFER_SIZE_RECORD <>
MenuEvent MENU_EVENT_RECORD <>
FocusEvent FOCUS_EVENT_RECORD <>
EVENT ends
INPUT_RECORD struct
EventType WORD ?
Event EVENT <>
INPUT_RECORD ends
.code
main proc
local @stKeyRec:INPUT_RECORD
local @hStdIn,@dwRes
invoke GetStdHandle,STD_INPUT_HANDLE
mov @hStdIn,eax
@StartLoop:
invoke ReadConsoleInput,@hStdIn,addr @stKeyRec,1,addr @dwRes
.if @stKeyRec.EventType == KEY_EVENT
.if @stKeyRec.Event.KeyEvent.bKeyDown
mov al,@stKeyRec.Event.KeyEvent.uChar.AsciiChar
.if al > 20h && al < 7eh
invoke putchar,eax
.endif
.endif
.endif
jmp @StartLoop
xor eax,eax
ret
main endp
end main
一开始以为是自己代码编写错误,后来仔细检查了许久,觉得不会是代码的问题。想着会不会是编译环境的问题就去VC6用C编写了功能一样的程序。
这下居然没有错误。访问KeyEvent.uChar.AsciiChar字段得到的就是键盘输入的字符。
下面的是不存在此问题的C代码。
//VCDemo.cpp, 编译环境 VC++ 6.0
#include <windows.h>
#include <stdio.h>
int main(void)
{
INPUT_RECORD stKeyRec;
HANDLE hStdIn;
DWORD dwRes;
char ch;
hStdIn = GetStdHandle(STD_INPUT_HANDLE);
while (true)
{
ReadConsoleInput(hStdIn, &stKeyRec, 1, &dwRes);
if (stKeyRec.EventType == KEY_EVENT)
{
if (stKeyRec.Event.KeyEvent.bKeyDown)
{
ch = stKeyRec.Event.KeyEvent.uChar.AsciiChar;
if (ch>0x20 && ch<0x7e)
{
putchar(ch);
}
}
}
}
return 0;
}
直觉很可能是自己定义的INPUT_RECORD有问题。因为MasmDemo中的是自己定义的。而VC中的是早已定义的。但是出了什么问题就不清楚了。
没办法。我的能力决定我已没办法干想就找出问题了。只好分别阅读两个程序的汇编代码。看看编译之后,程序哪里不一样了。
VC6的结果
00401066 mov ecx,dword ptr [ebp-14h] //ebp-14h 指向 stKeyRec.EventType
00401069 and ecx,0FFFFh
0040106F cmp ecx,1
00401072 jne main+0D5h (004010e5)
00401074 cmp dword ptr [ebp-10h],0 //ebp-10h 指向 stKeyRec.Event.KeyEvent.bKeyDown
00401078 je main+0D5h (004010e5)
MASM的结果
00401022 cmp word ptr ss:[ebp-0x12],0x1 ; ebp-12h 指向 stKeyRec.EventType
00401027 jnz short 00401043
00401029 cmp dword ptr ss:[ebp-0x10],0x0 ; ebp-10h 指向 stKeyRec.Event.KeyEvent.bKeyDown
0040102D je short 00401043
通过两处关键代码的对比可以知道。 VCDemo中EventType之后显然有2个字节的无用空间。这是怎么产生的呢?显然跟编译器密切有关。
根据VC6的对齐规则,可以推断Event联合体将会被放置在相对stKeyRect首址偏移为4的地方。而EventType只占用2个字节,剩余的2个字节便被废弃不用了。正是这种对齐机制影响了之后所有字段的偏移。
而Masm编译器的默认对齐是1字节。这就导致了MasmDemo通过访问KeyEvent.uChar.AsciiChar得不到正确的输入。如果MasmDemo想得到正确的输入字符,需要修正对应字段的偏移,以模拟出VC6内存对齐的效果。
MSDN中对INPUT_RECORD的定义如下:
typedef struct _INPUT_RECORD {
WORD EventType;
union {
KEY_EVENT_RECORD KeyEvent;
MOUSE_EVENT_RECORD MouseEvent;
WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent;
MENU_EVENT_RECORD MenuEvent;
FOCUS_EVENT_RECORD FocusEvent;
} Event;
} INPUT_RECORD;
这个定义不适合MASM程序。从MasmDemo的执行结构就可以知道(看来MSDN上的东西不是能拿来就用的,还是得多想想)。
因此可以对改结构稍作修改。
其中的联合类型不用修改。
EVENT union
KeyEvent KEY_EVENT_RECORD <>
MouseEvent MOUSE_EVENT_RECORD <>
WindowBufferSizeEvent WINDOW_BUFFER_SIZE_RECORD <>
MenuEvent MENU_EVENT_RECORD <>
FocusEvent FOCUS_EVENT_RECORD <>
EVENT ends
可以为INPUT_RECORD添加一个WORD类型的Reverse或者把EventType定义为DWORD。
如果选后者,在读取EventType的时候需要注意只能读低字部分。
INPUT_RECORD struct
EventType WORD ?
Reverse WORD ?
Event EVENT <>
INPUT_RECORD ends
或者
INPUT_RECORD struct
EventType DWORD ?
Event EVENT <>
INPUT_RECORD ends
如此,MasmDemo.ASM的的代码执行可以得到正确的结果了。
快写完这篇文章的时候突然想起机子上有RadAsm。于是把MasmDemo.ASM复制到RadAsm中运行。惊讶地发现INPUT_RECORD已经有定义了。
而这个定义跟我探索的也差不多,但是它的更合理,明确指出了2个字节用于内存对齐。RadAsm对INPUT_RECORD的定义如下。
INPUT_RECORD STRUCT
EventType WORD ?
two_byte_alignment WORD ?
UNION
KeyEvent KEY_EVENT_RECORD <>
MouseEvent MOUSE_EVENT_RECORD <>
WindowBufferSizeEvent WINDOW_BUFFER_SIZE_RECORD <>
MenuEvent MENU_EVENT_RECORD <>
FocusEvent FOCUS_EVENT_RECORD <>
ENDS
INPUT_RECORD ENDS
第一次写博文,写得没什么技术含量,也不通顺,有错也难免。但总算还是有些收获。