Статья ASM – ода о клавиатуре

Если компьютер не "вещь в себе", в нём должна быть заложена система взаимодействия с пользователем. Во всех активных микро/процессорных устройствах, для ввода информации предусмотрена клавиатура, которая представляет собой вполне самостоятельный мини/компьютер со-своим встроенным процессором. В данной статье рассматривается как внутреннее устройство этого интересного девайса, так и поддержка его на уровне BIOS и операционной системы Win. Кроме того уделяется внимание системной "очереди сообщений" – основного механизма взаимодействия с окнами.

Оглавление:

1. Введение;
2. Устройство клавиатуры

• задачи встроенного контролёра i8049;
• контролёр интерфейса на мат.плате i8242;
3. Системная поддержка;
• стек драйверов клавиатуры;
• очередь сообщений клавиатуры;
4. Практика;
5. Заключение.
------------------------------------------------------


1. Введение

Не секрет, что основными устройствами ввода информации в компьютер являются мышь и клавиатура. Впрочем первая – не обязательный аппендикс и была введена в строй гораздо позже, когда привычная консоль преобразилась в окна window's. В те времена, интерфейс клавиатуры был односторонним, т.е. информацию можно было передавать только к системному блоку. Позже, связь с центральным процессором стала полно-дуплексной, что позволило конечному пользователю настраивать такие атрибуты как задержка авто-повтора и скорость реакции на нажатие клавиш.

Keyboard – это устройство со встроенным контролёром i8049 (или более современным его аналогом MS-51), в следствии чего для своей работы требует лишь питания и шину обмена данными. Однако этот контролёр не может напрямую общаться с центральным процессором системы, поэтому на мат.плате предусмотрен ещё один ..обслуживающий исключительно "интерфейс клавиатуры" контролёр i8042, который с появлением мыши был унифицирован по типу "два-в-одном" i8242. На данный момент он встроен в чипсет и обслуживает сразу как клавиатуру, так и мышь. На рисунке ниже представлена общая схема подключения устройств ввода к центральному процессору:


8242.png


Как видим, любое нажатие клавиш или движение мышью, через внешний порт PS/2 доставляются контролёру интерфейса i8242 на материнской плате, который сразу подаёт сигнал IRQ контролёру прерываний i8259 (сейчас это APIC). Последний анализирует запрос 1 или 12, и посредством своего сигнала INTR (Request) оповещает об этом ЦП, на что процессор отвечает ему по линии подтверждения INTA (Acknowledge). Теперь APIC снимает INTR и процессор приступает к обработке возникшей ситуации. Это характер событий в грубой форме, в то время как за кулисами вращается более интересный алгоритм – рассмотрим его подробней..


2. Устройство клавиатуры

2.1. Задачи встроенного контролёра
i8049

Механическая часть клавиатуры оформлена в виде двумерной матрицы кнопок. Строки этой матрицы в непрерывном режиме сканирует встроенный в клавиатуру контролёр i8049. В момент нажатия нами какой-либо клавиши, с соответствующего столбца снимается и поступает на вход дешифратора логическая единица. Чтобы преобразовать этот сигнал в "скан-код", дешифратор считывает его номер с локальной ROM-памяти, где и прописаны 1-байтные коды в соответствии с расположением клавиш внутри матрицы. Другими словами скан-код – это просто порядковый номер пимпы на клавиатуре. Поскольку в одном байте можно закодировать 256 символов, этого вполне хватает не только для обычных, но и для мульти/медийных клавиатур.

Таким образом, на вход дешифратора поступает сигнал нажатой клавиши, а с его выхода снимается уже так-называемый "скан-код" – код сканирования матрицы. Контролёр клавиатуры генерит отдельные скан-коды при нажатии и при отпускании клавиш. В последнем случае получаем 2-байтный код, младший из которых остаётся прежним, а старший принимает значение F0h. Например если при нажатии клавиши(А) контролёр выдаёт скан-код 1Сh, то при её-же отпускании получим F01Сh. Список всех скан-кодов можно найти например в
.

На случай, когда пользователь печатает быстро, в контролёре i8049 предусмотрено буферное ОЗУ на 16-клавиш, от куда по интерфейсу PS/2 скан-коды прямиком доставляются в порт-данных(60h) контролёра i8242 на материнской плате. Графически и грязными красками это можно представить так:


MS51.gif


При нажатии и удержании не функциональных клавиш, логика контролёра переходит в режим авто-повтора. Чтобы была возможность регулировать скорость и задержку этого повтора перед началом, инженеры ввели двусторонний интерфейс. Теперь программируя i8242 на мат.плате, можно через порты 60/64h передавать в контролёр управляющие команды, или запрашивать его текущее состояние. Кстати эти рычаги заложены и в мастдаевское окно "свойств клавиатуры", в виде одноимённых слайдеров.


2.2. Контролёр интерфейса i8042 на материнской плате

Будем считать, что клавиатура доставила интерфейсному контролёру i8042 скан-код (порядковый номер) нажатой клавиши. Однако нашей программе нужен не номер клавиши, а указанный на ней ASCII-код символа. Причём этот код не всегда зависит от скан-кода, ведь одной и той же клавише может быть присвоено несколько значений, в зависимости от состояния функциональных клавиш Shift и Alt. Здесь-то и приступает к своим обязанностям контролёр-интерфейса i8042.

Преобразование скан-кода в ASCII выполняется исключительно программными средствами BIOS, а потому до появления более продвинутого EFI, символов кириллицы в реальном режиме не было – привычную нам RU-кодировку мы могли наблюдать только после загрузки операционной системы и её клавиатурного драйвера.

Основная задача интерфейсного контролёра i8042 – это принимать в свой порт 60h скан-коды от клавиатуры, и посредством сигнала IRQ-1 оповещать об этом процессор. Обнаружив, что это IRQ-1, ЦП прерывает свою работу и передаёт управление по вектору биос INT-9h, обработчик которого и выполняет всю черновую работу по преобразованию кода в ASCII (для этого биос имеет свои таблицы соответствий). По окончании, этот-же обработчик помещает 2-байтный результат своей работы в формате "Scan+Ascii" в клавиатурный буфер, от куда их может считать прикладная программа дёрнув прерывание INT-16h (сервис клавиатуры).

А как на счёт аккордов, в виде комбинаций клавиш с зажатыми Shift, Alt или "Caps-Lock" (регистр букв)?
Положение этих клавиш не имеют ASCII-кодов, поэтому в контролёре i8042 предусмотрен регистр-состояния клавиатуры, который отображается в специальной области памяти биос там-же, где и клавиатурный буфер – это самые нижние адреса физической памяти, в диапазоне 0417...043Dh. Если вы "счастливый" обладатель системы х32, то первый мегабайт физ.памяти можно просмотреть прямо под виндой, в отладчике debug.com. Тогда клавиатурный буфер и регистр её состояния будут выглядеть примерно так:

Keyb_Buff.png


Кольцевой FIFO-буфер клавиатуры имеет размер 32-байта и способен вместить 16 двубайтных кодов-клавиш в формате "Scan+Ascii". На голову и хвост этого буфера указывают два линка начиная со-смещения 041Ah – если они равны (как в данном случае 0036h), значит буфер пуст. На моей клавиатуре сейчас включён только "NumLock", поэтому в слове-состояния взведён лишь бит(5). Такие клавиши как "Pause, PrintScreen", а так-же комбинация "Ctrl+Alt+Del" вообще не отображаются в слове-состояния, и их налету перехватывает биос.


3. Системная поддержка в защищённом режиме

3.1. Стек драйверов клавиатуры


Ну а теперь поговорим о более возвышенных вещах: – "как организован буфер клавиатуры в защищённом режиме Win"? Это отдельная тема, не имеющая ничего общего с реальным режимом RM, где проблемой обработки нажатия клавиш озадачен BIOS. Такая смена общих правил требуется в первую очередь потому, что в защищённом режиме операционная система представляет собой многозадачную среду, и устройства ввода (типа мышь и клавиатура) становятся общим для всех программ, разделяемым ресурсом.

Значит клавиатурный ввод обслуживает уже драйвер-порта i8042prt.sys, после которого в иерархии стоит ещё и класс-драйвер kbdclass.sys – оба они образуют "стек драйверов" клавиатуры. В состав этого стека могут входить пользовательские фильтр-драйверы верхнего или нижнего уровня. Стек любого устройства отображает отладчик WinDbg, если потянуть за его расширение !drvobj с указанием нужного драйвера:

C-подобный:
lkd> !drvobj i8042prt
     Driver object (8b158c80) is for: \Driver\i8042prt
     Driver Extension List: (id, addr)

     Device Object list:  8b162978, 8b15c4f8

Судя по логу, данный драйвер обслуживает 2-объекта, которые лежат в памяти по адресам 0x8B162978 и 0x8B15C4F8. Под определение "объект устройства" (Device Object) попадает здесь не физ.устройство, а его драйвер – стек данного устройства состоит именно из этих/двух драйверов. Теперь зовём расширение !devobj и получаем инфу об этих девайсах:

C-подобный:
lkd> !devobj 8b162978
     Device object (8b162978) is for: \Driver\i8042prt, DriverObject 8b158c80
     Current Irp 00000000, RefCount 0, Type 00000027, Flags 00002004
     DevExt 8b162a30, DevObjExt 8b162cc0
     ExtensionFlags (0xb0000800)  DOE_RAW_FDO, DOE_DESIGNATED_FDO

     AttachedDevice (Upper) 8b162790 \Driver\mouclass   ;//<------- драйвер мыши
     AttachedTo     (Lower) 8987a5c8 \Driver\ACPI
     Device queue is not busy.
;//----------------------------

lkd> !devobj 8b15c4f8
     Device object (8b15c4f8) is for: \Driver\i8042prt, DriverObject 8b158c80
     Current Irp 00000000, RefCount 0, Type 00000027, Flags 00002004
     DevExt 8b15c5b0, DevObjExt 8b15c840
     ExtensionFlags (0xa0000800)  DOE_RAW_FDO, DOE_DESIGNATED_FDO

     AttachedDevice (Upper) 8b15c300 \Driver\kbdclass   ;//<------- драйвер клавиатуры
     AttachedTo     (Lower) 8b15c8a0 \Driver\RequestTrace
     Device queue is not busy.

Этот лист показывает, что стек клавиатуры состоит из трёх драйверов.
На самом нижнем уровне находится драйвер порта клавиатуры i8042prt.sys, к которому сверху (Upper) приаттачены два класс-драйвера – это драйвер мыши "mouclass.sys", и драйвер клавиатуры "kbdclass.sys". А вот сам i8042prt цепляется к ACPI-драйверу (AttachedTo Lower), что даём ему возможность нажатием клавиши или движением мыши управлять системным питанием и выводить машину из глубокого сна (ACPI, Advanced Configuration and Power Interface).


3.2. Очередь сообщений клавиатуры

В операционных системах класса Win нет такого понятия как клавиатурный буфер – его функции выполняет теперь "очередь клавиатурных сообщений". По сути, работа любой программы с графическим интерфейсом основана на беспрерывной обработке поступающих из-вне различных оконных сообщений. Если приложение много-поточное, то у каждого потока своя очередь и поток извлекает из неё поступившее сообщение, обрабатывает его, потом берёт следующее и так по замкнутому кругу, пока не будет получено последнее сообщение WM_QUIT, требующее прихлопнуть данный поток.

На этапе загрузки ОС, ядро создаёт системный процесс csrss.exe (см.диспетчер задач), что означает "Client-Server Runtime Subsystem". На моём узле Win-7 данный процесс порождает 11-дочерних потоков, одним из которых является обслуживающий системную "очередь-сообщений" поток RITRaw Input Thread (поток необработанного ввода). Сама очередь лежит в памяти и известна как SHIQSystem Hardware Input Queue. Как видно из названия, это очередь исключительно аппаратного Hardware ввода, а из таких аппаратных устройств мы имеем всего-лишь мышь, да клавиатуру. Другими словами, очередь SHIQ является временным боксом только для клавиатурных и мышиных сообщений, хотя в системе крутится огромный зоопарк и других (программных) мессаг. Визуализировать эту схему можно примерно так:


SHIQ.png


Большую часть времени, забросив удочку в SHIQ, поток RIT находится в спячке. Установленная им функция WaitForSingleObject() пробуждает его только когда по прерываниям IRQ-1(12) соответствующий класс-драйвер сбрасывает в эту очередь скан-код клавиатуры, или координаты мыши. Важным моментом является то, что в отличии от обработчика биос INT-9, виндовые драйвера не занимаются преобразованием scan-to-ascii, и в очередь сообщений SHIQ попадает всё тот-же (прочитанный с порта 60h) скан клавиатуры. Сняв с очереди, RIT превращает скан-код в виртуальную клавишу VK_хх, которую передаёт уже в локальную очередь VIQ активного потока.

Поскольку операционная система Windows не может одновременно работать сразу с несколькими окнами (активное только одно, а остальные уходят в фон), то RIT знает, в виртуальную очередь какого именно потока направлять запросы. То-есть к RIT может быть подключён только один какой-нибудь поток, что представлено на рисунке пунктирной линией.

Когда функцией CreateThread() мы создаём программный поток, ядро системы Ntoskrnl.exe привязывает к нему свою внутреннюю (недокументированную) структуру THREADINFO. В этой структуре имеются три указателя – на очередь аппаратных (клава+мышь) сообщений(1), на очередь асинхронных сообщений(2), и на очередь синхронных сообщений(3). Таким образом, виртуальная очередь потока VIQ включает в себя три независимых пула очередей, хотя на рисунке выше представлена только одна, аппаратная. Формат отдельно взятого сообщения в этих очередях одинаков для всех типов, и описывается структурой "MSG" с таким прототипом:


C-подобный:
struct MSG
  hwnd     dd 0     ;// дескриптор окна, которому адресовано сообщение
  message  dd 0     ;// идентификатор (код) сообщения
  wParam   dd 0     ;// параметр #1 сообщения (зависит от кода)
  lParam   dd 0     ;// параметр #2
  time     dd 0     ;// время, когда сообщение было поставлено в очередь
  pt       dd 0,0   ;// позиция X/Y курсора мыши в момент сообщения
ends

Здесь нужно прояснить, чем отличаются синхронные сообщения от асинхронных.
Во-первых это исключительно программные сообщения, которые посылает окну наш код, или сама система. Все они имеют тип WM, что означает Window Message (оконное сообщение). Если мы посылаем окну или его элементам-управления синхронную мессагу, то система не вернёт нам управление, пока не обработает её должным образом. Соответственно наш программный код будет вынужден простаивать в ожидании ответа. Чтобы ликвидировать подобные задержки, был введён асинхронный обмен, когда система ставит сообщение в очередь, и сразу-же возвращает нам ответ.

Таким образом нет никакой нужды собирать в очередь синхронные сообщения, поскольку они доставляются адресату сразу и следующее сообщение мы сможем отправить только после того-как система обработает предыдущую. Тогда зачем-же нужна в локальной VIQ очередь синхронных сообщений? Она предусмотрена на случай, когда из своего потока мы захотим отправить сообщение в чужое, неактивное на данный момент окно. В этом случае, сообщение попадёт именно в синхронную очередь удалённого потока и пролежит там до тех пор, пока этот поток не получит управление (т.е. пользователь не сменит окно). Зато все асинхронные сообщения всегда выстраиваются в очередь, заполняя свой буфер в VIQ.

В системе зарегистрировано порядка 1000 типов сообщений, под которые она забирает первые 400h кодов. Пользователь может устанавливать и свои, но только с кодами выше 400h. В инклуде фасма ..\equates\user32.inc имеется список констант знакомых компилятору сообщений, а по
можно ознакомиться с их описанием. Вот некоторые из распространённых сообщений, которые наиболее часто встречаются в процедурах обработки окна. Здесь в столбце "Очередь" указано, помещается или нет сообщение в системную очередь VIQ:

WM.png


Для работы с очередями сообщений в составе Win32-API имеются специальные функции – в большинстве случаях достаточно только четырёх:

C-подобный:
GetMessage ()  – чтение сообщений из  синхронной очереди;
PeekMessage () – чтение сообщений из асинхронной очереди;

SendMessage () – запись сообщений в  синхронную очередь чужого потока;
PostMessage () – запись сообщений в асинхронную очередь;
;//----------------------------------------------------

BOOL GetMessage (
  lpMsg          ;// адрес структуры MSG для получения сообщения
  hWnd           ;// дескриптор окна, если NULL то извлекаются сообщения всех окон данного приложения
  wMsgFilterMin, ;// мин.код извлекаемого сообщения
  wMsgFilterMax  ;// макс.код извлекаемого сообщения
);

BOOL PeekMessage (
  lpMsg          ;// адрес структуры MSG для получения сообщения
  hWnd           ;// дескриптор окна, если NULL то извлекаются сообщения всех окон данного приложения
  wMsgFilterMin, ;// мин.код извлекаемого сообщения (используется для фильтрации сообщений)
  wMsgFilterMax  ;// макс.код извлекаемого сообщения
  wRemoveMsg     ;// как обрабатывать: 
);               ;//   PM_NOREMOVE(0) = не удалять из очереди, PM_REMOVE(1) = удалить.

LRESULT Send/ PostMessage (
   hWnd          ;// handle окна, которому посылается сообщение
   Msg           ;// код посылаемого сообщения
   wParam        ;// параметры сообщения
   lParam        ;// ^^^ (зависят от кода)
);


4. Практика

В качестве демонстрационного примера посмотрим, в каком виде хранится сообщение от клавиатуры в виртуальной очереди потока VIQ. Для этого, при помощи функции PostMessage() в цикле будем отправлять окну рабочему стола (с дескриптором нуль) мессагу от клавиатуры с кодом WM_KEYDOWN, после чего сразу-же изымать записанное сообщение из очереди функцией PeekMessage() и выводить его на консоль (см.структуру MSG). Здесь есть несколько нюансов, которые требуют пояснения.

Как видно выше из прототипа PostMessage(), в аргументах ей нужно передать код сообщения – в нашем случае это будет WM_KEYDOWN с константой 100h. Так-как это сообщение от клавиатуры, то в аргумент wParam нужно поместить код виртуальной клавиши (например 'A', или VK_F5), а в аргументе lParam необходимо уточнить его. Таблица ниже описывает биты этого аргумента:


lParam.png


В идеале, чтобы проэмулировать нажатие какой-либо клавиши, мы должны посылать окну сразу два кода – нажатия(Down) и отпускания(Up). Но в данном случае наше сообщение никто не будет обрабатывать, а потому будем отправлять только код-нажатия WM_KEYDOWN. Счётчик повторов в аргументе lParam ставим на 1, а скан-код виртуальной клавиши VK получим при помощи функции MapVirtualKey(). Поскольку это нажатие, то биты 30:31 взводить не нужно – в единицу их выставляют при посылке кода-отпускания WM_KEYUP. Все эти биты удобно определять при помощи операций сдвига SHL, и логического сложения OR. Вот как это выглядит на практике:

C-подобный:
format   pe console
include 'win32ax.inc'
entry    start
;//--------
.data
msg      MSG            ;// структура MSG
wParam   db  'A',0,0,0  ;// виртуальная клавиша для посылки (в цикле будем менять)
lParam   dd  0          ;// под аргумент 'lParam'
cycle    dd  3          ;// кол-во повторов чтений
buff     db  0          ;//
;//--------

.code
start:  invoke   SetConsoleTitle,<'Message Queue v0.1',0>

;//--- Оформляем аргумент 'lParam'
@prnQueue:
        invoke   MapVirtualKey,dword[wParam],0  ;// EAX = скан-код клавиши
        shl      eax,16           ;// сдигаем его на 16-влево (биты 16:23)
        or       eax,1            ;// счётчик авто-повторов (биты 00:15)
;//        or       eax,0xC0000000  ;// для кода отпускания WM_KEYUP
        mov      [lParam],eax     ;// сохранить EAX как 'lParam'

;//--- Отправить сообщение в асинхронную очередь!
       cinvoke   printf,<10,'PostMessage --> WM_KEYDOWN virtual key %s',0>,wParam
        invoke   PostMessage,0,WM_KEYDOWN,dword[wParam],[lParam]

;//--- Сразу читаем и выводим структуру 'MSG' на консоль
        call     PrintMessageQueue
        invoke   Sleep,2000       ;// задержка 2-сек..
        inc      [wParam]         ;// сл.виртуальный код VK_**
        dec      [cycle]          ;// уменьшить счётчик вывода
        jnz      @prnQueue        ;// повторить, если счётчик не нуль..

       cinvoke   gets,buff        ;// выход!
        invoke   exit,0           ;//
;//--------------------
;//---- Аргументы PeekMessage(): -----------------------------
;// msg = адрес структуры, 0 = дескриптор раб.стола,
;// 0,0 = нет фильтрации, т.е. читать все сообщения из очереди
;// PM_REMOVE = удалить прочитанное сообщение из очереди.
proc    PrintMessageQueue
        nop
        invoke   PeekMessage,msg,0,0,0,PM_REMOVE
       cinvoke   printf,<10,' Handle...: %04X',\
                         10,' Message..: %04X',\
                         10,' wParam...: %04X',\
                         10,' lParam...: %08X',\
                         10,' Time.....: %u ms',\
                         10,' Mouse....: X=%04X, Y=%04X',10,0>,\
                         [msg.hwnd],[msg.message],[msg.wParam],\
                         [msg.lParam],[msg.time],dword[msg.pt],dword[msg.pt+4]
        ret
endp
;//--------------------
section '.idata' import data readable writeable
library  msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',user32,'user32.dll'
import   msvcrt, printf,'printf',scanf,'scanf',gets,'gets',exit,'exit'
include 'api\kernel32.inc'
include 'api\user32.inc'


WM_KEYDOWN.png


Значит на скрине выше видим содержимое структуры MSG, где код-сообщения в поле 'Message' определён как 0х100 = WM_KEYDOWN. Дальше в поле 'wParam' наблюдаем код виртуальной клавиши со-значение 0х41 = 'A'. В старшем слове аргумента 'lParam' лежит скан-код данной клавиши, а в мл.слове количество авто-повторов =1. Поле 'Time' хранит время, когда сообщение было помещено нами в очередь – оно исчисляется в мили/секундах, с момента последней перезагрузки. Ну и в последнее поле 'Mouse' система сбрасывает координаты мыши по горизонтали(Х) и вертикали(Y).

Перечисленные выше функции Win32-API – это всего лишь малая капля в океане обслуживающих клавиатуру системных функций. С наиболее исчерпывающим списком можно ознакомиться на MSDN или в переводе его от энтузиастов, например
.


5. Заключение

Модель сообщений Win это то, с чего начинают обучение программированию окон. Поскольку они распространяются на всю систему, то становятся оружием ближнего боя хакеров. Например ещё во-времена бородатой Win-2000, в широком обиходе был способ инжекта шелл-кодов на основе именно сообщений. Если в двух словах, то поиском контрола "Edit-Box" (поле ввода) выбиралось уязвимое приложение. Дальше, посылкой сообщения EM_SETLIMITTEXT (EM=Edit_Message), буфер этого контрола расширялся до размеров шелл-кода, и при помощи сообщения WM_PASTE код инжектился в пространство чужого приложения. На заключительном этапе, шелл получал управление через сообщение WM_TIMER, во-втором аргументе 'lParam' которого необходимо указать адрес Callback-функции при срабатывании таймера, т.е. непосредственно адрес шелл-кода. Сам вредоносный код копировался в буфер при помощи двойки OpenClipboard() + SetClipboardData().

Более того, на основе перехвата оконных сообщений строятся и различного рода кейлоггеры, и прочие шпионы. Это огромное поле для деятельности, хотя и требует определённого уровня знаний внутренней архитектуры Win. Почитать на эту тему можно Д.Рихтера "Windows для профессионалов" – единственное найденное мной детальное освещение данного материала. Кое-что доступным языком излагает и А.Григорьев в своём труде "О чём не пишут в книгах по DELPHI". Возможно есть и другая литература, но для понимания основ этого вполне хватит, а дальше – кругозор расширяет только практика с отладчиком. Всем удачи и до скорого!
 

xornor

New member
24.07.2021
2
0
BIT
0
Не совсем понимаю что определяет значение авто-повтора. Оно увеличивается если одна и та же клавиша нажата несколько раз?
 

xornor

New member
24.07.2021
2
0
BIT
0
а по можно ознакомиться с их описанием.
А еще у вас мертвый линк
что даём ему возможность нажатием клавиши или движением мыши управлять системным питанием
И небольшая опечатка в тексте
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!