Для считывания данных в WinAPI применяется функция ReadFile, которая имеет следующее определение на языке C++:
BOOL ReadFile( [in] HANDLE hFile, [out] LPVOID lpBuffer, [in] DWORD nNumberOfBytesToRead, [out, optional] LPDWORD lpNumberOfBytesRead, [in, out, optional] LPOVERLAPPED lpOverlapped );
Она принимает следующие параметры:
hFile
: дескриптор файла для считывания, передается через регистр RCX
lpBuffer
: буфер, который получает считываемые данные, передается через регистр RDX
nNumberOfBytesToRead
: максимальное число байтов для чтения., передается через регистр R8
lpNumberOfBytesRead
: адрес переменной, которая получает количество считанных байтов, передается через регистр R9
lpOverlapped
: обычно имеет значение NULL (0), передается через стек по адресу [rsp + 32]
После выполнения функция ReadFile
помещает в регистр RAX ненулевое значение, если запись прошла успешно, и ноль, если произошла ошибка.
Считаем данные с консоли и для этого определим следующую программу:
includelib kernel32.lib ; подключаем библиотеку kernel32.lib ; подключаем функции ReadFile и GetStdHandle extrn GetStdHandle: PROC extrn ReadFile: PROC .data buffer byte 200 dup (?) ; буфер для считывания данных len = $ - buffer bytesRead qword ? ; количество считанных данных .code main proc sub rsp, 40 ; 32 байта (shadow storage) + 8 байт (5-й параметр функции ReadFile) mov rcx, -10 ; Аргумент для GetStdHandle - STD_INPUT call GetStdHandle ; вызываем функцию GetStdHandle mov rcx, rax ; Первый параметр ReadFile - дескриптор файла lea rdx, buffer ; Второй параметр ReadFile - адрес буфера mov r8, len ;Третий параметр ReadFile - размер буфера lea r9, bytesRead ; Четвертый параметр ReadFile - количество считанных байтов mov qword ptr [rsp + 32], 0 ; Пятый параметр ReadFile - 0 call ReadFile ; вызов функции RaedFile test rax, rax ; проверяем на ошибку mov rax, bytesRead ; если ошибки нет, в RAX количество считанных байтов jnz exit ; если в RAX ненулевое значение mov rax, -1 ; помещаем условный код ошибки exit: add rsp, 40 ret main endp end
При считывании с консоли ввода пользователя нам надо получить дескриптор стандартного ввода. Для этого функции GetStdHandle
число -10. После вызова этой функции в RAX
будет дескриптор консольного ввода.
Полученный дескриптор передаем через регистр RCX в функцию ReadFile. Для считывания данных в RDX загружаем адрес переменной buffer, в которую будет считываться ввод пользователя. В регистр R8 помещается размер этого буфера. А в R9 загружается адрес переменной bytesRead для хранения реально считанных байтов, так как пользователь может ввести всего 1-2 символа, что будут меньше размера буфера.
После вызова функции ReadFile
инструкцией test
проверяем результат функции из регистра RAX. Если он не равен нулю (то есть функция ReadFile выполнена успешно),
то в RAX помещаем количество реально считанных байтов. Если результат функции - 0, то в RAX помещаем условный код ошибки - число -1.
Пример компиляции и работы программы (в моем случае программа располагается в файле hello.asm):
c:\asm>ml64 hello.asm /link /entry:main Microsoft (R) Macro Assembler (x64) Version 14.36.32532.0 Copyright (C) Microsoft Corporation. All rights reserved. Assembling: hello.asm Microsoft (R) Incremental Linker Version 14.36.32532.0 Copyright (C) Microsoft Corporation. All rights reserved. /OUT:hello.exe hello.obj /entry:main c:\asm>hello hello c:\asm>echo %ERRORLEVEL% 7 c:\asm>
В консольном выводе выше видно, что я ввожу текст "hello", однако реально считанное количество равно 7, поскольку при завершении ввода в строку добавляются символы "/r/n" (перевол каретки и перевод строки).
Мы можем пойти дальше и вынести считывание данных в отдельную процедуру, отделив от взаимодействия с консолью:
includelib kernel32.lib ; подключаем библиотеку kernel32.lib ; подключаем функции ReadFile и GetStdHandle extrn GetStdHandle: PROC extrn ReadFile: PROC .data buffer byte 200 dup (?) ; буфер дляссчитывания данных len = $ - buffer .code ; Процедура для считывания данных ; Параметры: ; RAX - дескриптор файла, ; RDI - буфер для считывания ; RCX - длина буфера ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка read proc sub rsp, 56 ; 32 байта (shadow storage) + 8 байт (5-й параметр ReadFile) + 8 байт (bytesRead) + 8 байт (выравнивание) mov r8, rcx ;Третий параметр ReadFile - размер буфера mov rcx, rax ; Первый параметр ReadFile - дескриптор файла mov rdx, rdi ; Второй параметр ReadFile - адрес буфера lea r9, bytesRead ; Четвертый параметр ReadFile - количество считанных байтов mov qword ptr [rsp + 32], 0 ; Пятый параметр ReadFile - 0 call ReadFile ; вызов функции RaedFile test rax, rax ; проверяем на ошибку mov eax, bytesRead ; если ошибки нет, в RAX количество считанных байтов jnz exit ; если в RAX ненулевое значение mov rax, -1 ; помещаем условный код ошибки exit: add rsp, 56 ret bytesRead equ [rsp+40] ; область в стеке для хранения количества считанных байтов read endp ; Процедура для считывания данных с консоли ; Параметры: ; RDI - буфер для считывания ; RCX - длина буфера ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка readFromConsole proc sub rsp, 32 push rcx ; сохраняем количество символов mov rcx, -10 ; Аргумент для GetStdHandle - STD_INPUT call GetStdHandle ; вызываем функцию GetStdHandle pop rcx call read add rsp, 32 ret readFromConsole endp main proc lea rdi, buffer ; буфер для считывания данных mov rcx, len ; размер буфера call readFromConsole ; считываем данные с консоли ret main endp end
В данном случае процедура считывания данных - read получает через регистр RAX дескриптор файла (в данном случае дескриптор стандартного ввода), через регистр RDI буфер для считывания данных и через RCX - максимальное количество считываемых символов. И также возвращает либо количество считанных символов, либо число -1 - условный код ошибки.
Процедура readFromConsole получает дескриптор консольного ввода и вызывает процедуру read для считывания с консоли.
Правда стоит отметить, что это не самый оптимальный способ ввода с клавиатуры - если нам предстоит несколько раз вводить данные с консоли, то каждый раз придется вызывать функцию GetStdHandle для получения дескриптора консольного ввода. Вместо этого мы можем сохранить дескриптор в переменной и затем многократно его использовать:
includelib kernel32.lib ; подключаем библиотеку kernel32.lib ; подключаем функции ReadFile и GetStdHandle extrn GetStdHandle: proc extrn ReadFile: proc extrn WriteFile: proc .data buffer byte 200 dup (?) ; буфер для считывания данных len = $ - buffer stdin qword 0 ; для дескриптора консольного ввода stdinSet byte 0 ; был ли установлен дескриптор консольного ввода .code ; Процедура для считывания данных ; Параметры: ; RAX - дескриптор файла, ; RDI - буфер для считывания ; RCX - длина буфера ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка read proc sub rsp, 56 mov r8, rcx ;Третий параметр ReadFile - размер буфера mov rcx, rax ; Первый параметр ReadFile - дескриптор файла mov rdx, rdi ; Второй параметр ReadFile - адрес буфера lea r9, bytesRead ; Четвертый параметр ReadFile - количество считанных байтов mov qword ptr [rsp + 32], 0 ; Пятый параметр ReadFile - 0 call ReadFile ; вызов функции RaedFile test rax, rax ; проверяем на ошибку mov eax, bytesRead ; если ошибки нет, в RAX количество считанных байтов jnz exit ; если в RAX ненулевое значение mov rax, -1 ; помещаем условный код ошибки exit: add rsp, 56 ret bytesRead equ [rsp+40] ; область в стеке для хранения количества считанных байтов read endp ; Процедура для считывания данных с консоли ; Параметры: ; RDI - буфер для считывания ; RCX - длина буфера ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка readFromConsole proc cmp stdinSet, 1 ; если дескриптор стандартного ввода установлен jz readData ; то переходим к считыванию данных sub rsp, 32 push rcx ; сохраняем количество символов mov rcx, -10 ; Аргумент для GetStdHandle - STD_INPUT call GetStdHandle ; вызываем функцию GetStdHandle mov stdin, rax ; сохраняем дескриптор стандартного ввода mov stdinSet, 1 pop rcx add rsp, 32 readData: mov rax, stdin call read ret readFromConsole endp main proc lea rdi, buffer ; буфер для считывания данных mov rcx, len ; размер буфера call readFromConsole ; считываем данные с консоли ret main endp end
Здесь функция GetStdHandle
вызывается один раз, после чего дескриптор консольного ввода сохраняется в переменной stdin
и устанавливается переменная stdinSet
.
Далее используя этот дескриптор, можно многократно выводить данные на консоль.
Нередко возникает задача считать один символ. Рассмотрим, как это сделать:
includelib kernel32.lib extrn GetStdHandle: proc extrn ReadFile: proc .data buffer byte 200 dup (?) ; буфер для считывания данных len = $ - buffer stdin qword 0 ; для хранения дескриптора консольного ввода stdinSet byte 0 ; индикатор, что дескриптор консольного ввода установлен .code ; Процедура для считывания данных ; Параметры: ; RAX - дескриптор файла, ; RDI - буфер для считывания ; RCX - длина буфера ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка read proc sub rsp, 56 mov r8, rcx ;Третий параметр ReadFile - размер буфера mov rcx, rax ; Первый параметр ReadFile - дескриптор файла mov rdx, rdi ; Второй параметр ReadFile - адрес буфера lea r9, bytesRead ; Четвертый параметр ReadFile - количество считанных байтов mov qword ptr [rsp + 32], 0 ; Пятый параметр ReadFile - 0 call ReadFile ; вызов функции RaedFile test rax, rax ; проверяем на ошибку mov eax, bytesRead ; если ошибки нет, в RAX количество считанных байтов jnz exit ; если в RAX ненулевое значение mov rax, -1 ; помещаем условный код ошибки exit: add rsp, 56 ret bytesRead equ [rsp+40] ; область в стеке для хранения количества считанных байтов read endp ; Процедура для считывания данных с консоли ; Параметры: ; RDI - буфер для считывания ; RCX - длина буфера ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка readFromConsole proc cmp stdinSet, 1 ; если дескриптор стандартного ввода установлен jz readData ; то переходим к считыванию данных sub rsp, 32 push rcx ; сохраняем количество символов mov rcx, -10 ; Аргумент для GetStdHandle - STD_INPUT call GetStdHandle ; вызываем функцию GetStdHandle mov stdin, rax ; сохраняем дескриптор стандартного ввода mov stdinSet, 1 pop rcx add rsp, 32 readData: mov rax, stdin call read ret readFromConsole endp ; Процедура считывания одного символа с консоли ; Результат: в AL код считанного символа readChar proc sub rsp, 8 ; выделяем 8 байт для считывания символа mov rdi, rsp ; в качестве буфера будет использоваться область в стеке mov rcx, 1 ; считываем 1 байт call readFromConsole movzx rax, byte ptr [rsp] ; В RAX (точнее в AL) помещаем считанный символ add rsp, 8 ret readChar endp main proc call readChar ; считываем символ ret main endp end
Здесь символ считывается процедурой readChar. Эта процедура выделяет в стеке 8 байт в качестве буфера для считывания данных (хотя в реальности нам достаточно 1 байта для хранения символа). Затем для считывания символа с консоли вызываем ранее рассмотренную процедуру readFromConsole. После выполнения процедуры помещаем символ из буфера (в стеке) в регистр AL. Пример работы программы:
c:\asm>hello.exe A c:\asm>echo %ERRORLEVEL% 65 c:\asm>
В данном случае был введен символ "A", соответственно в регистре AL был помещен код данного символа - число 65.
Как выше было указано, при вводе в буфер автоматически добавляются символы возврата каретки и перевода строки /r/n, которые имеют в такблице ASCII коды 13 и 10 соответственно. То есть мы вводим 2 символа, а в буфер записываются 4 символа. Это может быть не очень удобно. Кроме того, возможно, мы захотим, чтобы введенная строка заканчивалась каким-нибудь концевым признаком, например, нулевым байтом, как строки в языке C. Для этого мы можем использовать выше описанную процедуру для считывания символа - считывать по одному символу и проверять его код. Если код представляет обычный символ (не возврат каретки или перевод строки), то добавлять буфер. Возможная реализация:
includelib kernel32.lib ; подключаем библиотеку kernel32.lib ; подключаем функции ReadFile и GetStdHandle extrn GetStdHandle: proc extrn ReadFile: proc .data buffer byte 200 dup (?) ; буфер для считывания данных len = $ - buffer stdin qword 0 ; для хранения дескриптора консольного ввода stdinSet byte 0 ; индикатор, что дескриптор консольного ввода установлен .code ; Процедура для считывания данных ; Параметры: ; RAX - дескриптор файла, ; RDI - буфер для считывания ; RCX - длина буфера ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка read proc sub rsp, 56 mov r8, rcx ;Третий параметр ReadFile - размер буфера mov rcx, rax ; Первый параметр ReadFile - дескриптор файла mov rdx, rdi ; Второй параметр ReadFile - адрес буфера lea r9, bytesRead ; Четвертый параметр ReadFile - количество считанных байтов mov qword ptr [rsp + 32], 0 ; Пятый параметр ReadFile - 0 call ReadFile ; вызов функции RaedFile test rax, rax ; проверяем на ошибку mov eax, bytesRead ; если ошибки нет, в RAX количество считанных байтов jnz exit ; если в RAX ненулевое значение mov rax, -1 ; помещаем условный код ошибки exit: add rsp, 56 ret bytesRead equ [rsp+40] ; область в стеке для хранения количества считанных байтов read endp ; Процедура для считывания данных с консоли ; Параметры: ; RDI - буфер для считывания ; RCX - длина буфера ; Результат - в RAX количество записанных байтов или -1, если произошла ошибка readFromConsole proc cmp stdinSet, 1 ; если дескриптор стандартного ввода установлен jz readData ; то переходим к считыванию данных sub rsp, 32 push rcx ; сохраняем количество символов mov rcx, -10 ; Аргумент для GetStdHandle - STD_INPUT call GetStdHandle ; вызываем функцию GetStdHandle mov stdin, rax ; сохраняем дескриптор стандартного ввода mov stdinSet, 1 pop rcx add rsp, 32 readData: mov rax, stdin call read ret readFromConsole endp ; Процедура считывания одного символа с консоли ; Результат: в AL код считанного символа readChar proc push rcx push rdi sub rsp, 8 ; выделяем 8 байт для считывания символа mov rdi, rsp ; в качестве буфера будет использоваться область в стеке mov rcx, 1 ; считываем 1 байт call readFromConsole movzx rax, byte ptr [rsp] ; В RAX (точнее в AL) помещаем считанный символ add rsp, 8 pop rdi pop rcx ret readChar endp ; Процедура для считывания строки текста ; Параметры ; RDI - буфер для считывания данных ; RCX - размер буфера ; Результат: в RAX - количество считанных символов readLine proc xor rbx, rbx ; счетчик символов test rcx, rcx ; проверяем, что размер буфера больше 0 je exit ; если размер буфера - 0, выходим из процедуры dec rcx ; уменьшаем на 1, чтобы потом добавить нулевой байт while_start: cmp rbx, rcx ; проверяем, что буфер не заполнен jz while_end ; если RBX = RCX, то выходим из цикла call readChar ; если буфер не запонен, считываем 1 символ test eax, eax ; проверяем, что был считан 1 символ jz while_end ; если считано 0 симвлов, выходим из цикла cmp al, 13 ; если нажата клавиша Enter (символ /r) je while_end ; выходим из цикла cmp al, 10 ; проверяем на перевод строки - символ /n je while_end ; выходим из цикла cmp al, 8 ; проверяем нажатие клавиша backspace jne addChar ; если она не нажата, добавляем новый символ в буфер ; если нажата клавиша backspace, удаляем предыдущий символ test rbx, rbx ; проверяем, что введено не 0 символов jz while_start ; если в буфере нет символов, переходим к вводу нового символа dec rbx ; если в буфере есть символы, уменьшаем счетчик - как бы сдвигаем ввод назад jmp while_start ; и переходим к вводу нового символа ; Если обычный символ, добавляем символ в буфер addChar: mov [rdi][rbx], al ; сохраняем символ в буфер inc rbx ; инкрементируем счетчик jmp while_start ; переходим к вводу нового символа ; если пользователь нажал на ENTER, добавляем к строке нулевой байт while_end: mov byte ptr [rdi][rbx], 0 exit: mov rax, rbx ; в RAX количество считанных символов ret readLine endp main proc lea rdi, buffer mov rcx, len call readLine ret main endp end
Для считывания строки с консоли определена процедура readLine
. Через RDI она получает буфер для считывания данных, а через RCX - размер буфера.
Вначале в этой процедуре устанавливается счетчик считанных символов - в регистре RBX, а также уменьшается на 1 значение в RCX, чтобы выделить место для нулевого байта. Кроме того, проверяем размер буфера, что он не равен 0:
xor rbx, rbx ; счетчик символов test rcx, rcx ; проверяем, что размер буфера больше 0 je exit ; если размер буфера - 0, выходим из процедуры dec rcx ; уменьшаем на 1, чтобы потом добавить нулевой байт
Затем в цикле проверяем количество считанных символов - если оно равно размеру буфера, то прекращаем считывание символов и выходим из цикла:
while_start: cmp rbx, rcx ; проверяем, что буфер не заполнен jz while_end ; если RBX = RCX, то выходим из цикла call readChar ; если буфер не запонен, считываем 1 символ test eax, eax ; проверяем, что был считан 1 символ jz while_end ; если считано 0 симвлов, выходим из цикла cmp al, 13 ; если нажата клавиша Enter (символ /r) je while_end ; выходим из цикла cmp al, 10 ; проверяем на перевод строки - символ /n je while_end ; выходим из цикла cmp al, 8 ; проверяем нажатие клавиша backspace jne addChar ; если она не нажата, добавляем новый символ в буфер
Также проверяем нажатие на Enter, и в этом случае также выходим из цикла ввода символов.
Если нажата клавиша Backspace (то есть идет удаление введенного символа) и если ранее были уже введены символы, то уменьшаем счетчик символов в RBX на 1, то есть формально сдвигаемся в буфере для ввода нового символа на 1 байт назад:
test rbx, rbx ; проверяем, что введено не 0 символов jz while_start ; если в буфере нет символов, переходим к вводу нового символа dec rbx ; если в буфере есть символы, уменьшаем счетчик - как бы сдвигаем ввод назад jmp while_start ; и переходим к вводу нового символа
Если введен обычный символ, то добавляем его в буфер, увеличиваем счетчик символов RBX и переходим к вводу нового символа
addChar: mov [rdi][rbx], al ; сохраняем символ в буфер inc rbx ; инкрементируем счетчик jmp while_start ; переходим к вводу нового символа
Если мы достигли конца буфера или нажата клавиша Enter, то добавляем после последнего введенного символа нулевой байт, а количество введенных символов помещаем в регистр RAX - своего рода результат процедуры:
while_end: mov byte ptr [rdi][rbx], 0 exit: mov rax, rbx ; в RAX количество считанных символов
Пример работы программы:
c:\asm>hello Hello c:\asm>echo %ERRORLEVEL% 5 c:\asm>