Описание файла который подключается к программе ассемблер

Обновлено: 02.07.2024

Чтение данных из файла можно произвести с помощью функции ReadFile():

BOOL ReadFile(HANDLE hFile, (1)

LPVOID lpBuffer, (2)

DWORD nNumberOfBytesToRead, (3)

LPDWORD lpNumberOfBytesRead, (4)

LPOVERLAPPED lpOverlapped); (5)

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

– hFile – хэндл того файла, из которого будет производиться чтение.

– lpBuffer – указывает на буфер, в который будет производиться чтение данных.

– nNumberOfBytesToRead определяет число байтов, которые необходимо прочесть из файла.

– в буфер lpNumberOfBytesRead будет записано число реально прочитанных байтов.

– аргумент lpOverlapped используется только для асинхронного ввода-вывода. При синхронном вводе необходимо этому параметру передать значение NULL.

; Считать данные из файла data.txt в массив структур. (struk4.asm)

.model flat, stdcall

worker struc ;информация о сотруднике

nam db 30 dup (" ") ;фамилия, имя, отчество

position db 30 dup (" ") ;должность

age db 2 dup (" ") ;возраст

standing db 2 dup (" ") ;стаж

salary db 4 dup (" ") ;оклад в гривнах

birthdate db 8 dup (" ") ;дата рождения

worker ends ; 76 байт в строке в файле

mas_sotr worker 10 dup (<>)

filename db 'data.txt',0

Ttl db 'Massiv',0h

mes1 db 'Massiv: ',0ah,0dh

mov ebx,type worker

; CreateFileA

mov eax,offset filename

; ReadFile

mov ebx,offset mas_sotr

mov esi,(type worker)*edi ; edi - индекс элемента в массиве

lea edi, bufread

Не обещаю, что работает!

Пример программы для практики

; Программа формирует массив структур. В массиве 3 элемента. Массив вводится вручную.

; На экран выводится возраст всех сотрудников

worker struc ;информация о сотруднике

nam db 15 dup (' ') ;имя

lastnam db 15 dup (' ') ; фамилия

age db 2 dup (' ') ;возраст

buf1 db 15 dup (20h)

pole1 db 2 dup (?) ; поле для вывода возраста

N=3 ; размерность массива

mas_sotr worker N dup (<>)

mes1 db 'Vvedite 3 elementa strukturi: ','$'

mnam db 10,13,'Vvedite imya: ', '$'

mlastnam db 10,13,'Vvedite familiyou: ','$'

mage db 10,13,'Vvedite vozrast: ','$'

mes db 10,13,'Vozrast=',10,13,'$'

mov dx,offset mes1 ; mes1 = 'Vvedite 3 elementa strukturi: ','$'

mov bx, offset mas_sotr

mov ax, type worker

; Цикл cykl2 – цикл формирования массива структур в памяти.

mov dx,offset mnam

mov ah,0ah ; Функция 0ah записывает в буфер buf1, находящийся по адресу в dx

mov cl,byte ptr [si]

mov dx,offset mlastnam

mov cl,byte ptr [si]

mov dx,offset mage

mov cl,byte ptr [si]

; ----------------------------------------Метка label_1: продолжение цикла cykl2, так как он получается

; очень большим, то часть цикла вынесли за его пределы.

mov ax, type worker

mov byte ptr [di],'$'

mov dx,offset mes

; ------------------------------------------В поле pole1 записывается возраст каждого сотрудника из

; массива структур в памяти. Далее содержимое поля pole1 выводится на экран.

mov bx, type worker

lea dx,[si].age ;lea dx,[si+1E]

mov dx,offset pole1

Самостоятельно на практике:

1) Переделать программу под WIN32.

Создать и открыть новый файл

Функция DOS 5Bh — Создать и открыть новый файл.

СХ = атрибут файла

DS:DX = адрес ASCIZ-строки с полным именем файла

CF = 0 и АХ = идентификатор файла, открытого для чтения/записи в режиме совместимости, если не произошла ошибка.

CF = 1 и АХ = код ошибки (03h — путь не найден, 04h — слишком много открытых файлов, 05h — доступ запрещен, 50h — файл уже существует).

Чтение из файла или устройства

Функция DOS 3Fh — Чтение из файла или устройства

АН = 3Fh

ВХ = идентификатор файла

СХ = число байт

DS:DX = адрес буфера для приема данных

Вывод:

CF = 0 и АХ = число считанных байт, если не произошла ошибка

CF = 1 и АХ = 05h, если доступ запрещен, 06h, если неправильный идентификатор

Если при чтении из файла число фактически считанных байт в АХ меньше, чем заказанное число в СХ, то при чтении был достигнут конец файла. Каждая следующая операция чтения, так же как и записи, начинается не с начала файла, а с того байта, на котором остановилась предыдущая операция чтения/записи. Если требуется считать (или записать) произвольный участок файла, используют функцию 42h (функция lseek в С).

При работе с файлами на ассемблере единичной записью является байт. Все записи имеют номера от 0 до L-1, где L длина файла. При открытии файла указатель устанавливается на запись 0. При чтении или записи указатель автоматически передвигается на n байт (где n - число прочитанных или записанных байт).

При чтении файла (функция 3FH) в АХ помещается считанное количество байт. Поэтому следует каждый раз сравнивать АХ и СХ. Если АХ>СХ, то обычно это означает, что в процессе чтения произошел переход через конец файла (устанавливается флаг СF). При записи в файл ситуация аналогична, но в этом случае неравенство содержимого АХ и СХ будет означать, что в процессе записи произошла ошибка.

Переместить указатель чтения/записи

Функция DOS 42h — Переместить указатель чтения/записи

Ввод:

АН = 42h

ВХ = идентификатор

CX:DX = расстояние, на которое надо переместить указатель (со знаком) (4 ГБ)

AL = перемещение от:

Начала файла

Текущей позиции

Конца файла

Вывод:

CF = 0 и CX:DX = новое значение указателя (в байтах от начала файла), если не произошла ошибка

CF = 1 и АХ = 06h, если неправильный идентификатор

Указатель можно установить за реальными пределами файла: если указатель устанавливается в отрицательное число, следующая операция чтения/записи вызовет ошибку; если указатель устанавливается в положительное число, большее длины файла, следующая операция записи увеличит размер файла. Эта функция также часто используется для определения длины файла. Для этого необходимо вызвать ее с параметрами СХ = 0, DX = 0, AL = 2, и в CX:DX будет возвращена длина файла в байтах.

Используя функцию 42Н, можно переместиться к любому байту файла. Ниже дается полное описание этой функции.

ВХ описатель файла

CX:DX на сколько передвинуть

AL как передвигать

0 начало файла + CX:DX,

1 от текущей позиции файла + CX:DX,

2 от конца файла + СХ:DX

Если флаг переноса установлен, то в АХ помещен код ошибки, в противном случае AX:DX показывает новую позицию в файле.

В наше время редко возникает необходимость писать на чистом ассемблере, но я определённо рекомендую это всем, кто интересуется программированием. Вы увидите вещи под иным углом, а навыки пригодятся при отладке кода на других языках.

В этой статье мы напишем с нуля калькулятор обратной польской записи (RPN) на чистом ассемблере x86. Когда закончим, то сможем использовать его так:


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

Начнём с написания базовой программы Hello world! для проверки настроек среды. Затем перейдём к системным вызовам, стеку вызовов, стековым кадрам и соглашению о вызовах x86. Потом для практики напишем некоторые базовые функции на ассемблере x86 — и начнём писать калькулятор RPN.

Предполагается, что у читателя есть некоторый опыт программирования на C и базовые знания компьютерной архитектуры (например, что такое регистр процессора). Поскольку мы будем использовать Linux, вы также должны уметь использовать командную строку Linux.

Как уже сказано, мы используем Linux (64- или 32-битный). Приведённый код не работает в Windows или Mac OS X.

Для установки нужен только компоновщик GNU ld из binutils , который предварительно установлен в большинстве дистрибутивов, и ассемблер NASM. На Ubuntu и Debian можете установить и то, и другое одной командой:


Я бы также рекомендовал держать под рукой таблицу ASCII.

Для проверки среды сохраните следующий код в файле calc.asm :


Комментарии объясняют общую структуру. Список регистров и общих инструкций можете изучить в «Руководстве по ассемблеру x86 университета Вирджинии». При дальнейшем обсуждении системных вызовов это тем более понадобится.

Следующие команды собирают файл ассемблера в объектный файл, а затем компонует исполняемый файл:


После запуска вы должны увидеть:

Makefile

Это необязательная часть, но для упрощения сборки и компоновки в будущем можно сделать Makefile . Сохраните его в том же каталоге, что и calc.asm :


Затем вместо вышеприведённых инструкций просто запускаем make.

Системные вызовы Linux указывают ОС выполнить для нас какие-то действия. В этой статье мы используем только два системных вызова: write() для записи строки в файл или поток (в нашем случае это стандартное устройство вывода и стандартная ошибка) и exit() для выхода из программы:


Системные вызовы настраиваются путём сохранения номера системного вызова в регистре eax , а затем его аргументов в ebx , ecx , edx в таком порядке. Можете заметить, что у exit() только один аргумент — в этом случае ecx и edx не имеют значения.

eax ebx ecx edx
Номер системного вызова arg1 arg2 arg3


Стек вызовов — структура данных, в которой хранится информация о каждом обращении к функции. У каждого вызова собственный раздел в стеке — «фрейм». Он хранит некоторую информацию о текущем вызове: локальные переменные этой функции и адрес возврата (куда программа должна перейти после выполнения функции).

Сразу отмечу одну неочевидную вещь: стек увеличивается вниз по памяти. Когда вы добавляете что-то на верх стека, оно вставляется по адресу памяти ниже, чем предыдущий элемент. Другими словами, по мере роста стека адрес памяти в верхней части стека уменьшается. Чтобы избежать путаницы, я буду всё время напоминать об этом факте.

Инструкция push заносит что-нибудь на верх стека, а pop уносит данные оттуда. Например, push еах выделяет место наверху стека и помещает туда значение из регистра eax , а pop еах переносит любые данные из верхней части стека в eax и освобождает эту область памяти.

Цель регистра esp — указать на вершину стека. Любые данные выше esp считаются не попавшими в стек, это мусорные данные. Выполнение инструкции push (или pop ) перемещает esp . Вы можете манипулировать esp и напрямую, если отдаёте отчёт своим действиям.

Регистр ebp похож на esp , только он всегда указывает примерно на середину текущего кадра стека, непосредственно перед локальными переменными текущей функции (поговорим об этом позже). Однако вызов другой функции не перемещает ebp автоматически, это нужно каждый раз делать вручную.

В х86 нет встроенного понятия функции как в высокоуровневых языках. Инструкция call — это по сути просто jmp ( goto ) в другой адрес памяти. Чтобы использовать подпрограммы как функции в других языках (которые могут принимать аргументы и возвращать данные обратно), нужно следовать соглашению о вызовах (существует много конвенций, но мы используем CDECL, самое популярное соглашение для x86 среди компиляторов С и программистов на ассемблере). Это также гарантирует, что регистры подпрограммы не перепутаются при вызове другой функции.

Правила вызывающей стороны

Перед вызовом функции вызывающая сторона должна:

  1. Сохранить в стек регистры, которые обязан сохранять вызывающий. Вызываемая функция может изменить некоторые регистры: чтобы не потерять данные, вызывающая сторона должна сохранить их в памяти до помещения в стек. Речь идёт о регистрах eax , ecx и edx . Если вы не используете какие-то из них, то их можно не сохранять.
  2. Записать аргументы функции на стек в обратном порядке (сначала последний аргумент, в конце первый аргумент). Такой порядок гарантирует, что вызываемая функция получит из стека свои аргументы в правильном порядке.
  3. Вызвать подпрограмму.
  1. Удалить из стека аргументы функции. Обычно это делается путём простого добавления числа байтов в esp . Не забывайте, что стек растёт вниз, поэтому для удаления из стека необходимо добавить байты.
  2. Восстановить сохранённые регистры, забрав их из стека в обратном порядке инструкцией pop . Вызываемая функция не изменит никакие другие регистры.

Правила вызываемой подпрограммы

Перед вызовом подпрограмма должна:

  1. Сохранить указатель базового регистра ebp предыдущего фрейма, записав его на стек.
  2. Отрегулировать ebp с предыдущего фрейма на текущий (текущее значение esp ).
  3. Выделить больше места в стеке для локальных переменных, при необходимости переместить указатель esp . Поскольку стек растёт вниз, нужно вычесть недостающую память из esp .
  4. Сохранить в стек регистры вызываемой подпрограммы. Это ebx , edi и esi . Необязательно сохранять регистры, которые не планируется изменять.


Стек вызовов после шага 2:


Стек вызовов после шага 4:


На этих диаграммах в каждом стековом фрейме указан адрес возврата. Его автоматически вставляет в стек инструкция call . Инструкция ret извлекает адрес с верхней части стека и переходит на него. Эта инструкция нам не нужна, я просто показал, почему локальные переменные функции находятся на 4 байта выше ebp , но аргументы функции — на 8 байт ниже ebp .

На последней диаграмме также можно заметить, что локальные переменные функции всегда начинается на 4 байта выше ebp с адреса ebp-4 (здесь вычитание, потому что мы двигаемся вверх по стеку), а аргументы функции всегда начинается на 8 байт ниже ebp с адреса ebp+8 (сложение, потому что мы двигаемся вниз по стеку). Если следовать правилам из этой конвенции, так будет c переменными и аргументами любой функции.

Когда функция выполнена и вы хотите вернуться, нужно сначала установить eax на возвращаемое значение функции, если это необходимо. Кроме того, нужно:

  1. Восстановить сохранённые регистры, вынеся их из стека в обратном порядке.
  2. Освободить место в стеке, выделенное локальным переменным на шаге 3, если необходимо: делается простой установкой esp в ebp
  3. Восстановить указатель базы ebp предыдущего фрейма, вынеся его из стека.
  4. Вернуться с помощью ret

В приведённом примере вы можете заметить, что функция всегда запускается одинаково: push ebp , mov ebp , esp и выделение памяти для локальных переменных. В наборе x86 есть удобная инструкция, которая всё это выполняет: enter a b , где a — количество байт, которые вы хотите выделить для локальных переменных, b — «уровень вложенности», который мы всегда будем выставлять на 0 . Кроме того, функция всегда заканчивается инструкциями pop ebp и mov esp , ebp (хотя они необходимы только при выделении памяти для локальных переменных, но в любом случае не причиняют вреда). Это тоже можно заменить одной инструкцией: leave . Вносим изменения:

Усвоив соглашение о вызовах, можно приступить к написанию некоторых подпрограмм. Почему бы не обобщить код, который выводит "Hello world!", для вывода любых строк: функция _print_msg .

Здесь понадобится ещё одна функция _strlen для подсчёта длины строки. На C она может выглядеть так:


Другими словами, с самого начала строки мы добавляем 1 к возвращаемым значением для каждого символа, кроме нуля. Как только замечен нулевой символ, возвращаем накопленное в цикле значение. В ассемблере это тоже довольно просто: можно использовать как базу ранее написанную функцию _subtract :


Уже неплохо, верно? Сначала написать код на C может помочь, потому что большая его часть непосредственно преобразуется в ассемблер. Теперь можно использовать эту функцию в _print_msg , где мы применим все полученные знания:


И посмотрим плоды нашей тяжёлой работы, используя эту функцию в полной программе “Hello, world!”.


Хотите верьте, хотите нет, но мы рассмотрели все основные темы, которые нужны для написания базовых программ на ассемблере x86! Теперь у нас есть весь вводный материал и теория, так что полностью сосредоточимся на коде и применим полученные знания для написания нашего калькулятора RPN. Функции будут намного длиннее и даже станут использовать некоторые локальные переменные. Если хотите сразу увидеть готовую программу, вот она.

Для тех из вас, кто не знаком с обратной польской записью (иногда называемой обратной польской нотацией или постфиксной нотацией), то здесь выражения вычисляются с помощью стека. Поэтому нужно создать стек, а также функции _pop и _push для манипуляций с этим стеком. Понадобится ещё функция _print_answer , которая выведет в конце вычислений строковое представление числового результата.

Сначала определим для нашего стека пространство в памяти, а также глобальную переменную stack_size . Желательно изменить эти переменные так, чтобы они попали не в раздел .rodata , а в .data .


Теперь можно реализовать функции _push и _pop :

_print_answer намного сложнее: придётся конвертировать числа в строки и использовать несколько других функций. Понадобится функция _putc , которая выводит один символ, функция mod для вычисления остатка от деления (модуля) двух аргументов и _pow_10 для возведения в степень 10. Позже вы поймёте, зачем они нужны. Это довольно просто, вот код:


Итак, как мы выводим отдельные цифры в числе? Во-первых, обратите внимание, что последняя цифра числа равна остатку от деления на 10 (например, 123 % 10 = 3 ), а следующая цифра — это остаток от деления на 100, поделенный на 10 (например, (123 % 100)/10 = 2 ). В общем, можно найти конкретную цифру числа (справа налево), найдя (число % 10**n) / 10**(n-1) , где число единиц будет равно n = 1 , число десятков n = 2 и так далее.

Используя это знание, можно найти все цифры числа с n = 1 до n = 10 (это максимальное количество разрядов в знаковом 4-байтовом целом). Но намного проще идти слева направо — так мы сможем печатать каждый символ, как только находим его, и избавиться от нулей в левой части. Поэтому перебираем числа от n = 10 до n = 1 .

На C программа будет выглядеть примерно так:


Теперь вы понимаете, зачем нам эти три функции. Давайте реализуем это на ассемблере:


Это было тяжкое испытание! Надеюсь, комментарии помогают разобраться. Если вы сейчас думаете: «Почему нельзя просто написать printf("%d") ?», то вам понравится окончание статьи, где мы заменим функцию именно этим!

Теперь у нас есть все необходимые функции, осталось реализовать основную логику в _start — и на этом всё!

Как мы уже говорили, обратная польская запись вычисляется с помощью стека. При чтении число заносится на стек, а при чтении оператор применяется к двум объектам наверху стека.

Например, если мы хотим вычислить 84/3+6* (это выражение также можно записать в виде 6384/+* ), процесс выглядит следующим образом:

Шаг Символ Стек перед Стек после
1 8 [] [8]
2 4 [8] [8, 4]
3 / [8, 4] [2]
4 3 [2] [2, 3]
5 + [2, 3] [5]
6 6 [5] [5, 6]
7 * [5, 6] [30]

Если на входе допустимое постфиксное выражение, то в конце вычислений на стеке остаётся лишь один элемент — это и есть ответ, результат вычислений. В нашем случае число равно 30.

В ассемблере нужно реализовать нечто вроде такого кода на C:


Теперь у нас имеются все функции, необходимые для реализации этого, давайте начнём.


Понадобится ещё добавить строку error_msg в раздел .rodata :


И мы закончили! Удивите всех своих друзей, если они у вас есть. Надеюсь, теперь вы с большей теплотой отнесётесь к языкам высокого уровня, особенно если вспомнить, что многие старые программы писали полностью или почти полностью на ассемблере, например, оригинальный RollerCoaster Tycoon!

Ассемблер рассматривает любые входные или выходные данные в качестве потока байтов. Есть три стандартных файловых потока:

стандартный ввод ( stdin );

стандартный вывод ( stdout );

стандартная ошибка ( stderr ).

Файловый дескриптор

Файловый дескриптор стандартных файловых потоков:

Файловый указатель

Файловый указатель указывает местоположение для последующей операции чтения/записи в файл. Каждый файл рассматривается как последовательность байтов и ассоциируется с файловым указателем, который задает смещение в байтах относительно начала файла. Когда файл открыт, значением файлового указателя является 0 .

Системные вызовы обработки файлов

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

Необходимые шаги для использования системных вызовов:

поместите номер системного вызова в регистр EAX;

сохраните аргументы системного вызова в регистрах EBX, ECX и EDX;

вызовите соответствующее прерывание ( 80h );

результат обычно возвращается в регистр EAX.

Создание и открытие файла

Для создания и открытия файла выполняются следующие шаги:

поместите системный вызов sys_creat() — номер 8 в регистр EAX;

поместите имя файла в регистр EBX;

поместите права доступа к файлу в регистр ECX.

Системный вызов возвращает файловый дескриптор созданного файла в регистр EAX. В случае ошибки, код ошибки также будет находиться в регистре EAX.

Открытие существующего файла

Для открытия существующего файла выполняются следующие шаги:

поместите системный вызов sys_open() — номер 5 в регистр EAX;

поместите имя файла в регистр EBX;

поместите режим доступа к файлу в регистр ECX;

поместите права доступа к файлу в регистр EDX.

Системный вызов возвращает файловый дескриптор открытого файла в регистр EAX. В случае ошибки, код ошибки также будет находиться в регистре EAX.

Среди режимов доступа к файлам чаще всего используются:

только чтение ( 0 );

только запись ( 1 );

Чтение файла

Для чтения данных из файла выполняются следующие шаги:

поместите системный вызов sys_read() — номер 3 в регистр EAX;

поместите файловый дескриптор в регистр EBX;

поместите указатель на входной буфер в регистр ECX;

поместите размер буфера, т.е. количество байтов для чтения, в регистр EDX.

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

Запись в файл

Для записи в файл выполняются следующие шаги:

поместите системный вызов sys_write() — номер 4 в регистр EAX;

поместите файловый дескриптор в регистр EBX;

поместите указатель на выходной буфер в регистр ECX;

поместите размер буфера, т.е. количество байтов для записи, в регистр EDX.

Системный вызов возвращает фактическое количество записанных байтов в регистр EAX. В случае ошибки, код ошибки также будет находиться в регистре EAX.

Закрытие файла

Для закрытия файла выполняются следующие шаги:

поместите системный вызов sys_close() — номер 6 в регистр EAX;

поместите файловый дескриптор в регистр EBX.

В случае ошибки, системный вызов возвращает код ошибки в регистр EAX.

Обновление файла

Для обновления файла выполняются следующие шаги:

поместите системный вызов sys_lseek() — номер 19 в регистр EAX;

поместите файловый дескриптор в регистр EBX;

поместите значение смещения в регистр ECX;

поместите исходную позицию для смещения в регистр EDX.

Исходная позиция может быть:

началом файла — значение 0 ;

текущей позицией — значение 1 ;

концом файла — значение 2 .

В случае ошибки, системный вызов возвращает код ошибки в регистр EAX.

Продолжаем публиковать книгу о программирование на языке ассемблера в операционной системе Linux. Сегодня, мы опубликуем очередной параграф. Параграф является первым в новой главе, посвященной взаимодействию программы с операционной системой.

Параграф 4.1

Открытие и чтение файла на ассемблере

Файловая система является одной из самых важных элементов операционной системы. К файлам, в которых хранятся те или иные данные, нужно иметь доступ. При работе в командном режиме операционной системы мы используем команды или возможности тех или иных программ. Но если мы сами пишем программу, то должны иметь инструмент доступа к файловой системе. В частности к конкретным файлам. Именно работе с конкретными файлами будут посвящены первые параграфы данной главы.

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

В данном параграфе мы рассмотрим стандартную тройку работы с файлами:

1. Открытие файла.

2. Чтение из файла.

3. Закрытие файла.

В программе, которая представлена ниже (листинг 31), мы как раз демонстрируем указанную «тройку» в действии. Наша программа читает из указанного файла и выводит результат на консоль. Для того, чтоб эксперимент прошел «чисто» конечно, следует взять именно текстовый файл. Тогда мы сможем увидеть его содержимое в консоли или путем перенаправления вывода, получить копию этого файла.

В программе из листинга 31 мы используем известные уже нам элементы, связанные с использованием системных функций: вывод на консоль, выход из программы и новые: открытие файла, закрытие файла и чтение из файла.

Компиляция файла осуществляется известным нам способом:

as --64 l31.s -o l31.o
ld -s l31.o -o l31

Пояснения к программе из листинга 31.

1. Открытие файла осуществляется с помощью системной функции с номером 2. Если вызов произошел успешно, то в регистре rax возвращается число — номер дескриптора файла. Все остальные операции с файлом осуществляются уже с использованием этого номера. При открытии файла в регистр rdi помещается адрес строки с именем файла. Строка заканчивается символом с кодом 0. Регистр rsi должен содержать режим открытия. В нашем случае число 0 означает, что файл открывается только для чтения.Если файл открыть не удалось, то в регистре rax будет содержаться отрицательное число номер ошибки с минусом.

2. Закрытие файла осуществляется с помощью системной функции с номером 3 . В регистре rdi следует поместить номер дескриптора открытого файла. Из текста программы видно, что в регистре r8 мы, при открытии файла, сохранили номер дескриптора файла, который потом используем при чтении из файла и при закрытии файла.

3. Наконец обратимся к функции чтения файла. Ее номер 0 . Регистр rdi содержит номер дескриптора открытого файла, регистр rsi — адрес буфера, куда будет читаться файл, регистр rdx — длина буфера для чтения. Важно отметить один ключевой момент: длина буфера может быть любой. Поскольку в rax при чтении возвращается количество считанных байтов, то мы всегда можем узнать закончилось чтение файла или нет. Если количество считанных байтов окажется меньше длины буфера, то это будет означать, что чтение файла закончилось. Т.е. мы имеем критерий окончания процесса чтения, что и отражено в программе — cmp $100, %rdi . Сам процесс чтения из файла можно прояснить таким понятием как текущий указатель. При открытии файла указатель направлен на первый байт файла. При каждом акте чтения указатель продвигается вперед (к концу файла) на количество прочитанных байтов. И еще важный момент, с которым мы уже неоднократно встречались и который не мешало бы напомнить: при вызове системной функции не сохраняются три регистра: rcx , r11 и rax . Учитывая это, мы вынесли за границы цикла команды заполнения регистров rsi и rdx, т.е. осуществили некоторый элемент оптимизации ассемблерного кода.

На сегодня все. Пока! Подписываемся на мой канал Old Programmer и ставьте "лайки". Изучайте ассемблер!

Читайте также: