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
Однако если переданные строки не будут содержать нулевой байт, то выполнение программы завершится печальным образом.