Считывание файла и ввод с консоли

Последнее обновление: 03.07.2023

Для считывания данных в 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>
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850