Nikita Mandrykin

Практикуюсь в программировании. C • Python • Assembly • Linux

Назад

Работа с файлами в NASM: Open, Read, Write на Syscalls

📚 Содержание

Файловый ввод-вывод в NASM на платформе Linux x86-64 реализуется через системные вызовы (open, read, write, close) и требует понимания архитектуры VFS ядра. Это руководство покрывает теорию файловых дескрипторов, практику syscall и интеграцию с модулями I/O.


🌊 1. Теоретический фундамент

Философия Unix: Всё Есть Файл

Unix-подобные системы строятся на элегантной абстракции: любой источник или приёмник данных представляется файлом. Это означает единый интерфейс для работы с:

  • Обычными файлами на диске (текст, бинарные данные)
  • Директориями (список файлов)
  • Устройствами (/dev/sda — диск, /dev/null — чёрная дыра)
  • Каналами (pipes для межпроцессного взаимодействия)
  • Сетевыми сокетами (TCP/UDP соединения)

Один набор системных вызовов (open, read, write, close) работает со всеми этими сущностями.

📘 Краткая справка: Основные syscall и соглашения о регистрах описаны в шпаргалке по NASM.


Virtual File System (VFS) и архитектура ядра

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

Архитектура:

┌──────────────────────────────────────────┐
│             Ваша программа               │
│    (системные вызовы open/read/write)    │
└─────────────────────┬────────────────────┘
┌──────────────────────────────────────────┐
│              VFS (ядро)                  │
│  Маршрутизатор запросов к драйверам      │
└──────┬──────────────┬──────────────┬─────┘
       │              │              │
       ▼              ▼              ▼
  ┌────────┐    ┌─────────┐   ┌──────────┐
  │ ext4   │    │   NFS   │   │ /dev/sda │
  │ драйвер│    │ драйвер │   │ драйвер  │
  └────────┘    └─────────┘   └──────────┘
       │              │              │
       ▼              ▼              ▼
    [Диск]         [Сеть]      [Устройство]

Принцип работы:

  1. Программа вызывает read(fd, buf, 100)
  2. VFS определяет тип файла по дескриптору
  3. Вызывается соответствующая функция драйвера
  4. Данные возвращаются программе

Благодаря VFS программист не думает о физической реализации — работа с сетевым файлом NFS идентична локальному ext4.


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

Файловый дескриптор (FD) — неотрицательное целое число, идентифицирующее открытый файл в процессе.

Структура доступа:

Процесс
  └─> Таблица дескрипторов процесса
       ├─> FD 0 (stdin)  ──┐
       ├─> FD 1 (stdout) ──┼──> Системная таблица открытых файлов
       ├─> FD 3          ──┘       └──> inode (метаданные файла)
       └─> FD 4          ─────> Другой entry

Ключевые свойства:

  1. Локальность: FD уникален только внутри процесса
  2. Последовательность: Ядро выдаёт наименьший свободный номер
  3. Стандартные потоки:
    • 0 — stdin (стандартный ввод)
    • 1 — stdout (стандартный вывод)
    • 2 — stderr (поток ошибок)
  4. Наследование: При fork() дочерний процесс копирует все FD родителя

Практический пример: Перенаправление вывода

Когда вы пишете ./program > output.txt, shell выполняет:

1. close(1)              # Освобождает FD 1
2. open("output.txt")    # Получает FD 1 (наименьший свободный)
3. exec("./program")     # Запускает программу

Программа пишет в FD 1, “думая” что это экран, но физически данные идут в файл.

Демонстрация в коде
section .data
    msg db "Hello, World!", 10
    
section .text
    global _start
_start:
    ; Запись в FD 1 (stdout)
    mov rax, 1
    mov rdi, 1          ; Независимо от того, что под FD 1
    lea rsi, [rel msg]  ; программа просто пишет в этот дескриптор
    mov rdx, 14
    syscall
    
    mov rax, 60
    xor rdi, rdi
    syscall

Запуск:

  • ./program → вывод на экран
  • ./program > file.txt → вывод в файл

Код программы не меняется!


Системные вызовы и переход Ring 3 → Ring 0

Системный вызов — переключение процессора из пользовательского режима (Ring 3) в режим ядра (Ring 0) для выполнения привилегированных операций.

Механика перехода:

Ring 3 (User Mode)          Ring 0 (Kernel Mode)
┌──────────────┐            ┌───────────────┐
│              │  syscall   │               │
│    Ваша      │───────────>│   Ядро Linux  │
│  программа   │            │   (драйверы,  │
│              │  sysret    │     VFS)      │
│              │<───────────│               │
└──────────────┘            └───────────────┘
     Огранич.                   Полный
     доступ                     доступ

Что происходит при syscall

  1. Сохранение контекста: RIP → RCX, RFLAGS → R11
  2. Смена привилегий: Ring 3 → Ring 0
  3. Переход в ядро: Процессор прыгает на адрес обработчика (LSTAR)
  4. Валидация: Ядро проверяет права доступа и корректность аргументов
  5. Выполнение: Драйвер выполняет операцию
  6. Возврат: sysret переключает Ring 0 → Ring 3

Соглашение о Регистрах (x86-64 System V ABI)

Регистр Назначение
rax Номер syscall (вход), результат (выход)
rdi 1-й аргумент
rsi 2-й аргумент
rdx 3-й аргумент
r10 4-й аргумент (не rcx!)
r8 5-й аргумент
r9 6-й аргумент

Важно: rcx и r11 уничтожаются ядром. rbx, rbp, r12-r15 сохраняются.

📖 Дополнительно: Полное описание System V ABI и ролей регистров см. в разделе «Архитектура регистров» шпаргалки по NASM.

Обычная функция (call)
  • Выполняется в Ring 3
  • Цена: 1-3 такта CPU
  • Просто переход по адресу
  • Без смены прав доступа
Системный вызов (syscall)
  • Переход Ring 3 → Ring 0 → Ring 3
  • Цена: сотни-тысячи тактов
  • Полная смена контекста
  • Валидация всех параметров

Права Доступа Unix

При создании файла с O_CREAT необходимо указать права доступа (mode) — битовую маску, определяющую операции для трёх категорий пользователей.

Структура прав:

Формат: 0[User][Group][Others]

Каждая позиция — сумма битов:
4 (read) + 2 (write) + 1 (execute) = 7 (все права)

Примеры:

Восьм. Символ. Описание
0644 rw-r--r-- Владелец: чтение+запись, остальные: только чтение
0755 rwxr-xr-x Владелец: всё, остальные: чтение+выполнение
0600 rw------- Только владелец может читать и писать

Механизм проверки: При каждом обращении к файлу ядро сравнивает UID (User ID) процесса с правами в inode файла и разрешает/запрещает операцию.


Буферизация

Системные вызовы дороги из-за переключения контекста. Буферизация — техника минимизации syscall путём накопления данных в памяти.

❌ Побайтовое чтение

Задача:

Прочитать 4 КБ

Реализация:

mov rcx, 4096
.loop:
    mov rax, 0
    mov rdi, [fd]
    lea rsi, [buffer]
    mov rdx, 1      ; 1 байт!
    syscall
    loop .loop

Цена:

4096 системных вызовов

✅ Блочное чтение

Задача:

Прочитать 4 КБ

Реализация:

mov rax, 0
mov rdi, [fd]
lea rsi, [buffer]
mov rdx, 4096   ; Весь блок
syscall

Цена:

1 системный вызов

Оптимальный размер буфера: 4096 байт (размер страницы памяти x86-64). Ядро работает с памятью страницами, выравнивание по этой границе даёт максимальную эффективность.

🔌 2. Практика системных вызовов

Таблица Системных Вызовов

syscall RAX Описание Основное применение
open 2 Открыть файл Получение файлового дескриптора
read 0 Читать данные Загрузка содержимого в память
write 1 Записать данные Сохранение данных на диск
close 3 Закрыть файл Освобождение дескриптора
lseek 8 Изменить позицию Произвольный доступ к данным

📋 Справочник: Полная таблица номеров системных вызовов (rax) и аргументов находится в «Шпаргалке NASM».


open — Получение Доступа к Файлу

Системный вызов open — это запрос у ядра на доступ к файлу. Если запрос одобрен, ядро возвращает файловый дескриптор.

Регистры:

rax = 2                     ; Номер системного вызова
rdi = pathname              ; Адрес строки с путём к файлу
rsi = flags                 ; Битовая маска флагов
rdx = mode                  ; Права доступа (только при создании)

Возвращаемое значение:

  • rax ≥ 0 — файловый дескриптор (успех)
  • rax < 0 — код ошибки (отрицательный errno)

Флаги Открытия

Режим доступа (обязателен, выбрать один):

O_RDONLY  equ 0         ; Только чтение
O_WRONLY  equ 1         ; Только запись
O_RDWR    equ 2         ; Чтение и запись

Модификаторы поведения (опциональны, комбинируются через OR):

O_CREAT   equ 0o100     ; Создать файл, если не существует
O_EXCL    equ 0o200     ; Ошибка, если файл уже существует (с O_CREAT)
O_TRUNC   equ 0o1000    ; Обрезать файл до нуля при открытии
O_APPEND  equ 0o2000    ; Все записи добавляются в конец файла

⚠️ Распространённая ошибка: Забыть указать права доступа rdx при использовании O_CREAT приведёт к созданию файла со случайными правами. Эта и другие частые ошибки разобраны в статье «Топ ошибок в NASM: Почему падает Segfault и неверные расчёты».

Типичные Комбинации

Чтение существующего файла
mov rax, 2
lea rdi, [rel filename]
mov rsi, 0              ; O_RDONLY
syscall

Поведение:

  • Открыть файл для чтения
  • Позиция на начало
  • Ошибка, если файл не существует
Создание нового файла
mov rax, 2
lea rdi, [rel filename]
mov rsi, 0o101          ; O_WRONLY | O_CREAT
mov rdx, 0644o          ; rw-r--r--
syscall

Поведение:

  • Создать файл, если не существует
  • Открыть для записи
  • Установить права 0644
Перезапись файла
mov rax, 2
lea rdi, [rel filename]
mov rsi, 0o1101         ; O_WRONLY | O_CREAT | O_TRUNC
mov rdx, 0644o
syscall

Поведение:

  • Создать, если нет
  • Стереть содержимое, если есть
  • Писать с начала

Аналог shell: > file.txt

Добавление в конец
mov rax, 2
lea rdi, [rel filename]
mov rsi, 0o2101         ; O_WRONLY | O_CREAT | O_APPEND
mov rdx, 0644o
syscall

Поведение:

  • Создать, если нет
  • Сохранить содержимое, если есть
  • Писать в конец

Аналог shell: >> file.txt

🔗 Связь: Если имя файла нужно брать не из константы, а из аргументов командной строки (например, ./prog data.txt), изучите статью «Аргументы командной строки в NASM: Работа с argc и argv».

Специальный Случай: Атомарная Блокировка (O_EXCL)

Флаг O_EXCL в комбинации с O_CREAT превращает проверку существования и создание файла в одну неделимую (атомарную) операцию.

❌ Проблема: Race Condition

Без атомарности между проверкой и созданием есть “окно уязвимости”:

[ Процесс A ]           [ Процесс B ]
            │                       │
  1. Проверка: "Файл есть?"         │
     ↳ Ответ: ❌ НЕТ                │
            │                       │
            │           2. Проверка: "Файл есть?"
            │              ↳ Ответ: ❌ НЕТ
  ┌─────────┴───────────────────────┴──────┐
  │    ОПАСНАЯ ЗОНА (Оба считают первыми)  │
  └─────────┬───────────────────────┬──────┘
            │                       │
  3. creat() -> ✅         4. creat() -> ⚠️
     Создал                   ПЕРЕЗАПИСАЛ!
✅ Решение: Атомарность

Ядро гарантирует, что никто не вклинится:

[ Процесс A ]           [ Процесс B ]
            │                       │
  1. open(O_CREAT | O_EXCL)         │
            │                       │
     ↳ 🟢 АТОМАРНЫЙ УСПЕХ           │
        (Нет -> Создан, FD=3)       │
            │                       │
            │           2. open(O_CREAT | O_EXCL)
            │              │
            │              ↳ 🔴 АТОМАРНЫЙ СБОЙ
            ▼                 (Есть -> EEXIST)
      Владеет ресурсом        (Блокировка не получена)
Реализация lock-файла
; ============================================================================
; lockfile_demo.asm
;
; Демонстрация реализации lock-файла для обеспечения эксклюзивного доступа.
; Использует атомарную операцию open() с флагами O_CREAT | O_EXCL для
; гарантии, что только один процесс может владеть блокировкой одновременно.
;
; АЛГОРИТМ:
;   1. Попытка атомарного создания lock-файла (open с O_CREAT | O_EXCL)
;   2. Если успех:
;      - Вывод сообщения о захвате блокировки
;      - Выполнение критической секции (здесь: заглушка)
;      - Освобождение блокировки (close + unlink)
;      - Завершение программы
;   3. Если файл уже существует (EEXIST):
;      - Вывод сообщения о занятой блокировке
;      - Ожидание 1 секунда (nanosleep)
;      - Повторная попытка захвата (goto шаг 1)
;   4. Если другая ошибка - немедленное завершение
;
; ПРИМЕНЕНИЕ:
;   • Обеспечение единственного экземпляра программы (singleton)
;   • Защита критических секций между процессами
;   • Координация доступа к разделяемым ресурсам
;
; ОГРАНИЧЕНИЯ:
;   ⚠️  Если процесс аварийно завершится (SIGKILL, crash), lock-файл
;       останется и потребует ручного удаления!
;   ✅  Для production использовать: PID в файле + проверку существования
;       процесса через kill(pid, 0) или /proc/[pid]
;
; АТОМАРНОСТЬ:
;   Операция open(O_CREAT | O_EXCL) выполняется ядром атомарно:
;   - Если файл НЕ существует → создаётся и возвращается FD
;   - Если файл существует → возвращается -EEXIST (не создаётся)
;   - Между проверкой и созданием НЕТ временного окна (race condition)
;
; ТЕСТИРОВАНИЕ:
;   Терминал 1:  ./lockfile_demo    # Захватит блокировку
;   Терминал 2:  ./lockfile_demo    # Будет ждать освобождения
; ============================================================================

default rel                             ; Использовать RIP-relative адресацию по умолчанию

section .data
    ; Путь к lock-файлу (в /tmp для автоочистки при перезагрузке)
    lockfile db "/tmp/myapp.lock", 0
    
    ; Сообщения о состоянии блокировки
    msg_acquired db "Lock acquired", 10
    msg_acquired_len equ $ - msg_acquired
    
    msg_busy db "Lock is busy, waiting...", 10
    msg_busy_len equ $ - msg_busy

section .bss
    lock_fd resq 1                      ; Файловый дескриптор lock-файла
    timespec resb 16                    ; Структура для nanosleep (2 x qword)

section .text
    global _start

; ============================================================================
; ОСНОВНАЯ ПРОГРАММА
; ============================================================================

_start:
    ; --- Инициализация структуры timespec (1 секунда) ---
    mov qword [timespec], 1             ; tv_sec = 1 секунда
    mov qword [timespec + 8], 0         ; tv_nsec = 0 наносекунд

; --- Попытка захвата блокировки (повторяется при занятости) ---
.try_acquire:
    ; syscall: open(filename, flags, mode)
    ; Атомарная попытка создания lock-файла
    mov rax, 2
    lea rdi, [lockfile]
    mov rsi, 0o301                      ; Флаги: O_WRONLY | O_CREAT | O_EXCL
                                        ; O_WRONLY (0o001 = 1)   - только запись
                                        ; O_CREAT  (0o100 = 64)  - создать если нет
                                        ; O_EXCL   (0o200 = 128) - ошибка если есть
                                        ; Сумма: 1 + 64 + 128 = 193 = 0o301
    mov rdx, 0o600                      ; Права доступа: 0600 (rw-------)
                                        ; Только владелец может читать/писать
    syscall
    
    ; Проверка результата операции
    test rax, rax                       ; Проверка: rax < 0? (ошибка)
    js .check_error                     ; Если да - анализ типа ошибки

    ; --- Блокировка успешно захвачена ---
    mov [lock_fd], rax                  ; Сохранение файлового дескриптора
    
    ; Вывод сообщения о захвате блокировки
    ; syscall: write(STDOUT, msg, len)
    mov rax, 1
    mov rdi, 1
    lea rsi, [msg_acquired]
    mov rdx, msg_acquired_len
    syscall

; ============================================================================
; КРИТИЧЕСКАЯ СЕКЦИЯ
; ============================================================================
; Здесь выполняется работа, требующая эксклюзивного доступа к ресурсу.
; Примеры:
;   - Обработка общего файла данных
;   - Запись в лог без гонок (race conditions)
;   - Обновление shared memory
;
; В этом демо добавим sleep 5 секунд, чтобы успеть запустить второй процесс
; ============================================================================

    ; === НАЧАЛО КРИТИЧЕСКОЙ СЕКЦИИ ===
    
    ; Ждём 5 секунд (для демонстрации блокировки)
    mov qword [timespec], 5             ; tv_sec = 5 секунд
    mov rax, 35                         ; sys_nanosleep
    lea rdi, [timespec]
    xor rsi, rsi
    syscall
    
    ; === КОНЕЦ КРИТИЧЕСКОЙ СЕКЦИИ ===

    ; --- Освобождение блокировки ---
    ; Шаг 1: Закрытие файлового дескриптора
    ; syscall: close(fd)
    mov rax, 3
    mov rdi, [lock_fd]
    syscall
    
    ; Шаг 2: Удаление lock-файла из файловой системы
    ; syscall: unlink(filename)
    mov rax, 87
    lea rdi, [lockfile]
    syscall
    
    ; Переход к завершению программы
    jmp .exit

; ============================================================================
; ОБРАБОТКА ОШИБОК ЗАХВАТА БЛОКИРОВКИ
; ============================================================================

.check_error:
    ; Проверка типа ошибки: EEXIST (файл уже существует) или другая
    cmp rax, -17                        ; EEXIST = 17, возвращается как -17
    jne .exit                           ; Если другая ошибка - немедленный выход
    
    ; --- Блокировка занята другим процессом ---
    ; Вывод информационного сообщения
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    lea rsi, [msg_busy]
    mov rdx, msg_busy_len
    syscall
    
    ; Ожидание перед повторной попыткой
    ; syscall: nanosleep(req, rem)
    mov qword [timespec], 1             ; tv_sec = 1 секунда (восстановить)
    mov rax, 35
    lea rdi, [timespec]
    xor rsi, rsi                        ; rem = NULL (не нужно остаток времени)
    syscall
    
    ; Повторная попытка захвата блокировки
    jmp .try_acquire

; ============================================================================
; ЗАВЕРШЕНИЕ ПРОГРАММЫ
; ============================================================================

.exit:
    ; syscall: exit(0)
    mov rax, 60
    xor rdi, rdi
    syscall

Применение: Гарантия, что только один экземпляр программы работает одновременно (демоны, cron-задачи).


read — Чтение Данных из Файла

Регистры:

rax = 0                     ; Номер системного вызова
rdi = fd                    ; Файловый дескриптор
rsi = buffer                ; Адрес буфера для размещения данных
rdx = count                 ; Максимальное количество байт

Возвращаемое значение:

  • rax > 0 — количество реально прочитанных байт
  • rax = 0 — достигнут конец файла (EOF)
  • rax < 0 — ошибка (отрицательный errno)

Критическая Особенность: Частичное Чтение

read НЕ гарантирует чтение всех запрошенных байт. Это происходит из-за:

  • Достижения конца файла
  • Прерывания сигналом (EINTR)
  • Особенностей устройства (терминалы, сокеты)
❌ Неправильно
; Предполагаем полное чтение
mov rax, 0
mov rdi, [fd]
lea rsi, [rel buffer]
mov rdx, 1000
syscall

; rax может быть: 100, 500, 1000
; или ЛЮБОЕ другое значение  1000!
✅ Правильно
read_exact:
    ; rdi=fd, rsi=buffer, rdx=count
    mov r12, rsi        ; текущий указатель
    mov r13, rdx        ; осталось
.loop:
    mov rax, 0
    mov rdi, [fd]
    mov rsi, r12
    mov rdx, r13
    syscall
    
    test rax, rax
    jle .error          ; ≤ 0: ошибка/EOF
    
    add r12, rax        ; сдвиг
    sub r13, rax        ; уменьшение
    jnz .loop           ; повтор
    ret
.error:
    ret

Файловая позиция: После read позиция автоматически сдвигается на количество прочитанных байт.


write — Запись Данных в Файл

Регистры:

rax = 1                     ; Номер системного вызова
rdi = fd                    ; Файловый дескриптор
rsi = buffer                ; Адрес данных для записи
rdx = count                 ; Количество байт для записи

Возвращаемое значение:

  • rax > 0 — количество реально записанных байт
  • rax < 0 — ошибка (отрицательный errno)

Особенности:

  1. Частичная запись — аналогично read, может записать меньше данных
  2. Буферизация ядра — данные попадают в кеш страниц, физическая запись позже
  3. Синхронизация — используйте fsync(fd) для гарантированного сохранения на диск

lseek — Управление Позицией в Файле

Регистры:

rax = 8                     ; Номер системного вызова
rdi = fd                    ; Файловый дескриптор
rsi = offset                ; Смещение (может быть отрицательным)
rdx = whence                ; Точка отсчёта

Точки отсчёта:

SEEK_SET equ 0              ; От начала файла
SEEK_CUR equ 1              ; От текущей позиции
SEEK_END equ 2              ; От конца файла

Возвращаемое значение: rax = новая позиция от начала файла (или < 0 при ошибке)

Получение размера файла
get_file_size:
    ; Сохранить позицию
    mov rax, 8
    mov rdi, [fd]
    xor rsi, rsi
    mov rdx, 1          ; SEEK_CUR
    syscall
    push rax
    
    ; Перейти в конец
    mov rax, 8
    mov rdi, [fd]
    xor rsi, rsi
    mov rdx, 2          ; SEEK_END
    syscall
    mov rbx, rax        ; размер
    
    ; Восстановить
    pop rsi
    mov rax, 8
    mov rdi, [fd]
    xor rdx, rdx        ; SEEK_SET
    syscall
    
    mov rax, rbx
    ret
Переход к N-й записи
; RECORD_SIZE должен быть определён
seek_to_record:
    ; rdi=fd, rsi=record_index
    push rbx
    
    mov rbx, rsi
    imul rbx, RECORD_SIZE
    
    mov rax, 8
    mov rdi, [fd]
    mov rsi, rbx
    xor rdx, rdx        ; SEEK_SET
    syscall
    
    pop rbx
    ret

; Использование:
; mov rdi, [fd]
; mov rsi, 42      ; 42-я запись
; call seek_to_record
; ; Теперь можно читать

close — Освобождение Файлового Дескриптора

Регистры:

rax = 3                     ; Номер системного вызова
rdi = fd                    ; Файловый дескриптор

Возвращаемое значение: rax = 0 (успех) или < 0 (ошибка)

Что происходит:

  1. Освобождение номера FD
  2. Декремент счётчика ссылок
  3. Если последний FD: сброс буферов на диск
❌ Утечка ресурсов
bad_function:
    mov rax, 2
    lea rdi, [rel file]
    xor rsi, rsi
    syscall
    mov [fd], rax
    
    ; Работа с файлом
    
    ret  ; ЗАБЫЛИ ЗАКРЫТЬ!

После 1000+ вызовов процесс исчерпает лимит FD

✅ Корректное управление
good_function:
    mov rax, 2
    lea rdi, [rel file]
    xor rsi, rsi
    syscall
    test rax, rax
    js .error
    mov [fd], rax
    
    ; Работа
    
    mov rax, 3      ; ВСЕГДА закрываем
    mov rdi, [fd]
    syscall
    ret
.error:
    ret

Детерминированное освобождение ресурсов

🧩 3. Паттерн fd-as-parameter и Dependency Injection

Почему “файл внутри функции” — антипаттерн

❌ Плохо: open() внутри функции
read_number_from_file:
    ; Функция сама открывает файл
    mov rax, 2
    lea rdi, [rel filename]
    xor rsi, rsi
    syscall
    
    ; Жёстко привязана к одному файлу
    ; Нельзя подставить stdin или пайп
    ; Невозможно тестировать

Проблемы:

  • Жёсткая привязка к конкретному файлу
  • Нельзя подставить пайп, stdin или сокет
  • Невозможно тестировать изолированно
  • Утечка FD при ошибке внутри функции
✅ Хорошо: fd как параметр
read_number_from_stream:
    ; Вход: rdi = fd (файловый дескриптор)
    ; Функция НЕ открывает, просто читает
    
    ; Работает с любым источником:
    ; - файлом (fd = 3)
    ; - stdin (fd = 0)
    ; - пайпом (fd = 0 при перенаправлении)
    ; - сокетом (fd = 5)

Преимущества:

  • Универсальность: один код для всех источников
  • Тестируемость: подставить mock-fd в тестах
  • Гибкость: программист управляет ресурсом
  • Безопасность: вызывающий отвечает за close()

Принцип Dependency Injection:

  • Зависимость (файловый дескриптор) передаётся извне, а не создаётся внутри функции
  • Функция становится тестируемой: можно подставить любой источник данных
  • Функция становится переиспользуемой: один код для файлов, пайпов, сокетов

🔗 См. также: Концепция передачи параметров через регистры подробно разобрана в руководстве «Связь C и NASM: Конвенции вызовов (ABI) и Оптимизация», где показаны практические примеры использования calling conventions.


Как и зачем передавать fd через параметры

Регистровое соглашение (x86-64 System V):

Регистр Назначение
rdi 1-й параметр (обычно fd)
rsi 2-й параметр (обычно адрес буфера)
rdx 3-й параметр (обычно размер)
rax Возвращаемое значение

Типичная сигнатура читающей функции:

; read_int16_stream(rdi=fd, rsi=&result) → rax
; 
; Вход:
;   rdi = файловый дескриптор
;   rsi = адрес переменной для результата
;
; Выход:
;   rax > 0: успех, число прочитано
;   rax = 0: EOF
;   rax < 0: ошибка (отрицательный код)

Унификация для файлов, пайпов, stdin/out и тестов

┌─────────────────────────────────────────────────────────────┐
│  ПРИМЕР 1: Чтение из файла                                  │
├─────────────────────────────────────────────────────────────┤
│  mov rax, 2                   ; sys_open                    │
│  lea rdi, [filename]                                        │
│  xor rsi, rsi                 ; O_RDONLY                    │
│  syscall                                                    │
│  mov r15, rax                 ; r15 = fd                    │
│                                                             │
│  mov rdi, r15                 ; Передаём fd в функцию       │
│  lea rsi, [result]                                          │
│  call read_int16_stream       ; ← Читаем из файла!          │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  ПРИМЕР 2: Чтение из пайпа                                  │
├─────────────────────────────────────────────────────────────┤
│  ; В тесте: echo "12345" | ./program                        │
│                                                             │
│  xor rdi, rdi                 ; fd = 0 (stdin = пайп)       │
│  lea rsi, [result]                                          │
│  call read_int16_stream       ; ← Читаем из пайпа!          │
│                                                             │
│  ; Программа не знает, откуда данные — stdin или пайп       │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  ПРИМЕР 3: Автоматическое тестирование                      │
├─────────────────────────────────────────────────────────────┤
│  ; test_runner.sh:                                          │
│  echo "100 200 300" > test_input.txt                        │
│  ./program < test_input.txt > test_output.txt               │
│  diff test_output.txt expected_output.txt                   │
│                                                             │
│  ; Программа использует read_int16_stream с fd=0            │
│  ; Shell подменяет stdin на файл — программа не меняется!   │
└─────────────────────────────────────────────────────────────┘

📌 Заметка: В примерах выше filename — строковая константа в секции .data. Если имя файла передаётся при запуске программы (например, ./program data.txt), используйте доступ к argv — подробности в статье «Аргументы командной строки в NASM: Работа с argc и argv».

Принцип Dependency Injection:

  • Зависимость (файловый дескриптор) передаётся извне, а не создаётся внутри функции
  • Функция становится тестируемой: можно подставить любой источник данных
  • Функция становится переиспользуемой: один код для файлов, пайпов, сокетов

🎓 4. Интеграция с модулями ввода-вывода

Все три модуля (io_signed.asm, io_unsigned.asm, io_float.asm) изначально реализуют двухслойную архитектуру (Layer 1: парсинг, Layer 2: интерактивный I/O). Полные исходники этих модулей доступны в разделе «📦 2. Исходники модулей» статьи «Модули ввода-вывода NASM x86-64 без libc».

Для добавления поддержки файлового ввода-вывода (Layer 3) скопируйте готовый код из соответствующего <details>-блока ниже и вставьте его в свой модуль.


Расширение io_signed.asm

Модуль для знаковых 16-битных целых чисел (int16_t). После интеграции Layer 3 вы сможете читать числа из файлов и записывать 32-битные результаты.

📄 Полный код расширения Layer 3 для io_signed.asm

Шаг 1: Добавьте в секцию .bss

    ; Буферы файлового режима (Слой 3)
    io_file_buffer resb 32              ; Буфер для файловых операций
    io_file_char_buf resb 1             ; Буфер для одного символа
    io_file_stream_buffer resb 128      ; Буфер потокового чтения
    io_file_stream_pos resq 1           ; Позиция в потоковом буфере
    io_file_stream_size resq 1          ; Размер данных в потоке

Шаг 2: Добавьте в секцию .text (после существующих global)

    global read_int16_from_fd       ; Слой 3: чтение из файла
    global read_int16_stream        ; Слой 3: потоковое чтение
    global write_int32_to_fd        ; Слой 3: запись в файл

Шаг 3: Вставьте в конец файла (после Layer 2)

; ============================================================================
; СЛОЙ 3: ФАЙЛОВЫЙ ВВОД-ВЫВОД
; ============================================================================

; ----------------------------------------------------------------------------
; read_int16_from_fd
;
; Читает одно знаковое 16-битное целое число из файлового дескриптора.
; Подходит для однократного чтения из файлов.
;
; ПРИМЕНЕНИЕ:
;   • Чтение конфигурационных файлов
;   • Однократная загрузка значений
;   • Простые случаи без необходимости потоковой обработки
;
; ОГРАНИЧЕНИЯ:
;   • Максимальная длина строки: 31 символ
;   • Не оптимально для множественных чтений (используйте read_int16_stream)
;
; ДИАПАЗОН: -32768 до 32767
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     int fd = open("config.txt", O_RDONLY);
;     int16_t value;
;     ssize_t result = read_int16_from_fd(fd, &value);
;     if (result > 0) {
;       // value содержит число
;     }
;     close(fd);
;
;   Assembly:
;     ; Открыть файл
;     mov rax, 2              ; sys_open
;     lea rdi, [filename]
;     xor rsi, rsi            ; O_RDONLY
;     syscall
;     mov r15, rax            ; Сохранить FD
;
;     ; Прочитать число
;     mov rdi, r15
;     lea rsi, [my_value]
;     call read_int16_from_fd
;
;     ; Закрыть файл
;     mov rax, 3              ; sys_close
;     mov rdi, r15
;     syscall
;
; @param  rdi  Файловый дескриптор (int fd)
;         rsi  Адрес переменной для результата (int16_t *)
; @return rax  >0 = количество прочитанных байт
;              0  = EOF (конец файла)
;              -1 = ошибка I/O (чтение из файла)
;              -2 = ошибка формата (некорректное число)
;              -3 = переполнение (выход за границы int16_t)
;              -4 = переполнение буфера (строка > 30 символов)
; @uses   rbx, rdx, r12, r13
; @calls  parse_int16
;
; @see read_int16_stream - для множественных чтений
; @see parse_int16 - базовая функция парсинга
; ----------------------------------------------------------------------------
read_int16_from_fd:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13

    mov r12, rdi                ; Сохранение файлового дескриптора
    mov r13, rsi                ; Сохранение адреса результата

    ; --- Чтение данных из файла ---
    ; syscall: read(fd, buffer, count)
    xor rax, rax
    mov rdi, r12
    lea rsi, [io_file_buffer]   ; Адрес буфера
    mov rdx, 31
    syscall

    test rax, rax               ; Проверка результата
    js .io_error                ; rax < 0 → ошибка I/O
    jz .eof                     ; rax = 0 → EOF (конец файла)

    mov rbx, rax                ; Сохранение количества прочитанных байт

    ; --- Добавление null-терминатора ---
    lea rdx, [io_file_buffer]   ; Загрузка базового адреса буфера
    mov byte [rdx + rax], 0     ; Установка null-терминатора после данных

    ; --- Парсинг прочитанной строки ---
    lea rdi, [io_file_buffer]   ; Адрес строки для парсинга
    call parse_int16            ; Вызов функции парсинга

    test rdx, rdx               ; Проверка кода ошибки (0 = успех)
    jnz .parse_error            ; rdx != 0 → ошибка парсинга

    ; --- Сохранение результата ---
    mov word [r13], ax          ; Сохранение распарсенного значения (16 бит)
    mov rax, rbx                ; Возврат количества прочитанных байт
    jmp .return

; --- Обработка ошибок ---
.io_error:
    mov rax, -1                 ; Код ошибки I/O
    jmp .return

.eof:
    xor rax, rax                ; Код EOF (0)
    jmp .return

.parse_error:
    ; Преобразование кода ошибки парсинга: 1,2,3 → -2,-3,-4
    mov rax, rdx                ; Копирование кода ошибки
    neg rax                     ; Инвертирование знака
    dec rax                     ; Декремент (1→-2, 2→-3, 3→-4)

.return:
    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

; ----------------------------------------------------------------------------
; read_int16_stream
;
; Читает одно знаковое 16-битное целое число из потока с буферизацией.
; Идеально для последовательного чтения нескольких чисел из файла.
; Пропускает ведущие whitespace символы (пробелы, табы, LF, CR).
;
; ПРИМЕНЕНИЕ:
;   • Парсинг файлов с множественными значениями
;   • Обработка потоков данных
;   • Эффективное чтение из больших файлов
;
; ОСОБЕННОСТИ:
;   • Внутренняя буферизация 128 байт минимизирует syscall
;   • Автоматический пропуск whitespace между числами
;   • Сохраняет позицию для следующего вызова
;
; ДИАПАЗОН: -32768 до 32767
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   Чтение файла с числами:
;     numbers.txt: "100 -200 300\n400 500"
;
;     int fd = open("numbers.txt", O_RDONLY);
;     int16_t values[5];
;     for (int i = 0; i < 5; i++) {
;       int result = read_int16_stream(fd, &values[i]);
;       if (result != 1) break;
;     }
;     close(fd);
;
;   Assembly (читаем 3 числа):
;     mov rax, 2
;     lea rdi, [filename]
;     xor rsi, rsi
;     syscall
;     mov r15, rax
;
;     ; Чтение первого числа
;     mov rdi, r15
;     lea rsi, [num1]
;     call read_int16_stream
;
;     ; Чтение второго числа
;     mov rdi, r15
;     lea rsi, [num2]
;     call read_int16_stream
;
;     ; Чтение третьего числа
;     mov rdi, r15
;     lea rsi, [num3]
;     call read_int16_stream
;
;     mov rax, 3
;     mov rdi, r15
;     syscall
;
; @param  rdi  Файловый дескриптор (int fd)
;         rsi  Адрес переменной для результата (int16_t *)
; @return rax  1  = успех (число прочитано)
;              0  = EOF (конец файла)
;              -1 = ошибка I/O
;              -2 = ошибка формата
;              -3 = переполнение
;              -4 = переполнение буфера
; @uses   rbx, rdx, r12, r13, r14
; @calls  parse_int16, .read_char (внутренняя)
;
; @complexity O(n), где n - длина числа
; @memory     Использует статический буфер 128 байт
;
; @see read_int16_from_fd - для однократного чтения
; @see .read_char - внутренняя функция буферизованного чтения символа
; ----------------------------------------------------------------------------
read_int16_stream:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13
    push r14

    mov r12, rdi                ; Сохранение файлового дескриптора
    mov r14, rsi                ; Сохранение адреса результата

    lea r13, [io_file_buffer]   ; r13 = база для записи числа
    xor rbx, rbx                ; rbx = позиция записи (начинаем с 0)

; --- Пропуск начальных whitespace символов ---
.skip_whitespace:
    call .read_char             ; Чтение одного символа
    test rax, rax               ; Проверка результата
    jle .handle_read_error      ; Если <= 0 (ошибка или EOF) - обработка

    movzx rax, byte [io_file_char_buf] ; Загрузка прочитанного символа

    ; Проверка whitespace: ' ', '\t', '\n', '\r'
    cmp al, ' '                 ; Проверка: пробел?
    je .skip_whitespace         ; Если да - пропускаем
    cmp al, 10                  ; Проверка: LF?
    je .skip_whitespace         ; Если да - пропускаем
    cmp al, 9                   ; Проверка: TAB?
    je .skip_whitespace         ; Если да - пропускаем
    cmp al, 13                  ; Проверка: CR?
    je .skip_whitespace         ; Если да - пропускаем

    ; --- Первый значащий символ - начало числа ---
    mov byte [r13 + rbx], al    ; Сохранение первого символа числа
    inc rbx                     ; Увеличение позиции записи

; --- Чтение остальных символов числа ---
.read_number:
    cmp rbx, 30                 ; Проверка: буфер переполнен?
    jge .buffer_overflow        ; Если да - ошибка переполнения буфера

    call .read_char             ; Чтение следующего символа
    test rax, rax               ; Проверка результата
    jle .handle_read_error_in_number ; Если <= 0 - обработка

    movzx rax, byte [io_file_char_buf] ; Загрузка прочитанного символа

    ; Проверка разделителей (конец числа)
    cmp al, ' '                 ; Проверка: пробел?
    je .parse                   ; Если да - конец числа, парсим
    cmp al, 10                  ; Проверка: LF?
    je .parse                   ; Если да - конец числа, парсим
    cmp al, 9                   ; Проверка: TAB?
    je .parse                   ; Если да - конец числа, парсим
    cmp al, 13                  ; Проверка: CR?
    je .parse                   ; Если да - конец числа, парсим

    ; --- Продолжение числа (цифра или знак) ---
    mov byte [r13 + rbx], al    ; Сохранение символа
    inc rbx                     ; Увеличение позиции записи
    jmp .read_number            ; Продолжение чтения

; --- Парсинг собранного числа ---
.parse:
    mov byte [r13 + rbx], 0     ; Установка null-терминатора

    lea rdi, [io_file_buffer]   ; Адрес строки для парсинга
    call parse_int16            ; Вызов функции парсинга

    test rdx, rdx               ; Проверка кода ошибки
    jnz .parse_error            ; Если ошибка - обработка

    ; --- Сохранение результата ---
    mov word [r14], ax          ; Сохранение распарсенного значения (16 бит)
    mov rax, 1                  ; Код успеха
    jmp .ret

; --- Обработка ошибок чтения (до начала числа) ---
.handle_read_error:
    test rax, rax               ; Проверка кода возврата
    jz .eof                     ; rax = 0 → EOF
    mov rax, -1                 ; rax < 0 → I/O error
    jmp .ret

; --- Обработка ошибок чтения (внутри числа) ---
.handle_read_error_in_number:
    test rax, rax               ; Проверка кода возврата
    js .io_error                ; rax < 0 → I/O error
    jmp .parse                  ; rax = 0 → EOF, но есть данные для парсинга

; --- Коды возврата ---
.eof:
    xor rax, rax                ; Код EOF (0)
    jmp .ret

.io_error:
    mov rax, -1                 ; Код ошибки I/O
    jmp .ret

.buffer_overflow:
    mov rax, -4                 ; Код ошибки переполнения буфера
    jmp .ret

.parse_error:
    ; Преобразование кода ошибки парсинга: 1,2,3 → -2,-3,-4
    mov rax, rdx                ; Копирование кода ошибки
    neg rax                     ; Инвертирование знака
    dec rax                     ; Декремент

.ret:
    pop r14
    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

; ----------------------------------------------------------------------------
; .read_char (внутренняя функция read_int16_stream)
;
; Читает один символ из файла с использованием буферизации.
; Минимизирует количество системных вызовов read.
;
; АЛГОРИТМ:
;   1. Проверка наличия данных в буфере
;   2. Если буфер пуст: системный вызов read() на 128 байт
;   3. Извлечение одного символа из буфера
;   4. Обновление позиции и размера
;
; БУФЕРИЗАЦИЯ:
;   Буфер: io_file_stream_buffer (128 байт)
;   Позиция: io_file_stream_pos
;   Размер: io_file_stream_size
;
; @param  r12  Файловый дескриптор (сохранён вызывающей функцией)
; @return rax  1 = успех (символ в io_file_char_buf)
;              0 = EOF
;              <0 = ошибка I/O
; @uses   rcx, rdx, rdi, rsi (сохраняются и восстанавливаются)
;
; @complexity O(1) амортизированная (O(n/128) syscall для n символов)
; @internal   Используется только внутри read_int16_stream
; ----------------------------------------------------------------------------
.read_char:
    push rdi
    push rsi
    push rdx
    push rcx

    ; --- Проверка наличия данных в буфере ---
    mov rax, [io_file_stream_size] ; Загрузка количества доступных байт
    test rax, rax                  ; Проверка: есть ли данные в буфере?
    jnz .has_buffered              ; Если да - извлечение из буфера

    ; --- Буфер пуст - системный вызов --- 
    ; syscall: read(fd, buffer, count)
    xor rax, rax
    mov rdi, r12
    lea rsi, [io_file_stream_buffer]
    mov rdx, 128
    syscall

    test rax, rax                     ; Проверка результата
    jle .read_done                    ; Если <= 0 (EOF или ошибка) - завершение

    mov [io_file_stream_size], rax    ; Сохранение количества прочитанных байт
    mov qword [io_file_stream_pos], 0 ; Сброс позиции чтения

; --- Извлечение символа из буфера ---
.has_buffered:
    mov rcx, [io_file_stream_pos]              ; Загрузка текущей позиции чтения
    mov al, byte [io_file_stream_buffer + rcx] ; Чтение символа из буфера
    mov byte [io_file_char_buf], al            ; Сохранение символа в выходной буфер

    inc qword [io_file_stream_pos]   ; Увеличение позиции чтения
    dec qword [io_file_stream_size]  ; Уменьшение количества доступных байт

    mov rax, 1                       ; Код успешного чтения

.read_done:
    pop rcx
    pop rdx
    pop rsi
    pop rdi
    ret

; ----------------------------------------------------------------------------
; write_int32_to_fd
;
; Записывает знаковое 32-битное целое число в файловый дескриптор
; в текстовом виде (десятичный формат) с завершающим символом LF.
; Поддерживает отрицательные числа.
;
; ПРИМЕНЕНИЕ:
;   • Запись результатов вычислений в файл
;   • Экспорт данных в текстовом формате
;   • Логирование числовых значений
;
; ДИАПАЗОН: -2147483648 до 2147483647 (int32_t)
; ФОРМАТ ЗАПИСИ: "<число>\n"
;
; ПРИМЕРЫ:
;   write_int32_to_fd(3, 0)       → "0\n"
;   write_int32_to_fd(3, 12345)   → "12345\n"
;   write_int32_to_fd(3, -98765)  → "-98765\n"
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     int fd = open("output.txt", O_WRONLY | O_CREAT, 0644);
;     write_int32_to_fd(fd, -12345);
;     write_int32_to_fd(fd, 67890);
;     close(fd);
;     // Файл содержит: "-12345\n67890\n"
;
;   Assembly:
;     ; Открыть файл для записи
;     mov rax, 2              ; sys_open
;     lea rdi, [filename]
;     mov rsi, 0x241          ; O_WRONLY | O_CREAT
;     mov rdx, 0x1A4          ; 0644
;     syscall
;     mov r15, rax            ; Сохранить FD
;
;     ; Записать число
;     mov rdi, r15
;     mov esi, -12345
;     call write_int32_to_fd
;
;     ; Закрыть файл
;     mov rax, 3              ; sys_close
;     mov rdi, r15
;     syscall
;
; @param  rdi  Файловый дескриптор (int fd)
;         esi  Значение для записи (int32_t)
; @return rax  Количество записанных байт (или код ошибки от write)
; @uses   rbx, rdx, r12, r13, r14
;
; @complexity O(log₁₀(n)), где n - абсолютное значение числа
; @memory     O(1), использует фиксированный буфер 32 байта
; ----------------------------------------------------------------------------
write_int32_to_fd:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13
    push r14

    mov r14, rdi                ; Сохранение файлового дескриптора
    movsxd rax, esi             ; Знаковое расширение int32 → int64

    ; --- Формирование строки справа налево в io_file_buffer ---
    lea r13, [io_file_buffer + 31] ; r13 = указатель на конец буфера
    mov byte [r13], 10             ; Установка LF (символ новой строки) в конце
    dec r13                        ; Смещение указателя назад

    ; --- Специальная обработка нуля ---
    test rax, rax               ; Проверка: число равно нулю?
    jnz .check_sign             ; Если нет - обработка знака
    mov byte [r13], '0'         ; Запись символа '0'
    jmp .write                  ; Переход к записи в файл

; --- Обработка знака ---
.check_sign:
    xor rbx, rbx                ; rbx = флаг знака (0 по умолчанию)
    test rax, rax               ; Проверка знака числа (установка флагов)
    jge .convert                ; Если >= 0 - переход к конвертации
    neg rax                     ; Взятие модуля (двухкомплементное отрицание)
    mov rbx, 1                  ; Установка флага отрицательного числа

; --- Конвертация числа в строку (справа налево) ---
; Инвариант цикла: r13 указывает на следующую позицию для записи
.convert:
    dec r13                     ; Смещение указателя назад
    xor rdx, rdx                ; Обнуление rdx перед делением
    mov r12, 10                 ; Делитель = 10 (основание системы счисления)
    div r12                     ; rax = rax / 10, rdx = rax % 10 (остаток)
    add dl, '0'                 ; Преобразование цифры (0-9) в ASCII ('0'-'9')
    mov [r13], dl               ; Запись ASCII-символа в буфер
    test rax, rax               ; Проверка: остались ли ещё цифры?
    jnz .convert                ; Если да - продолжение конвертации

    ; --- Добавление знака минус ---
    test rbx, rbx               ; Проверка флага отрицательного числа
    jz .write                   ; Если положительное - переход к записи
    dec r13                     ; Смещение указателя назад
    mov byte [r13], '-'         ; Запись символа минуса

; --- Запись строки в файл ---
.write:
    ; syscall: write(fd, string, length)
    mov rax, 1
    mov rdi, r14
    mov rsi, r13                    ; Адрес начала строки
    lea rdx, [io_file_buffer + 32]  ; Адрес конца буфера
    sub rdx, r13                    ; Вычисление длины: конец - начало
    syscall

    pop r14
    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

Расширение io_unsigned.asm

Модуль для беззнаковых 16-битных целых чисел (uint16_t).

📄 Полный код расширения Layer 3 для io_unsigned.asm

Шаг 1: Добавьте в секцию .bss

    ; Буферы файлового режима (Слой 3)
    io_file_buffer resb 32              ; Буфер для файловых операций
    io_file_char_buf resb 1             ; Буфер для одного символа
    io_file_stream_buffer resb 128      ; Буфер потокового чтения
    io_file_stream_pos resq 1           ; Позиция в потоковом буфере
    io_file_stream_size resq 1          ; Размер данных в потоке

Шаг 2: Добавьте в секцию .text (после существующих global)

    global read_uint16_from_fd      ; Слой 3: чтение из файла
    global read_uint16_stream       ; Слой 3: потоковое чтение
    global write_uint32_to_fd       ; Слой 3: запись в файл

Шаг 3: Вставьте в конец файла (после Layer 2)

; ============================================================================
; СЛОЙ 3: ФАЙЛОВЫЙ ВВОД-ВЫВОД
; ============================================================================

; ----------------------------------------------------------------------------
; read_uint16_from_fd
;
; Читает одно беззнаковое 16-битное целое число из файлового дескриптора.
; Подходит для однократного чтения из файлов.
;
; ПРИМЕНЕНИЕ:
;   • Чтение конфигурационных файлов
;   • Однократная загрузка значений
;   • Простые случаи без необходимости потоковой обработки
;
; ОГРАНИЧЕНИЯ:
;   • Максимальная длина строки: 31 символ
;   • Не оптимально для множественных чтений (используйте read_uint16_stream)
;
; ДИАПАЗОН: 0 до 65535
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     int fd = open("config.txt", O_RDONLY);
;     uint16_t value;
;     ssize_t result = read_uint16_from_fd(fd, &value);
;     if (result > 0) {
;       // value содержит число
;     }
;     close(fd);
;
;   Assembly:
;     ; Открыть файл
;     mov rax, 2              ; sys_open
;     lea rdi, [filename]
;     xor rsi, rsi            ; O_RDONLY
;     syscall
;     mov r15, rax            ; Сохранить FD
;
;     ; Прочитать число
;     mov rdi, r15
;     lea rsi, [my_value]
;     call read_uint16_from_fd
;
;     ; Закрыть файл
;     mov rax, 3              ; sys_close
;     mov rdi, r15
;     syscall
;
; @param  rdi  Файловый дескриптор (int fd)
;         rsi  Адрес переменной для результата (uint16_t *)
; @return rax  >0 = количество прочитанных байт
;              0  = EOF (конец файла)
;              -1 = ошибка I/O (чтение из файла)
;              -2 = ошибка формата (некорректное число)
;              -3 = переполнение (выход за границы uint16_t)
;              -4 = переполнение буфера (строка > 30 символов)
; @uses   rbx, rdx, r12, r13
; @calls  parse_uint16
;
; @see read_uint16_stream - для множественных чтений
; @see parse_uint16 - базовая функция парсинга
; ----------------------------------------------------------------------------
read_uint16_from_fd:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13

    mov r12, rdi                ; Сохранение файлового дескриптора
    mov r13, rsi                ; Сохранение адреса результата

    ; --- Чтение данных из файла ---
    ; syscall: read(fd, buffer, count)
    xor rax, rax
    mov rdi, r12
    lea rsi, [io_file_buffer]
    mov rdx, 31
    syscall

    test rax, rax               ; Проверка результата
    js .io_error                ; rax < 0 → ошибка I/O
    jz .eof                     ; rax = 0 → EOF (конец файла)

    mov rbx, rax                ; Сохранение количества прочитанных байт

    ; --- Добавление null-терминатора ---
    lea rdx, [io_file_buffer]   ; Загрузка базового адреса буфера
    mov byte [rdx + rax], 0     ; Установка null-терминатора после данных

    ; --- Парсинг прочитанной строки ---
    lea rdi, [io_file_buffer]   ; Адрес строки для парсинга
    call parse_uint16           ; Вызов функции парсинга

    test rdx, rdx               ; Проверка кода ошибки (0 = успех)
    jnz .parse_error            ; rdx != 0 → ошибка парсинга

    ; --- Сохранение результата ---
    mov word [r13], ax          ; Сохранение распарсенного значения (16 бит)
    mov rax, rbx                ; Возврат количества прочитанных байт
    jmp .return

; --- Обработка ошибок ---
.io_error:
    mov rax, -1                 ; Код ошибки I/O
    jmp .return

.eof:
    xor rax, rax                ; Код EOF (0)
    jmp .return

.parse_error:
    ; Преобразование кода ошибки парсинга: 1,2,3 → -2,-3,-4
    mov rax, rdx                ; Копирование кода ошибки
    neg rax                     ; Инвертирование знака
    dec rax                     ; Декремент (1→-2, 2→-3, 3→-4)

.return:
    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

; ----------------------------------------------------------------------------
; read_uint16_stream
;
; Читает одно беззнаковое 16-битное целое число из потока с буферизацией.
; Идеально для последовательного чтения нескольких чисел из файла.
; Пропускает ведущие whitespace символы (пробелы, табы, LF, CR).
;
; ПРИМЕНЕНИЕ:
;   • Парсинг файлов с множественными значениями
;   • Обработка потоков данных
;   • Эффективное чтение из больших файлов
;
; ОСОБЕННОСТИ:
;   • Внутренняя буферизация 128 байт минимизирует syscall
;   • Автоматический пропуск whitespace между числами
;   • Сохраняет позицию для следующего вызова
;
; ДИАПАЗОН: 0 до 65535
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   Чтение файла с числами:
;     numbers.txt: "100 200 300\n400 500"
;
;     int fd = open("numbers.txt", O_RDONLY);
;     uint16_t values[5];
;     for (int i = 0; i < 5; i++) {
;       int result = read_uint16_stream(fd, &values[i]);
;       if (result != 1) break;
;     }
;     close(fd);
;
;   Assembly (читаем 3 числа):
;     mov rax, 2
;     lea rdi, [filename]
;     xor rsi, rsi
;     syscall
;     mov r15, rax
;
;     ; Чтение первого числа
;     mov rdi, r15
;     lea rsi, [num1]
;     call read_uint16_stream
;
;     ; Чтение второго числа
;     mov rdi, r15
;     lea rsi, [num2]
;     call read_uint16_stream
;
;     ; Чтение третьего числа
;     mov rdi, r15
;     lea rsi, [num3]
;     call read_uint16_stream
;
;     mov rax, 3
;     mov rdi, r15
;     syscall
;
; @param  rdi  Файловый дескриптор (int fd)
;         rsi  Адрес переменной для результата (uint16_t *)
; @return rax  1  = успех (число прочитано)
;              0  = EOF (конец файла)
;              -1 = ошибка I/O
;              -2 = ошибка формата
;              -3 = переполнение
;              -4 = переполнение буфера
; @uses   rbx, rdx, r12, r13, r14
; @calls  parse_uint16, .read_char (внутренняя)
;
; @complexity O(n), где n - длина числа
; @memory     Использует статический буфер 128 байт
;
; @see read_uint16_from_fd - для однократного чтения
; @see .read_char - внутренняя функция буферизованного чтения символа
; ----------------------------------------------------------------------------
read_uint16_stream:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13
    push r14

    mov r12, rdi                ; Сохранение файлового дескриптора
    mov r14, rsi                ; Сохранение адреса результата

    lea r13, [io_file_buffer]   ; r13 = база для записи числа
    xor rbx, rbx                ; rbx = позиция записи (начинаем с 0)

; --- Пропуск начальных whitespace символов ---
.skip_whitespace:
    call .read_char             ; Чтение одного символа
    test rax, rax               ; Проверка результата
    jle .handle_read_error      ; Если <= 0 (ошибка или EOF) - обработка

    movzx rax, byte [io_file_char_buf] ; Загрузка прочитанного символа

    ; Проверка whitespace: ' ', '\t', '\n', '\r'
    cmp al, ' '                 ; Проверка: пробел?
    je .skip_whitespace         ; Если да - пропускаем
    cmp al, 10                  ; Проверка: LF?
    je .skip_whitespace         ; Если да - пропускаем
    cmp al, 9                   ; Проверка: TAB?
    je .skip_whitespace         ; Если да - пропускаем
    cmp al, 13                  ; Проверка: CR?
    je .skip_whitespace         ; Если да - пропускаем

    ; --- Первый значащий символ - начало числа ---
    mov byte [r13 + rbx], al    ; Сохранение первого символа числа
    inc rbx                     ; Увеличение позиции записи

; --- Чтение остальных символов числа ---
.read_number:
    cmp rbx, 30                 ; Проверка: буфер переполнен?
    jge .buffer_overflow        ; Если да - ошибка переполнения буфера

    call .read_char                    ; Чтение следующего символа
    test rax, rax                      ; Проверка результата
    jle .handle_read_error_in_number   ; Если <= 0 - обработка

    movzx rax, byte [io_file_char_buf] ; Загрузка прочитанного символа

    ; Проверка разделителей (конец числа)
    cmp al, ' '                 ; Проверка: пробел?
    je .parse                   ; Если да - конец числа, парсим
    cmp al, 10                  ; Проверка: LF?
    je .parse                   ; Если да - конец числа, парсим
    cmp al, 9                   ; Проверка: TAB?
    je .parse                   ; Если да - конец числа, парсим
    cmp al, 13                  ; Проверка: CR?
    je .parse                   ; Если да - конец числа, парсим

    ; --- Продолжение числа (цифра или знак) ---
    mov byte [r13 + rbx], al    ; Сохранение символа
    inc rbx                     ; Увеличение позиции записи
    jmp .read_number            ; Продолжение чтения

; --- Парсинг собранного числа ---
.parse:
    mov byte [r13 + rbx], 0     ; Установка null-терминатора

    lea rdi, [io_file_buffer]   ; Адрес строки для парсинга
    call parse_uint16           ; Вызов функции парсинга

    test rdx, rdx               ; Проверка кода ошибки
    jnz .parse_error            ; Если ошибка - обработка

    ; --- Сохранение результата ---
    mov word [r14], ax          ; Сохранение распарсенного значения (16 бит)
    mov rax, 1                  ; Код успеха
    jmp .ret

; --- Обработка ошибок чтения (до начала числа) ---
.handle_read_error:
    test rax, rax               ; Проверка кода возврата
    jz .eof                     ; rax = 0 → EOF
    mov rax, -1                 ; rax < 0 → I/O error
    jmp .ret

; --- Обработка ошибок чтения (внутри числа) ---
.handle_read_error_in_number:
    test rax, rax               ; Проверка кода возврата
    js .io_error                ; rax < 0 → I/O error
    jmp .parse                  ; rax = 0 → EOF, но есть данные для парсинга

; --- Коды возврата ---
.eof:
    xor rax, rax                ; Код EOF (0)
    jmp .ret

.io_error:
    mov rax, -1                 ; Код ошибки I/O
    jmp .ret

.buffer_overflow:
    mov rax, -4                 ; Код ошибки переполнения буфера
    jmp .ret

.parse_error:
    ; Преобразование кода ошибки парсинга: 1,2,3 → -2,-3,-4
    mov rax, rdx                ; Копирование кода ошибки
    neg rax                     ; Инвертирование знака
    dec rax                     ; Декремент

.ret:
    pop r14
    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

; ----------------------------------------------------------------------------
; .read_char (внутренняя функция read_uint16_stream)
;
; Читает один символ из файла с использованием буферизации.
; Минимизирует количество системных вызовов read.
;
; АЛГОРИТМ:
;   1. Проверка наличия данных в буфере
;   2. Если буфер пуст: системный вызов read() на 128 байт
;   3. Извлечение одного символа из буфера
;   4. Обновление позиции и размера
;
; БУФЕРИЗАЦИЯ:
;   Буфер: io_file_stream_buffer (128 байт)
;   Позиция: io_file_stream_pos
;   Размер: io_file_stream_size
;
; @param  r12  Файловый дескриптор (сохранён вызывающей функцией)
; @return rax  1 = успех (символ в io_file_char_buf)
;              0 = EOF
;              <0 = ошибка I/O
; @uses   rcx, rdx, rdi, rsi (сохраняются и восстанавливаются)
;
; @complexity O(1) амортизированная (O(n/128) syscall для n символов)
; @internal   Используется только внутри read_uint16_stream
; ----------------------------------------------------------------------------
.read_char:
    push rdi
    push rsi
    push rdx
    push rcx

    ; --- Проверка наличия данных в буфере ---
    mov rax, [io_file_stream_size]    ; Загрузка количества доступных байт
    test rax, rax                     ; Проверка: есть ли данные в буфере?
    jnz .has_buffered                 ; Если да - извлечение из буфера

    ; --- Буфер пуст - системный вызов
    ; syscall: read(fd, buffer, count) ---
    xor rax, rax
    mov rdi, r12
    lea rsi, [io_file_stream_buffer]
    mov rdx, 128
    syscall

    test rax, rax                     ; Проверка результата
    jle .read_done                    ; Если <= 0 (EOF или ошибка) - завершение

    mov [io_file_stream_size], rax    ; Сохранение количества прочитанных байт
    mov qword [io_file_stream_pos], 0 ; Сброс позиции чтения

; --- Извлечение символа из буфера ---
.has_buffered:
    mov rcx, [io_file_stream_pos]              ; Загрузка текущей позиции чтения
    mov al, byte [io_file_stream_buffer + rcx] ; Чтение символа из буфера
    mov byte [io_file_char_buf], al            ; Сохранение символа в выходной буфер

    inc qword [io_file_stream_pos]             ; Увеличение позиции чтения
    dec qword [io_file_stream_size]            ; Уменьшение количества доступных байт

    mov rax, 1                                 ; Код успешного чтения

.read_done:
    pop rcx
    pop rdx
    pop rsi
    pop rdi
    ret

; ----------------------------------------------------------------------------
; write_uint32_to_fd
;
; Записывает беззнаковое 32-битное целое число в файловый дескриптор
; в текстовом виде (десятичный формат) с завершающим символом LF.
;
; ПРИМЕНЕНИЕ:
;   • Запись результатов вычислений в файл
;   • Экспорт данных в текстовом формате
;   • Логирование числовых значений
;
; ДИАПАЗОН: 0 до 4294967295 (uint32_t)
; ФОРМАТ ЗАПИСИ: "<число>\n"
;
; ПРИМЕРЫ:
;   write_uint32_to_fd(3, 0)          → "0\n"
;   write_uint32_to_fd(3, 12345)      → "12345\n"
;   write_uint32_to_fd(3, 4000000000) → "4000000000\n"
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     int fd = open("output.txt", O_WRONLY | O_CREAT, 0644);
;     write_uint32_to_fd(fd, 12345);
;     write_uint32_to_fd(fd, 67890);
;     close(fd);
;     // Файл содержит: "12345\n67890\n"
;
;   Assembly:
;     ; Открыть файл для записи
;     mov rax, 2              ; sys_open
;     lea rdi, [filename]
;     mov rsi, 0x241          ; O_WRONLY | O_CREAT
;     mov rdx, 0x1A4          ; 0644
;     syscall
;     mov r15, rax            ; Сохранить FD
;
;     ; Записать число
;     mov rdi, r15
;     mov esi, 12345
;     call write_uint32_to_fd
;
;     ; Закрыть файл
;     mov rax, 3              ; sys_close
;     mov rdi, r15
;     syscall
;
; @param  rdi  Файловый дескриптор (int fd)
;         esi  Значение для записи (uint32_t)
; @return rax  Количество записанных байт (или код ошибки от write)
; @uses   rbx, rdx, r12, r13, r14
;
; @complexity O(log₁₀(n)), где n - значение числа
; @memory     O(1), использует фиксированный буфер 32 байта
; ----------------------------------------------------------------------------
write_uint32_to_fd:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13
    push r14

    mov r14, rdi                ; Сохранение файлового дескриптора
    mov eax, esi                ; Загрузка беззнакового 32-битного числа

    ; --- Формирование строки справа налево в io_file_buffer ---
    lea r13, [io_file_buffer + 31] ; r13 = указатель на конец буфера
    mov byte [r13], 10             ; Установка LF (символ новой строки) в конце
    dec r13                        ; Смещение указателя назад

    ; --- Специальная обработка нуля ---
    test rax, rax               ; Проверка: число равно нулю?
    jnz .convert                ; Если нет - переход к конвертации
    mov byte [r13], '0'         ; Запись символа '0'
    jmp .write                  ; Переход к записи в файл

; --- Конвертация числа в строку (справа налево) ---
; Инвариант цикла: r13 указывает на следующую позицию для записи
.convert:
    dec r13                     ; Смещение указателя назад
    xor rdx, rdx                ; Обнуление rdx перед делением
    mov r12, 10                 ; Делитель = 10 (основание системы счисления)
    div r12                     ; rax = rax / 10, rdx = rax % 10 (остаток - цифра)
    add dl, '0'                 ; Преобразование цифры (0-9) в ASCII ('0'-'9')
    mov [r13], dl               ; Запись ASCII-символа в буфер
    test rax, rax               ; Проверка: остались ли ещё цифры?
    jnz .convert                ; Если да - продолжение конвертации

; --- Запись строки в файл ---
.write:
    ; syscall: write(fd, string, length)
    mov rax, 1
    mov rdi, r14
    mov rsi, r13                   ; Адрес начала строки
    lea rdx, [io_file_buffer + 32] ; Адрес конца буфера
    sub rdx, r13                   ; Вычисление длины: конец - начало
    syscall

    pop r14
    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

Расширение io_float.asm

Модуль для чисел с плавающей точкой (float) и знаковых 32-битных целых (int32_t).

📄 Полный код расширения Layer 3 для io_float.asm

Шаг 1: Добавьте в секцию .bss

    ; Буферы файлового режима (Слой 3)
    io_file_buffer resb 64              ; Буфер для файловых операций
    io_file_char_buf resb 1             ; Буфер для одного символа
    io_file_stream_buffer resb 128      ; Буфер потокового чтения
    io_file_stream_pos resq 1           ; Позиция в потоковом буфере
    io_file_stream_size resq 1          ; Размер данных в потоке

Шаг 2: Добавьте в секцию .text (после существующих global)

    global read_float_from_fd       ; Слой 3: чтение float из файла
    global read_float_stream        ; Слой 3: потоковое чтение float
    global write_float_to_fd        ; Слой 3: запись float в файл
    global read_int32_from_fd_float ; Слой 3: чтение int32 из файла
    global read_int32_stream_float  ; Слой 3: потоковое чтение int32
    global write_int32_to_fd_float  ; Слой 3: запись int32 в файл

Шаг 3: Вставьте в конец файла (после Layer 2 и перед вспомогательными функциями)

; ============================================================================
; СЛОЙ 3: ФАЙЛОВЫЙ ВВОД-ВЫВОД
; ============================================================================

; ----------------------------------------------------------------------------
; read_float_from_fd
;
; Читает одно число с плавающей точкой из файлового дескриптора.
; Подходит для однократного чтения из файлов.
;
; ПРИМЕНЕНИЕ:
;   • Чтение конфигурационных файлов
;   • Однократная загрузка значений
;   • Простые случаи без необходимости потоковой обработки
;
; ОГРАНИЧЕНИЯ:
;   • Максимальная длина строки: 63 символа
;   • Не оптимально для множественных чтений (используйте read_float_stream)
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     int fd = open("config.txt", O_RDONLY);
;     float value;
;     ssize_t result = read_float_from_fd(fd, &value);
;     if (result > 0) {
;       // value содержит число
;     }
;     close(fd);
;
;   Assembly:
;     ; Открыть файл
;     mov rax, 2              ; sys_open
;     lea rdi, [filename]
;     xor rsi, rsi            ; O_RDONLY
;     syscall
;     mov r15, rax            ; Сохранить FD
;
;     ; Прочитать число
;     mov rdi, r15
;     lea rsi, [my_float]
;     call read_float_from_fd
;
;     ; Закрыть файл
;     mov rax, 3              ; sys_close
;     mov rdi, r15
;     syscall
;
; @param  rdi  Файловый дескриптор (int fd)
;         rsi  Адрес переменной для результата (float *)
; @return rax  >0 = количество прочитанных байт
;              0  = EOF (конец файла)
;              -1 = ошибка I/O (чтение из файла)
;              -2 = ошибка формата (некорректное число)
;              -3 = переполнение
; @uses   rbx, rdx, r12, r13
; @calls  parse_float
;
; @see read_float_stream - для множественных чтений
; @see parse_float - базовая функция парсинга
; ----------------------------------------------------------------------------
read_float_from_fd:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13

    mov r12, rdi                ; Сохранение файлового дескриптора
    mov r13, rsi                ; Сохранение адреса результата

    ; --- Чтение данных из файла ---
    ; syscall: read(fd, buffer, count)
    xor rax, rax
    mov rdi, r12
    lea rsi, [io_file_buffer]   ; Адрес буфера
    mov rdx, 63
    syscall

    test rax, rax               ; Проверка результата
    js .io_error                ; rax < 0 → ошибка I/O
    jz .eof                     ; rax = 0 → EOF (конец файла)

    mov rbx, rax                ; Сохранение количества прочитанных байт

    ; --- Добавление null-терминатора ---
    lea rdx, [io_file_buffer]   ; Загрузка базового адреса буфера
    mov byte [rdx + rax], 0     ; Установка null-терминатора после данных

    ; --- Парсинг прочитанной строки ---
    lea rdi, [io_file_buffer]   ; Адрес строки для парсинга
    call parse_float            ; Вызов функции парсинга

    test rdx, rdx               ; Проверка кода ошибки (0 = успех)
    jnz .parse_error            ; rdx != 0 → ошибка парсинга

    ; --- Сохранение результата ---
    mov [r13], eax              ; Сохранение битового представления float
    mov rax, rbx                ; Возврат количества прочитанных байт
    jmp .return

; --- Обработка ошибок ---
.io_error:
    mov rax, -1                 ; Код ошибки I/O
    jmp .return

.eof:
    xor rax, rax                ; Код EOF (0)
    jmp .return

.parse_error:
    ; Преобразование кода ошибки парсинга: 1,2 → -2,-3
    mov rax, rdx                ; Копирование кода ошибки
    neg rax                     ; Инвертирование знака
    dec rax                     ; Декремент (1→-2, 2→-3)

.return:
    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

; ----------------------------------------------------------------------------
; read_int32_from_fd_float
;
; Читает одно знаковое 32-битное целое число из файлового дескриптора.
; Подходит для однократного чтения из файлов.
; Суффикс _float используется для избежания конфликтов имён.
;
; ПРИМЕНЕНИЕ:
;   • Чтение конфигурационных файлов
;   • Однократная загрузка значений
;   • Простые случаи без необходимости потоковой обработки
;
; ОГРАНИЧЕНИЯ:
;   • Максимальная длина строки: 63 символа
;   • Не оптимально для множественных чтений
;
; ДИАПАЗОН: -2147483648 до 2147483647
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     int fd = open("config.txt", O_RDONLY);
;     int32_t value;
;     ssize_t result = read_int32_from_fd_float(fd, &value);
;     if (result > 0) {
;       // value содержит число
;     }
;     close(fd);
;
;   Assembly:
;     mov rax, 2
;     lea rdi, [filename]
;     xor rsi, rsi
;     syscall
;     mov r15, rax
;
;     mov rdi, r15
;     lea rsi, [my_int]
;     call read_int32_from_fd_float
;
;     mov rax, 3
;     mov rdi, r15
;     syscall
;
; @param  rdi  Файловый дескриптор (int fd)
;         rsi  Адрес переменной для результата (int32_t *)
; @return rax  >0 = количество прочитанных байт
;              0  = EOF (конец файла)
;              -1 = ошибка I/O (чтение из файла)
;              -2 = ошибка формата (некорректное число)
;              -3 = переполнение
; @uses   rbx, rdx, r12, r13
; @calls  parse_int32
;
; @see read_int32_stream_float - для множественных чтений
; @see parse_int32 - базовая функция парсинга
; ----------------------------------------------------------------------------
read_int32_from_fd_float:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13

    mov r12, rdi                ; Сохранение файлового дескриптора
    mov r13, rsi                ; Сохранение адреса результата

    ; --- Чтение данных из файла ---
    ; syscall: read(fd, buffer, count)
    xor rax, rax
    mov rdi, r12
    lea rsi, [io_file_buffer]   ; Адрес буфера
    mov rdx, 63
    syscall

    test rax, rax               ; Проверка результата
    js .io_error                ; rax < 0 → ошибка I/O
    jz .eof                     ; rax = 0 → EOF (конец файла)

    mov rbx, rax                ; Сохранение количества прочитанных байт

    ; --- Добавление null-терминатора ---
    lea rdx, [io_file_buffer]   ; Загрузка базового адреса буфера
    mov byte [rdx + rax], 0     ; Установка null-терминатора после данных

    ; --- Парсинг прочитанной строки ---
    lea rdi, [io_file_buffer]   ; Адрес строки для парсинга
    call parse_int32            ; Вызов функции парсинга

    test rdx, rdx               ; Проверка кода ошибки (0 = успех)
    jnz .parse_error            ; rdx != 0 → ошибка парсинга

    ; --- Сохранение результата ---
    mov [r13], eax              ; Сохранение значения int32
    mov rax, rbx                ; Возврат количества прочитанных байт
    jmp .return

; --- Обработка ошибок ---
.io_error:
    mov rax, -1                 ; Код ошибки I/O
    jmp .return

.eof:
    xor rax, rax                ; Код EOF (0)
    jmp .return

.parse_error:
    ; Преобразование кода ошибки парсинга: 1,2 → -2,-3
    mov rax, rdx                ; Копирование кода ошибки
    neg rax                     ; Инвертирование знака
    dec rax                     ; Декремент (1→-2, 2→-3)

.return:
    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

; ----------------------------------------------------------------------------
; read_float_stream
;
; Читает одно число с плавающей точкой из потока с буферизацией.
; Идеально для последовательного чтения нескольких чисел из файла.
; Пропускает ведущие whitespace символы (пробелы, табы, LF, CR).
;
; ПРИМЕНЕНИЕ:
;   • Парсинг файлов с множественными значениями
;   • Обработка потоков данных
;   • Эффективное чтение из больших файлов
;
; ОСОБЕННОСТИ:
;   • Внутренняя буферизация 128 байт минимизирует syscall
;   • Автоматический пропуск whitespace между числами
;   • Сохраняет позицию для следующего вызова
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   Чтение файла с числами:
;     numbers.txt: "1.5 2.25 3.75\n4.0 5.5"
;
;     int fd = open("numbers.txt", O_RDONLY);
;     float values[5];
;     for (int i = 0; i < 5; i++) {
;       int result = read_float_stream(fd, &values[i]);
;       if (result != 1) break;
;     }
;     close(fd);
;
;   Assembly (читаем 3 числа):
;     mov rax, 2
;     lea rdi, [filename]
;     xor rsi, rsi
;     syscall
;     mov r15, rax
;
;     mov rdi, r15
;     lea rsi, [num1]
;     call read_float_stream
;
;     mov rdi, r15
;     lea rsi, [num2]
;     call read_float_stream
;
;     mov rdi, r15
;     lea rsi, [num3]
;     call read_float_stream
;
;     mov rax, 3
;     mov rdi, r15
;     syscall
;
; @param  rdi  Файловый дескриптор (int fd)
;         rsi  Адрес переменной для результата (float *)
; @return rax  1  = успех (число прочитано)
;              0  = EOF (конец файла)
;              -1 = ошибка I/O
;              -2 = ошибка формата
;              -3 = переполнение
;              -4 = переполнение буфера
; @uses   rbx, rdx, r12, r13, r14
; @calls  parse_float, .read_char (внутренняя)
;
; @complexity O(n), где n - длина числа
; @memory     Использует статический буфер 128 байт
;
; @see read_float_from_fd - для однократного чтения
; @see .read_char - внутренняя функция буферизованного чтения символа
; ----------------------------------------------------------------------------
read_float_stream:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13
    push r14

    mov r12, rdi                ; Сохранение файлового дескриптора
    mov r14, rsi                ; Сохранение адреса результата

    lea r13, [io_file_buffer]   ; r13 = база для записи числа
    xor rbx, rbx                ; rbx = позиция записи (начинаем с 0)

; --- Пропуск начальных whitespace символов ---
.skip_whitespace:
    call .read_char             ; Чтение одного символа
    test rax, rax               ; Проверка результата
    jle .handle_read_error      ; Если <= 0 (ошибка или EOF) - обработка

    movzx rax, byte [io_file_char_buf] ; Загрузка прочитанного символа

    ; Проверка whitespace: ' ', '\t', '\n', '\r'
    cmp al, ' '                 ; Проверка: пробел?
    je .skip_whitespace         ; Если да - пропускаем
    cmp al, 10                  ; Проверка: LF?
    je .skip_whitespace         ; Если да - пропускаем
    cmp al, 9                   ; Проверка: TAB?
    je .skip_whitespace         ; Если да - пропускаем
    cmp al, 13                  ; Проверка: CR?
    je .skip_whitespace         ; Если да - пропускаем

    ; --- Первый значащий символ - начало числа ---
    mov byte [r13 + rbx], al    ; Сохранение первого символа числа
    inc rbx                     ; Увеличение позиции записи

; --- Чтение остальных символов числа ---
.read_number:
    cmp rbx, 62                        ; Проверка: буфер переполнен?
    jge .buffer_overflow               ; Если да - ошибка переполнения буфера

    call .read_char                    ; Чтение следующего символа
    test rax, rax                      ; Проверка результата
    jle .handle_read_error_in_number   ; Если <= 0 - обработка

    movzx rax, byte [io_file_char_buf] ; Загрузка прочитанного символа

    ; Проверка разделителей (конец числа)
    cmp al, ' '                 ; Проверка: пробел?
    je .parse                   ; Если да - конец числа, парсим
    cmp al, 10                  ; Проверка: LF?
    je .parse                   ; Если да - конец числа, парсим
    cmp al, 9                   ; Проверка: TAB?
    je .parse                   ; Если да - конец числа, парсим
    cmp al, 13                  ; Проверка: CR?
    je .parse                   ; Если да - конец числа, парсим

    ; --- Продолжение числа (цифра, точка или знак) ---
    mov byte [r13 + rbx], al    ; Сохранение символа
    inc rbx                     ; Увеличение позиции записи
    jmp .read_number            ; Продолжение чтения

; --- Парсинг собранного числа ---
.parse:
    mov byte [r13 + rbx], 0     ; Установка null-терминатора

    lea rdi, [io_file_buffer]   ; Адрес строки для парсинга
    call parse_float            ; Вызов функции парсинга

    test rdx, rdx               ; Проверка кода ошибки
    jnz .parse_error            ; Если ошибка - обработка

    ; --- Сохранение результата ---
    mov [r14], eax              ; Сохранение битового представления float
    mov rax, 1                  ; Код успеха
    jmp .ret

; --- Обработка ошибок чтения (до начала числа) ---
.handle_read_error:
    test rax, rax               ; Проверка кода возврата
    jz .eof                     ; rax = 0 → EOF
    mov rax, -1                 ; rax < 0 → I/O error
    jmp .ret

; --- Обработка ошибок чтения (внутри числа) ---
.handle_read_error_in_number:
    test rax, rax               ; Проверка кода возврата
    js .io_error                ; rax < 0 → I/O error
    jmp .parse                  ; rax = 0 → EOF, но есть данные для парсинга

; --- Коды возврата ---
.eof:
    xor rax, rax                ; Код EOF (0)
    jmp .ret

.io_error:
    mov rax, -1                 ; Код ошибки I/O
    jmp .ret

.buffer_overflow:
    mov rax, -4                 ; Код ошибки переполнения буфера
    jmp .ret

.parse_error:
    ; Преобразование кода ошибки парсинга: 1,2 → -2,-3
    mov rax, rdx                ; Копирование кода ошибки
    neg rax                     ; Инвертирование знака
    dec rax                     ; Декремент

.ret:
    pop r14
    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

; ----------------------------------------------------------------------------
; .read_char (внутренняя функция read_float_stream)
;
; Читает один символ из файла с использованием буферизации.
; Минимизирует количество системных вызовов read.
;
; АЛГОРИТМ:
;   1. Проверка наличия данных в буфере
;   2. Если буфер пуст: системный вызов read() на 128 байт
;   3. Извлечение одного символа из буфера
;   4. Обновление позиции и размера
;
; БУФЕРИЗАЦИЯ:
;   Буфер: io_file_stream_buffer (128 байт)
;   Позиция: io_file_stream_pos
;   Размер: io_file_stream_size
;
; @param  r12  Файловый дескриптор (сохранён вызывающей функцией)
; @return rax  1 = успех (символ в io_file_char_buf)
;              0 = EOF
;              <0 = ошибка I/O
; @uses   rcx, rdx, rdi, rsi (сохраняются и восстанавливаются)
;
; @complexity O(1) амортизированная (O(n/128) syscall для n символов)
; @internal   Используется только внутри read_float_stream
; ----------------------------------------------------------------------------
.read_char:
    push rdi
    push rsi
    push rdx
    push rcx

    ; --- Проверка наличия данных в буфере ---
    mov rax, [io_file_stream_size] ; Загрузка количества доступных байт
    test rax, rax                  ; Проверка: есть ли данные в буфере?
    jnz .has_buffered              ; Если да - извлечение из буфера

    ; --- Буфер пуст - системный вызов ---
    ; syscall: read(fd, buffer, count)
    xor rax, rax
    mov rdi, r12
    lea rsi, [io_file_stream_buffer] ; Адрес буфера
    mov rdx, 128
    syscall

    test rax, rax               ; Проверка результата
    jle .read_done              ; Если <= 0 (EOF или ошибка) - завершение

    mov [io_file_stream_size], rax    ; Сохранение количества прочитанных байт
    mov qword [io_file_stream_pos], 0 ; Сброс позиции чтения

; --- Извлечение символа из буфера ---
.has_buffered:
    mov rcx, [io_file_stream_pos]              ; Загрузка текущей позиции чтения
    mov al, byte [io_file_stream_buffer + rcx] ; Чтение символа из буфера
    mov byte [io_file_char_buf], al            ; Сохранение символа в выходной буфер

    inc qword [io_file_stream_pos]  ; Увеличение позиции чтения
    dec qword [io_file_stream_size] ; Уменьшение количества доступных байт

    mov rax, 1                      ; Код успешного чтения

.read_done:
    pop rcx
    pop rdx
    pop rsi
    pop rdi
    ret

; ----------------------------------------------------------------------------
; read_int32_stream_float
;
; Читает одно знаковое 32-битное целое число из потока с буферизацией.
; Идеально для последовательного чтения нескольких чисел из файла.
; Пропускает ведущие whitespace символы (пробелы, табы, LF, CR).
; Суффикс _float используется для избежания конфликтов имён.
;
; ПРИМЕНЕНИЕ:
;   • Парсинг файлов с множественными значениями
;   • Обработка потоков данных
;   • Эффективное чтение из больших файлов
;
; ОСОБЕННОСТИ:
;   • Внутренняя буферизация 128 байт минимизирует syscall
;   • Автоматический пропуск whitespace между числами
;   • Сохраняет позицию для следующего вызова
;
; ДИАПАЗОН: -2147483648 до 2147483647
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   Чтение файла с числами:
;     numbers.txt: "100 -200 300\n400 500"
;
;     int fd = open("numbers.txt", O_RDONLY);
;     int32_t values[5];
;     for (int i = 0; i < 5; i++) {
;       int result = read_int32_stream_float(fd, &values[i]);
;       if (result != 1) break;
;     }
;     close(fd);
;
; @param  rdi  Файловый дескриптор (int fd)
;         rsi  Адрес переменной для результата (int32_t *)
; @return rax  1  = успех (число прочитано)
;              0  = EOF (конец файла)
;              -1 = ошибка I/O
;              -2 = ошибка формата
;              -3 = переполнение
;              -4 = переполнение буфера
; @uses   rbx, rdx, r12, r13, r14
; @calls  parse_int32, .read_char_int (внутренняя)
;
; @complexity O(n), где n - длина числа
; @memory     Использует статический буфер 128 байт
;
; @see read_int32_from_fd_float - для однократного чтения
; @see .read_char_int - внутренняя функция буферизованного чтения символа
; ----------------------------------------------------------------------------
read_int32_stream_float:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13
    push r14

    mov r12, rdi                ; Сохранение файлового дескриптора
    mov r14, rsi                ; Сохранение адреса результата

    lea r13, [io_file_buffer]   ; r13 = база для записи числа
    xor rbx, rbx                ; rbx = позиция записи (начинаем с 0)

; --- Пропуск начальных whitespace символов ---
.skip_whitespace:
    call .read_char_int         ; Чтение одного символа
    test rax, rax               ; Проверка результата
    jle .handle_read_error      ; Если <= 0 (ошибка или EOF) - обработка

    movzx rax, byte [io_file_char_buf] ; Загрузка прочитанного символа

    ; Проверка whitespace: ' ', '\t', '\n', '\r'
    cmp al, ' '                 ; Проверка: пробел?
    je .skip_whitespace         ; Если да - пропускаем
    cmp al, 10                  ; Проверка: LF?
    je .skip_whitespace         ; Если да - пропускаем
    cmp al, 9                   ; Проверка: TAB?
    je .skip_whitespace         ; Если да - пропускаем
    cmp al, 13                  ; Проверка: CR?
    je .skip_whitespace         ; Если да - пропускаем

    ; --- Первый значащий символ - начало числа ---
    mov byte [r13 + rbx], al    ; Сохранение первого символа числа
    inc rbx                     ; Увеличение позиции записи

; --- Чтение остальных символов числа ---
.read_number:
    cmp rbx, 62                 ; Проверка: буфер переполнен?
    jge .buffer_overflow        ; Если да - ошибка переполнения буфера

    call .read_char_int         ; Чтение следующего символа
    test rax, rax               ; Проверка результата
    jle .handle_read_error_in_number ; Если <= 0 - обработка

    movzx rax, byte [io_file_char_buf] ; Загрузка прочитанного символа

    ; Проверка разделителей (конец числа)
    cmp al, ' '                 ; Проверка: пробел?
    je .parse                   ; Если да - конец числа, парсим
    cmp al, 10                  ; Проверка: LF?
    je .parse                   ; Если да - конец числа, парсим
    cmp al, 9                   ; Проверка: TAB?
    je .parse                   ; Если да - конец числа, парсим
    cmp al, 13                  ; Проверка: CR?
    je .parse                   ; Если да - конец числа, парсим

    ; --- Продолжение числа (цифра или знак) ---
    mov byte [r13 + rbx], al    ; Сохранение символа
    inc rbx                     ; Увеличение позиции записи
    jmp .read_number            ; Продолжение чтения

; --- Парсинг собранного числа ---
.parse:
    mov byte [r13 + rbx], 0     ; Установка null-терминатора

    lea rdi, [io_file_buffer]   ; Адрес строки для парсинга
    call parse_int32            ; Вызов функции парсинга

    test rdx, rdx               ; Проверка кода ошибки
    jnz .parse_error            ; Если ошибка - обработка

    ; --- Сохранение результата ---
    mov [r14], eax              ; Сохранение значения int32
    mov rax, 1                  ; Код успеха
    jmp .ret

; --- Обработка ошибок чтения (до начала числа) ---
.handle_read_error:
    test rax, rax               ; Проверка кода возврата
    jz .eof                     ; rax = 0 → EOF
    mov rax, -1                 ; rax < 0 → I/O error
    jmp .ret

; --- Обработка ошибок чтения (внутри числа) ---
.handle_read_error_in_number:
    test rax, rax               ; Проверка кода возврата
    js .io_error                ; rax < 0 → I/O error
    jmp .parse                  ; rax = 0 → EOF, но есть данные для парсинга

; --- Коды возврата ---
.eof:
    xor rax, rax                ; Код EOF (0)
    jmp .ret

.io_error:
    mov rax, -1                 ; Код ошибки I/O
    jmp .ret

.buffer_overflow:
    mov rax, -4                 ; Код ошибки переполнения буфера
    jmp .ret

.parse_error:
    ; Преобразование кода ошибки парсинга: 1,2 → -2,-3
    mov rax, rdx                ; Копирование кода ошибки
    neg rax                     ; Инвертирование знака
    dec rax                     ; Декремент

.ret:
    pop r14
    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

; ----------------------------------------------------------------------------
; .read_char_int (внутренняя функция read_int32_stream_float)
;
; Читает один символ из файла с использованием буферизации.
; Минимизирует количество системных вызовов read.
; Идентична .read_char, но с отдельным именем для ясности контекста.
;
; АЛГОРИТМ:
;   1. Проверка наличия данных в буфере
;   2. Если буфер пуст: системный вызов read() на 128 байт
;   3. Извлечение одного символа из буфера
;   4. Обновление позиции и размера
;
; БУФЕРИЗАЦИЯ:
;   Буфер: io_file_stream_buffer (128 байт)
;   Позиция: io_file_stream_pos
;   Размер: io_file_stream_size
;
; @param  r12  Файловый дескриптор (сохранён вызывающей функцией)
; @return rax  1 = успех (символ в io_file_char_buf)
;              0 = EOF
;              <0 = ошибка I/O
; @uses   rcx, rdx, rdi, rsi (сохраняются и восстанавливаются)
;
; @complexity O(1) амортизированная (O(n/128) syscall для n символов)
; @internal   Используется только внутри read_int32_stream_float
; ----------------------------------------------------------------------------
.read_char_int:
    push rdi
    push rsi
    push rdx
    push rcx

    ; --- Проверка наличия данных в буфере ---
    mov rax, [io_file_stream_size] ; Загрузка количества доступных байт
    test rax, rax                  ; Проверка: есть ли данные в буфере?
    jnz .has_buffered              ; Если да - извлечение из буфера

    ; --- Буфер пуст - системный вызов ---
    ; syscall: read(fd, buffer, count)
    xor rax, rax
    mov rdi, r12
    lea rsi, [io_file_stream_buffer] ; Адрес буфера
    mov rdx, 128
    syscall

    test rax, rax                   ; Проверка результата
    jle .read_done                  ; Если <= 0 (EOF или ошибка) - завершение

    mov [io_file_stream_size], rax    ; Сохранение количества прочитанных байт
    mov qword [io_file_stream_pos], 0 ; Сброс позиции чтения

; --- Извлечение символа из буфера ---
.has_buffered:
    mov rcx, [io_file_stream_pos]              ; Загрузка текущей позиции чтения
    mov al, byte [io_file_stream_buffer + rcx] ; Чтение символа из буфера
    mov byte [io_file_char_buf], al            ; Сохранение символа в выходной буфер

    inc qword [io_file_stream_pos]  ; Увеличение позиции чтения
    dec qword [io_file_stream_size] ; Уменьшение количества доступных байт

    mov rax, 1                      ; Код успешного чтения

.read_done:
    pop rcx
    pop rdx
    pop rsi
    pop rdi
    ret

; ----------------------------------------------------------------------------
; write_float_to_fd
;
; Записывает число с плавающей точкой в файловый дескриптор
; в текстовом формате с точностью 6 знаков после запятой + LF.
;
; АЛГОРИТМ:
;   1. Загрузка float в FPU
;   2. Определение знака и добавление '-' в начало буфера при необходимости
;   3. Отделение целой части через FPU truncation
;   4. Формирование целой части в локальный буфер (справа налево)
;   5. Копирование целой части в выходной буфер (слева направо)
;   6. Добавление десятичной точки
;   7. Формирование дробной части (6 цифр слева направо)
;   8. Добавление символа новой строки
;   9. Запись в файл
;
; ПРИМЕНЕНИЕ:
;   • Запись результатов вычислений в файл
;   • Экспорт данных в текстовом формате
;   • Логирование числовых значений
;
; ФОРМАТ ЗАПИСИ: "[-]целая.дробная\n"
; ТОЧНОСТЬ: 6 знаков после запятой (FRAC_DIGITS)
;
; ПРИМЕРЫ:
;   write_float_to_fd(3, 0x40490FDB) → "3.141593\n"  (π)
;   write_float_to_fd(3, 0xC0000000) → "-2.000000\n"
;   write_float_to_fd(3, 0x00000000) → "0.000000\n"
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     int fd = open("output.txt", O_WRONLY | O_CREAT, 0644);
;     float pi = 3.14159f;
;     write_float_to_fd(fd, *(uint32_t*)&pi);
;     close(fd);
;     // Файл содержит: "3.141593\n"
;
;   Assembly:
;     ; Открыть файл для записи
;     mov rax, 2              ; sys_open
;     lea rdi, [filename]
;     mov rsi, 0x241          ; O_WRONLY | O_CREAT
;     mov rdx, 0x1A4          ; 0644
;     syscall
;     mov r15, rax            ; Сохранить FD
;
;     ; Записать число
;     mov rdi, r15
;     mov esi, 0x40490FDB     ; π
;     call write_float_to_fd
;
;     ; Закрыть файл
;     mov rax, 3              ; sys_close
;     mov rdi, r15
;     syscall
;
; @param  rdi  Файловый дескриптор (int fd)
;         esi  Битовое представление float (IEEE 754)
; @return rax  Количество записанных байт (или код ошибки от write)
; @uses   rbx, rdx, r12, r13, r14, r15, FPU stack
;
; @complexity O(1), фиксированная точность вывода
; @memory     O(1), использует фиксированный буфер 64 байта
;
; @note Использует FPU для разделения на целую и дробную части
; ----------------------------------------------------------------------------
write_float_to_fd:
    push rbp
    mov rbp, rsp
    sub rsp, 64                 ; Локальное пространство для FPU операций и временных данных
    push rbx
    push r12
    push r13
    push r14
    push r15

    mov r14, rdi                ; Сохранение файлового дескриптора
    mov [rbp-4], esi            ; Сохранение битового представления в памяти для загрузки в FPU
    fld dword [rbp-4]           ; Загрузка float в FPU: ST(0) = число

; --- Инициализация выходного буфера ---
; Формирование строки слева направо упрощает логику и исключает ошибки порядка символов
    lea r15, [io_file_buffer]   ; r15 = указатель на начало буфера
    xor rbx, rbx                ; rbx = позиция записи (начинаем с 0)

    ; --- Проверка и обработка знака числа ---
    ; Проверка знака числа через FPU
    ftst                        ; Сравнение ST(0) с 0.0 (устанавливает флаги в FPU status word)
    fstsw ax                    ; Копирование FPU status word в AX
    sahf                        ; Загрузка AH в флаги процессора (для условных переходов)
    jae .positive               ; Если >= 0 - переход к обработке положительного числа

    ; Число отрицательное - добавление знака минус
    mov byte [r15 + rbx], '-'   ; Запись символа '-' в текущую позицию буфера
    inc rbx                     ; Увеличение позиции записи
    fabs                        ; Взятие модуля: ST(0) = |ST(0)| (работаем с положительным значением)

; --- Разделение на целую и дробную части ---
; Используем FPU для точного разделения числа на компоненты
.positive:
    fld st0                     ; Дублирование числа в стеке FPU: ST(0) = число, ST(1) = число

    ; Усечение до целой части (truncation к нулю)
    push rax
    sub rsp, 8                  ; Выделение места для control word на стеке
    fnstcw [rsp]                ; Сохранение текущего control word FPU
    mov ax, [rsp]               ; Загрузка control word в AX
    or ax, FPU_TRUNC_MODE       ; Установка режима усечения (0x0C00 - округление к нулю)
    mov [rsp+4], ax             ; Сохранение модифицированного control word
    fldcw [rsp+4]               ; Загрузка нового control word в FPU
    frndint                     ; Округление ST(0) к целому (с учётом режима усечения)
    fldcw [rsp]                 ; Восстановление исходного control word
    add rsp, 8                  ; Освобождение стека
    pop rax

    ; Сохранение целой части
    fist dword [rbp-8]          ; ST(0) → [rbp-8] (сохранение целой части как int32)
    mov r13d, [rbp-8]           ; Загрузка целой части в r13d для последующей обработки

    ; Вычисление дробной части: исходное_число - целая_часть
    fsubr st0, st1              ; ST(0) = ST(1) - ST(0) = исходное - целое
    fstp dword [rbp-12]         ; Сохранение дробной части в память: [rbp-12] = дробная
    fstp st0                    ; Удаление исходного числа из стека FPU (очистка стека)

    ; --- Формирование целой части числа ---
    ; Алгоритм: сначала конвертация справа налево во временный буфер,
    ;           затем копирование слева направо в выходной буфер
    mov eax, r13d               ; Загрузка целой части для обработки
    test eax, eax               ; Проверка: равно нулю?
    jnz .int_convert            ; Если не ноль - переход к конвертации

    ; Специальная обработка нуля
    mov byte [r15 + rbx], '0'   ; Запись символа '0' для нулевой целой части
    inc rbx                     ; Увеличение позиции записи
    jmp .decimal_point          ; Переход к добавлению десятичной точки

; --- Конвертация целой части в строку (справа налево во временный буфер) ---
.int_convert:
    lea r12, [rbp-32]           ; r12 = адрес локального временного буфера
    xor rcx, rcx                ; rcx = количество цифр (счётчик, начинаем с 0)

; Инвариант цикла: rcx = количество записанных цифр
.int_loop:
    xor edx, edx                ; Обнуление EDX перед делением (для 64-битного деления)
    mov esi, 10                 ; Делитель = 10 (основание десятичной системы)
    div esi                     ; EAX = EAX / 10, EDX = EAX % 10 (остаток - младшая цифра)
    add dl, '0'                 ; Преобразование цифры (0-9) в ASCII-символ ('0'-'9')
    mov [r12 + rcx], dl         ; Запись цифры во временный буфер
    inc rcx                     ; Увеличение счётчика цифр
    test eax, eax               ; Проверка: остались ли ещё цифры?
    jnz .int_loop               ; Если да - продолжение конвертации

; --- Копирование цифр в правильном порядке (обратный к записи) ---
; Временный буфер содержит цифры в обратном порядке (младшие сначала),
; копируем их в выходной буфер слева направо (старшие сначала)
.int_copy:
    dec rcx                     ; Переход к последней записанной цифре (самой старшей)
    mov al, [r12 + rcx]         ; Загрузка цифры из временного буфера
    mov [r15 + rbx], al         ; Запись в выходной буфер в текущую позицию
    inc rbx                     ; Увеличение позиции в выходном буфере
    test rcx, rcx               ; Проверка: остались ли цифры для копирования?
    jnz .int_copy               ; Если да - продолжение копирования

; --- Добавление десятичной точки ---
.decimal_point:
    mov byte [r15 + rbx], '.'   ; Запись символа '.' (десятичный разделитель)
    inc rbx                     ; Увеличение позиции записи

    ; --- Формирование дробной части (6 цифр слева направо) ---
    ; Алгоритм: последовательное умножение дробной части на 10,
    ;           извлечение целой части (текущей цифры), повторение
    fld dword [rbp-12]          ; Загрузка дробной части в FPU: ST(0) = дробная часть
    mov rcx, FRAC_DIGITS        ; rcx = 6 (количество знаков после запятой для вывода)

; Инвариант цикла: rcx = количество оставшихся цифр для вывода
.frac_loop:
    fmul dword [io_const_ten]   ; ST(0) = ST(0) * 10.0 (сдвиг разряда влево)
    fld st0                     ; Дублирование: ST(0) = ST(1) = дробная * 10^i

    ; Усечение для получения текущей цифры (целая часть от умножения)
    push rax
    sub rsp, 8                  ; Выделение места для control word
    fnstcw [rsp]                ; Сохранение текущего control word
    mov ax, [rsp]
    or ax, FPU_TRUNC_MODE       ; Установка режима усечения
    mov [rsp+4], ax
    fldcw [rsp+4]               ; Загрузка режима усечения в FPU
    frndint                     ; Усечение до целого (получение текущей цифры)
    fldcw [rsp]                 ; Восстановление исходного control word
    add rsp, 8
    pop rax

    fist dword [rbp-20]         ; Сохранение текущей цифры в память
    mov eax, [rbp-20]           ; Загрузка цифры в EAX для валидации

    ; Валидация цифры (защита от ошибок округления FPU)
    ; Должна быть в диапазоне 0-9
    cmp eax, 0                  ; Проверка: меньше 0?
    jl .frac_zero               ; Если да - использовать безопасное значение '0'
    cmp eax, 9                  ; Проверка: больше 9?
    jg .frac_zero               ; Если да - использовать безопасное значение '0'
    add al, '0'                 ; Преобразование цифры (0-9) в ASCII ('0'-'9')
    jmp .frac_out

.frac_zero:
    mov al, '0'                 ; Использование '0' для недопустимых значений (защита)

.frac_out:
    mov [r15 + rbx], al         ; Запись ASCII-цифры в выходной буфер
    inc rbx                     ; Увеличение позиции записи

    fsubp st1, st0              ; ST(0) = ST(1) - ST(0) (удаление целой части цифры)
                                ; Остаётся дробная часть для следующей итерации
    dec rcx                     ; Уменьшение счётчика оставшихся цифр
    jnz .frac_loop              ; Если остались цифры - продолжение цикла

    fstp st0                    ; Удаление остатка дробной части из стека FPU (очистка)

    ; --- Добавление символа новой строки ---
    mov byte [r15 + rbx], 10    ; Запись LF (line feed, символ новой строки '\n')
    inc rbx                     ; Увеличение позиции (rbx = итоговая длина строки)

    ; --- Запись сформированной строки в файл ---
    ; syscall: write(fd, buffer, length)
    mov rax, 1
    mov rdi, r14
    mov rsi, r15                ; Адрес начала строки (io_file_buffer)
    mov rdx, rbx                ; Длина строки (количество записанных байт)
    syscall

    pop r15
    pop r14
    pop r13
    pop r12
    pop rbx
    mov rsp, rbp
    pop rbp
    ret

; ----------------------------------------------------------------------------
; write_int32_to_fd_float
;
; Записывает знаковое 32-битное целое число в файловый дескриптор
; в текстовом виде (десятичный формат) с завершающим символом LF.
; Поддерживает отрицательные числа.
; Суффикс _float используется для избежания конфликтов имён.
;
; ПРИМЕНЕНИЕ:
;   • Запись результатов вычислений в файл
;   • Экспорт данных в текстовом формате
;   • Логирование числовых значений
;
; ДИАПАЗОН: -2147483648 до 2147483647 (int32_t)
; ФОРМАТ ЗАПИСИ: "[-]<число>\n"
;
; ПРИМЕРЫ:
;   write_int32_to_fd_float(3, 0)       → "0\n"
;   write_int32_to_fd_float(3, 12345)   → "12345\n"
;   write_int32_to_fd_float(3, -98765)  → "-98765\n"
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     int fd = open("output.txt", O_WRONLY | O_CREAT, 0644);
;     write_int32_to_fd_float(fd, -12345);
;     write_int32_to_fd_float(fd, 67890);
;     close(fd);
;     // Файл содержит: "-12345\n67890\n"
;
;   Assembly:
;     ; Открыть файл для записи
;     mov rax, 2              ; sys_open
;     lea rdi, [filename]
;     mov rsi, 0x241          ; O_WRONLY | O_CREAT
;     mov rdx, 0x1A4          ; 0644
;     syscall
;     mov r15, rax            ; Сохранить FD
;
;     ; Записать число
;     mov rdi, r15
;     mov esi, -12345
;     call write_int32_to_fd_float
;
;     ; Закрыть файл
;     mov rax, 3              ; sys_close
;     mov rdi, r15
;     syscall
;
; @param  rdi  Файловый дескриптор (int fd)
;         esi  Значение для записи (int32_t)
; @return rax  Количество записанных байт (или код ошибки от write)
; @uses   rbx, rdx, r12, r13, r14
;
; @complexity O(log₁₀(n)), где n - абсолютное значение числа
; @memory     O(1), использует фиксированный буфер 64 байта
; ----------------------------------------------------------------------------
write_int32_to_fd_float:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13
    push r14

    mov r14, rdi                ; Сохранение файлового дескриптора
    movsxd rax, esi             ; Знаковое расширение int32 → int64

    ; --- Формирование строки справа налево в io_file_buffer ---
    lea r13, [io_file_buffer + 63] ; r13 = указатель на конец буфера
    mov byte [r13], 10          ; Установка LF (символ новой строки) в конце
    dec r13                     ; Смещение указателя назад

    ; --- Специальная обработка нуля ---
    test rax, rax               ; Проверка: число равно нулю?
    jnz .check_sign             ; Если нет - обработка знака
    mov byte [r13], '0'         ; Запись символа '0'
    jmp .write                  ; Переход к записи в файл

; --- Обработка знака ---
.check_sign:
    xor rbx, rbx                ; rbx = флаг знака (0 по умолчанию)
    test rax, rax               ; Проверка знака числа (установка флагов)
    jge .convert                ; Если >= 0 - переход к конвертации
    neg rax                     ; Взятие модуля (двухкомплементное отрицание)
    mov rbx, 1                  ; Установка флага отрицательного числа

; --- Конвертация числа в строку (справа налево) ---
; Инвариант цикла: r13 указывает на следующую позицию для записи
.convert:
    dec r13                     ; Смещение указателя назад
    xor rdx, rdx                ; Обнуление rdx перед делением
    mov r12, 10                 ; Делитель = 10 (основание системы счисления)
    div r12                     ; rax = rax / 10, rdx = rax % 10 (остаток - цифра)
    add dl, '0'                 ; Преобразование цифры (0-9) в ASCII ('0'-'9')
    mov [r13], dl               ; Запись ASCII-символа в буфер
    test rax, rax               ; Проверка: остались ли ещё цифры?
    jnz .convert                ; Если да - продолжение конвертации

    ; --- Добавление знака минус ---
    test rbx, rbx               ; Проверка флага отрицательного числа
    jz .write                   ; Если положительное - переход к записи
    dec r13                     ; Смещение указателя назад
    mov byte [r13], '-'         ; Запись символа минуса

; --- Запись строки в файл ---
.write:
    ; syscall: write(fd, string, length)
    mov rax, 1
    mov rdi, r14
    mov rsi, r13                   ; Адрес начала строки
    lea rdx, [io_file_buffer + 64] ; Адрес конца буфера
    sub rdx, r13                   ; Вычисление длины: конец - начало
    syscall

    pop r14
    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

🏛️ 5. Архитектурные концепции Layer 3

Данный раздел дополняет концепции, описанные в статье «Ввод-вывод на чистом NASM: Готовые модули (без libc)». Там рассматривается двухслойная модель (Layer 1 — парсинг, Layer 2 — интерактивный I/O), буферизация stdin, weak symbols и трёхуровневая обработка ошибок.

Здесь мы фокусируемся на новых принципах, которые вводит Layer 3 (файловый ввод-вывод).

Концепция: Коды возврата вместо exit()

Проблема:

Layer 2 (интерактивный режим) завершает программу при первой же ошибке ввода: вывел сообщение в stderr, вызвал exit(1). Это удобно для диалоговых утилит, но неприемлемо для batch-обработки файлов.

Сценарий 1: Интерактивный режим (Layer 2)

┌─────────────────────────────────────────────────────────────┐
│  КАЛЬКУЛЯТОР (интерактивная программа)                      │
├─────────────────────────────────────────────────────────────┤
│  Пользователь вводит: "abc" (не число)                      │
│         ↓                                                   │
│  read_signed_input_vars:                                    │
│    1. Чтение из stdin → "abc"                               │
│    2. Вызов parse_int16 → rdx=1 (ошибка формата)            │
│    3. Проверка: test rdx, rdx → ошибка!                     │
│    4. Вывод в stderr: "ERROR: Invalid number format"        │
│    5. syscall exit(1)                                       │
│         ↓                                                   │
│  ПРОГРАММА ЗАВЕРШЕНА                                        │
│                                                             │
│  ✅ ДЛЯ КАЛЬКУЛЯТОРА ЭТО OK:                                │
│     Пользователь видит ошибку, перезапускает программу      │
└─────────────────────────────────────────────────────────────┘

Сценарий 2: Файловый режим с exit() (ПРОБЛЕМА)

┌─────────────────────────────────────────────────────────────┐
│  ФАЙЛ data.txt СОДЕРЖИТ 10000 ЧИСЕЛ:                        │
│  "100 200 abc 400 500 600 ... 10000"                        │
│            ↑ ошибка на строке 3                             │
├─────────────────────────────────────────────────────────────┤
│  ГИПОТЕТИЧЕСКИЙ Layer 3 с exit():                           │
│                                                             │
│  Число 1: 100   ✅ OK                                       │
│  Число 2: 200   ✅ OK                                       │
│  Число 3: "abc" → parse_int16 → rdx=1                       │
│         ↓                                                   │
│  ❌ exit(1)                                                 │
│                                                             │
│  РЕЗУЛЬТАТ:                                                 │
│    -  Обработано 2 из 10000 чисел (0.02%)                   │
│    -  9997 корректных чисел ПРОИГНОРИРОВАНЫ                 │
│    -  Пользователь не знает, где именно ошибка              │
│    -  Нет возможности пропустить проблемную строку          │
│                                                             │
│  ❌ ЭТО КАТАСТРОФА ДЛЯ BATCH-ОБРАБОТКИ                      │
└─────────────────────────────────────────────────────────────┘

Сценарий 3: Файловый режим с кодами возврата (РЕШЕНИЕ)

┌─────────────────────────────────────────────────────────────┐
│  ТОТ ЖЕ ФАЙЛ: "100 200 abc 400 500 ..."                     │
├─────────────────────────────────────────────────────────────┤
│  Layer 3 с кодами возврата:                                 │
│                                                             │
│  Число 1: read_int16_stream → rax=1 (успех), value=100      │
│           Программа: сохраняем 100, продолжаем              │
│                                                             │
│  Число 2: read_int16_stream → rax=1 (успех), value=200      │
│           Программа: сохраняем 200, продолжаем              │
│                                                             │
│  Число 3: read_int16_stream → rax=-2 (ошибка формата!)      │
│           ┌─────────────────────────────────────────┐       │
│           │ ПРОГРАММА РЕШАЕТ, ЧТО ДЕЛАТЬ:           │       │
│           │   -  Вариант A: Пропустить, продолжить  │       │
│           │   -  Вариант B: Логировать, продолжить  │       │
│           │   -  Вариант C: Остановиться            │       │
│           └─────────────────────────────────────────┘       │
│           Программа: логируем "строка 3: ошибка", skip      │
│                                                             │
│  Число 4: read_int16_stream → rax=1 (успех), value=400      │
│           Программа: сохраняем 400, продолжаем              │
│                                                             │
│  ...                                                        │
│                                                             │
│  Число 10000: read_int16_stream → rax=0 (EOF)               │
│               Программа: завершаем цикл нормально           │
│                                                             │
│  РЕЗУЛЬТАТ:                                                 │
│    -  Обработано 9999 из 10000 чисел (99.99%)               │
│    -  Ошибка залогирована с точным местоположением          │
│    -  Программа завершилась успешно                         │
│                                                             │
│  ✅ ЭТО ПРАВИЛЬНОЕ ПОВЕДЕНИЕ ДЛЯ BATCH-ОБРАБОТКИ            │
└─────────────────────────────────────────────────────────────┘

Таблица кодов возврата Layer 3

Код rax Значение Действие программиста
> 0 Количество прочитанных байт ✅ Успех, данные валидны
0 EOF (конец файла) Нормальное завершение цикла
-1 I/O error (read syscall) Проверить дескриптор, закрыть файл
-2 Ошибка формата (не число) Пропустить / логировать
-3 Переполнение типа данных Пропустить / использовать int32
-4 Переполнение буфера (>30 символов) Увеличить буфер / прервать

Сравнение философий

Layer 2: Fail-Fast (exit)

Философия: Программа общается с человеком. Ошибка = нет смысла продолжать.

; Внутри read_signed_input_vars:
call parse_int16
test rdx, rdx
jnz .handle_error

.handle_error:
    ; stderr: "ERROR: ..."
    mov rax, 60         ; sys_exit
    mov rdi, 1
    syscall             ; ← УБИЛИ ПРОЦЕСС

ПОСЛЕДСТВИЯ

  • Простота для программиста: не нужно проверять результат
  • Идеально для CLI-утилит
  • Неприменимо для batch-обработки
Layer 3: Return Codes

Философия: Программа обрабатывает данные. Решение об ошибке — за вызывающим кодом.

; Внутри read_int16_stream:
call parse_int16
test rdx, rdx
jnz .parse_error

.parse_error:
    mov rax, rdx        ; Копируем код ошибки
    neg rax             ; 1 → -1
    dec rax             ; -1 → -2 (формат)
    jmp .ret            ; ← ВОЗВРАЩАЕМСЯ, НЕ УБИВАЕМ

ПОСЛЕДСТВИЯ

  • Программист обязан проверять rax после каждого вызова
  • Гибкость: skip/log/abort — на выбор
  • Единственный способ для batch-обработки

Концепция: Потоковое чтение с буферизацией

Проблема:

При чтении файла с множеством чисел наивный подход — один read() syscall на каждое число. Это убивает производительность: ~1000 тактов на syscall × 10000 чисел = 10 миллионов тактов впустую.

Сценарий 1: Однократное чтение (read_*_from_fd)

┌─────────────────────────────────────────────────────────────┐
│  ФАЙЛ: "12345"                                              │
├─────────────────────────────────────────────────────────────┤
│  read_int16_from_fd(fd, &result):                           │
│                                                             │
│  1. syscall read(fd, io_file_buffer, 31)                    │
│     Ядро читает: "12345" (5 байт)                           │
│     Возвращает: rax = 5                                     │
│                                                             │
│  2. Добавляем null-терминатор:                              │
│     io_file_buffer = '\0'                                   │
│     Буфер: "12345\0"                                        │
│                                                             │
│  3. call parse_int16                                        │
│     rax = 12345, rdx = 0 (успех)                            │
│                                                             │
│  4. Сохраняем результат и возвращаемся                      │
│                                                             │
│  ✅ ХОРОШО ДЛЯ ОДНОГО ЧИСЛА                                 │
│  ❌ ПЛОХО ДЛЯ ФАЙЛА С 10000 ЧИСЕЛ:                          │
│     10000 syscall = ~10 миллионов тактов                    │
└─────────────────────────────────────────────────────────────┘

Сценарий 2: Потоковое чтение (read_*_stream)

┌─────────────────────────────────────────────────────────────┐
│  ФАЙЛ: "100 200 300 400 500 ... 10000\\n" (50000 байт)      │
├─────────────────────────────────────────────────────────────┤
│  ПЕРВЫЙ ВЫЗОВ read_int16_stream(fd, &result):               │
│                                                             │
│  1. Проверка: [io_file_stream_size] == 0? → Да, пуст        │
│                                                             │
│  2. syscall read(fd, io_file_stream_buffer, 128)            │
│     Ядро читает 128 байт: "100 200 300 400 500 600 7..."    │
│     io_file_stream_size = 128                               │
│     io_file_stream_pos = 0                                  │
│                                                             │
│  3. Извлекаем символы из буфера (БЕЗ syscall!):             │
│     '1' ← buffer, pos=1, size=127                           │
│     '0' ← buffer, pos=2, size=126                           │
│     '0' ← buffer, pos=3, size=125                           │
│     ' ' ← buffer → КОНЕЦ ЧИСЛА                              │
│                                                             │
│  4. Парсим "100", возвращаем rax=1, result=100              │
│                                                             │
│  СОСТОЯНИЕ: pos=4, size=124 (ещё 124 байта в буфере!)       │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  ВТОРОЙ ВЫЗОВ read_int16_stream(fd, &result):               │
├─────────────────────────────────────────────────────────────┤
│  1. Проверка: [io_file_stream_size] == 124? → НЕТ syscall!  │
│                                                             │
│  2. Извлекаем символы из буфера:                            │
│     '2' ← buffer, pos=5, size=123                           │
│     '0' ← buffer, pos=6, size=122                           │
│     '0' ← buffer, pos=7, size=121                           │
│     ' ' ← buffer → КОНЕЦ ЧИСЛА                              │
│                                                             │
│  3. Парсим "200", возвращаем rax=1, result=200              │
│                                                             │
│  БЕЗ ЕДИНОГО SYSCALL!                                       │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  ...после ~30 чисел буфер исчерпан...                       │
├─────────────────────────────────────────────────────────────┤
│  ВЫЗОВ №31: read_int16_stream(fd, &result):                 │
│                                                             │
│  1. Проверка: [io_file_stream_size] == 0? → Да, пуст        │
│                                                             │
│  2. syscall read(fd, io_file_stream_buffer, 128)            │
│     Ядро читает следующие 128 байт                          │
│     io_file_stream_size = 128                               │
│     io_file_stream_pos = 0                                  │
│                                                             │
│  3. Продолжаем чтение из нового буфера...                   │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  ИТОГО ДЛЯ 10000 ЧИСЕЛ:                                     │
├─────────────────────────────────────────────────────────────┤
│  Побайтовый подход:     ~40000 syscall (~40 млн тактов)     │
│  Однократный подход:    ~10000 syscall (~10 млн тактов)     │
│  Потоковый подход:      ~400 syscall   (~400 тыс тактов)    │
│                                                             │
│  УСКОРЕНИЕ: ×25 по сравнению с однократным чтением          │
│             ×100 по сравнению с побайтовым                  │
└─────────────────────────────────────────────────────────────┘

Архитектура потокового буфера

┌─────────────────────────────────────────────────────────────┐
│  ГЛОБАЛЬНОЕ СОСТОЯНИЕ (в .bss)                              │
├─────────────────────────────────────────────────────────────┤
│  io_file_stream_buffer:   resb 128   ← Буфер данных         │
│  io_file_stream_pos:      resq 1     ← Текущая позиция      │
│  io_file_stream_size:     resq 1     ← Остаток непрочитанных│
│  io_file_char_buf:        resb 1     ← Текущий символ       │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  ВНУТРЕННЯЯ ФУНКЦИЯ .read_char                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────┐                        │
│  │ Проверка: есть данные в буфере? │                        │
│  │ mov rax, [io_file_stream_size]  │                        │
│  │ test rax, rax                   │                        │
│  └─────────────────────────────────┘                        │
│            │                                                │
│     ┌──────┴──────┐                                         │
│     │             │                                         │
│     ▼             ▼                                         │
│  size == 0     size > 0                                     │
│  (буфер пуст)  (данные есть)                                │
│     │             │                                         │
│     ▼             ▼                                         │
│  ┌─────────┐  ┌───────────────────────────────┐             │
│  │ syscall │  │ Извлечь символ:               │             │
│  │ read    │  │ rcx = [io_file_stream_pos]    │             │
│  │ (128    │  │ al = [buffer + rcx]           │             │
│  │  байт)  │  │ [io_file_char_buf] = al       │             │
│  │         │  │ pos++                         │             │
│  │ size=n  │  │ size--                        │             │
│  │ pos=0   │  │ rax = 1 (успех)               │             │
│  └─────────┘  └───────────────────────────────┘             │
│       │             │                                       │
│       └─────┬───────┘                                       │
│             ▼                                               │
│    Символ в io_file_char_buf                                │
└─────────────────────────────────────────────────────────────┘

Концепция: Разделение буферов по источникам

Проблема:

Если использовать общие буферы для stdin (Layer 2) и файлового ввода (Layer 3), возникает состязание за данные — один слой перезаписывает данные другого.

Сценарий: Конфликт буферов (БАГ)

┌─────────────────────────────────────────────────────────────┐
│  ПРОГРАММА: Запрашивает подтверждение, затем читает файл    │
├─────────────────────────────────────────────────────────────┤
│  1. Пользователь вводит: "yes" + Enter                      │
│     read_signed_input читает в io_interactive_buffer:       │
│     ┌───┬───┬───┬───┬───────────────────────────┐           │
│     │'y'│'e'│'s'│\n │ ... (остаток буфера)      │           │
│     └───┴───┴───┴───┴───────────────────────────┘           │
│     io_interactive_pos = 0                                  │
│     io_interactive_size = 4                                 │
│                                                             │
│  2. БЕЗ РАЗДЕЛЕНИЯ БУФЕРОВ:                                 │
│     read_int16_stream использует ТОТ ЖЕ буфер!              │
│     ┌───┬───┬───┬───┬───────────────────────────┐           │
│     │'1'│'0'│'0'│' '│'2'│'0'│'0'│ ... (из файла)│           │
│     └───┴───┴───┴───┴───────────────────────────┘           │
│                                                             │
│  ❌ ДАННЫЕ "yes\n" ПЕРЕЗАПИСАНЫ!                            │
│     Если программа попытается дочитать stdin — получит      │
│     данные из файла вместо пользовательского ввода          │
└─────────────────────────────────────────────────────────────┘

Решение: Изолированные пространства имён

┌─────────────────────────────────────────────────────────────┐
│  РАЗДЕЛЬНЫЕ БУФЕРЫ                                          │
├─────────────────────────────────────────────────────────────┤
│  LAYER 2 (stdin):                                           │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ io_interactive_buffer:   resb 128  ← Только для stdin  │ │
│  │ io_interactive_pos:      resq 1                        │ │
│  │ io_interactive_size:     resq 1                        │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                             │
│  LAYER 3 (файлы):                                           │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ io_file_buffer:          resb 32   ← Однократное чтение│ │
│  │ io_file_stream_buffer:   resb 128  ← Потоковое чтение  │ │
│  │ io_file_stream_pos:      resq 1                        │ │
│  │ io_file_stream_size:     resq 1                        │ │
│  │ io_file_char_buf:        resb 1    ← Текущий символ    │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                             │
│  ✅ НЕЗАВИСИМЫЕ СОСТОЯНИЯ                                   │
│     -  Чтение из stdin не влияет на чтение из файла         │
│     -  Можно миксовать интерактивный и файловый ввод        │
│     -  Каждый буфер сохраняет своё состояние (pos, size)    │
└─────────────────────────────────────────────────────────────┘

Таблица буферов по назначению

Буфер Размер Слой Назначение
io_interactive_buffer 128 байт Layer 2 Чтение из stdin
io_interactive_pos 8 байт Layer 2 Позиция в буфере stdin
io_interactive_size 8 байт Layer 2 Остаток байт в буфере stdin
io_file_buffer 32-64 байта Layer 3 Однократное чтение из файла
io_file_stream_buffer 128 байт Layer 3 Потоковое чтение из файла
io_file_stream_pos 8 байт Layer 3 Позиция в файловом потоке
io_file_stream_size 8 байт Layer 3 Остаток байт в файловом потоке
io_file_char_buf 1 байт Layer 3 Текущий извлечённый символ

Концепция: Автоматический пропуск whitespace

Проблема:

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

Сценарий: Проблемный файл

┌─────────────────────────────────────────────────────────────────┐
│  ФАЙЛ numbers.txt (реальный файл, созданный в редакторе):       │
├─────────────────────────────────────────────────────────────────┤
│  100  200     300                                               │
│  400                                                            │
│    500                                                          │
│                                                                 │
│  БАЙТОВОЕ ПРЕДСТАВЛЕНИЕ:                                        │
│  '1' '0' '0' ' ' ' ' '2' '0' '0' ' ' ' ' ' ' ' ' '3' '0'        │
│               ↑   ↑               ↑       ↑                     │
│           множественные          тоже пробелы                   │
│              пробелы                                            │
│                                                                 │
│  '0' '\n' '4' '0' '0' '\n' ' ' ' ' '5' '0' '0' '\n'             │
│       ↑                 ↑   ↑   ↑                               │
│    перенос         перенос  ведущие                             │
│    строки          строки   пробелы                             │
└─────────────────────────────────────────────────────────────────┘

Алгоритм пропуска whitespace

┌─────────────────────────────────────────────────────────────┐
│  ФУНКЦИЯ read_*_stream: ЦИКЛ ПРОПУСКА                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  .skip_whitespace:                                          │
│      call .read_char          ; Читаем один символ          │
│      test rax, rax                                          │
│      jle .handle_read_error   ; EOF или ошибка              │
│                                                             │
│      movzx rax, byte [io_file_char_buf]                     │
│                                                             │
│      cmp al, ' '              ; Пробел (ASCII 32)?          │
│      je .skip_whitespace      ; → Пропускаем                │
│                                                             │
│      cmp al, 10               ; LF (ASCII 10)?              │
│      je .skip_whitespace      ; → Пропускаем                │
│                                                             │
│      cmp al, 9                ; TAB (ASCII 9)?              │
│      je .skip_whitespace      ; → Пропускаем                │
│                                                             │
│      cmp al, 13               ; CR (ASCII 13)?              │
│      je .skip_whitespace      ; → Пропускаем                │
│                                                             │
│      ; Символ НЕ whitespace → начало числа!                 │
│      mov byte [r13 + rbx], al ; Сохраняем первый символ     │
│      inc rbx                  ; Увеличиваем позицию         │
│      jmp .read_number         ; Переходим к чтению числа    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Обрабатываемые символы

Символ ASCII Название Встречается в
' ' 32 Пробел Любой файл
'\t' 9 Табуляция TSV-файлы, исходный код
'\n' 10 Line Feed Unix, Linux, macOS
'\r' 13 Carriage Return Windows (\r\n), старые Mac

Результат: Код корректно обрабатывает файлы с любым форматированием и из любой ОС.


Концепция: Dependency Injection на уровне syscall

Проблема:

Layer 2 жёстко привязан к stdin (дескриптор 0) и stdout (дескриптор 1). Невозможно переключить источник данных без переписывания кода.

Сценарий: Жёсткая привязка (Layer 2)

┌─────────────────────────────────────────────────────────────┐
│  LAYER 2: read_signed_input_vars                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  layer2_read_string_signed:                                 │
│      ...                                                    │
│      xor rax, rax             ; sys_read                    │
│      xor rdi, rdi             ; fd = 0 (STDIN)  ← ЖЁСТКО!   │
│      lea rsi, [buffer]                                      │
│      mov rdx, 128                                           │
│      syscall                                                │
│                                                             │
│  ❌ НЕВОЗМОЖНО:                                             │
│     -  Читать из файла                                      │
│     -  Читать из пайпа                                      │
│     -  Читать из сокета                                     │
│     -  Тестировать без участия человека                     │
└─────────────────────────────────────────────────────────────┘

Сценарий: Параметризованный дескриптор (Layer 3)

┌─────────────────────────────────────────────────────────────┐
│  LAYER 3: read_int16_stream(fd, &result)                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  read_int16_stream:                                         │
│      push rbp                                               │
│      mov rbp, rsp                                           │
│      ...                                                    │
│      mov r12, rdi             ; r12 = fd (ПАРАМЕТР!)        │
│      ...                                                    │
│                                                             │
│  .read_char:                                                │
│      ...                                                    │
│      xor rax, rax             ; sys_read                    │
│      mov rdi, r12             ; fd из параметра ← ГИБКО!    │
│      lea rsi, [buffer]                                      │
│      mov rdx, 128                                           │
│      syscall                                                │
│                                                             │
│  ✅ МОЖНО ПЕРЕДАТЬ ЛЮБОЙ ДЕСКРИПТОР:                        │
│     -  fd = результат open("file.txt")                      │
│     -  fd = результат pipe()                                │
│     -  fd = результат socket()                              │
│     -  fd = 0 (stdin) — для совместимости                   │
└─────────────────────────────────────────────────────────────┘

Примеры использования

┌─────────────────────────────────────────────────────────────┐
│  ПРИМЕР 1: Чтение из файла                                  │
├─────────────────────────────────────────────────────────────┤
│  mov rax, 2                   ; sys_open                    │
│  lea rdi, [filename]                                        │
│  xor rsi, rsi                 ; O_RDONLY                    │
│  syscall                                                    │
│  mov r15, rax                 ; r15 = fd                    │
│                                                             │
│  mov rdi, r15                 ; Передаём fd в функцию       │
│  lea rsi, [result]                                          │
│  call read_int16_stream       ; ← Читаем из файла!          │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  ПРИМЕР 2: Чтение из пайпа (для тестирования)               │
├─────────────────────────────────────────────────────────────┤
│  ; В тесте: echo "12345" | ./program                        │
│                                                             │
│  xor rdi, rdi                 ; fd = 0 (stdin = пайп)       │
│  lea rsi, [result]                                          │
│  call read_int16_stream       ; ← Читаем из пайпа!          │
│                                                             │
│  ; Программа не знает, откуда данные — stdin или пайп       │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  ПРИМЕР 3: Автоматическое тестирование                      │
├─────────────────────────────────────────────────────────────┤
│  ; test_runner.sh:                                          │
│  echo "100 200 300" > test_input.txt                        │
│  ./program < test_input.txt > test_output.txt               │
│  diff test_output.txt expected_output.txt                   │
│                                                             │
│  ; Программа использует read_int16_stream с fd=0            │
│  ; Shell подменяет stdin на файл — программа не меняется!   │
└─────────────────────────────────────────────────────────────┘

Принцип Dependency Injection:

  • Зависимость (файловый дескриптор) передаётся извне, а не создаётся внутри функции
  • Функция становится тестируемой: можно подставить любой источник данных
  • Функция становится переиспользуемой: один код для файлов, пайпов, сокетов

📋 6. Практические примеры интеграции

Этот раздел демонстрирует реальные паттерны использования файлового ввода-вывода (Layer 3) в типичных задачах программирования на ассемблере. Все примеры используют модули из статьи «Ввод-вывод на чистом NASM: Готовые модули (без libc)» с добавленной поддержкой Layer 3 (см. раздел «🎓 4. Интеграция с модулями ввода-вывода» выше).


Чтение и сумма массива (int16_t)

Задача: прочитать неизвестное количество знаковых 16-битных чисел из файла, вычислить сумму и вывести результат.

Полный код sum_from_file.asm (io_signed)
; ============================================================================
; sum_numbers.asm
;
; Программа для суммирования знаковых 16-битных целых чисел из файла.
; Читает числа из текстового файла, вычисляет их сумму и выводит результат
; в стандартный поток вывода.
;
; АЛГОРИТМ:
;   1. Открытие входного файла для чтения
;   2. Инициализация суммы нулём
;   3. Циклическое чтение чисел из файла:
;      - Чтение очередного числа (int16_t)
;      - Знаковое расширение до int32_t
;      - Добавление к накопленной сумме
;   4. Вывод итоговой суммы в формате "Result = <сумма>"
;   5. Корректное завершение с кодом возврата
;
; ФОРМАТ ВХОДНОГО ФАЙЛА:
;   Текстовый файл с числами, разделёнными пробелами, табуляцией или
;   переводами строк. Каждое число должно быть в диапазоне -32768..32767.
;
;   Пример (numbers.txt):
;     10 20 30
;     -5 15
;     100
;
; ФОРМАТ ВЫВОДА:
;   Result = <сумма>\n
;
;   Пример:
;     Result = 170
;
; ОБРАБОТКА ОШИБОК:
;   • Ошибка открытия файла → exit(1)
;   • Ошибка парсинга числа → пропуск числа, продолжение чтения
;   • EOF без данных → сумма = 0
;
; ИСПОЛЬЗУЕМЫЕ МОДУЛИ:
;   • io_signed.asm - для чтения int16 и вывода int32
; ============================================================================

default rel                             ; Использовать RIP-relative адресацию по умолчанию

section .data
    ; Имя входного файла с числами для суммирования
    fname db "numbers.txt", 0           ; Null-терминированная строка

section .bss
    ; Переменные программы
    fd resq 1                           ; Файловый дескриптор открытого файла (int fd)
    current_num resw 1                  ; Текущее прочитанное число (int16_t)
    sum resd 1                          ; Накопленная сумма всех чисел (int32_t)

section .text
    ; Точка входа программы
    global _start
    
    ; Импортируемые функции из модуля io_signed.asm
    extern read_int16_stream            ; Потоковое чтение int16 из файла
    extern print_signed_output_var      ; Вывод int32 в формате "Result = N"

; ============================================================================
; ОСНОВНАЯ ПРОГРАММА
; ============================================================================

_start:
    ; --- Открытие входного файла ---
    ; syscall: open(filename, flags, mode)
    ; Открываем файл только для чтения (O_RDONLY = 0)
    mov rax, 2
    lea rdi, [fname]
    xor rsi, rsi                        ; Флаги: O_RDONLY (0) - только чтение
    syscall
    
    ; Проверка результата открытия файла
    test rax, rax                       ; Проверка: rax < 0? (ошибка открытия)
    js .error_open                      ; Если да - переход к обработке ошибки
    mov [fd], rax                       ; Сохранение файлового дескриптора
    
    ; --- Инициализация суммы ---
    mov dword [sum], 0                  ; Установка начального значения суммы = 0

; --- Главный цикл чтения и суммирования ---
; Инвариант цикла: [sum] содержит сумму всех прочитанных чисел
.read_loop:
    ; Чтение очередного числа из файла
    ; Параметры: rdi = fd, rsi = адрес для сохранения числа
    mov rdi, [fd]
    lea rsi, [current_num]              ; Адрес переменной для сохранения числа
    call read_int16_stream
    
    ; Анализ результата чтения
    ; rax: 1 = успех, 0 = EOF, <0 = ошибка
    test rax, rax                       ; Проверка значения rax
    jz .calc_done                       ; rax = 0 → EOF, завершение чтения
    js .read_loop                       ; rax < 0 → ошибка парсинга, пропускаем число
    
    ; --- Добавление прочитанного числа к сумме ---
    ; Знаковое расширение int16 → int32 для корректной арифметики
    movsx eax, word [current_num]       ; Загрузка и знаковое расширение:
                                        ; int16 (-32768..32767) → int32
    add [sum], eax                      ; Добавление к накопленной сумме
    jmp .read_loop

; --- Завершение вычислений ---
.calc_done:
    ; Закрытие входного файла
    ; syscall: close(fd)
    mov rax, 3
    mov rdi, [fd]
    syscall
    
    ; --- Вывод результата в stdout ---
    ; print_signed_output_var автоматически выводит "Result = <число>\n"
    mov edi, [sum]                      ; Загрузка итоговой суммы (int32)
    call print_signed_output_var
    
    ; --- Успешное завершение программы ---
    ; syscall: exit(0)
    mov rax, 60
    xor rdi, rdi
    syscall

; ============================================================================
; ОБРАБОТКА ОШИБОК
; ============================================================================

; --- Ошибка открытия файла ---
; Возникает, если файл не существует, нет прав доступа и т.д.
.error_open:
    mov rdi, 1
    jmp .exit

; --- Ошибка парсинга числа ---
; Возникает при некорректном формате числа в файле
; В текущей версии не используется, так как ошибки обрабатываются в цикле
.error_parse:
    mov rdi, 2

; --- Общая точка завершения с ошибкой ---
.exit:
    ; syscall: exit(error_code)
    mov rax, 60
    ; rdi уже содержит код ошибки (1 или 2)
    syscall

Компиляция и тест:

echo "10 20 30 40 50" > numbers.txt
nasm -f elf64 io_signed.asm -o io_signed.o
nasm -f elf64 sum_from_file.asm -o sum_from_file.o
ld -o sum_from_file sum_from_file.o io_signed.o
./sum_from_file


Входной файл numbers.txt:

10 20 30
40 50

Ожидаемый вывод:

Result = 150

Генерация таблицы квадратов (int32_t запись)

Задача: вычислить и записать в файл квадраты чисел от 1 до 10.

Полный код write_squares.asm (io_signed запись)
; ============================================================================
; write_squares.asm
;
; Программа для генерации таблицы квадратов чисел от 1 до 10.
; Вычисляет квадраты целых чисел и записывает результаты в текстовый файл.
;
; АЛГОРИТМ:
;   1. Открытие выходного файла для записи (с созданием/перезаписью)
;   2. Цикл от 1 до 10:
;      - Вычисление квадрата текущего числа (i²)
;      - Запись результата в файл в текстовом формате
;   3. Закрытие файла
;   4. Корректное завершение программы
;
; ФОРМАТ ВЫХОДНОГО ФАЙЛА:
;   Текстовый файл с квадратами чисел, каждое значение на отдельной строке.
;
;   Пример (squares.txt):
;     1
;     4
;     9
;     16
;     25
;     36
;     49
;     64
;     81
;     100
;
; ОБРАБОТКА ОШИБОК:
;   • Ошибка создания файла → exit(1)
;   • Успешное выполнение → exit(0)
;
; ИСПОЛЬЗУЕМЫЕ МОДУЛИ:
;   • io_signed.asm - для записи int32 в файл
;
; ОСОБЕННОСТИ РЕАЛИЗАЦИИ:
;   • Используется регистр RBX для счётчика цикла, так как RCX может
;     изменяться внутри системных вызовов, выполняемых write_int32_to_fd
;   • Диапазон вычислений: 1² до 10² (результаты: 1..100)
; ============================================================================

default rel                             ; Использовать RIP-relative адресацию по умолчанию

section .data
    ; Имя выходного файла для записи квадратов
    fname db "squares.txt", 0           ; Null-терминированная строка

section .bss
    ; Переменные программы
    fd resq 1                           ; Файловый дескриптор открытого файла (int fd)

section .text
    ; Точка входа программы
    global _start
    
    ; Импортируемые функции из модуля io_signed.asm
    extern write_int32_to_fd            ; Запись int32 в файл в текстовом формате

; ============================================================================
; ОСНОВНАЯ ПРОГРАММА
; ============================================================================

_start:
    ; --- Открытие выходного файла ---
    ; syscall: open(filename, flags, mode)
    mov rax, 2
    lea rdi, [fname]
    mov rsi, 0x241                      ; Флаги: O_WRONLY | O_CREAT | O_TRUNC
                                        ; O_WRONLY (0x01) - только запись
                                        ; O_CREAT  (0x40) - создать если не существует
                                        ; O_TRUNC  (0x200) - очистить если существует
    mov rdx, 0x1A4                      ; Права доступа: 0644 (rw-r--r--)
                                        ; Владелец: чтение+запись
                                        ; Группа: чтение
                                        ; Остальные: чтение
    syscall
    
    ; Проверка результата открытия файла
    test rax, rax                       ; Проверка: rax < 0? (ошибка создания)
    js .error                           ; Если да - переход к обработке ошибки
    mov [fd], rax                       ; Сохранение файлового дескриптора

    ; --- Инициализация счётчика цикла ---
    ; ВАЖНО: используем RBX, так как RCX портится в syscall внутри write_int32_to_fd
    mov rbx, 1                          ; rbx = 1 (начальное значение счётчика)

; --- Главный цикл вычисления и записи квадратов ---
; Инвариант цикла: rbx ∈ [1, 10], в файл записаны квадраты от 1² до (rbx-1)²
.loop:
    ; Проверка условия завершения цикла
    cmp rbx, 11                         ; Проверка: rbx >= 11?
    jge .done                           ; Если да - выход из цикла (обработано 1..10)
    
    ; --- Вычисление квадрата текущего числа ---
    ; Алгоритм: i² = i × i
    mov rax, rbx                        ; rax = текущее значение счётчика (i)
    imul rax, rax                       ; rax = rax × rax = i²
    
    ; --- Запись результата в файл ---
    ; Параметры write_int32_to_fd:
    ;   rdi = файловый дескриптор
    ;   esi = значение для записи (int32)
    mov rdi, [fd]                       ; Загрузка файлового дескриптора
    mov esi, eax                        ; esi = квадрат числа (младшие 32 бита rax)
    call write_int32_to_fd              ; Запись числа в файл (добавляется '\n')
    
    ; --- Переход к следующей итерации ---
    inc rbx                             ; rbx++ (следующее число)
    jmp .loop

; --- Завершение вычислений ---
.done:
    ; Закрытие выходного файла
    ; syscall: close(fd)
    mov rax, 3
    mov rdi, [fd]
    syscall
    
    ; --- Успешное завершение программы ---
    ; syscall: exit(0)
    mov rax, 60
    xor rdi, rdi
    syscall

; ============================================================================
; ОБРАБОТКА ОШИБОК
; ============================================================================

; --- Ошибка создания/открытия файла ---
; Возникает, если невозможно создать файл (нет прав, файловая система и т.д.)
.error:
    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

Компиляция и тест:

nasm -f elf64 io_signed.asm -o io_signed.o
nasm -f elf64 write_squares.asm -o write_squares.o
ld -o write_squares write_squares.o io_signed.o
./write_squares
cat squares.txt


Ожидаемый выходной файл squares.txt:

1
4
9
16
25
36
49
64
81
100

Фильтрация и обработка беззнаковых чисел (uint16_t)

Задача: прочитать беззнаковые 16-битные числа из файла, удвоить каждое и записать результаты в новый файл.

Полный код process_unsigned.asm (io_unsigned)
; ============================================================================
; process_unsigned.asm
;
; Программа для обработки беззнаковых 16-битных чисел из файла.
; Читает числа типа uint16 из входного файла, удваивает каждое значение
; и записывает результаты в выходной файл в формате uint32.
;
; АЛГОРИТМ:
;   1. Открытие входного файла для чтения (unsigned.txt)
;   2. Открытие выходного файла для записи (doubled.txt, с созданием/перезаписью)
;   3. Цикл обработки:
;      - Чтение очередного uint16 числа из входного файла
;      - Удвоение значения (value × 2)
;      - Запись результата в выходной файл как uint32
;      - Повтор до достижения конца файла (EOF)
;   4. Закрытие обоих файлов
;   5. Корректное завершение программы
;
; ФОРМАТ ВХОДНОГО ФАЙЛА:
;   Текстовый файл с беззнаковыми 16-битными числами (uint16).
;   Каждое число на отдельной строке.
;   Диапазон значений: 0..65535
;
;   Пример (unsigned.txt):
;     100
;     200
;     32767
;     65535
;
; ФОРМАТ ВЫХОДНОГО ФАЙЛА:
;   Текстовый файл с беззнаковыми 32-битными числами (uint32).
;   Каждое значение — удвоенное входное число.
;
;   Пример (doubled.txt):
;     200
;     400
;     65534
;     131070
;
; ОБРАБОТКА ОШИБОК:
;   • Ошибка открытия входного/выходного файла → exit(1)
;   • Ошибка чтения числа из входного файла → exit(2)
;   • Успешное выполнение → exit(0)
;
; ИСПОЛЬЗУЕМЫЕ МОДУЛИ:
;   • io_unsigned.asm - для чтения uint16 и записи uint32
;
; ОСОБЕННОСТИ РЕАЛИЗАЦИИ:
;   • Используется регистр RBX для счётчика обработанных строк, так как
;     он сохраняется через системные вызовы внутри внешних функций
;   • Беззнаковое расширение (movzx) гарантирует корректную обработку uint16
;   • Результат удвоения записывается как uint32 для предотвращения переполнения
;     (максимум: 65535 × 2 = 131070, помещается в uint32)
; ============================================================================

default rel                             ; Использовать RIP-relative адресацию по умолчанию

section .data
    ; Имена файлов для обработки
    fin db "unsigned.txt", 0            ; Входной файл с uint16 числами
    fout db "doubled.txt", 0            ; Выходной файл с удвоенными значениями (uint32)

section .bss
    ; Переменные программы
    fd_in resq 1                        ; Файловый дескриптор входного файла (int fd)
    fd_out resq 1                       ; Файловый дескриптор выходного файла (int fd)
    current resw 1                      ; Буфер для текущего прочитанного числа (uint16)

section .text
    ; Точка входа программы
    global _start
    
    ; Импортируемые функции из модуля io_unsigned.asm
    extern read_uint16_stream           ; Чтение uint16 из файла в текстовом формате
    extern write_uint32_to_fd           ; Запись uint32 в файл в текстовом формате

; ============================================================================
; ОСНОВНАЯ ПРОГРАММА
; ============================================================================

_start:
    ; --- Открытие входного файла ---
    ; syscall: open(filename, flags, mode)
    mov rax, 2
    lea rdi, [fin]
    xor rsi, rsi                        ; Флаги: O_RDONLY (0x00) - только чтение
    syscall
    
    ; Проверка результата открытия входного файла
    test rax, rax                       ; Проверка: rax < 0? (ошибка открытия)
    js .error                           ; Если да - переход к обработке ошибки
    mov [fd_in], rax                    ; Сохранение файлового дескриптора входного файла
    
    ; --- Открытие выходного файла ---
    ; syscall: open(filename, flags, mode)
    mov rax, 2
    lea rdi, [fout]
    mov rsi, 0x241                      ; Флаги: O_WRONLY | O_CREAT | O_TRUNC
                                        ; O_WRONLY (0x01) - только запись
                                        ; O_CREAT  (0x40) - создать если не существует
                                        ; O_TRUNC  (0x200) - очистить если существует
    mov rdx, 0x1A4                      ; Права доступа: 0644 (rw-r--r--)
                                        ; Владелец: чтение+запись
                                        ; Группа: чтение
                                        ; Остальные: чтение
    syscall
    
    ; Проверка результата открытия выходного файла
    test rax, rax                       ; Проверка: rax < 0? (ошибка создания)
    js .error                           ; Если да - переход к обработке ошибки
    mov [fd_out], rax                   ; Сохранение файлового дескриптора выходного файла

    ; --- Инициализация счётчика обработанных строк ---
    ; ВАЖНО: используем RBX, так как он сохраняется через syscall в функциях I/O
    xor rbx, rbx                        ; rbx = 0 (счётчик прочитанных строк)

; --- Главный цикл обработки чисел ---
; Инвариант цикла: обработано rbx чисел, результаты записаны в выходной файл
.loop:
    inc rbx                             ; rbx++ (увеличение счётчика строк)
    
    ; --- Чтение очередного числа из входного файла ---
    ; Параметры read_uint16_stream:
    ;   rdi = файловый дескриптор
    ;   rsi = адрес буфера для записи прочитанного uint16
    ; Возвращает:
    ;   rax > 0  - успешное чтение
    ;   rax = 0  - конец файла (EOF)
    ;   rax < 0  - ошибка чтения/парсинга
    mov rdi, [fd_in]                    ; Загрузка файлового дескриптора входного файла
    lea rsi, [current]                  ; Адрес буфера для числа
    call read_uint16_stream             ; Чтение uint16 из файла
    
    ; Проверка результата чтения
    test rax, rax                       ; Проверка значения rax
    jz .done                            ; Если rax = 0 → EOF, завершение обработки
    js .error_read                      ; Если rax < 0 → ошибка чтения
    
    ; --- Загрузка и удвоение прочитанного числа ---
    ; ВАЖНО: movzx для беззнакового расширения uint16 → uint32
    movzx eax, word [current]           ; eax = прочитанное число (uint16 → uint32)
    
    ; Алгоритм удвоения: result = value × 2
    add eax, eax                        ; eax = eax + eax = 2 × value
    
    ; --- Запись результата в выходной файл ---
    ; Параметры write_uint32_to_fd:
    ;   rdi = файловый дескриптор
    ;   esi = значение для записи (uint32)
    mov rdi, [fd_out]                   ; Загрузка файлового дескриптора выходного файла
    mov esi, eax                        ; esi = удвоенное значение (uint32)
    call write_uint32_to_fd             ; Запись числа в файл (добавляется '\n')
    
    ; --- Переход к следующей итерации ---
    jmp .loop

; --- Завершение обработки (достигнут EOF) ---
.done:
    ; Закрытие входного файла
    ; syscall: close(fd)
    mov rax, 3
    mov rdi, [fd_in]
    syscall
    
    ; Закрытие выходного файла
    ; syscall: close(fd)
    mov rax, 3
    mov rdi, [fd_out]
    syscall
    
    ; --- Успешное завершение программы ---
    ; syscall: exit(0)
    mov rax, 60
    xor rdi, rdi
    syscall

; ============================================================================
; ОБРАБОТКА ОШИБОК
; ============================================================================

; --- Ошибка чтения/парсинга числа из входного файла ---
; Возникает при некорректных данных или проблемах чтения
.error_read:
    ; syscall: exit(2)
    mov rdi, 2
    jmp .exit

; --- Ошибка открытия/создания файла ---
; Возникает, если невозможно открыть входной файл или создать выходной файл
; (нет прав доступа, файл не существует, файловая система и т.д.)
.error:
    ; syscall: exit(1)
    mov rdi, 1

; --- Общая точка выхода с кодом ошибки ---
.exit:
    ; syscall: exit(rdi)
    ; rdi уже содержит код возврата (1 или 2)
    mov rax, 60
    syscall

Компиляция и тест:

echo "5 10 100 1000 65535" > unsigned.txt
nasm -f elf64 io_unsigned.asm -o io_unsigned.o
nasm -f elf64 process_unsigned.asm -o process_unsigned.o
ld -o process_unsigned process_unsigned.o io_unsigned.o
./process_unsigned
cat doubled.txt


Входной файл unsigned.txt:

5 10 100 1000 65535

Ожидаемый выходной файл doubled.txt:

10
20
200
2000
131070

Обработка чисел с плавающей точкой (float)

Задача: прочитать числа типа float из файла, умножить каждое на 2.5 и записать результаты.

Полный код process_floats.asm (io_float)
; ============================================================================
; process_floats.asm
;
; Программа для обработки вещественных чисел одинарной точности (float).
; Читает числа типа float из входного файла, масштабирует каждое значение
; умножением на константу 2.5 и записывает результаты в выходной файл.
;
; АЛГОРИТМ:
;   1. Открытие входного файла для чтения (floats.txt)
;   2. Открытие выходного файла для записи (scaled.txt, с созданием/перезаписью)
;   3. Цикл обработки:
;      - Чтение очередного float числа из входного файла
;      - Загрузка значения в стек сопроцессора FPU
;      - Умножение на константу 2.5
;      - Сохранение результата из FPU в память
;      - Запись результата в выходной файл
;      - Повтор до достижения конца файла (EOF)
;      - При ошибке чтения строки — пропуск и продолжение
;   4. Закрытие обоих файлов
;   5. Корректное завершение программы
;
; ФОРМАТ ВХОДНОГО ФАЙЛА:
;   Текстовый файл с вещественными числами одинарной точности (float).
;   Каждое число на отдельной строке в десятичной нотации.
;   Диапазон значений: ±3.4 × 10³⁸ (IEEE 754 single precision)
;
;   Пример (floats.txt):
;     10.0
;     -5.5
;     3.14159
;     100.25
;
; ФОРМАТ ВЫХОДНОГО ФАЙЛА:
;   Текстовый файл с вещественными числами одинарной точности (float).
;   Каждое значение — масштабированное входное число (× 2.5).
;
;   Пример (scaled.txt):
;     25.0
;     -13.75
;     7.8539749
;     250.625
;
; ОБРАБОТКА ОШИБОК:
;   • Ошибка открытия входного/выходного файла → exit(1)
;   • Ошибка чтения/парсинга числа → пропуск строки, продолжение обработки
;   • Успешное выполнение → exit(0)
;
; ИСПОЛЬЗУЕМЫЕ МОДУЛИ:
;   • io_float.asm - для чтения и записи float в текстовом формате
;
; ОСОБЕННОСТИ РЕАЛИЗАЦИИ:
;   • Использование стека сопроцессора x87 FPU для вычислений с плавающей точкой
;   • Константа масштабирования (2.5) хранится в секции .data как float (4 байта)
;   • Некорректные строки пропускаются без прерывания программы
;   • Битовое представление float передаётся в write_float_to_fd через регистр esi
; ============================================================================

default rel                             ; Использовать RIP-relative адресацию по умолчанию

section .data
    ; Имена файлов для обработки
    fin db "floats.txt", 0              ; Входной файл с float числами
    fout db "scaled.txt", 0             ; Выходной файл с масштабированными значениями
    scale_factor dd 2.5                 ; Константа масштабирования (float, 4 байта)

section .bss
    ; Переменные программы
    fd_in resq 1                        ; Файловый дескриптор входного файла (int fd)
    fd_out resq 1                       ; Файловый дескриптор выходного файла (int fd)
    current resd 1                      ; Буфер для текущего числа (float, 4 байта)

section .text
    ; Точка входа программы
    global _start
    
    ; Импортируемые функции из модуля io_float.asm
    extern read_float_stream            ; Чтение float из файла в текстовом формате
    extern write_float_to_fd            ; Запись float в файл в текстовом формате

; ============================================================================
; ОСНОВНАЯ ПРОГРАММА
; ============================================================================

_start:
    ; --- Открытие входного файла ---
    ; syscall: open(filename, flags, mode)
    mov rax, 2
    lea rdi, [fin]
    xor rsi, rsi                        ; Флаги: O_RDONLY (0x00) - только чтение
    syscall
    
    ; Проверка результата открытия входного файла
    test rax, rax                       ; Проверка: rax < 0? (ошибка открытия)
    js .error                           ; Если да - переход к обработке ошибки
    mov [fd_in], rax                    ; Сохранение файлового дескриптора входного файла
    
    ; --- Открытие выходного файла ---
    ; syscall: open(filename, flags, mode)
    mov rax, 2
    lea rdi, [fout]
    mov rsi, 0x241                      ; Флаги: O_WRONLY | O_CREAT | O_TRUNC
                                        ; O_WRONLY (0x01) - только запись
                                        ; O_CREAT  (0x40) - создать если не существует
                                        ; O_TRUNC  (0x200) - очистить если существует
    mov rdx, 0x1A4                      ; Права доступа: 0644 (rw-r--r--)
                                        ; Владелец: чтение+запись
                                        ; Группа: чтение
                                        ; Остальные: чтение
    syscall
    
    ; Проверка результата открытия выходного файла
    test rax, rax                       ; Проверка: rax < 0? (ошибка создания)
    js .error                           ; Если да - переход к обработке ошибки
    mov [fd_out], rax                   ; Сохранение файлового дескриптора выходного файла

; --- Главный цикл обработки чисел ---
; Инвариант цикла: все корректные числа из обработанных строк масштабированы
; и записаны в выходной файл; некорректные строки пропущены
.loop:
    ; --- Чтение очередного числа из входного файла ---
    ; Параметры read_float_stream:
    ;   rdi = файловый дескриптор
    ;   rsi = адрес буфера для записи прочитанного float
    ; Возвращает:
    ;   rax > 0  - успешное чтение
    ;   rax = 0  - конец файла (EOF)
    ;   rax < 0  - ошибка чтения/парсинга
    mov rdi, [fd_in]                    ; Загрузка файлового дескриптора входного файла
    lea rsi, [current]                  ; Адрес буфера для числа (float)
    call read_float_stream              ; Чтение float из файла
    
    ; Проверка результата чтения
    test rax, rax                       ; Проверка значения rax
    jz .done                            ; Если rax = 0 → EOF, завершение обработки
    js .loop                            ; Если rax < 0 → ошибка, пропуск строки

    ; --- Вычисление масштабированного значения (FPU) ---
    ; Алгоритм: result = value × scale_factor
    
    ; Загрузка исходного значения в стек FPU
    fld dword [current]                 ; ST(0) = прочитанное float число
    
    ; Умножение на константу масштабирования
    fmul dword [scale_factor]           ; ST(0) = ST(0) × 2.5
    
    ; Сохранение результата из стека FPU в память
    fstp dword [current]                ; [current] = ST(0), затем pop стека FPU
                                        ; После fstp стек FPU пуст
    
    ; --- Запись результата в выходной файл ---
    ; Параметры write_float_to_fd:
    ;   rdi = файловый дескриптор
    ;   esi = битовое представление float (4 байта, IEEE 754)
    mov rdi, [fd_out]                   ; Загрузка файлового дескриптора выходного файла
    mov esi, [current]                  ; esi = битовое представление результата (float)
    call write_float_to_fd              ; Запись числа в файл (добавляется '\n')
    
    ; --- Переход к следующей итерации ---
    jmp .loop

; --- Завершение обработки (достигнут EOF) ---
.done:
    ; Закрытие входного файла
    ; syscall: close(fd)
    mov rax, 3
    mov rdi, [fd_in]
    syscall
    
    ; Закрытие выходного файла
    ; syscall: close(fd)
    mov rax, 3
    mov rdi, [fd_out]
    syscall
    
    ; --- Успешное завершение программы ---
    ; syscall: exit(0)
    mov rax, 60
    xor rdi, rdi
    syscall

; ============================================================================
; ОБРАБОТКА ОШИБОК
; ============================================================================

; --- Ошибка открытия/создания файла ---
; Возникает, если невозможно открыть входной файл или создать выходной файл
; (нет прав доступа, файл не существует, файловая система и т.д.)
.error:
    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

Компиляция и тест:

echo "1.0 2.5 10.0 -3.14" > floats.txt
nasm -f elf64 io_float.asm -o io_float.o
nasm -f elf64 process_floats.asm -o process_floats.o
ld -o process_floats process_floats.o io_float.o
./process_floats
cat scaled.txt


Входной файл floats.txt:

1.0 2.5 10.0 -3.14

Ожидаемый выходной файл scaled.txt:

2.500000
6.250000
25.000000
-7.850000

Пакетная обработка: множественные файлы (int16_t)

Задача: обработать несколько входных файлов, вычислить статистику (минимум, максимум, среднее) для каждого и записать результаты.

Полный код batch_stats.asm (io_signed)
; ============================================================================
; batch_stats.asm
;
; Программа для пакетного статистического анализа целочисленных данных.
; Обрабатывает несколько входных файлов, вычисляя для каждого файла
; статистики: минимум, максимум и среднее арифметическое значение.
; Результаты записываются в единый лог-файл с форматированными метками.
;
; АЛГОРИТМ:
;   1. Открытие выходного файла для записи статистик (stats.log)
;   2. Цикл по списку входных файлов:
;      a. Открытие очередного входного файла из таблицы files[]
;      b. Инициализация статистических аккумуляторов:
;         - min_val = 2147483647 (MAX_INT32)
;         - max_val = -2147483648 (MIN_INT32)
;         - sum = 0
;         - count = 0
;      c. Чтение и обработка всех int16 чисел из файла:
;         - Обновление минимума
;         - Обновление максимума
;         - Накопление суммы
;         - Подсчёт количества чисел
;         - При ошибке парсинга — пропуск строки
;      d. Вычисление среднего: avg = sum / count
;      e. Запись в лог форматированных строк:
;         "Min = <значение>\n"
;         "Max = <значение>\n"
;         "Avg = <значение>\n"
;      f. Закрытие входного файла
;   3. Закрытие выходного лог-файла
;   4. Корректное завершение программы
;
; ФОРМАТ ВХОДНЫХ ФАЙЛОВ:
;   Текстовые файлы с целыми числами типа int16.
;   Каждое число на отдельной строке.
;   Диапазон значений: -32768..32767
;
;   Пример (data1.txt):
;     10
;     -5
;     20
;     15
;
;   Пример (data2.txt):
;     100
;     -50
;     75
;
; ФОРМАТ ВЫХОДНОГО ФАЙЛА:
;   Лог-файл stats.log с результатами анализа всех входных файлов.
;   Для каждого файла записываются три форматированные строки.
;
;   Пример (stats.log) для двух файлов выше:
;     Min = -5
;     Max = 20
;     Avg = 10
;     Min = -50
;     Max = 100
;     Avg = 41
;
; СПИСОК ОБРАБАТЫВАЕМЫХ ФАЙЛОВ:
;   Задаётся таблицей указателей files[] в секции .data:
;     files[0] → "data1.txt"
;     files[1] → "data2.txt"
;     files[2] → NULL (sentinel, конец списка)
;
; ОБРАБОТКА ОШИБОК:
;   • Ошибка парсинга числа → пропуск строки, продолжение обработки
;   • Пустой файл (count = 0) → пропуск записи статистик
;   • Успешное выполнение → exit(0)
;
; ИСПОЛЬЗУЕМЫЕ МОДУЛИ:
;   • io_signed.asm - для чтения int16 и записи int32
;
; ОСОБЕННОСТИ РЕАЛИЗАЦИИ:
;   • Используется регистр R12 для индекса в таблице файлов, так как
;     он сохраняется через системные вызовы внутри внешних функций
;   • Минимум инициализируется MAX_INT32, максимум — MIN_INT32
;   • Знаковое расширение (movsx) int16 → int32 для корректных сравнений
;   • Целочисленное деление с расширением знака (cdq) для вычисления среднего
;   • Регистр EBX используется для временного хранения среднего значения
;   • Текстовые префиксы ("Min = ", "Max = ", "Avg = ") записываются
;     системным вызовом write перед записью чисел
; ============================================================================

default rel                             ; Использовать RIP-relative адресацию по умолчанию

section .data
    ; Имена файлов для обработки
    file1 db "data1.txt", 0             ; Первый входной файл
    file2 db "data2.txt", 0             ; Второй входной файл
    fout db "stats.log", 0              ; Выходной лог-файл со статистиками
    
    ; Таблица указателей на имена входных файлов
    files:
        dq file1                        ; files[0] → "data1.txt"
        dq file2                        ; files[1] → "data2.txt"
        dq 0                            ; Sentinel: NULL завершает список
    
    ; Текстовые префиксы для форматированного вывода
    str_min db "Min = "                 ; Префикс для минимума (6 байт)
    str_min_len equ $ - str_min
    
    str_max db "Max = "                 ; Префикс для максимума (6 байт)
    str_max_len equ $ - str_max
    
    str_avg db "Avg = "                 ; Префикс для среднего (6 байт)
    str_avg_len equ $ - str_avg

section .bss
    ; Переменные программы
    fd_in resq 1                        ; Файловый дескриптор текущего входного файла
    fd_out resq 1                       ; Файловый дескриптор выходного лог-файла
    current_num resw 1                  ; Буфер для текущего прочитанного числа (int16)
    
    ; Статистические аккумуляторы (int32)
    min_val resd 1                      ; Минимальное значение в текущем файле
    max_val resd 1                      ; Максимальное значение в текущем файле
    sum resd 1                          ; Сумма всех чисел в текущем файле
    count resd 1                        ; Количество чисел в текущем файле

section .text
    ; Точка входа программы
    global _start
    
    ; Импортируемые функции из модуля io_signed.asm
    extern read_int16_stream            ; Чтение int16 из файла в текстовом формате
    extern write_int32_to_fd            ; Запись int32 в файл в текстовом формате

; ============================================================================
; ОСНОВНАЯ ПРОГРАММА
; ============================================================================

_start:
    ; --- Открытие выходного лог-файла ---
    ; syscall: open(filename, flags, mode)
    mov rax, 2
    lea rdi, [fout]
    mov rsi, 0x241                      ; Флаги: O_WRONLY | O_CREAT | O_TRUNC
                                        ; O_WRONLY (0x01) - только запись
                                        ; O_CREAT  (0x40) - создать если не существует
                                        ; O_TRUNC  (0x200) - очистить если существует
    mov rdx, 0x1A4                      ; Права доступа: 0644 (rw-r--r--)
    syscall
    mov [fd_out], rax                   ; Сохранение файлового дескриптора лог-файла
    
    ; --- Инициализация индекса таблицы файлов ---
    ; ВАЖНО: используем R12, так как он сохраняется через syscall в функциях I/O
    xor r12, r12                        ; r12 = 0 (индекс первого файла в таблице)

; ============================================================================
; ЦИКЛ ОБРАБОТКИ ВХОДНЫХ ФАЙЛОВ
; ============================================================================
; Инвариант: обработано r12 файлов, результаты записаны в stats.log

.process_files:
    ; --- Получение имени очередного входного файла ---
    ; Загрузка адреса из таблицы: files[r12]
    lea rax, [files]                    ; rax = адрес начала таблицы files[]
    mov rdi, [rax + r12*8]              ; rdi = files[r12] (указатель на имя файла)
    
    ; Проверка sentinel (конец списка)
    test rdi, rdi                       ; Проверка: rdi == NULL?
    jz .all_done                        ; Если да - все файлы обработаны
    
    ; --- Открытие очередного входного файла ---
    ; syscall: open(filename, flags, mode)
    ; rdi уже содержит адрес имени файла
    mov rax, 2
    xor rsi, rsi                        ; Флаги: O_RDONLY (0x00) - только чтение
    syscall
    mov [fd_in], rax                    ; Сохранение файлового дескриптора входного файла
    
    ; --- Инициализация статистических аккумуляторов ---
    ; min = MAX_INT32 (чтобы любое число было меньше начального min)
    ; max = MIN_INT32 (чтобы любое число было больше начального max)
    ; sum = 0, count = 0
    mov dword [min_val], 0x7FFFFFFF     ; min = 2147483647
    mov dword [max_val], 0x80000000     ; max = -2147483648
    mov dword [sum], 0                  ; sum = 0
    mov dword [count], 0                ; count = 0

; ============================================================================
; ЦИКЛ ЧТЕНИЯ И ОБРАБОТКИ ЧИСЕЛ ИЗ ТЕКУЩЕГО ФАЙЛА
; ============================================================================
; Инвариант: обработано count чисел, аккумуляторы min/max/sum актуальны

.read_loop:
    ; --- Чтение очередного числа из текущего входного файла ---
    ; Параметры read_int16_stream:
    ;   rdi = файловый дескриптор
    ;   rsi = адрес буфера для записи прочитанного int16
    ; Возвращает:
    ;   rax > 0  - успешное чтение
    ;   rax = 0  - конец файла (EOF)
    ;   rax < 0  - ошибка чтения/парсинга
    mov rdi, [fd_in]                    ; Загрузка файлового дескриптора входного файла
    lea rsi, [current_num]              ; Адрес буфера для числа (int16)
    call read_int16_stream              ; Чтение int16 из файла
    
    ; Проверка результата чтения
    test rax, rax                       ; Проверка значения rax
    jz .file_done                       ; Если rax = 0 → EOF, файл обработан
    js .read_loop                       ; Если rax < 0 → ошибка, пропуск строки
    
    ; --- Знаковое расширение int16 → int32 ---
    ; ВАЖНО: movsx для корректного сравнения знаковых чисел
    movsx eax, word [current_num]       ; eax = прочитанное число (int16 → int32)
    
    ; --- Обновление минимального значения ---
    ; Алгоритм: if (current < min) min = current
    cmp eax, [min_val]                  ; Сравнение: eax < min_val?
    jge .check_max                      ; Если eax >= min_val, пропуск обновления
    mov [min_val], eax                  ; min_val = eax (новый минимум)
    
; --- Обновление максимального значения ---
; Алгоритм: if (current > max) max = current
.check_max:
    cmp eax, [max_val]                  ; Сравнение: eax > max_val?
    jle .add_sum                        ; Если eax <= max_val, пропуск обновления
    mov [max_val], eax                  ; max_val = eax (новый максимум)
    
; --- Накопление суммы и подсчёт количества ---
.add_sum:
    add [sum], eax                      ; sum += eax
    inc dword [count]                   ; count++
    jmp .read_loop                      ; Переход к чтению следующего числа

; ============================================================================
; ЗАВЕРШЕНИЕ ОБРАБОТКИ ТЕКУЩЕГО ФАЙЛА
; ============================================================================

.file_done:
    ; --- Закрытие текущего входного файла ---
    ; syscall: close(fd)
    mov rax, 3
    mov rdi, [fd_in]
    syscall
    
    ; --- Вычисление среднего арифметического ---
    ; Алгоритм: avg = sum / count
    mov eax, [sum]                      ; eax = sum
    mov ecx, [count]                    ; ecx = count
    
    ; Проверка на пустой файл (count = 0)
    test ecx, ecx                       ; Проверка: count == 0?
    jz .next_file                       ; Если да - пропуск записи статистик
    
    ; Целочисленное деление со знаком
    cdq                                 ; Расширение знака: eax → edx:eax
    idiv ecx                            ; eax = edx:eax / ecx (sum / count)
    mov ebx, eax                        ; ebx = среднее (сохранение для последующей записи)
    
; --- Запись статистик в выходной лог-файл ---
    
    ; --- Запись "Min = <значение>\n" ---
    ; Запись префикса "Min = "
    ; syscall: write(fd, buffer, count)
    mov rax, 1
    mov rdi, [fd_out]
    lea rsi, [str_min]
    mov rdx, str_min_len
    syscall
    
    ; Запись значения минимума
    mov rdi, [fd_out]                   ; Загрузка файлового дескриптора лог-файла
    mov esi, [min_val]                  ; esi = минимальное значение
    call write_int32_to_fd              ; Запись числа в лог (добавляется '\n')
    
    ; --- Запись "Max = <значение>\n" ---
    ; syscall: write(fd, buffer, count)
    mov rax, 1
    mov rdi, [fd_out]
    lea rsi, [str_max]
    mov rdx, str_max_len
    syscall
    
    ; Запись значения максимума
    mov rdi, [fd_out]
    mov esi, [max_val]                  ; esi = максимальное значение
    call write_int32_to_fd              ; Запись числа в лог (добавляется '\n')
    
    ; --- Запись "Avg = <значение>\n" ---
    ; syscall: write(fd, buffer, count)
    mov rax, 1
    mov rdi, [fd_out]
    lea rsi, [str_avg]
    mov rdx, str_avg_len
    syscall
    
    ; Запись значения среднего
    mov rdi, [fd_out]
    mov esi, ebx                        ; esi = среднее значение (из ebx)
    call write_int32_to_fd              ; Запись числа в лог (добавляется '\n')

; --- Переход к следующему файлу ---
.next_file:
    inc r12                             ; r12++ (следующий индекс в таблице files[])
    jmp .process_files                  ; Обработка следующего файла

; ============================================================================
; ЗАВЕРШЕНИЕ ОБРАБОТКИ ВСЕХ ФАЙЛОВ
; ============================================================================

.all_done:
    ; --- Закрытие выходного лог-файла ---
    ; syscall: close(fd)
    mov rax, 3
    mov rdi, [fd_out]
    syscall
    
    ; --- Успешное завершение программы ---
    ; syscall: exit(0)
    mov rax, 60
    xor rdi, rdi
    syscall

Компиляция и тест:

echo "5 -10 15 20" > data1.txt
echo "100 -50 0 75" > data2.txt
nasm -f elf64 io_signed.asm -o io_signed.o
nasm -f elf64 batch_stats.asm -o batch_stats.o
ld -o batch_stats batch_stats.o io_signed.o
./batch_stats
cat stats.log


Входные файлы:

  • data1.txt: 5 -10 15 20
  • data2.txt: 100 -50 0 75

Ожидаемый выходной файл stats.log:

Min = -10
Max = 20
Avg = 7
Min = -50
Max = 100
Avg = 31

📚 7. Полные примеры программ

Программа 1: Аналог команды cat

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

Алгоритм:

  1. Открыть файл для чтения
  2. Цикл: читать блок 4096 байт → писать в stdout
  3. При EOF — закрыть файл и завершиться
Полная реализация
; ============================================================================
; file_cat.asm - Аналог команды cat
; Использование: ./file_cat (читает input.txt)
; ============================================================================

section .data
    filename db "input.txt", 0
    
    error_open db "Error: Cannot open file input.txt", 10
    error_open_len equ $ - error_open

section .bss
    buffer resb 4096            ; Буфер размером со страницу памяти
    fd resq 1                   ; Файловый дескриптор

section .text
    global _start

_start:
    ; === ОТКРЫТИЕ ФАЙЛА ===
    mov rax, 2                  ; open
    lea rdi, [rel filename]
    xor rsi, rsi                ; O_RDONLY (0)
    syscall
    
    test rax, rax
    js .error_open              ; Если rax < 0
    
    mov [fd], rax               ; Сохранить дескриптор

    ; === ЦИКЛ ЧТЕНИЯ-ЗАПИСИ ===
.loop:
    ; Читать блок из файла
    mov rax, 0                  ; read
    mov rdi, [fd]
    lea rsi, [rel buffer]
    mov rdx, 4096               ; Максимум 4096 байт
    syscall
    
    test rax, rax
    js .error_read              ; Ошибка чтения
    jz .close_file              ; EOF (rax = 0)
    
    ; Записать в stdout
    mov rdx, rax                ; !!! ВАЖНО: длина = прочитанные байты
    mov rax, 1                  ; write
    mov rdi, 1                  ; stdout
    lea rsi, [rel buffer]
    syscall
    
    jmp .loop                   ; Следующий блок

    ; === ЗАКРЫТИЕ И ЗАВЕРШЕНИЕ ===
.close_file:
    mov rax, 3                  ; close
    mov rdi, [fd]
    syscall
    
    mov rax, 60                 ; exit
    xor rdi, rdi                ; код 0 (успех)
    syscall

.error_open:
.error_read:
    mov rax, 1                  ; write
    mov rdi, 2                  ; stderr
    lea rsi, [rel error_open]
    mov rdx, error_open_len
    syscall
    
    mov rax, 60
    mov rdi, 1                  ; код 1 (ошибка)
    syscall

Сборка и тестирование:

# Создать тестовый файл
echo 'Hello from NASM assembly!' > input.txt

# Сборка
nasm -f elf64 file_cat.asm -o file_cat.o
ld file_cat.o -o file_cat

# Запуск
./file_cat
# Вывод: Hello from NASM assembly!

Критический момент: Строка mov rdx, rax перед write. Мы записываем ровно столько байт, сколько прочитали, а не размер буфера.


Программа 2: Запись в Файл

Программа создаёт файл и записывает в него текстовое сообщение.

Полная реализация
; ============================================================================
; file_write.asm - Создание и запись в файл
; ============================================================================

section .data
    filename db "output.txt", 0
    
    message db "This file was created by NASM x86-64!", 10
    msg_len equ $ - message
    
    error_create db "Error: Cannot create file", 10
    error_create_len equ $ - error_create

section .bss
    fd resq 1

section .text
    global _start

_start:
    ; === СОЗДАНИЕ ФАЙЛА ===
    mov rax, 2                  ; open
    lea rdi, [rel filename]
    mov rsi, 0o1101             ; O_WRONLY | O_CREAT | O_TRUNC
    mov rdx, 0644o              ; rw-r--r--
    syscall
    
    test rax, rax
    js .error_create
    
    mov [fd], rax

    ; === ЗАПИСЬ ДАННЫХ ===
    mov rax, 1                  ; write
    mov rdi, [fd]
    lea rsi, [rel message]
    mov rdx, msg_len
    syscall
    
    cmp rax, msg_len            ; Проверка полной записи
    jne .error_write

    ; === ЗАКРЫТИЕ ===
    mov rax, 3                  ; close
    mov rdi, [fd]
    syscall
    
    mov rax, 60                 ; exit
    xor rdi, rdi
    syscall

.error_create:
.error_write:
    mov rax, 1                  ; write
    mov rdi, 2                  ; stderr
    lea rsi, [rel error_create]
    mov rdx, error_create_len
    syscall
    
    mov rax, 60
    mov rdi, 1
    syscall

Проверка результата:

# Сборка
nasm -f elf64 file_write.asm -o file_write.o
ld file_write.o -o file_write

# Запуск
./file_write

# Проверка содержимого
cat output.txt
# Вывод: This file was created by NASM x86-64!

# Проверка прав доступа
ls -l output.txt
# -rw-r--r-- ... output.txt
# └─ Права 0644 применились корректно

Программа 3: Копирование Файла

Полнофункциональная утилита копирования с обработкой ошибок.

Полная реализация
; ============================================================================
; file_copy.asm - Копирование файла
; Использование: ./file_copy (копирует source.txt → dest.txt)
; ============================================================================

section .data
    src_file db "source.txt", 0
    dst_file db "dest.txt", 0
    
    err_src db "Error: Cannot open source.txt", 10
    err_src_len equ $ - err_src
    
    err_dst db "Error: Cannot create dest.txt", 10
    err_dst_len equ $ - err_dst

section .bss
    buffer resb 4096
    fd_src resq 1
    fd_dst resq 1

section .text
    global _start

_start:
    ; === ОТКРЫТИЕ ИСХОДНОГО ФАЙЛА ===
    mov rax, 2
    lea rdi, [rel src_file]
    xor rsi, rsi                ; O_RDONLY
    syscall
    
    test rax, rax
    js .error_src
    
    mov [fd_src], rax

    ; === СОЗДАНИЕ ФАЙЛА НАЗНАЧЕНИЯ ===
    mov rax, 2
    lea rdi, [rel dst_file]
    mov rsi, 0o1101             ; O_WRONLY | O_CREAT | O_TRUNC
    mov rdx, 0644o
    syscall
    
    test rax, rax
    js .error_dst
    
    mov [fd_dst], rax

    ; === ЦИКЛ КОПИРОВАНИЯ ===
.copy_loop:
    ; Читать из источника
    mov rax, 0
    mov rdi, [fd_src]
    lea rsi, [rel buffer]
    mov rdx, 4096
    syscall
    
    test rax, rax
    jle .done                   ; EOF или ошибка
    
    mov r12, rax                ; Сохранить количество байт
    
    ; Писать в назначение
    mov rax, 1
    mov rdi, [fd_dst]
    lea rsi, [rel buffer]
    mov rdx, r12
    syscall
    
    cmp rax, r12                ; Проверка полной записи
    jne .done
    
    jmp .copy_loop

    ; === ЗАКРЫТИЕ ФАЙЛОВ ===
.done:
    mov rax, 3
    mov rdi, [fd_src]
    syscall
    
    mov rax, 3
    mov rdi, [fd_dst]
    syscall
    
    mov rax, 60
    xor rdi, rdi
    syscall

.error_src:
    mov rax, 1
    mov rdi, 2
    lea rsi, [rel err_src]
    mov rdx, err_src_len
    syscall
    jmp .exit_error

.error_dst:
    ; Закрыть исходный файл перед выходом
    mov rax, 3
    mov rdi, [fd_src]
    syscall
    
    mov rax, 1
    mov rdi, 2
    lea rsi, [rel err_dst]
    mov rdx, err_dst_len
    syscall

.exit_error:
    mov rax, 60
    mov rdi, 1
    syscall

Тестирование:

# Создать исходный файл
echo "Original content for copying" > source.txt

# Сборка и запуск
nasm -f elf64 file_copy.asm -o file_copy.o
ld file_copy.o -o file_copy
./file_copy

# Проверка
cat dest.txt
# Вывод: Original content for copying

# Сравнение файлов
diff source.txt dest.txt
# Нет вывода = файлы идентичны

⚠️ 8. Обработка ошибок

Коды Ошибок Linux (errno)

Системные вызовы возвращают отрицательный errno при ошибке:

mov rax, 2
lea rdi, [rel filename]
xor rsi, rsi
syscall

test rax, rax
jns .success            ; rax ≥ 0: успех

neg rax                 ; Преобразуем -errno → errno
; Теперь rax содержит положительный код ошибки

Основные Коды

Код Константа Описание
2 ENOENT Файл или директория не существует
9 EBADF Неверный файловый дескриптор
13 EACCES Нет прав доступа
17 EEXIST Файл уже существует (с O_EXCL)
21 EISDIR Это директория, не обычный файл
28 ENOSPC Нет места на устройстве

Полная Обработка Ошибок

Реализация обработчика ошибок
section .data
    err_noent db "Error: File not found", 10
    err_noent_len equ $ - err_noent
    
    err_acces db "Error: Permission denied", 10
    err_acces_len equ $ - err_acces
    
    err_nospc db "Error: No space left on device", 10
    err_nospc_len equ $ - err_nospc
    
    err_generic db "Error: Operation failed", 10
    err_generic_len equ $ - err_generic

section .text
handle_error:
    ; Вход: rax = отрицательный errno
    neg rax
    
    cmp rax, 2
    je .print_noent
    
    cmp rax, 13
    je .print_acces
    
    cmp rax, 28
    je .print_nospc
    
    jmp .print_generic

.print_noent:
    mov rax, 1
    mov rdi, 2                  ; stderr
    lea rsi, [rel err_noent]
    mov rdx, err_noent_len
    syscall
    ret

.print_acces:
    mov rax, 1
    mov rdi, 2
    lea rsi, [rel err_acces]
    mov rdx, err_acces_len
    syscall
    ret

.print_nospc:
    mov rax, 1
    mov rdi, 2
    lea rsi, [rel err_nospc]
    mov rdx, err_nospc_len
    syscall
    ret

.print_generic:
    mov rax, 1
    mov rdi, 2
    lea rsi, [rel err_generic]
    mov rdx, err_generic_len
    syscall
    ret

🐛 Отладка: При возникновении ошибок файлового I/O (например, EACCES или ENOENT) используйте техники, описанные в руководстве «Отладка ASM в VS Code: Настройка GDB и визуальный интерфейс», чтобы пошагово проследить значения регистров и вызовы syscall.

⚡ 9. Оптимизация и лучшие практики

Рекомендации по Производительности

Медленно

Множественные мелкие операции:

; Чтение по 100 байт
mov rcx, 100
.loop:
    mov rax, 0
    mov rdi, [fd]
    lea rsi, [buffer]
    mov rdx, 100
    syscall
    ; обработка
    loop .loop

Проблемы:

  • 100 системных вызовов
  • Overhead переключения контекста
  • Неэффективное использование кеша
Быстро

Одна большая операция:

; Чтение 10 КБ
mov rax, 0
mov rdi, [fd]
lea rsi, [buffer]
mov rdx, 10240
syscall

; Обработка в памяти
mov rcx, rax
lea rsi, [buffer]
.process:
    ; обработка байта [rsi]
    inc rsi
    loop .process

Преимущества:

  • 1 системный вызов
  • Минимальный overhead
  • Locality of reference

Паттерны Управления Ресурсами

Паттерн 1: RAII-подобное Управление

process_file:
    push rbp
    mov rbp, rsp
    push r12
    
    ; Открытие
    mov rax, 2
    lea rdi, [rel filename]
    xor rsi, rsi
    syscall
    test rax, rax
    js .error_open
    mov r12, rax            ; Сохранить FD
    
    ; === Работа с файлом ===
    ; ...
    
    ; Закрытие в любом случае
.cleanup:
    mov rax, 3
    mov rdi, r12
    syscall
    
    pop r12
    pop rbp
    ret
    
.error_open:
    ; Обработка ошибки без закрытия
    pop r12
    pop rbp
    ret

Паттерн 2: Множественные Файлы

process_multiple:
    push rbp
    mov rbp, rsp
    sub rsp, 32
    
    ; Открытие файлов с отслеживанием состояния
    mov qword [rbp-8], 0    ; fd1 (0 = не открыт)
    mov qword [rbp-16], 0   ; fd2
    
    ; Открыть первый
    mov rax, 2
    lea rdi, [rel file1]
    xor rsi, rsi
    syscall
    test rax, rax
    js .cleanup
    mov [rbp-8], rax
    
    ; Открыть второй
    mov rax, 2
    lea rdi, [rel file2]
    xor rsi, rsi
    syscall
    test rax, rax
    js .cleanup
    mov [rbp-16], rax
    
    ; === Работа ===
    ; ...
    
    ; Закрытие всех открытых файлов
.cleanup:
    cmp qword [rbp-8], 0
    je .check_fd2
    mov rax, 3
    mov rdi, [rbp-8]
    syscall
    
.check_fd2:
    cmp qword [rbp-16], 0
    je .done
    mov rax, 3
    mov rdi, [rbp-16]
    syscall
    
.done:
    mov rsp, rbp
    pop rbp
    ret

Обработка Больших Файлов

Для файлов > 100 МБ используйте потоковую обработку:

process_large_file:
    push rbp
    mov rbp, rsp
    push r12
    push r13
    
    ; Открытие
    mov rax, 2
    lea rdi, [rel large_file]
    xor rsi, rsi
    syscall
    mov r12, rax
    
.process_loop:
    ; Чтение блока
    mov rax, 0
    mov rdi, r12
    lea rsi, [rel buffer]
    mov rdx, 4096           ; Размер страницы
    syscall
    
    test rax, rax
    jle .done               ; EOF или ошибка
    
    mov r13, rax            ; Количество байт
    
    ; === ОБРАБОТКА БЛОКА В ПАМЯТИ ===
    lea rsi, [rel buffer]
    mov rcx, r13
.process_block:
    ; Обработка байта [rsi]
    ; ...
    inc rsi
    loop .process_block
    
    jmp .process_loop
    
.done:
    mov rax, 3
    mov rdi, r12
    syscall
    
    pop r13
    pop r12
    pop rbp
    ret

✅ Заключение

Файловый ввод-вывод на ассемблере требует понимания трёх уровней: теория VFS и syscalls, практика системных вызовов, архитектура Layer 3 модулей.

Ключевые принципы:

  • Всегда закрывайте файлы — предотвращайте утечки дескрипторов
  • Проверяйте возврат syscall — обрабатывайте все ошибки
  • Используйте блочное чтение — минимизируйте системные вызовы
  • Обрабатывайте частичное чтение — не предполагайте полноты
💜

Полезный материал?

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

Поддержать через CloudTips