Чтение текстового файла

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

Для чтения данных мы можем использовать функцию ReadFile, которая рассматривалась на примерах консольного ввода. Только в данном случае ввод будет идти из файла, для чего функции ReadFile передается дескриптор открытого файла. Допустим, нам надо считать файл test.txt, который имеет следующее содержимое:

Hello METANIT.COM1
Hello METANIT.COM2
Hello METANIT.COM3
Hello METANIT.COM4
Hello METANIT.COM5

Для считавания этого файла и вывода считанных данных на консоль определим следующую программу:

includelib kernel32.lib  

extrn GetStdHandle: proc
extrn ReadFile: proc
extrn WriteFile: proc
extrn CreateFileA: proc
extrn CloseHandle: proc

.data
buffer byte 32 dup (?)      ; буфер для чтения
bufferLen = $ - buffer      ; длина буфера
filename byte "test.txt"  ; имя файла

fHandle qword 0  ; дескриптор файла

stdout qword 0  ; дескриптор консольного вывода
stdoutSet byte 0  ; установлен ли дескриптор консольного вывода

.code

; Параметры:
; RSI - адрес строки
; RCX - длина строки
; RAX - дескриптор файла
; Результат - через RAX возвращаем количество записанных байтов
write proc
  sub  rsp, 56
  mov rdx, rsi          ; Второй параметр - строка
  mov r8, rcx           ; Третий параметр - длина строки
  mov  rcx, rax         ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консольного вывода
  lea  r9, bytesWritten       ; Четвертый параметр WriteFile - адрес для получения записанных байтов
  mov qword ptr [rsp + 32], 0  ; Пятый параметр WriteFile
  call WriteFile
    
  test rax, rax ; проверяем на наличие ошибки
  mov eax, bytesWritten ; если все нормально, помещаем в RAX количество записанных байтов
  jnz exit 
  mov rax, -1 ; Возвращаем через RAX код ошибки
exit:
  add  rsp, 56
  ret
bytesWritten equ [rsp+40]
write endp

; Процедура вывода произвольной строки на консоль 
; Параметры
; RSI - адрес строки
; RCX - количество символов
; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
writeToConsole proc
  cmp stdoutSet, 1
  jz writeData    ; если дескриптор консольного вывода установлен
  sub rsp, 32
  push rcx            ; сохраняем количество символов
  mov rcx, -11         ; Аргумент для GetStdHandle - STD_OUTPUT
  call GetStdHandle     ; вызываем функцию GetStdHandle
  pop rcx         ; восстанавливаем RCX - количество символов
  add rsp, 32
  mov stdout, rax   ; помещаем в stdout дескриптор консольного вывода
  mov stdoutSet, 1  ; дескриптор консольного вывода установлен
writeData:
  mov rax, stdout
  call write
  ret
writeToConsole endp

; Процедура для считывания данных
; Параметры: 
; 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       ; вызов функции ReadFile
  test rax, rax       ; проверяем на ошибку
  mov eax, bytesRead  ; если ошибки нет, в RAX количество считанных байтов
  jnz exit          ; если в RAX ненулевое значение
  mov rax, -1       ; помещаем -1
exit:
  add rsp, 56
  ret
bytesRead equ [rsp+40] ; область в стеке для хранения количества считанных байтов
read endp

; Процедура открытия существующего файла
; Параметр - в RSI - имя файла
; Результат - в RAX - дескриптор открытого файла
openFile proc
  sub rsp, 56
  mov rcx, rsi ; имя файла
  mov rdx, 0C0000000h ; доступ на чтение
  xor r8, r8 ; Эксклюзивный доступ
  xor r9, r9 ; атрибуты безопасности отсутствуют
  mov r10, 4 ; открыть существующий файл, если он не существует - создать
  mov [rsp + 32], r10
  mov r10, 128  ; обычный файл без атрибутов
  mov [rsp + 40], r10 
  mov [rsp + 48], r9    ; NULL - шаблон не используется
  call CreateFileA

  add rsp, 56
  ret
openFile endp

; Процедура закрытия существующего файла
; Параметр - в RAX - дескриптор открытого файла
; Результат - в RAX - состояние операции (ошибка или нет)
closeFile proc
  sub rsp, 40
  mov rcx, rax      ; В RCX помещаем дескриптор файла
  call CloseHandle
  add rsp, 40
  ret
closeFile endp

main proc
  lea rsi, filename
  call openFile   ; открываем файл
  mov fHandle, rax        ; сохраняем дескриптор файла
  
  mov rax, fHandle  ; в RAX дескриптор файла
  lea rdi, buffer
  mov rcx, bufferLen
  call read       ; считываем за раз bufferLen байт в массив buffer

  mov rcx, rax    ; далее передаем считанное количество байт для вывода на консоль
  lea rsi, buffer
  call writeToConsole ; выводим прочитанные данные на консоль

  mov rax, fHandle  ; в RAX дескриптор файла
  call closeFile  ; закрываем файл
  ret
main endp
end

В процедуре main вызываем процедуру openFile, которая в свою очередь использует функцию CreateFileA для открытия файла и получения его дескриптора. Полученный дескриптор файла сохраняется в переменную fHandle.

Далее считываем в процедуре read bufferLen байт в массив buffer. Процедура read получает через регистр RAX дескриптор файла и вызывает функцию ReadFile для считывания данных.

После этого данные считаны в buffer, а в регистре RAX содержится количество считанных байт. Для вывода считанных символов на консоль вызываем процедуру writeToConsole, которая в свою очередь получает дескриптор консольного вывода с помощью функции GetStdHandle для собственно вывода данных вызывает процедуру write. Процедура write для вывода данных получает дескриптор консольного вывода и вызывает функцию WriteFile.

После завершения считывания вызываем процедуру closeFile для закрытия файла.

Но в данном случае мы сталкиваемся с проблемой. Если мы скомпилируем и запустим приложение, то увидим, что файл считывается не полностью:

c:\asm>hello
Hello METANIT.COM1
Hello METANIT
c:\asm>

Мы имеем ограниченный размер буфера (в примере выше - 32 байта), однако мы не знаем точный размер файла. Вполне возможно он будет гораздо больше размера буфера. В этом случае мы можем считывать часть файла в буфер в цикле, пока не считаем весь файл. Для этого изменим процедуру main следующим образом:

main proc
  lea rsi, filename
  call openFile   ; открываем файл
  mov fHandle, rax        ; сохраняем дескриптор файла
  ; считываем в цикле за раз bufferLen байт 
readLoop: 
  mov rax, fHandle  ; в RAX дескриптор файла
  lea rdi, buffer
  mov rcx, bufferLen
  call read       ; считываем bufferLen байт в buffer

  test eax, eax   ; проверяем количество считанных байт
  jz done         ; если считано 0 байт, завершаем считывание

  mov rcx, rax    ; иначе передаем количество байт для вывода на консоль
  lea rsi, buffer
  call writeToConsole ; выводим прочитанные данные на консоль  
  jmp readLoop

  
done:
  mov rax, fHandle  ; в RAX дескриптор файла
  call closeFile  ; закрываем файл

  ret
main endp

Теперь считывание и вывод символов проецируется на метку readLoop. За раз считываем bufferLen байт (32 байта) и сохраняем в буфер и сразу выводим их на консоль. Если количестве считанных байт равно 0, значит, весь файл считан, поэтому выходим из цикла и переходим к закрытию файла. Таким образом, мы считаем весь файл. Пример работы приложения:

c:\asm>hello.exe
Hello METANIT.COM1
Hello METANIT.COM2
Hello METANIT.COM3
Hello METANIT.COM4
Hello METANIT.COM5

c:\asm>
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850