Взаимодействие с WinAPI

Запись в файл и вывод на консоль

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

WinAPI предоставляет множество различных функций для работы с файлами. Рассмотрим некоторые из них:

  • CreateFileA: открывает существующие файлы или создает новые

  • WriteFile: записывает данные в файл

  • ReadFile: считывает данные из файла

  • CloseHandle: закрывает файл

  • GetStdHandle: возвращает дескриптор одного из стандартных устройств ввода или вывода

  • GetLastError: возвращает код ошибки Windows, если ошибка возникает при выполнении любой из выше перечисленных функций.

За работу с файлами отвечает библиотека kernel32.lib

Запись в файл

Для записи в файл применяется функция WriteFile, которая имеет следующее определение на языке C++:

BOOL WriteFile(
  [in]                HANDLE       hFile,
  [in]                LPCVOID      lpBuffer,
  [in]                DWORD        nNumberOfBytesToWrite,
  [out, optional]     LPDWORD      lpNumberOfBytesWritten,
  [in, out, optional] LPOVERLAPPED lpOverlapped
);

WriteFile имеет следующие аргументы:

  • hFile: дескриптор файла, передается через регистр RCX

  • lpBuffer: адрес буфера для записи, передается через регистр RDX

  • nNumberOfBytesToWrite: размер буфера, передается через регистр R8 (поскольку представляет тип dword и занимает 32 бита, то передайется в R8D)

  • lpNumberOfBytesWritten: адрес переменной типа dword для получения количества байтов, записанных в файл. Равно размеру буфера, если операция записи прошла успешно. Передается через регистр R9

  • lpOverlapped: обычно имеет значение NULL (0), передается через стек по адресу [rsp + 32]

После выполнения функция WriteFile помещает в регистр RAX ненулевое значение, если запись прошла успешно, и ноль, если произошла ошибка. Если произошла ошибка, можно вызвать другую функцию WinAPI - GetLastError, чтобы получить код ошибки.

Используем функцию WriteFile для записи (вывода) данных на консоль, а точнее в стандартный поток вывода:

includelib kernel32.lib   ; подключаем библиотеку kernel32.lib

; подключаем функции WriteFile и GetStdHandle
extrn WriteFile: PROC
extrn GetStdHandle: PROC
.data
text byte "Hello METANIT.COM!"    ; выводимая строка
len = $-text
bytesWritten dword ?     ; количество записанных байтов

.code
main proc
  sub  rsp, 40          ; Для параметров функций WriteFile и GetStdHandle
  mov  rcx, -11         ; Аргумент для GetStdHandle - STD_OUTPUT
  call GetStdHandle     ; вызываем функцию GetStdHandle
  mov  rcx, rax         ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консоли
  lea  rdx, text        ; Второй параметр WriteFile - загружаем указатель на строку в регистр RDX
  mov  r8d, len         ; Третий параметр WriteFile - длина строки для записи в регистре R8D 
  lea  r9, bytesWritten       ; Четвертый параметр WriteFile - адрес для получения записанных байтов
  mov  qword ptr [rsp + 32], 0  ; Пятый параметр WriteFile
  call WriteFile ; вызываем функцию WriteFile
    
  test rax, rax ; проверяем на наличие ошибки
  mov eax, bytesWritten ; если все нормально, помещаем в RAX количество записанных байтов
  jnz exit 
  mov rax, -1 ; Возвращаем через RAX код ошибки
exit:
  add  rsp, 40
  ret
main endp
end

Здесь функции GetStdHandle в качестве параметра через регистр RCX передается значение -11, которое указывает, что надо получить дескриптор стандартного вывода.

Полученный через регистр RAX дескриптор передаем функции WriteFile через регистр RCX. Через регистр RDX передается адрес выводимой строки - переменной text, а через регистр R8D - длина строки. В регистр R9 загружаем адрес переменной bytesWritten, через которую получим реальное число записанных байтов.

После выполнения функции WriteFile и соответственно вывода строки на консоль проверяем результат функции в регистре RAX - если он не равен 0 (то есть все прошло без ошибок), то в RAX помещаем количество записанных байтов. Если же произошла ошибка, то в RAX помещаем число -1, которое будет условно обозначать, что произошла ошибка.

Для абстрагирования от конкретной строки и упрощения вывода на консоль мы можем выделить все соответствующие действия в отдельную процедуру:

includelib kernel32.lib   ; подключаем библиотеку kernel32.lib

; подключаем функции WriteFile и GetStdHandle
extrn WriteFile: PROC
extrn GetStdHandle: PROC
.data
text byte "Hello METANIT.COM!"    ; выводимая строка
len = $-text

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

.code

; Параметры:
; RSI - адрес строки
; RCX - длина строки
; RAX - дескриптор файла
; Результат - через RAX возвращаем количество записанных байтов
write proc
  sub  rsp, 56          ; 40 байт (5 параметров WriteLine) + 8 байт (bytesWritten) + 8 байт выравнивание
  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

main proc
  lea rsi, text       ; загружаем адрес строки
  mov rcx, len        ; загружаем размер строки
  call writeToConsole   ; выводим строку на консоль
  ret
main endp
end

Здесь запись разбита на две процедуры. Первая процедура - write собственно выполняет запись в файл. Через регистр RAX она получает дескриптор файла. Причем это может быть дескриптор консольного вывода, а может быть и дескриптор какого-то другого файла. Таким образом, мы абстрагируемся от консоли и опредлеяем универсальную функцию записи. Через регистр RSI она получает адрес строки, а через RCX - размер строки. Внутри функции устанавливаются все необходимые регистры и вызывается внешняя функция WriteFile. Результат функции - или количество записанных байтов, которое хранится в стеке по адресу rsp+40, либо -1, если произошла ошибка. В данном случае мы никак не обыгрываем данную ситуацию, но в принципе мы можем проверить результат данной процедуры для последующих действий, например, для вывода пользователю сообщения об ошибке и т.д.

Непосредственно для вывода данных на консоль определена функция writeToConsole. В ней получаем дескриптор консольного вывода с помощью вызова функции GetStdHandle и сохраняем в переменную stdout. Чтобы указать, что дескриптор стандартного вывода установлен, присваиваем переменной stdoutSet. Сравнивая ее значение, мы можем определить, был ли ранее получен дескриптор. Это позволит при повторном вызове процедуры writeToConsole избежать ненужных обращений к функции GetStdHandle. После получения дескриптора передаем его процедуре write и с ее помощью собственно выполняем запись.

Благодаря этому мы можем удобным образом многократно выполнять вывод на консоль строк. Например:

main proc
  lea rsi, text1
  mov rcx, len1
  call writeToConsole   ; выводим первую строку на консоль

  lea rsi, text2
  mov rcx, len2
  call writeToConsole   ; выводим вторую строку на консоль

  ret

  text1 byte "Hello METANIT.COM!",10
  len1 = $-text1
  text2 byte "Hello Assembler!",10
  len2 = $-text2
main endp

Вывод символов с переводом на новую строку

В примере выше, чтобы при выводе строк на консоль они не сливались, в конце строки определялся байт 10, который в таблице ASCII соответствует символу \n или символу перевода строки. Чтобы уйти от необходимости определять в конце каждой строки такой символ, можно его добавлять при выводе строки на консоль. Например:

includelib kernel32.lib   ; подключаем библиотеку kernel32.lib

; подключаем функции WriteFile и GetStdHandle
extrn WriteFile: PROC
extrn GetStdHandle: PROC
.data
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

; Процедура вывода символов c переводом на новую строку 
; Параметры
; RSI - адрес строки
; RCX - количество символов
writeLine proc

  call writeToConsole        ; записываем текст из RSI длиной RCX байт
  lea rsi, crlf 
  mov rcx, 2
  call writeToConsole   ; после записи текста записываем перевод строки = \r\n
  ret
  crlf byte 10, 13  ; для перевода на новую строку
writeLine endp


main proc
  lea rsi, text1
  mov rcx, len1
  call writeLine   ; выводим первую строку на консоль

  lea rsi, text2
  mov rcx, len2
  call writeLine   ; выводим вторую строку на консоль

  ret

  text1 byte "Hello METANIT.COM!"
  len1 = $-text1
  text2 byte "Hello Assembler!"
  len2 = $-text2
main endp
end

Здесь добавлена процедура writeLine, которая принимает строку, выводит ее на консоль с помощью writeToConsole, а затем дополнительно выводит символы \r\n, которые хранятся в строке crlf.

Вывод строк с концевым символом

Для идентификации конца строки может использоваться, как в примере выше, размер строки. Но, например, язык C и ряд других языков использует другой подход - завершение строки специальным концевым символом - нулевым байтом. В качестве концевого символа не обязательно должен выступать нулевой байт. Но если коду на ассемблере предстоит работать с такими строками, то мы можем просто расчитать количество символов до подобного концевого символа и также обрабатывать строку

includelib kernel32.lib   ; подключаем библиотеку kernel32.lib

; подключаем функции WriteFile и GetStdHandle
extrn WriteFile: PROC
extrn GetStdHandle: PROC
.data
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

; Процедура вывода символов c переводом на новую строку 
; Параметры
; RSI - адрес строки
; RCX - количество символов
writeLine proc

  call writeToConsole        ; записываем текст из RSI длиной RCX байт
  lea rsi, crlf 
  mov rcx, 2
  call writeToConsole   ; после записи текста записываем перевод строки = \r\n
  ret
  crlf byte 10, 13  ; для перевода на новую строку
writeLine endp
; Процедура вывода произвольной строки на консоль 
; Параметры
; RSI - адрес строки
; Результат - в RAX количество записанных байтов или -1, если произошла ошибка
writeText proc
  ; вычисляем длину строки
  mov rcx, -1    ; счетчик символов строки
calculateLen: 
  inc rcx
  cmp byte ptr [rsi][rcx * 1], 0    ; пока не дойдем до нулевого байта
  jne calculateLen
  call writeLine
  ret
writeText endp

main proc
  lea rsi, text1
  call writeText   ; выводим первую строку на консоль

  lea rsi, text2
  call writeText   ; выводим вторую строку на консоль

  ret

  text1 byte "Hello C/C++",0
  text2 byte "Hello Assembler!",0
main endp
end

Здесь добавлена процедура writeText, которая получает извне через RSI строку и вычисляет ее длину, пока не встретит концевой символ - нулевой байт и затем вызывает процедуру writeLine:

; вычисляем длину строки
  mov rcx, -1    ; счетчик символов строки
calculateLen: 
  inc rcx
  cmp byte ptr [rsi][rcx * 1], 0    ; пока не дойдем до нулевого байта
  jne calculateLen
  call writeLine

В процедуре main при вызове writeText передаются строки с концевым нулевым байтом:

main proc
  lea rsi, text1
  call writeText   ; выводим первую строку на консоль

  lea rsi, text2
  call writeText   ; выводим вторую строку на консоль

  ret

  text1 byte "Hello C/C++",0
  text2 byte "Hello Assembler!",0
main endp

Однако если переданные строки не будут содержать нулевой байт, то выполнение программы завершится печальным образом.

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