在 MS-DOS 中检查某个键是否按下 (C/C++)
Checking if a key is down in MS-DOS (C/C++)
是的,我指的是真正的 MS-DOS,而不是 Windows' cmd.exe
shell 控制台。
有没有办法在 MS-DOS 中检查某个键是否按下,类似于 WinAPI 中的 GetAsyncKeyState()
函数?
目前我正在使用 kbhit()
和 getch()
,但它真的很慢,在第一个字符后有延迟,不允许同时使用多个键等。
我正在使用 Turbo C++ 3.1。有人可以帮忙吗?
(顺便说一句,不要问我为什么要在这么古老的系统上编写游戏代码)
你为什么要在 su 上编写游戏代码……开玩笑!
在 MS-DOS 中,"API" 函数作为中断服务程序实现。在 x86 汇编语言中,您使用 INT
instruction 并指定要执行的中断号。大多数中断要求在执行 INT
之前在某些寄存器中设置它们的 "parameters"。 INT
指令 returns 控制您的代码后,其结果将被放置在某些寄存器 and/or 标志中,如中断调用的文档所定义。
我不知道 Turbo C++ 如何实现中断,因为那早于我参与编程,但我知道它允许您执行中断。 Google 了解语法,或查看 Turbo C++ 文档。
知道这些是中断将使您在搜索时获得 90% 的解决方案。 Ralf Brown compiled and published a famous list of DOS and BIOS interrupt codes. 在任何有关 DOS 编程的书籍中也应该可以找到它们 — 如果您认真对待复古编程,您绝对应该考虑尝试一下。在亚马逊上使用过的副本应该只花几美元。现在大多数人认为这些毫无价值。
Here 是一个列出可用于 DOS 中断 21h 的子函数的站点。与您的使用相关的是 01
、06
、07
和 08
。这些基本上就是像 getch
这样的 C 标准库函数在幕后要做的事情。我觉得这很难想象,但我听说过去的程序员发现直接调用 DOS 中断更快。我质疑的原因是我无法想象运行时库的实现者会愚蠢到提供不必要的缓慢实现。但也许他们是。
如果 DOS 中断对您来说仍然太慢,您最后的办法是直接使用 BIOS 中断。这可能会在速度上产生明显的差异,因为您正在绕过每个可能的抽象层。但它确实使您的程序的可移植性显着降低,这就是像 DOS 这样的操作系统一开始就提供这些高级函数调用的原因。再次检查 Ralf Brown 的列表以查找与您的使用相关的中断。例如,INT 16
with the 01h
sub-function.
pressing on the arrows keys shoots two Keyboard interrupts ? ( int 09h )
这个问题中的实现工作得很好,所以如果有人出于某种原因想要一个现成的函数,给你:
unsigned char read_scancode() {
unsigned char res;
_asm {
in al, 60h
mov res, al
in al, 61h
or al, 128
out 61h, al
xor al, 128
out 61h, al
}
return res;
}
(编辑:将 char 更正为 unsigned char,因此将此函数的 return 值放入 "if" 语句中,并使用 scancode & 0x80
之类的东西实际上有效)
按下某个键时,它 return 是那里列出的扫描码之一 http://www.ctyme.com/intr/rb-0045.htm 并且当它被释放时,它 return 是相同的扫描码,但与 80h 进行了或运算。
如果你真的 运行 在游戏循环中这样做,你最终会溢出 BIOS 键盘缓冲区,并且计算机会向你发出哔哔声。释放键盘缓冲区的一种方法当然是 while(kbhit()) getch();
,但是由于我们在 286 实模式下并且我们有所有的硬件可以使用,这里有一个更底层的解决方案:
void free_keyb_buf() {
*(char*)(0x0040001A) = 0x20;
*(char*)(0x0040001C) = 0x20;
}
如果您正在寻找它如何工作以及为什么工作的解释,请看这里:
BIOS 键盘缓冲区从 0040:001Ah
开始,如下所示:2 字节 "head" 指针、2 字节 "tail" 指针和 32 字节的 2 字节扫描码。 "tail" 指针指示从键盘缓冲区开始读取的位置,"head" 指针指示停止的位置。因此,通过将这两个设置为 0x20
(因此它们实际上指向 0040:0020h
),我们基本上可以欺骗计算机认为没有新的击键可以提取。
Turbo C++、MS-DOS 或 BIOS 没有提供对应于 Windows 函数 GetAsyncKeyState
的函数。 BIOS 仅跟踪按住了哪些修改键(Shift、Ctrl 或 Alt),它不会跟踪任何其他键。如果你想这样做,你需要直接与键盘控制器对话并监控它从键盘接收到的 make(按下键)和 break(释放键)扫描代码。
为此,您需要挂接键盘中断(IRQ 1,INT 0x09),从键盘控制器读取扫描码,然后更新您自己的键盘状态 table。
这是一个演示如何执行此操作的简单程序:
#include <conio.h>
#include <dos.h>
#include <stdio.h>
unsigned char normal_keys[0x60];
unsigned char extended_keys[0x60];
static void interrupt
keyb_int() {
static unsigned char buffer;
unsigned char rawcode;
unsigned char make_break;
int scancode;
rawcode = inp(0x60); /* read scancode from keyboard controller */
make_break = !(rawcode & 0x80); /* bit 7: 0 = make, 1 = break */
scancode = rawcode & 0x7F;
if (buffer == 0xE0) { /* second byte of an extended key */
if (scancode < 0x60) {
extended_keys[scancode] = make_break;
}
buffer = 0;
} else if (buffer >= 0xE1 && buffer <= 0xE2) {
buffer = 0; /* ingore these extended keys */
} else if (rawcode >= 0xE0 && rawcode <= 0xE2) {
buffer = rawcode; /* first byte of an extended key */
} else if (scancode < 0x60) {
normal_keys[scancode] = make_break;
}
outp(0x20, 0x20); /* must send EOI to finish interrupt */
}
static void interrupt (*old_keyb_int)();
void
hook_keyb_int(void) {
old_keyb_int = getvect(0x09);
setvect(0x09, keyb_int);
}
void
unhook_keyb_int(void) {
if (old_keyb_int != NULL) {
setvect(0x09, old_keyb_int);
old_keyb_int = NULL;
}
}
int
ctrlbrk_handler(void) {
unhook_keyb_int();
_setcursortype(_NORMALCURSOR);
return 0;
}
static
putkeys(int y, unsigned char const *keys) {
int i;
gotoxy(1, y);
for (i = 0; i < 0x30; i++) {
putch(keys[i] + '0');
}
}
void
game(void) {
_setcursortype(_NOCURSOR);
clrscr();
while(!normal_keys[1]) {
putkeys(1, normal_keys);
putkeys(2, normal_keys + 0x30);
putkeys(4, extended_keys);
putkeys(5, extended_keys + 0x30);
}
gotoxy(1, 6);
_setcursortype(_NORMALCURSOR);
}
int
main() {
ctrlbrk(ctrlbrk_handler);
hook_keyb_int();
game();
unhook_keyb_int();
return 0;
}
以上代码已使用 Borland C++ 3.1 编译,并在 VirtualBox 下的 DOSBox 和 MS-DOS 6.11 运行 下进行了测试。它显示键盘的当前状态,一串 0 和 1,一个 1
表示正在按下与该位置的扫描码对应的键。按ESC键退出程序。
请注意,该程序不会链接原始键盘处理程序,因此当键盘中断被挂钩时,正常的 MS-DOS 和 BIOS 键盘功能将不起作用。另请注意,它会在退出前恢复原始键盘处理程序。这一点很重要,因为 MS-DOS 自己不会这样做。它还可以正确处理发送两个字节扫描码的扩展键,这是您在此处的答案中链接到的问题中的代码问题。
所以,我最近仔细研究了所有这些东西,正好有您需要的代码。 (另外,我会 link 你一些很棒的书,以获取 pdf 格式的信息。)
因此,其工作方式是您需要覆盖内存中索引 9h 处的中断向量 Table。中断向量 Table 只是一个 table 的内存地址,当中断被触发时,它指向 运行 的一段代码(这些称为中断处理程序例程或 ISR)。当键盘控制器有一个扫描码可供使用时,中断 9h 被触发。
无论如何,我们首先需要通过调用 KeyboardInstallDriver() 函数来覆盖旧的 int9h ISR。现在,当触发 int9h 时,将调用 KeyboardIsr() 函数,它从键盘控制器获取扫描码,并将 keyStates[] 数组中的值设置为 1 (KEY_PRESSED) 或 0 (KEY_RELEASED) 基于从键盘控制器检索到的扫描码的值。
设置好keyStates[]数组中对应的值后,就可以调用KeyboardGetKey()给它你想知道状态的键的扫描码,它会在keyStates[] 数组和 return 无论状态是什么。
这方面有很多细节,但在这里写太多了。所有细节都可以在我将 link 的书中找到:
IBM PC Technical Reference, IBM PC XT Technical Reference,
IBM PC AT Technical Reference, Black Art of 3D Game Programming
希望那些 link 能保持活跃一段时间。此外,"Black Art of 3D Game Programming" 这本书并不总是在每个细节上都完全准确。有时会出现拼写错误,有时会出现错误信息,但 IBM 技术参考资料提供了所有详细信息(即使它们有时有点含糊),但没有示例代码。看书看大意,看参考书看细节。
这是我从键盘获取输入的代码:
(对于所有可能的键和某些其他东西,它还没有完全完成,但它对大多数程序和游戏都适用。)
此外,还有一些代码可以处理 "extended" 键。扩展键的常规扫描代码前面有 0xE0。这还有更多疯狂的细节,所以我不打算介绍它,但是,无论如何,这里有大部分工作代码。
keyboard.h
#ifndef KEYBOARD_H_INCLUDED
#define KEYBOARD_H_INCLUDED
#include "keyboard_scan_codes.h"
unsigned char KeyboardGetKey(unsigned int scanCode);
void KeyboardClearKeys();
void KeyboardInstallDriver();
void KeyboardUninstallDriver();
void KeyboardDumpScancodeLog();
#endif // KEYBOARD_H_INCLUDED
keyboard.c
#define MAX_SCAN_CODES 256
#define KEYBOARD_CONTROLLER_OUTPUT_BUFFER 0x60
#define KEYBOARD_CONTROLLER_STATUS_REGISTER 0x64
#define KEY_PRESSED 1
#define KEY_RELEASED 0
#define PIC_OPERATION_COMMAND_PORT 0x20
#define KEYBOARD_INTERRUPT_VECTOR 0x09
// PPI stands for Programmable Peripheral Interface (which is the Intel 8255A chip)
// The PPI ports are only for IBM PC and XT, however port A is mapped to the same
// I/O address as the Keyboard Controller's (Intel 8042 chip) output buffer for compatibility.
#define PPI_PORT_A 0x60
#define PPI_PORT_B 0x61
#define PPI_PORT_C 0x62
#define PPI_COMMAND_REGISTER 0x63
#include <dos.h>
#include <string.h>
#include <stdio.h>
#include <conio.h>
#include "keyboard.h"
void interrupt (*oldKeyboardIsr)() = (void *)0;
unsigned char keyStates[MAX_SCAN_CODES];
unsigned char keyCodeLog[256] = {0};
unsigned char keyCodeLogPosition = 0;
static unsigned char isPreviousCodeExtended = 0;
unsigned char KeyboardGetKey(unsigned int scanCode)
{
// Check for the extended code
if(scanCode >> 8 == 0xE0)
{
// Get rid of the extended code
scanCode &= 0xFF;
return keyStates[scanCode + 0x7F];
}
else
{
return keyStates[scanCode];
}
}
void KeyboardClearKeys()
{
memset(&keyStates[0], 0, MAX_SCAN_CODES);
}
void interrupt far KeyboardIsr()
{
static unsigned char scanCode;
unsigned char ppiPortB;
_asm {
cli // disable interrupts
};
/* The keyboard controller, by default, will send scan codes
// in Scan Code Set 1 (reference the IBM Technical References
// for a complete list of scan codes).
//
// Scan codes in this set come as make/break codes. The make
// code is the normal scan code of the key, and the break code
// is the make code bitwise "OR"ed with 0x80 (the high bit is set).
//
// On keyboards after the original IBM Model F 83-key, an 0xE0
// is prepended to some keys that didn't exist on the original keyboard.
//
// Some keys have their scan codes affected by the state of
// the shift, and num-lock keys. These certain
// keys have, potentially, quite long scan codes with multiple
// possible 0xE0 bytes along with other codes to indicate the
// state of the shift, and num-lock keys.
//
// There are two other Scan Code Sets, Set 2 and Set 3. Set 2
// was introduced with the IBM PC AT, and Set 3 with the IBM
// PS/2. Set 3 is by far the easiest and most simple set to work
// with, but not all keyboards support it.
//
// Note:
// The "keyboard controller" chip is different depending on
// which machine is being used. The original IBM PC uses the
// Intel 8255A-5, while the IBM PC AT uses the Intel 8042 (UPI-42AH).
// On the 8255A-5, port 0x61 can be read and written to for various
// things, one of which will clear the keyboard and disable it or
// re enable it. There is no such function on the AT and newer, but
// it is not needed anyways. The 8042 uses ports 0x60 and 0x64. Both
// the 8255A-5 and the 8042 give the scan codes from the keyboard
// through port 0x60.
// On the IBM PC and XT and compatibles, you MUST clear the keyboard
// after reading the scancode by reading the value at port 0x61,
// flipping the 7th bit to a 1, and writing that value back to port 0x61.
// After that is done, flip the 7th bit back to 0 to re-enable the keyboard.
//
// On IBM PC ATs and newer, writing and reading port 0x61 does nothing (as far
// as I know), and using it to clear the keyboard isn't necessary.*/
scanCode = 0;
ppiPortB = 0;
ppiPortB = inp(PPI_PORT_B); // get the current settings in PPI port B
scanCode = inp(KEYBOARD_CONTROLLER_OUTPUT_BUFFER); // get the scancode waiting in the output buffer
outp(PPI_PORT_B, ppiPortB | 0x80); // set the 7th bit of PPI port B (clear keyboard)
outp(PPI_PORT_B, ppiPortB); // clear the 7th bit of the PPI (enable keyboard)
// Log scancode
keyCodeLog[keyCodeLogPosition] = scanCode;
if(keyCodeLogPosition < 255)
{
++keyCodeLogPosition;
}
// Check to see what the code was.
// Note that we have to process the scan code one byte at a time.
// This is because we can't get another scan code until the current
// interrupt is finished.
switch(scanCode)
{
case 0xE0:
// Extended scancode
isPreviousCodeExtended = 1;
break;
default:
// Regular scancode
// Check the high bit, if set, then it's a break code.
if(isPreviousCodeExtended)
{
isPreviousCodeExtended = 0;
if(scanCode & 0x80)
{
scanCode &= 0x7F;
keyStates[scanCode + 0x7F] = KEY_RELEASED;
}
else
{
keyStates[scanCode + 0x7F] = KEY_PRESSED;
}
}
else if(scanCode & 0x80)
{
scanCode &= 0x7F;
keyStates[scanCode] = KEY_RELEASED;
}
else
{
keyStates[scanCode] = KEY_PRESSED;
}
break;
}
// Send a "Non Specific End of Interrupt" command to the PIC.
// See Intel 8259A datasheet for details.
outp(PIC_OPERATION_COMMAND_PORT, 0x20);
_asm
{
sti // enable interrupts
};
}
void KeyboardInstallDriver()
{
// Make sure the new ISR isn't already in use.
if(oldKeyboardIsr == (void *)0)
{
oldKeyboardIsr = _dos_getvect(KEYBOARD_INTERRUPT_VECTOR);
_dos_setvect(KEYBOARD_INTERRUPT_VECTOR, KeyboardIsr);
}
}
void KeyboardUninstallDriver()
{
// Make sure the new ISR is in use.
if(oldKeyboardIsr != (void *)0)
{
_dos_setvect(KEYBOARD_INTERRUPT_VECTOR, oldKeyboardIsr);
oldKeyboardIsr = (void *)0;
}
}
void KeyboardDumpScancodeLog()
{
FILE *keyLogFile = fopen("keylog.hex", "w+b");
if(!keyLogFile)
{
printf("ERROR: Couldn't open file for key logging!\n");
}
else
{
int i;
for(i = 0; i < 256; ++i)
{
fputc(keyCodeLog[i], keyLogFile);
}
fclose(keyLogFile);
}
}
keyboard_scan_codes.h(简单定义所有扫描码到 qwerty 按钮布局)
#ifndef KEYBOARD_SCAN_CODES_H_INCLUDED
#define KEYBOARD_SCAN_CODES_H_INCLUDED
// Original 83 Keys from the IBM 83-key Model F keyboard
#define SCAN_NONE 0x00
#define SCAN_ESC 0x01
#define SCAN_1 0x02
#define SCAN_2 0x03
#define SCAN_3 0x04
#define SCAN_4 0x05
#define SCAN_5 0x06
#define SCAN_6 0x07
#define SCAN_7 0x08
#define SCAN_8 0x09
#define SCAN_9 0x0A
#define SCAN_0 0x0B
#define SCAN_MINUS 0x0C
#define SCAN_EQUALS 0x0D
#define SCAN_BACKSPACE 0x0E
#define SCAN_TAB 0x0F
#define SCAN_Q 0x10
#define SCAN_W 0x11
#define SCAN_E 0x12
#define SCAN_R 0x13
#define SCAN_T 0x14
#define SCAN_Y 0x15
#define SCAN_U 0x16
#define SCAN_I 0x17
#define SCAN_O 0x18
#define SCAN_P 0x19
#define SCAN_LEFT_BRACE 0x1A
#define SCAN_RIGHT_BRACE 0x1B
#define SCAN_ENTER 0x1C
#define SCAN_LEFT_CONTROL 0x1D
#define SCAN_A 0x1E
#define SCAN_S 0x1F
#define SCAN_D 0x20
#define SCAN_F 0x21
#define SCAN_G 0x22
#define SCAN_H 0x23
#define SCAN_J 0x24
#define SCAN_K 0x25
#define SCAN_L 0x26
#define SCAN_SEMICOLON 0x27
#define SCAN_APOSTROPHE 0x28
#define SCAN_ACCENT 0x29
#define SCAN_TILDE 0x29 // Duplicate of SCAN_ACCENT with popular Tilde name.
#define SCAN_LEFT_SHIFT 0x2A
#define SCAN_BACK_SLASH 0x2B
#define SCAN_Z 0x2C
#define SCAN_X 0x2D
#define SCAN_C 0x2E
#define SCAN_V 0x2F
#define SCAN_B 0x30
#define SCAN_N 0x31
#define SCAN_M 0x32
#define SCAN_COMMA 0x33
#define SCAN_PERIOD 0x34
#define SCAN_FORWARD_SLASH 0x35
#define SCAN_RIGHT_SHIFT 0x36
#define SCAN_KP_STAR 0x37
#define SCAN_KP_MULTIPLY 0x37 // Duplicate of SCAN_KP_STAR
#define SCAN_LEFT_ALT 0x38
#define SCAN_SPACE 0x39
#define SCAN_CAPS_LOCK 0x3A
#define SCAN_F1 0x3B
#define SCAN_F2 0x3C
#define SCAN_F3 0x3D
#define SCAN_F4 0x3E
#define SCAN_F5 0x3F
#define SCAN_F6 0x40
#define SCAN_F7 0x41
#define SCAN_F8 0x42
#define SCAN_F9 0x43
#define SCAN_F10 0x44
#define SCAN_NUM_LOCK 0x45
#define SCAN_SCROLL_LOCK 0x46
#define SCAN_KP_7 0x47
#define SCAN_KP_8 0x48
#define SCAN_KP_9 0x49
#define SCAN_KP_MINUS 0x4A
#define SCAN_KP_4 0x4B
#define SCAN_KP_5 0x4C
#define SCAN_KP_6 0x4D
#define SCAN_KP_PLUS 0x4E
#define SCAN_KP_1 0x4F
#define SCAN_KP_2 0x50
#define SCAN_KP_3 0x51
#define SCAN_KP_0 0x52
#define SCAN_KP_PERIOD 0x53
// Extended keys for the IBM 101-key Model M keyboard.
#define SCAN_RIGHT_ALT 0xE038
#define SCAN_RIGHT_CONTROL 0xE01D
#define SCAN_LEFT_ARROW 0xE04B
#define SCAN_RIGHT_ARROW 0xE04D
#define SCAN_UP_ARROW 0xE048
#define SCAN_DOWN_ARROW 0xE050
#define SCAN_NUMPAD_ENTER 0xE01C
#define SCAN_INSERT 0xE052
#define SCAN_DELETE 0xE053
#define SCAN_HOME 0xE047
#define SCAN_END 0xE04F
#define SCAN_PAGE_UP 0xE049
#define SCAN_PAGE_DOWN 0xE051
#define SCAN_KP_FORWARD_SLASH 0xE035
#define SCAN_PRINT_SCREEN 0xE02AE037
#endif // KEYBOARD_SCAN_CODES_H_INCLUDED
是的,我指的是真正的 MS-DOS,而不是 Windows' cmd.exe
shell 控制台。
有没有办法在 MS-DOS 中检查某个键是否按下,类似于 WinAPI 中的 GetAsyncKeyState()
函数?
目前我正在使用 kbhit()
和 getch()
,但它真的很慢,在第一个字符后有延迟,不允许同时使用多个键等。
我正在使用 Turbo C++ 3.1。有人可以帮忙吗?
(顺便说一句,不要问我为什么要在这么古老的系统上编写游戏代码)
你为什么要在 su 上编写游戏代码……开玩笑!
在 MS-DOS 中,"API" 函数作为中断服务程序实现。在 x86 汇编语言中,您使用 INT
instruction 并指定要执行的中断号。大多数中断要求在执行 INT
之前在某些寄存器中设置它们的 "parameters"。 INT
指令 returns 控制您的代码后,其结果将被放置在某些寄存器 and/or 标志中,如中断调用的文档所定义。
我不知道 Turbo C++ 如何实现中断,因为那早于我参与编程,但我知道它允许您执行中断。 Google 了解语法,或查看 Turbo C++ 文档。
知道这些是中断将使您在搜索时获得 90% 的解决方案。 Ralf Brown compiled and published a famous list of DOS and BIOS interrupt codes. 在任何有关 DOS 编程的书籍中也应该可以找到它们 — 如果您认真对待复古编程,您绝对应该考虑尝试一下。在亚马逊上使用过的副本应该只花几美元。现在大多数人认为这些毫无价值。
Here 是一个列出可用于 DOS 中断 21h 的子函数的站点。与您的使用相关的是 01
、06
、07
和 08
。这些基本上就是像 getch
这样的 C 标准库函数在幕后要做的事情。我觉得这很难想象,但我听说过去的程序员发现直接调用 DOS 中断更快。我质疑的原因是我无法想象运行时库的实现者会愚蠢到提供不必要的缓慢实现。但也许他们是。
如果 DOS 中断对您来说仍然太慢,您最后的办法是直接使用 BIOS 中断。这可能会在速度上产生明显的差异,因为您正在绕过每个可能的抽象层。但它确实使您的程序的可移植性显着降低,这就是像 DOS 这样的操作系统一开始就提供这些高级函数调用的原因。再次检查 Ralf Brown 的列表以查找与您的使用相关的中断。例如,INT 16
with the 01h
sub-function.
pressing on the arrows keys shoots two Keyboard interrupts ? ( int 09h ) 这个问题中的实现工作得很好,所以如果有人出于某种原因想要一个现成的函数,给你:
unsigned char read_scancode() {
unsigned char res;
_asm {
in al, 60h
mov res, al
in al, 61h
or al, 128
out 61h, al
xor al, 128
out 61h, al
}
return res;
}
(编辑:将 char 更正为 unsigned char,因此将此函数的 return 值放入 "if" 语句中,并使用 scancode & 0x80
之类的东西实际上有效)
按下某个键时,它 return 是那里列出的扫描码之一 http://www.ctyme.com/intr/rb-0045.htm 并且当它被释放时,它 return 是相同的扫描码,但与 80h 进行了或运算。
如果你真的 运行 在游戏循环中这样做,你最终会溢出 BIOS 键盘缓冲区,并且计算机会向你发出哔哔声。释放键盘缓冲区的一种方法当然是 while(kbhit()) getch();
,但是由于我们在 286 实模式下并且我们有所有的硬件可以使用,这里有一个更底层的解决方案:
void free_keyb_buf() {
*(char*)(0x0040001A) = 0x20;
*(char*)(0x0040001C) = 0x20;
}
如果您正在寻找它如何工作以及为什么工作的解释,请看这里:
BIOS 键盘缓冲区从 0040:001Ah
开始,如下所示:2 字节 "head" 指针、2 字节 "tail" 指针和 32 字节的 2 字节扫描码。 "tail" 指针指示从键盘缓冲区开始读取的位置,"head" 指针指示停止的位置。因此,通过将这两个设置为 0x20
(因此它们实际上指向 0040:0020h
),我们基本上可以欺骗计算机认为没有新的击键可以提取。
Turbo C++、MS-DOS 或 BIOS 没有提供对应于 Windows 函数 GetAsyncKeyState
的函数。 BIOS 仅跟踪按住了哪些修改键(Shift、Ctrl 或 Alt),它不会跟踪任何其他键。如果你想这样做,你需要直接与键盘控制器对话并监控它从键盘接收到的 make(按下键)和 break(释放键)扫描代码。
为此,您需要挂接键盘中断(IRQ 1,INT 0x09),从键盘控制器读取扫描码,然后更新您自己的键盘状态 table。
这是一个演示如何执行此操作的简单程序:
#include <conio.h>
#include <dos.h>
#include <stdio.h>
unsigned char normal_keys[0x60];
unsigned char extended_keys[0x60];
static void interrupt
keyb_int() {
static unsigned char buffer;
unsigned char rawcode;
unsigned char make_break;
int scancode;
rawcode = inp(0x60); /* read scancode from keyboard controller */
make_break = !(rawcode & 0x80); /* bit 7: 0 = make, 1 = break */
scancode = rawcode & 0x7F;
if (buffer == 0xE0) { /* second byte of an extended key */
if (scancode < 0x60) {
extended_keys[scancode] = make_break;
}
buffer = 0;
} else if (buffer >= 0xE1 && buffer <= 0xE2) {
buffer = 0; /* ingore these extended keys */
} else if (rawcode >= 0xE0 && rawcode <= 0xE2) {
buffer = rawcode; /* first byte of an extended key */
} else if (scancode < 0x60) {
normal_keys[scancode] = make_break;
}
outp(0x20, 0x20); /* must send EOI to finish interrupt */
}
static void interrupt (*old_keyb_int)();
void
hook_keyb_int(void) {
old_keyb_int = getvect(0x09);
setvect(0x09, keyb_int);
}
void
unhook_keyb_int(void) {
if (old_keyb_int != NULL) {
setvect(0x09, old_keyb_int);
old_keyb_int = NULL;
}
}
int
ctrlbrk_handler(void) {
unhook_keyb_int();
_setcursortype(_NORMALCURSOR);
return 0;
}
static
putkeys(int y, unsigned char const *keys) {
int i;
gotoxy(1, y);
for (i = 0; i < 0x30; i++) {
putch(keys[i] + '0');
}
}
void
game(void) {
_setcursortype(_NOCURSOR);
clrscr();
while(!normal_keys[1]) {
putkeys(1, normal_keys);
putkeys(2, normal_keys + 0x30);
putkeys(4, extended_keys);
putkeys(5, extended_keys + 0x30);
}
gotoxy(1, 6);
_setcursortype(_NORMALCURSOR);
}
int
main() {
ctrlbrk(ctrlbrk_handler);
hook_keyb_int();
game();
unhook_keyb_int();
return 0;
}
以上代码已使用 Borland C++ 3.1 编译,并在 VirtualBox 下的 DOSBox 和 MS-DOS 6.11 运行 下进行了测试。它显示键盘的当前状态,一串 0 和 1,一个 1
表示正在按下与该位置的扫描码对应的键。按ESC键退出程序。
请注意,该程序不会链接原始键盘处理程序,因此当键盘中断被挂钩时,正常的 MS-DOS 和 BIOS 键盘功能将不起作用。另请注意,它会在退出前恢复原始键盘处理程序。这一点很重要,因为 MS-DOS 自己不会这样做。它还可以正确处理发送两个字节扫描码的扩展键,这是您在此处的答案中链接到的问题中的代码问题。
所以,我最近仔细研究了所有这些东西,正好有您需要的代码。 (另外,我会 link 你一些很棒的书,以获取 pdf 格式的信息。)
因此,其工作方式是您需要覆盖内存中索引 9h 处的中断向量 Table。中断向量 Table 只是一个 table 的内存地址,当中断被触发时,它指向 运行 的一段代码(这些称为中断处理程序例程或 ISR)。当键盘控制器有一个扫描码可供使用时,中断 9h 被触发。
无论如何,我们首先需要通过调用 KeyboardInstallDriver() 函数来覆盖旧的 int9h ISR。现在,当触发 int9h 时,将调用 KeyboardIsr() 函数,它从键盘控制器获取扫描码,并将 keyStates[] 数组中的值设置为 1 (KEY_PRESSED) 或 0 (KEY_RELEASED) 基于从键盘控制器检索到的扫描码的值。
设置好keyStates[]数组中对应的值后,就可以调用KeyboardGetKey()给它你想知道状态的键的扫描码,它会在keyStates[] 数组和 return 无论状态是什么。
这方面有很多细节,但在这里写太多了。所有细节都可以在我将 link 的书中找到: IBM PC Technical Reference, IBM PC XT Technical Reference, IBM PC AT Technical Reference, Black Art of 3D Game Programming
希望那些 link 能保持活跃一段时间。此外,"Black Art of 3D Game Programming" 这本书并不总是在每个细节上都完全准确。有时会出现拼写错误,有时会出现错误信息,但 IBM 技术参考资料提供了所有详细信息(即使它们有时有点含糊),但没有示例代码。看书看大意,看参考书看细节。
这是我从键盘获取输入的代码: (对于所有可能的键和某些其他东西,它还没有完全完成,但它对大多数程序和游戏都适用。)
此外,还有一些代码可以处理 "extended" 键。扩展键的常规扫描代码前面有 0xE0。这还有更多疯狂的细节,所以我不打算介绍它,但是,无论如何,这里有大部分工作代码。
keyboard.h
#ifndef KEYBOARD_H_INCLUDED
#define KEYBOARD_H_INCLUDED
#include "keyboard_scan_codes.h"
unsigned char KeyboardGetKey(unsigned int scanCode);
void KeyboardClearKeys();
void KeyboardInstallDriver();
void KeyboardUninstallDriver();
void KeyboardDumpScancodeLog();
#endif // KEYBOARD_H_INCLUDED
keyboard.c
#define MAX_SCAN_CODES 256
#define KEYBOARD_CONTROLLER_OUTPUT_BUFFER 0x60
#define KEYBOARD_CONTROLLER_STATUS_REGISTER 0x64
#define KEY_PRESSED 1
#define KEY_RELEASED 0
#define PIC_OPERATION_COMMAND_PORT 0x20
#define KEYBOARD_INTERRUPT_VECTOR 0x09
// PPI stands for Programmable Peripheral Interface (which is the Intel 8255A chip)
// The PPI ports are only for IBM PC and XT, however port A is mapped to the same
// I/O address as the Keyboard Controller's (Intel 8042 chip) output buffer for compatibility.
#define PPI_PORT_A 0x60
#define PPI_PORT_B 0x61
#define PPI_PORT_C 0x62
#define PPI_COMMAND_REGISTER 0x63
#include <dos.h>
#include <string.h>
#include <stdio.h>
#include <conio.h>
#include "keyboard.h"
void interrupt (*oldKeyboardIsr)() = (void *)0;
unsigned char keyStates[MAX_SCAN_CODES];
unsigned char keyCodeLog[256] = {0};
unsigned char keyCodeLogPosition = 0;
static unsigned char isPreviousCodeExtended = 0;
unsigned char KeyboardGetKey(unsigned int scanCode)
{
// Check for the extended code
if(scanCode >> 8 == 0xE0)
{
// Get rid of the extended code
scanCode &= 0xFF;
return keyStates[scanCode + 0x7F];
}
else
{
return keyStates[scanCode];
}
}
void KeyboardClearKeys()
{
memset(&keyStates[0], 0, MAX_SCAN_CODES);
}
void interrupt far KeyboardIsr()
{
static unsigned char scanCode;
unsigned char ppiPortB;
_asm {
cli // disable interrupts
};
/* The keyboard controller, by default, will send scan codes
// in Scan Code Set 1 (reference the IBM Technical References
// for a complete list of scan codes).
//
// Scan codes in this set come as make/break codes. The make
// code is the normal scan code of the key, and the break code
// is the make code bitwise "OR"ed with 0x80 (the high bit is set).
//
// On keyboards after the original IBM Model F 83-key, an 0xE0
// is prepended to some keys that didn't exist on the original keyboard.
//
// Some keys have their scan codes affected by the state of
// the shift, and num-lock keys. These certain
// keys have, potentially, quite long scan codes with multiple
// possible 0xE0 bytes along with other codes to indicate the
// state of the shift, and num-lock keys.
//
// There are two other Scan Code Sets, Set 2 and Set 3. Set 2
// was introduced with the IBM PC AT, and Set 3 with the IBM
// PS/2. Set 3 is by far the easiest and most simple set to work
// with, but not all keyboards support it.
//
// Note:
// The "keyboard controller" chip is different depending on
// which machine is being used. The original IBM PC uses the
// Intel 8255A-5, while the IBM PC AT uses the Intel 8042 (UPI-42AH).
// On the 8255A-5, port 0x61 can be read and written to for various
// things, one of which will clear the keyboard and disable it or
// re enable it. There is no such function on the AT and newer, but
// it is not needed anyways. The 8042 uses ports 0x60 and 0x64. Both
// the 8255A-5 and the 8042 give the scan codes from the keyboard
// through port 0x60.
// On the IBM PC and XT and compatibles, you MUST clear the keyboard
// after reading the scancode by reading the value at port 0x61,
// flipping the 7th bit to a 1, and writing that value back to port 0x61.
// After that is done, flip the 7th bit back to 0 to re-enable the keyboard.
//
// On IBM PC ATs and newer, writing and reading port 0x61 does nothing (as far
// as I know), and using it to clear the keyboard isn't necessary.*/
scanCode = 0;
ppiPortB = 0;
ppiPortB = inp(PPI_PORT_B); // get the current settings in PPI port B
scanCode = inp(KEYBOARD_CONTROLLER_OUTPUT_BUFFER); // get the scancode waiting in the output buffer
outp(PPI_PORT_B, ppiPortB | 0x80); // set the 7th bit of PPI port B (clear keyboard)
outp(PPI_PORT_B, ppiPortB); // clear the 7th bit of the PPI (enable keyboard)
// Log scancode
keyCodeLog[keyCodeLogPosition] = scanCode;
if(keyCodeLogPosition < 255)
{
++keyCodeLogPosition;
}
// Check to see what the code was.
// Note that we have to process the scan code one byte at a time.
// This is because we can't get another scan code until the current
// interrupt is finished.
switch(scanCode)
{
case 0xE0:
// Extended scancode
isPreviousCodeExtended = 1;
break;
default:
// Regular scancode
// Check the high bit, if set, then it's a break code.
if(isPreviousCodeExtended)
{
isPreviousCodeExtended = 0;
if(scanCode & 0x80)
{
scanCode &= 0x7F;
keyStates[scanCode + 0x7F] = KEY_RELEASED;
}
else
{
keyStates[scanCode + 0x7F] = KEY_PRESSED;
}
}
else if(scanCode & 0x80)
{
scanCode &= 0x7F;
keyStates[scanCode] = KEY_RELEASED;
}
else
{
keyStates[scanCode] = KEY_PRESSED;
}
break;
}
// Send a "Non Specific End of Interrupt" command to the PIC.
// See Intel 8259A datasheet for details.
outp(PIC_OPERATION_COMMAND_PORT, 0x20);
_asm
{
sti // enable interrupts
};
}
void KeyboardInstallDriver()
{
// Make sure the new ISR isn't already in use.
if(oldKeyboardIsr == (void *)0)
{
oldKeyboardIsr = _dos_getvect(KEYBOARD_INTERRUPT_VECTOR);
_dos_setvect(KEYBOARD_INTERRUPT_VECTOR, KeyboardIsr);
}
}
void KeyboardUninstallDriver()
{
// Make sure the new ISR is in use.
if(oldKeyboardIsr != (void *)0)
{
_dos_setvect(KEYBOARD_INTERRUPT_VECTOR, oldKeyboardIsr);
oldKeyboardIsr = (void *)0;
}
}
void KeyboardDumpScancodeLog()
{
FILE *keyLogFile = fopen("keylog.hex", "w+b");
if(!keyLogFile)
{
printf("ERROR: Couldn't open file for key logging!\n");
}
else
{
int i;
for(i = 0; i < 256; ++i)
{
fputc(keyCodeLog[i], keyLogFile);
}
fclose(keyLogFile);
}
}
keyboard_scan_codes.h(简单定义所有扫描码到 qwerty 按钮布局)
#ifndef KEYBOARD_SCAN_CODES_H_INCLUDED
#define KEYBOARD_SCAN_CODES_H_INCLUDED
// Original 83 Keys from the IBM 83-key Model F keyboard
#define SCAN_NONE 0x00
#define SCAN_ESC 0x01
#define SCAN_1 0x02
#define SCAN_2 0x03
#define SCAN_3 0x04
#define SCAN_4 0x05
#define SCAN_5 0x06
#define SCAN_6 0x07
#define SCAN_7 0x08
#define SCAN_8 0x09
#define SCAN_9 0x0A
#define SCAN_0 0x0B
#define SCAN_MINUS 0x0C
#define SCAN_EQUALS 0x0D
#define SCAN_BACKSPACE 0x0E
#define SCAN_TAB 0x0F
#define SCAN_Q 0x10
#define SCAN_W 0x11
#define SCAN_E 0x12
#define SCAN_R 0x13
#define SCAN_T 0x14
#define SCAN_Y 0x15
#define SCAN_U 0x16
#define SCAN_I 0x17
#define SCAN_O 0x18
#define SCAN_P 0x19
#define SCAN_LEFT_BRACE 0x1A
#define SCAN_RIGHT_BRACE 0x1B
#define SCAN_ENTER 0x1C
#define SCAN_LEFT_CONTROL 0x1D
#define SCAN_A 0x1E
#define SCAN_S 0x1F
#define SCAN_D 0x20
#define SCAN_F 0x21
#define SCAN_G 0x22
#define SCAN_H 0x23
#define SCAN_J 0x24
#define SCAN_K 0x25
#define SCAN_L 0x26
#define SCAN_SEMICOLON 0x27
#define SCAN_APOSTROPHE 0x28
#define SCAN_ACCENT 0x29
#define SCAN_TILDE 0x29 // Duplicate of SCAN_ACCENT with popular Tilde name.
#define SCAN_LEFT_SHIFT 0x2A
#define SCAN_BACK_SLASH 0x2B
#define SCAN_Z 0x2C
#define SCAN_X 0x2D
#define SCAN_C 0x2E
#define SCAN_V 0x2F
#define SCAN_B 0x30
#define SCAN_N 0x31
#define SCAN_M 0x32
#define SCAN_COMMA 0x33
#define SCAN_PERIOD 0x34
#define SCAN_FORWARD_SLASH 0x35
#define SCAN_RIGHT_SHIFT 0x36
#define SCAN_KP_STAR 0x37
#define SCAN_KP_MULTIPLY 0x37 // Duplicate of SCAN_KP_STAR
#define SCAN_LEFT_ALT 0x38
#define SCAN_SPACE 0x39
#define SCAN_CAPS_LOCK 0x3A
#define SCAN_F1 0x3B
#define SCAN_F2 0x3C
#define SCAN_F3 0x3D
#define SCAN_F4 0x3E
#define SCAN_F5 0x3F
#define SCAN_F6 0x40
#define SCAN_F7 0x41
#define SCAN_F8 0x42
#define SCAN_F9 0x43
#define SCAN_F10 0x44
#define SCAN_NUM_LOCK 0x45
#define SCAN_SCROLL_LOCK 0x46
#define SCAN_KP_7 0x47
#define SCAN_KP_8 0x48
#define SCAN_KP_9 0x49
#define SCAN_KP_MINUS 0x4A
#define SCAN_KP_4 0x4B
#define SCAN_KP_5 0x4C
#define SCAN_KP_6 0x4D
#define SCAN_KP_PLUS 0x4E
#define SCAN_KP_1 0x4F
#define SCAN_KP_2 0x50
#define SCAN_KP_3 0x51
#define SCAN_KP_0 0x52
#define SCAN_KP_PERIOD 0x53
// Extended keys for the IBM 101-key Model M keyboard.
#define SCAN_RIGHT_ALT 0xE038
#define SCAN_RIGHT_CONTROL 0xE01D
#define SCAN_LEFT_ARROW 0xE04B
#define SCAN_RIGHT_ARROW 0xE04D
#define SCAN_UP_ARROW 0xE048
#define SCAN_DOWN_ARROW 0xE050
#define SCAN_NUMPAD_ENTER 0xE01C
#define SCAN_INSERT 0xE052
#define SCAN_DELETE 0xE053
#define SCAN_HOME 0xE047
#define SCAN_END 0xE04F
#define SCAN_PAGE_UP 0xE049
#define SCAN_PAGE_DOWN 0xE051
#define SCAN_KP_FORWARD_SLASH 0xE035
#define SCAN_PRINT_SCREEN 0xE02AE037
#endif // KEYBOARD_SCAN_CODES_H_INCLUDED