Nikita Mandrykin

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

Назад

Ввод-вывод на чистом NASM: Готовые модули (без libc)

📚 Содержание

При разработке программ на чистом ассемблере NASM с меткой _start неизбежно возникает вопрос: как организовать ввод-вывод без libc? Эта статья описывает три готовых модуля (signed/unsigned/float) с двухслойной архитектурой, которая разделяет парсинг строк и операции I/O.

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


🚀 1. Введение и мотивация

Зачем отказываться от libc?

Когда вы используете scanf/printf из libc, программа становится зависимой от внешней библиотеки:

С libc (scanf/printf)
$ ldd program_hybrid
    linux-vdso.so.1
    libc.so.6 => /usr/lib/libc.so.6
    /lib64/ld-linux-x86-64.so.2

$ ls -lh program_hybrid
-rwxr-xr-x 1 user 16712 program_hybrid

  • Зависимость от динамического линкера
  • Требуется совместимая версия libc
  • Поведение зависит от версии библиотеки
Модули I/O (чистый ASM)
$ ldd program_pure
    not a dynamic executable

$ ls -lh program_pure
-rwxr-xr-x 1 user 9456 program_pure

  • Полностью автономный бинарник
  • Размер меньше на ~43%
  • Одинаковое поведение везде

Когда использовать модули I/O:

Сценарий Причина
Обучение Видеть механику I/O напрямую, без абстракций libc
Минималистичные утилиты Калькулятор, конвертер — без лишнего багажа
Embedded/контейнеры Меньше зависимостей, меньше размер образа
Воспроизводимость Одинаковое поведение на любом Linux x86-64

📘 Примечание: Для подготовки окружения разработки (NASM, GDB, Makefile, VS Code) читайте отдельное руководство «Настройка NASM x86-64 и VS Code: Гайд для Linux и Windows».


Три модуля для разных типов данных

Вместо одного универсального модуля используются три специализированных — каждый оптимизирован для своего типа данных:

Модуль Тип данных Диапазон Применение
io_signed.asm int16_t -32768..32767 Температуры, координаты, балансы, отклонения
io_unsigned.asm uint16_t 0..65535 Счётчики, индексы, возраст, порты, маски
io_float.asm float, int32_t ±3.4×10³⁸ Вычисления, цены, геометрия, физика

Почему нельзя один универсальный модуль?

Процессор использует разные инструкции для разных типов. Смешивание приводит к тонким багам:

Операция Signed Unsigned Ошибка при смешивании
Расширение 16→32 movsx movzx -100 станет 65436
Деление cdq + idiv xor edx,edx + div Деление мусора
Сравнение < jl (less) jb (below) Неверная ветка

Архитектурная идея: двухслойное разделение

Ключевая концепция — разделение парсинга и I/O на два независимых слоя:

╔══════════════════════════════════════════════════════════╗
║  СЛОЙ 1: ЧИСТЫЕ ФУНКЦИИ ПАРСИНГА                         ║
║  ────────────────────────────────────────────────────────║
║  • parse_int16 / parse_uint16 / parse_float              ║
║  • Вход: указатель на строку (rdi)                       ║
║  • Выход: число (rax) + код ошибки (rdx)                 ║
║  • БЕЗ syscall, БЕЗ побочных эффектов                    ║
║  • Детерминированные, тестируемые функции                ║
╚══════════════════════════════════════════════════════════╝
                    ⠀↓ вызывается из ↓
╔══════════════════════════════════════════════════════════╗
║  СЛОЙ 2: ИНТЕРАКТИВНЫЙ I/O (stdin/stdout)                ║
║  ────────────────────────────────────────────────────────║
║  • read_*_input, print_*_output                          ║
║  • Буферизованное чтение через syscall read              ║
║  • Вызов функций слоя 1 для парсинга                     ║
║  • Диагностика ошибок в stderr                           ║
╚══════════════════════════════════════════════════════════╝

Что даёт такое разделение:

  1. Переиспользование. Один парсер для stdin, файлов, буферов — код написан один раз
  2. Тестируемость. Чистые функции слоя 1 можно тестировать изолированно
  3. Отладка. Ошибка в парсинге → слой 1; данные не читаются → слой 2
  4. Расширяемость. Новый источник данных = новая обёртка слоя 2, парсер готов

📘 Примечание: Модули поддерживают слой 3 (файловый I/O) с функциями read_*_from_fd и write_*_to_fd. Подробности в статье «Работа с файлами в NASM: Open, Read, Write на Syscalls».

📦 2. Исходники модулей

Все модули содержат подробные комментарии к каждой секции и алгоритму. Готовы к использованию — скопируйте в проект и подключите через extern.

io_signed.asm — Знаковые 16-битные целые

Модуль для int16_t (-32768..32767). Поддерживает отрицательные значения с префиксом -, проверяет переполнение, валидирует формат.

📄 io_signed.asm — полный код модуля
; ============================================================================
; io_signed.asm
;
; Модуль ввода-вывода знаковых целых чисел с двухслойной архитектурой.
; Обеспечивает безопасное чтение 16-битных чисел со знаком и вывод
; 32-битных результатов с проверкой переполнения и валидацией формата.
;
; АРХИТЕКТУРА:
;
;   СЛОЙ 1: Чистые функции парсинга
;     • parse_int16 - Парсинг строки в знаковое 16-битное число
;     • Не выполняет I/O операций, только обработка данных
;     • Возвращает коды ошибок через регистры
;
;   СЛОЙ 2: Интерактивный ввод-вывод (stdin/stdout)
;     • read_signed_input_vars, print_signed_output_var - работа через параметры
;     • read_signed_input, print_signed_output - обёртки для глобальных переменных
;     • Буферизованное чтение, форматированный вывод
;     • Обработка ошибок с диагностикой в stderr
;
; СОВМЕСТИМОСТЬ:
;   Модуль поддерживает два режима работы:
;   1. Новый интерфейс - передача адресов через параметры (rdi, rsi)
;   2. Старый интерфейс - использование глобальных переменных a, b, output
;      (через механизм weak symbols)
;
; БЕЗОПАСНОСТЬ:
;   • Проверка переполнения (диапазон int16_t: -32768..32767)
;   • Валидация формата входных данных
;   • Защита от переполнения буфера (максимум 30 символов)
;   • Детальные коды ошибок для диагностики
;
; ПРОИЗВОДИТЕЛЬНОСТЬ:
;   • Сложность парсинга: O(n), где n - количество цифр
;   • Память: O(1), используются фиксированные буферы
;   • Буферизация I/O минимизирует системные вызовы
; ============================================================================

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

section .data
    ; Сообщения для интерактивного ввода
    msg_a: db "Enter a value for variable 'a': "
    len_a: equ $ - msg_a
    msg_b: db "Enter a value for variable 'b': "
    len_b: equ $ - msg_b
    msg_res: db "Result = "
    len_res: equ $ - msg_res

    newline db 10                       ; Символ новой строки (LF)

    ; Сообщения об ошибках парсинга
    error_format_msg db "ERROR: Invalid number format", 10
    error_format_len equ $ - error_format_msg
    error_overflow_msg db "ERROR: Number overflow (must be -32768 to 32767)", 10
    error_overflow_len equ $ - error_overflow_msg
    error_buffer_msg db "ERROR: Input too long (max 30 characters)", 10
    error_buffer_len equ $ - error_buffer_msg

    ; Сообщение об ошибке обёртки совместимости
    err_compat db "CRITICAL ERROR: Legacy wrapper called but 'a', 'b' or 'output' not defined!", 10
    err_compat_len equ $ - err_compat

section .bss
    ; Буферы для работы всех слоёв
    parse_temp_buffer resb 64           ; Общий буфер для парсинга строк

    ; Буферы интерактивного режима (Слой 2)
    io_interactive_buffer resb 128      ; Буфер чтения из stdin
    io_interactive_pos resq 1           ; Текущая позиция чтения в буфере
    io_interactive_size resq 1          ; Количество непрочитанных байт в буфере

section .text
    ; Экспорт функций всех слоёв
    global parse_int16              ; Слой 1: чистая функция парсинга
    global read_signed_input_vars   ; Слой 2: чтение через параметры
    global print_signed_output_var  ; Слой 2: вывод через параметры
    global read_signed_input        ; Слой 2: обёртка совместимости
    global print_signed_output      ; Слой 2: обёртка совместимости

; ============================================================================
; СЛОЙ 1: ФУНКЦИИ ПАРСИНГА
; ============================================================================

; ----------------------------------------------------------------------------
; parse_int16
;
; Парсит текстовую строку в знаковое 16-битное целое число.
; Поддерживает знаки '+' и '-', ведущие и завершающие пробелы.
; Выполняет проверку на переполнение int16_t и корректность формата.
;
; АЛГОРИТМ:
;   1. Пропуск начальных пробелов
;   2. Обработка знака (+/-)
;   3. Посимвольный парсинг цифр с накоплением: result = result*10 + digit
;   4. Проверка границ: -32768 ≤ result ≤ 32767
;   5. Применение знака и валидация финального значения
;
; АВТОМАТ СОСТОЯНИЙ:
;   [START] → skip_spaces → check_sign → parse_digits → finalize
;                ↓              ↓             ↓
;           [' ',\t]       ['+','-']     ['0'-'9']
;
; ДОПУСТИМЫЙ ДИАПАЗОН: -32768 до 32767
; ДОПУСТИМЫЕ ФОРМАТЫ: [пробелы][+/-]цифры[пробел/LF/CR/null]
;
; ПРИМЕРЫ:
;   "  -12345 " → -12345 (rdx=0)
;   "32767"     → 32767  (rdx=0)
;   "32768"     → 0      (rdx=2, переполнение)
;   "abc"       → 0      (rdx=1, неверный формат)
;
; @param  rdi  Указатель на null-терминированную строку
; @return rax  Распарсенное число (знаковое расширение до 64 бит)
;         rdx  Код ошибки:
;              0 = успех
;              1 = неверный формат (нет цифр, недопустимые символы)
;              2 = переполнение (выход за границы int16_t)
;              3 = переполнение буфера (строка длиннее 30 символов)
; @uses   rbx, rcx, r14, r15
;
; @complexity O(n), где n - длина входной строки
; @memory     O(1), не использует динамическую память
; ----------------------------------------------------------------------------
parse_int16:
    push rbp
    mov rbp, rsp
    push rbx
    push r14
    push r15

    mov r15, rdi                ; r15 = указатель на текущий символ строки
    xor rbx, rbx                ; rbx = флаг знака (0 = положит., 1 = отрицат.)
    xor rax, rax                ; rax = аккумулятор результата
    xor r14, r14                ; r14 = счётчик обработанных цифр
    xor rcx, rcx                ; rcx = общий счётчик символов (защита от переполнения)

; --- Пропуск начальных пробелов ---
.skip_spaces:
    movzx rdx, byte [r15]       ; Загрузка текущего символа с нулевым расширением
    cmp dl, ' '                 ; Проверка: является ли символ пробелом?
    jne .check_sign             ; Если нет - переход к обработке знака
    inc r15                     ; Переход к следующему символу
    inc rcx                     ; Увеличение счётчика символов
    cmp rcx, 30                 ; Проверка на превышение лимита длины
    jg .error_buffer            ; Если превышено - ошибка переполнения буфера
    jmp .skip_spaces            ; Продолжение пропуска пробелов

; --- Обработка знака числа ---
.check_sign:
    cmp byte [r15], '-'         ; Проверка: минус?
    jne .check_plus             ; Если нет - проверяем плюс
    mov rbx, 1                  ; Установка флага отрицательного числа
    inc r15                     ; Переход к следующему символу
    inc rcx                     ; Увеличение счётчика символов
    jmp .parse_digits           ; Переход к парсингу цифр

.check_plus:
    cmp byte [r15], '+'         ; Проверка: плюс?
    jne .parse_digits           ; Если нет - переход к парсингу без изменения знака
    inc r15                     ; Пропуск символа '+'
    inc rcx                     ; Увеличение счётчика символов

; --- Главный цикл парсинга цифр ---
; Инвариант цикла: rax содержит частично собранное число
.parse_digits:
    movzx rdx, byte [r15]       ; Загрузка текущего символа

    ; Проверка символов окончания числа (null/newline/carriage return/пробел)
    test dl, dl                 ; Проверка: null-терминатор (0)?
    jz .finalize                ; Если да - завершение парсинга
    cmp dl, 10                  ; Проверка: LF (line feed)?
    je .finalize                ; Если да - завершение парсинга
    cmp dl, 13                  ; Проверка: CR (carriage return)?
    je .finalize                ; Если да - завершение парсинга
    cmp dl, ' '                 ; Проверка: пробел?
    je .finalize                ; Если да - завершение парсинга

    inc rcx                     ; Увеличение счётчика символов
    cmp rcx, 30                 ; Проверка на превышение лимита длины
    jg .error_buffer            ; Если превышено - ошибка переполнения буфера

    ; Валидация символа как цифры (ASCII '0'-'9')
    cmp dl, '0'                 ; Проверка: символ < '0'?
    jb .error_format            ; Если да - это не цифра, ошибка формата
    cmp dl, '9'                 ; Проверка: символ > '9'?
    ja .error_format            ; Если да - это не цифра, ошибка формата

    inc r14                     ; Увеличение счётчика обработанных цифр

    ; Предварительная проверка переполнения перед умножением
    ; Для int16_t: max/10 = 32767/10 = 3276 (остаток 7)
    ;              min/10 = 32768/10 = 3276 (остаток 8)
    cmp rax, 3276               ; Проверка граничного случая
    jg .check_last_digit        ; Если превышено - особая обработка последней цифры

    ; Вычисление: result = result * 10 + digit
    imul rax, rax, 10           ; rax = rax * 10 (сдвиг разряда)
    sub dl, '0'                 ; Преобразование ASCII ('0'-'9') в число (0-9)
    movzx rdx, dl               ; Нулевое расширение до 64 бит
    add rax, rdx                ; Добавление текущей цифры к результату

    ; Проверка границ int16_t в зависимости от знака
    test rbx, rbx               ; Проверка флага знака (0=положит., 1=отрицат.)
    jnz .check_negative_range   ; Если отрицательное - другая проверка

    ; Проверка для положительных чисел (max 32767)
    cmp rax, 32767              ; Проверка: результат превысил максимум?
    jg .error_overflow          ; Если да - ошибка переполнения
    jmp .next_char              ; Переход к следующему символу

.check_negative_range:
    ; Проверка для отрицательных чисел (допускается 32768 → -32768)
    cmp rax, 32768              ; Проверка: результат превысил |min|?
    jg .error_overflow          ; Если да - ошибка переполнения

.next_char:
    inc r15                     ; Переход к следующему символу
    jmp .parse_digits           ; Продолжение парсинга

; --- Обработка граничного случая: rax == 3276 ---
; Требуется особая обработка последней цифры для предотвращения переполнения
.check_last_digit:
    cmp rax, 3276               ; Проверка: точное равенство граничному значению?
    jne .error_overflow         ; Если больше - гарантированное переполнение

    movzx rdx, byte [r15]       ; Загрузка последней цифры
    sub dl, '0'                 ; Преобразование ASCII в числовое значение

    ; Для положительных: последняя цифра ≤ 7 (32767 = 3276*10 + 7)
    ; Для отрицательных: последняя цифра ≤ 8 (32768 = 3276*10 + 8)
    test rbx, rbx               ; Проверка флага знака
    jnz .check_negative_limit   ; Если отрицательное - другой лимит

    cmp dl, 7                   ; Проверка последней цифры для положительных
    jg .error_overflow          ; Если > 7 - переполнение (32767 max)
    jmp .do_last_digit          ; Переход к обработке последней цифры

.check_negative_limit:
    cmp dl, 8                   ; Проверка последней цифры для отрицательных
    jg .error_overflow          ; Если > 8 - переполнение (32768 max по модулю)

.do_last_digit:
    imul rax, rax, 10           ; Умножение на 10 (безопасно, т.к. проверено)
    movzx rdx, dl               ; Нулевое расширение последней цифры
    add rax, rdx                ; Добавление последней цифры
    inc r15                     ; Переход к следующему символу
    jmp .parse_digits           ; Продолжение парсинга (может быть пробел/LF)

; --- Финализация: проверка наличия хотя бы одной цифры ---
.finalize:
    test r14, r14               ; Проверка: были ли обработаны цифры?
    jz .error_format            ; Если нет - ошибка формата (пустая строка или только знак)

    ; Применение знака к результату
    test rbx, rbx               ; Проверка флага знака
    jz .success                 ; Если положительное - переход к успеху
    neg rax                     ; Применение отрицательного знака (двухкомплементное отрицание)

.success:
    movsx rax, ax               ; Знаковое расширение 16 бит до 64 бит
    xor rdx, rdx                ; Установка кода успеха (0)
    jmp .return                 ; Возврат из функции

; --- Обработка ошибок ---
.error_format:
    xor rax, rax                ; Результат = 0
    mov rdx, 1                  ; Код ошибки = 1 (неверный формат)
    jmp .return

.error_overflow:
    xor rax, rax                ; Результат = 0
    mov rdx, 2                  ; Код ошибки = 2 (переполнение)
    jmp .return

.error_buffer:
    xor rax, rax                ; Результат = 0
    mov rdx, 3                  ; Код ошибки = 3 (переполнение буфера)

.return:
    pop r15
    pop r14
    pop rbx
    pop rbp
    ret
    
; ============================================================================
; СЛОЙ 2: ИНТЕРАКТИВНЫЙ ВВОД-ВЫВОД
; ============================================================================

; ----------------------------------------------------------------------------
; read_signed_input_vars
;
; Читает два знаковых 16-битных целых числа из стандартного ввода.
; Выводит приглашения для ввода каждого значения. Использует буферизацию
; для эффективного чтения из stdin.
;
; ВЗАИМОДЕЙСТВИЕ С ПОЛЬЗОВАТЕЛЕМ:
;   1. Вывод: "Enter a value for variable 'a': "
;   2. Чтение и парсинг значения для 'a'
;   3. Вывод: "Enter a value for variable 'b': "
;   4. Чтение и парсинг значения для 'b'
;   5. При ошибке - вывод диагностики в stderr и exit(1)
;
; ДИАПАЗОН: -32768 до 32767 для каждого числа
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     int16_t a, b;
;     read_signed_input_vars(&a, &b);
;     // a и b содержат введённые значения
;
;   Assembly:
;     section .bss
;       var_a resw 1
;       var_b resw 1
;     section .text
;       lea rdi, [var_a]
;       lea rsi, [var_b]
;       call read_signed_input_vars
;
; @param  rdi  Адрес первой 16-битной переменной (int16_t *a)
;         rsi  Адрес второй 16-битной переменной (int16_t *b)
; @return none (результаты сохраняются по переданным адресам)
; @uses   rax, rbx, rcx, rdx, rdi, rsi, r12, r13
; @calls  layer2_read_string_signed, parse_int16
; @exit   Код 1 при ошибке ввода (формат/переполнение/буфер)
;
; @see layer2_read_string_signed - внутренняя буферизация
; @see parse_int16 - валидация и преобразование
; ----------------------------------------------------------------------------
read_signed_input_vars:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13

    mov r12, rdi                ; Сохранение адреса переменной 'a'
    mov r13, rsi                ; Сохранение адреса переменной 'b'

; --- Чтение первого числа (переменная 'a') ---
    ; Вывод приглашения
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    lea rsi, [msg_a]
    mov rdx, len_a
    syscall

    call layer2_read_string_signed ; Чтение строки в parse_temp_buffer

    lea rdi, [parse_temp_buffer]   ; Адрес строки для парсинга
    call parse_int16               ; Парсинг введённого значения

    test rdx, rdx                  ; Проверка кода ошибки (0 = успех)
    jnz .handle_error              ; Если ошибка - переход к обработке

    mov word [r12], ax             ; Сохранение значения в *a (младшие 16 бит)

; --- Чтение второго числа (переменная 'b') ---
    ; Вывод приглашения
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    lea rsi, [msg_b]
    mov rdx, len_b
    syscall

    call layer2_read_string_signed ; Чтение строки в parse_temp_buffer

    lea rdi, [parse_temp_buffer]   ; Адрес строки для парсинга
    call parse_int16               ; Парсинг введённого значения

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

    mov word [r13], ax             ; Сохранение значения в *b

    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

; --- Обработка ошибок парсинга ---
.handle_error:
    push rdx                    ; Сохранение кода ошибки
    
    ; syscall: write(stderr, string, length)
    mov rax, 1
    mov rdi, 2

    pop rbx                     ; Восстановление кода ошибки в rbx
    cmp rbx, 1                  ; Проверка: код ошибки = 1 (неверный формат)?
    je .print_format
    cmp rbx, 2                  ; Проверка: код ошибки = 2 (переполнение)?
    je .print_overflow
    cmp rbx, 3                  ; Проверка: код ошибки = 3 (буфер)?
    je .print_buffer

.print_format:
    lea rsi, [error_format_msg]     ; Адрес сообщения об ошибке формата
    mov rdx, error_format_len       ; Длина сообщения
    jmp .do_print

.print_overflow:
    lea rsi, [error_overflow_msg]   ; Адрес сообщения о переполнении
    mov rdx, error_overflow_len     ; Длина сообщения
    jmp .do_print

.print_buffer:
    lea rsi, [error_buffer_msg]     ; Адрес сообщения о переполнении буфера
    mov rdx, error_buffer_len       ; Длина сообщения

.do_print:
    syscall
    
    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

; ----------------------------------------------------------------------------
; print_signed_output_var
;
; Выводит знаковое 32-битное целое число в stdout в десятичном формате
; с префиксом "Result = " и переводом строки. Поддерживает отрицательные
; числа.
;
; АЛГОРИТМ:
;   1. Обработка специального случая (ноль)
;   2. Определение знака и взятие модуля
;   3. Конвертация справа налево: число % 10 → ASCII
;   4. Добавление знака '-' при необходимости
;   5. Вывод префикса, числа и перевода строки
;
; ДИАПАЗОН: -2147483648 до 2147483647 (int32_t)
; ФОРМАТ ВЫВОДА: "Result = <число>\n"
;
; ПРИМЕРЫ:
;   print_signed_output_var(0)      → "Result = 0\n"
;   print_signed_output_var(12345)  → "Result = 12345\n"
;   print_signed_output_var(-9876)  → "Result = -9876\n"
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     int32_t result = -12345;
;     print_signed_output_var(result);
;
;   Assembly:
;     mov edi, -12345
;     call print_signed_output_var
;
; @param  edi  Значение для вывода (int32_t)
; @return none
; @uses   rax, rbx, rcx, rdx, rdi, rsi, r12, r13
;
; @complexity O(log₁₀(n)), где n - абсолютное значение числа
; @memory     O(1), использует фиксированный буфер
; ----------------------------------------------------------------------------
print_signed_output_var:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13

    movsxd rax, edi             ; Знаковое расширение int32 → int64

    mov r12, 10                         ; Делитель = 10 (основание системы счисления)
    lea r13, [parse_temp_buffer + 63]   ; r13 = указатель на конец буфера
    mov byte [r13], 0                   ; Установка null-терминатора

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

; --- Обработка знака числа ---
.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 перед делением
    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 .print                   ; Если положительное - переход к выводу
    dec r13                     ; Смещение указателя назад
    mov byte [r13], '-'         ; Запись символа минуса

; --- Вывод результата ---
.print:
    ; Вывод префикса "Result = "
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    lea rsi, [msg_res]
    mov rdx, len_res
    syscall

    ; Вывод преобразованного числа
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    mov rsi, r13                        ; Адрес начала числовой строки
    lea rdx, [parse_temp_buffer + 63]   ; Адрес конца буфера
    sub rdx, r13                        ; Вычисление длины строки (конец - начало)
    syscall

    ; Вывод символа новой строки
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    lea rsi, [newline]
    mov rdx, 1
    syscall

    pop r13
    pop r12
    pop rbx
    pop rbp
    ret
    
; ============================================================================
; ОБЁРТКИ СОВМЕСТИМОСТИ (для программ со старым интерфейсом)
; ============================================================================

; Объявление слабых внешних ссылок (weak symbols)
; Если программа не определяет эти переменные, их адрес будет 0
extern a:weak               ; int16_t - первая входная переменная
extern b:weak               ; int16_t - вторая входная переменная
extern output:weak          ; int32_t - выходная переменная для результата

; ----------------------------------------------------------------------------
; read_signed_input
;
; Обёртка совместимости для чтения во внешние глобальные переменные 'a' и 'b'.
; Проверяет наличие символов через механизм weak linking.
;
; ТРЕБОВАНИЯ:
;   Вызывающая программа должна определить:
;     global a
;     global b
;     section .bss
;       a resw 1
;       b resw 1
;
; ДИАПАЗОН: -32768 до 32767 для каждого числа
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   Old-style программа:
;     extern read_signed_input
;     global a, b
;     section .bss
;       a resw 1
;       b resw 1
;     section .text
;       call read_signed_input  ; Читает в a и b
;
; @param  none (читает в extern a, b)
; @return none
; @uses   rax, rdi, rsi (через вызов read_signed_input_vars)
; @calls  read_signed_input_vars
; @exit   Код 1 при ошибке ввода или отсутствии переменных a, b
;
; @see read_signed_input_vars - основная реализация
; ----------------------------------------------------------------------------
read_signed_input:
    push rbp
    mov rbp, rsp

; --- Проверка наличия символа 'a' ---
    mov rax, a                  ; Загрузка адреса переменной 'a'
    test rax, rax               ; Проверка: адрес равен 0? (символ не определён)
    jz .missing_symbols         ; Если да - переход к обработке ошибки

; --- Проверка наличия символа 'b' ---
    mov rax, b                  ; Загрузка адреса переменной 'b'
    test rax, rax               ; Проверка: адрес равен 0? (символ не определён)
    jz .missing_symbols         ; Если да - переход к обработке ошибки

; --- Вызов основной функции с адресами глобальных переменных ---
    lea rdi, [a]                ; Загрузка адреса переменной 'a' в rdi
    lea rsi, [b]                ; Загрузка адреса переменной 'b' в rsi
    call read_signed_input_vars ; Вызов функции чтения

    pop rbp
    ret

; --- Обработка отсутствия требуемых переменных ---
.missing_symbols:
    ; Вывод сообщения об ошибке
    ; syscall: write(stderr, string, length)
    mov rax, 1
    mov rdi, 2
    lea rsi, [err_compat]
    mov rdx, err_compat_len
    syscall
    
    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

; ----------------------------------------------------------------------------
; print_signed_output
;
; Обёртка совместимости для вывода внешней глобальной переменной 'output'.
; Проверяет наличие символа через механизм weak linking.
;
; ТРЕБОВАНИЯ:
;   Вызывающая программа должна определить:
;     global output
;     section .bss
;       output resd 1
;
; ДИАПАЗОН: -2147483648 до 2147483647 (int32_t)
; ФОРМАТ ВЫВОДА: "Result = <число>\n"
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   Old-style программа:
;     extern print_signed_output
;     global output
;     section .bss
;       output resd 1
;     section .text
;       mov dword [output], -12345
;       call print_signed_output  ; Выводит: Result = -12345
;
; @param  none (читает из extern output)
; @return none
; @uses   rax, rdi (через вызов print_signed_output_var)
; @calls  print_signed_output_var
; @exit   Код 1 при отсутствии переменной output
;
; @see print_signed_output_var - основная реализация
; ----------------------------------------------------------------------------
print_signed_output:
    push rbp
    mov rbp, rsp

; --- Проверка наличия символа 'output' ---
    mov rax, output             ; Загрузка адреса переменной 'output'
    test rax, rax               ; Проверка: адрес равен 0? (символ не определён)
    jz .missing_symbols_out     ; Если да - переход к обработке ошибки

; --- Загрузка значения и вызов основной функции ---
    mov edi, [output]           ; Загрузка значения переменной output в edi
    call print_signed_output_var ; Вызов функции вывода

    pop rbp
    ret

; --- Обработка отсутствия переменной output ---
.missing_symbols_out:
    ; syscall: write(stderr, string, length)
    mov rax, 1
    mov rdi, 2
    lea rsi, [err_compat]
    mov rdx, err_compat_len
    syscall
    
    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

; ----------------------------------------------------------------------------
; layer2_read_string_signed
;
; Внутренняя функция буферизованного чтения строки из stdin до символа LF.
; Использует внутреннюю буферизацию для минимизации системных вызовов.
; Результат сохраняется в parse_temp_buffer с null-терминатором.
;
; АЛГОРИТМ:
;   1. Очистка выходного буфера
;   2. Цикл чтения:
;      - Проверка наличия данных в буфере
;      - При необходимости: системный вызов read() для новой порции
;      - Копирование символа в выходной буфер
;      - Проверка на LF (конец строки)
;   3. Установка null-терминатора
;
; БУФЕРИЗАЦИЯ:
;   Внутренний буфер: 128 байт
;   Выходной буфер: 64 байта
;   Минимизирует количество syscall для повышения производительности
;
; @param  none
; @return none (результат в parse_temp_buffer)
; @uses   rax, rbx, rcx, rdx, rdi, rsi
; @exit   Код 1 при EOF без данных
;
; @complexity O(n), где n - длина строки
; @calls      syscall read(stdin)
; ----------------------------------------------------------------------------
layer2_read_string_signed:
    push rbx
    push rcx
    push rdi

; --- Очистка выходного буфера ---
    lea rdi, [parse_temp_buffer] ; Адрес буфера для очистки
    mov rcx, 64                  ; Количество байт для очистки
    xor al, al                   ; Значение для заполнения (0)
    rep stosb                    ; Повторение stosb rcx раз (заполнение нулями)

    xor rbx, rbx                 ; rbx = позиция записи в parse_temp_buffer (0)

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

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

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

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

; --- Извлечение символа из буфера ---
.has_data:
    mov rcx, [io_interactive_pos]              ; Загрузка текущей позиции чтения
    mov al, byte [io_interactive_buffer + rcx] ; Чтение символа из буфера

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

    ; Проверка 1: Достигнут ли лимит буфера parse_temp_buffer?
    cmp rbx, 63                 ; Проверка: буфер заполнен (осталось место для null)?
    jge .skip_write             ; Если да - НЕ записываем, но продолжаем читать

    ; Запись символа в выходной буфер (только если есть место)
    lea rdx, [parse_temp_buffer] ; Загрузка базового адреса выходного буфера
    mov byte [rdx + rbx], al     ; Запись символа по адресу [база + смещение]
    inc rbx                      ; Увеличение позиции записи

.skip_write:
    ; Проверка 2: Конец строки (LF)?
    cmp al, 10                  ; Проверка: LF (line feed)?
    je .done                    ; Если да - завершение чтения

    ; Продолжение чтения следующего символа (даже если буфер полон)
    jmp .read_char

; --- Обработка EOF без данных ---
.eof:
    test rbx, rbx               ; Проверка: были ли прочитаны данные?
    jnz .done                   ; Если да - завершение нормально (частичные данные)

    ; EOF без данных - это ошибка
    ; syscall: write(stderr, string, length)
    mov rax, 1
    mov rdi, 2
    lea rsi, [error_format_msg]
    mov rdx, error_format_len
    syscall

    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

; --- Успешное завершение ---
.done:
    pop rdi
    pop rcx
    pop rbx
    ret

io_unsigned.asm — Беззнаковые 16-битные целые

Модуль для uint16_t (0..65535). Отклоняет отрицательные значения, использует беззнаковые инструкции (movzx, div, jb/ja).

📄 io_unsigned.asm — полный код модуля
; ============================================================================
; io_unsigned.asm
;
; Модуль ввода-вывода беззнаковых целых чисел с двухслойной архитектурой.
; Обеспечивает безопасное чтение 16-битных беззнаковых чисел и вывод
; 32-битных результатов с проверкой переполнения и валидацией формата.
;
; АРХИТЕКТУРА:
;
;   СЛОЙ 1: Чистые функции парсинга
;     • parse_uint16 - Парсинг строки в беззнаковое 16-битное число
;     • Не выполняет I/O операций, только обработка данных
;     • Возвращает коды ошибок через регистры
;
;   СЛОЙ 2: Интерактивный ввод-вывод (stdin/stdout)
;     • read_unsigned_input_vars, print_unsigned_output_var - работа через параметры
;     • read_unsigned_input, print_unsigned_output - обёртки для глобальных переменных
;     • Буферизованное чтение, форматированный вывод
;     • Обработка ошибок с диагностикой в stderr
;
; СОВМЕСТИМОСТЬ:
;   Модуль поддерживает два режима работы:
;   1. Новый интерфейс - передача адресов через параметры (rdi, rsi)
;   2. Старый интерфейс - использование глобальных переменных a, b, output
;      (через механизм weak symbols)
;
; БЕЗОПАСНОСТЬ:
;   • Проверка переполнения (диапазон uint16_t: 0..65535)
;   • Валидация формата входных данных
;   • Защита от переполнения буфера (максимум 30 символов)
;   • Детальные коды ошибок для диагностики
;   • Отклонение отрицательных чисел для беззнакового типа
;
; ПРОИЗВОДИТЕЛЬНОСТЬ:
;   • Сложность парсинга: O(n), где n - количество цифр
;   • Память: O(1), используются фиксированные буферы
;   • Буферизация I/O минимизирует системные вызовы
; ============================================================================

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

section .data
    ; Сообщения для интерактивного ввода
    msg_a: db "Enter a value for variable 'a': "
    len_a: equ $ - msg_a
    msg_b: db "Enter a value for variable 'b': "
    len_b: equ $ - msg_b
    msg_res: db "Result = "
    len_res: equ $ - msg_res

    newline db 10                       ; Символ новой строки (LF)

    ; Сообщения об ошибках парсинга
    error_format_msg db "ERROR: Invalid number format", 10
    error_format_len equ $ - error_format_msg
    error_overflow_msg db "ERROR: Number overflow (must be 0 to 65535)", 10
    error_overflow_len equ $ - error_overflow_msg
    error_buffer_msg db "ERROR: Input too long (max 30 characters)", 10
    error_buffer_len equ $ - error_buffer_msg

    ; Сообщение об ошибке обёртки совместимости
    err_compat db "CRITICAL ERROR: Legacy wrapper called but 'a', 'b' or 'output' not defined!", 10
    err_compat_len equ $ - err_compat

section .bss
    ; Буферы для работы всех слоёв
    parse_temp_buffer resb 64           ; Общий буфер для парсинга строк

    ; Буферы интерактивного режима (Слой 2)
    io_interactive_buffer resb 128      ; Буфер чтения из stdin
    io_interactive_pos resq 1           ; Текущая позиция чтения в буфере
    io_interactive_size resq 1          ; Количество непрочитанных байт в буфере

section .text
    ; Экспорт функций всех слоёв
    global parse_uint16             ; Слой 1: чистая функция парсинга
    global read_unsigned_input_vars ; Слой 2: чтение через параметры
    global print_unsigned_output_var; Слой 2: вывод через параметры
    global read_unsigned_input      ; Слой 2: обёртка совместимости
    global print_unsigned_output    ; Слой 2: обёртка совместимости

; ============================================================================
; СЛОЙ 1: ФУНКЦИИ ПАРСИНГА
; ============================================================================

; ----------------------------------------------------------------------------
; parse_uint16
;
; Парсит текстовую строку в беззнаковое 16-битное целое число.
; Поддерживает знак '+', ведущие и завершающие пробелы.
; Выполняет проверку на переполнение uint16_t и корректность формата.
; ОТКЛОНЯЕТ отрицательные числа (знак '-').
;
; АЛГОРИТМ:
;   1. Пропуск начальных пробелов
;   2. Проверка знака ('+' допустим, '-' - ошибка формата)
;   3. Посимвольный парсинг цифр с накоплением: result = result*10 + digit
;   4. Проверка границ: 0 ≤ result ≤ 65535
;   5. Валидация финального значения
;
; АВТОМАТ СОСТОЯНИЙ:
;   [START] → skip_spaces → check_sign → parse_digits → finalize
;                ↓              ↓             ↓
;           [' ',\t]         ['+']       ['0'-'9']
;                              ↓
;                            ['-'] → ERROR
;
; ДОПУСТИМЫЙ ДИАПАЗОН: 0 до 65535
; ДОПУСТИМЫЕ ФОРМАТЫ: [пробелы][+]цифры[пробел/LF/CR/null]
;
; ПРИМЕРЫ:
;   "  12345 " → 12345 (rdx=0)
;   "65535"    → 65535 (rdx=0)
;   "65536"    → 0     (rdx=2, переполнение)
;   "-100"     → 0     (rdx=1, отрицательное недопустимо)
;   "abc"      → 0     (rdx=1, неверный формат)
;
; @param  rdi  Указатель на null-терминированную строку
; @return rax  Распарсенное число (нулевое расширение до 64 бит)
;         rdx  Код ошибки:
;              0 = успех
;              1 = неверный формат (нет цифр, недопустимые символы, минус)
;              2 = переполнение (выход за границы uint16_t)
;              3 = переполнение буфера (строка длиннее 30 символов)
; @uses   rbx, rcx, r14, r15
;
; @complexity O(n), где n - длина входной строки
; @memory     O(1), не использует динамическую память
; ----------------------------------------------------------------------------
parse_uint16:
    push rbp
    mov rbp, rsp
    push rbx
    push r14
    push r15

    mov r15, rdi                ; r15 = указатель на текущий символ строки
    xor rax, rax                ; rax = аккумулятор результата
    xor r14, r14                ; r14 = счётчик обработанных цифр
    xor rcx, rcx                ; rcx = общий счётчик символов (защита от переполнения)

; --- Пропуск начальных пробелов ---
.skip_spaces:
    movzx rdx, byte [r15]       ; Загрузка текущего символа с нулевым расширением
    cmp dl, ' '                 ; Проверка: является ли символ пробелом?
    jne .check_sign             ; Если нет - переход к проверке знака
    inc r15                     ; Переход к следующему символу
    inc rcx                     ; Увеличение счётчика символов
    cmp rcx, 30                 ; Проверка на превышение лимита длины
    jg .error_buffer            ; Если превышено - ошибка переполнения буфера
    jmp .skip_spaces            ; Продолжение пропуска пробелов

; --- Обработка знака числа ---
.check_sign:
    cmp byte [r15], '-'         ; Проверка: минус?
    je .error_format            ; Минус недопустим для беззнаковых чисел

    cmp byte [r15], '+'         ; Проверка: плюс?
    jne .parse_digits           ; Если нет - переход к парсингу без изменения
    inc r15                     ; Пропуск символа '+'
    inc rcx                     ; Увеличение счётчика символов

; --- Главный цикл парсинга цифр ---
; Инвариант цикла: rax содержит частично собранное число
.parse_digits:
    movzx rdx, byte [r15]       ; Загрузка текущего символа

    ; Проверка символов окончания числа (null/newline/carriage return/пробел)
    test dl, dl                 ; Проверка: null-терминатор (0)?
    jz .finalize                ; Если да - завершение парсинга
    cmp dl, 10                  ; Проверка: LF (line feed)?
    je .finalize                ; Если да - завершение парсинга
    cmp dl, 13                  ; Проверка: CR (carriage return)?
    je .finalize                ; Если да - завершение парсинга
    cmp dl, ' '                 ; Проверка: пробел?
    je .finalize                ; Если да - завершение парсинга

    inc rcx                     ; Увеличение счётчика символов
    cmp rcx, 30                 ; Проверка на превышение лимита длины
    jg .error_buffer            ; Если превышено - ошибка переполнения буфера

    ; Валидация символа как цифры (ASCII '0'-'9')
    cmp dl, '0'                 ; Проверка: символ < '0'?
    jb .error_format            ; Если да - это не цифра, ошибка формата
    cmp dl, '9'                 ; Проверка: символ > '9'?
    ja .error_format            ; Если да - это не цифра, ошибка формата

    inc r14                     ; Увеличение счётчика обработанных цифр

    ; Предварительная проверка переполнения перед умножением
    ; Для uint16_t: max/10 = 65535/10 = 6553 (остаток 5)
    cmp rax, 6553               ; Проверка граничного случая
    jg .check_last_digit        ; Если превышено - особая обработка последней цифры

    ; Вычисление: result = result * 10 + digit
    imul rax, rax, 10           ; rax = rax * 10 (сдвиг разряда)
    sub dl, '0'                 ; Преобразование ASCII ('0'-'9') в число (0-9)
    movzx rdx, dl               ; Нулевое расширение до 64 бит
    add rax, rdx                ; Добавление текущей цифры к результату

    ; Проверка границы uint16_t (максимум 65535)
    cmp rax, 65535              ; Проверка: результат превысил максимум?
    jg .error_overflow          ; Если да - ошибка переполнения

    inc r15                     ; Переход к следующему символу
    jmp .parse_digits           ; Продолжение парсинга

; --- Обработка граничного случая: rax == 6553 ---
; Требуется особая обработка последней цифры для предотвращения переполнения
.check_last_digit:
    cmp rax, 6553               ; Проверка: точное равенство граничному значению?
    jne .error_overflow         ; Если больше - гарантированное переполнение

    movzx rdx, byte [r15]       ; Загрузка последней цифры
    sub dl, '0'                 ; Преобразование ASCII в числовое значение

    ; Для uint16_t: последняя цифра ≤ 5 (65535 = 6553*10 + 5)
    cmp dl, 5                   ; Проверка последней цифры
    jg .error_overflow          ; Если > 5 - переполнение (65535 max)

    imul rax, rax, 10           ; Умножение на 10 (безопасно, т.к. проверено)
    movzx rdx, dl               ; Нулевое расширение последней цифры
    add rax, rdx                ; Добавление последней цифры

    ; Дополнительная проверка на всякий случай
    cmp rax, 65535              ; Проверка финального результата
    jg .error_overflow          ; Если превышено - ошибка

    inc r15                     ; Переход к следующему символу
    jmp .parse_digits           ; Продолжение парсинга (может быть пробел/LF)

; --- Финализация: проверка наличия хотя бы одной цифры ---
.finalize:
    test r14, r14               ; Проверка: были ли обработаны цифры?
    jz .error_format            ; Если нет - ошибка формата (пустая строка или только знак)

    movzx rax, ax               ; Нулевое расширение 16 бит до 64 бит
    xor rdx, rdx                ; Установка кода успеха (0)
    jmp .return                 ; Возврат из функции

; --- Обработка ошибок ---
.error_format:
    xor rax, rax                ; Результат = 0
    mov rdx, 1                  ; Код ошибки = 1 (неверный формат)
    jmp .return

.error_overflow:
    xor rax, rax                ; Результат = 0
    mov rdx, 2                  ; Код ошибки = 2 (переполнение)
    jmp .return

.error_buffer:
    xor rax, rax                ; Результат = 0
    mov rdx, 3                  ; Код ошибки = 3 (переполнение буфера)

.return:
    pop r15
    pop r14
    pop rbx
    pop rbp
    ret

; ============================================================================
; СЛОЙ 2: ИНТЕРАКТИВНЫЙ ВВОД-ВЫВОД
; ============================================================================

; ----------------------------------------------------------------------------
; read_unsigned_input_vars
;
; Читает два беззнаковых 16-битных целых числа из стандартного ввода.
; Выводит приглашения для ввода каждого значения. Использует буферизацию
; для эффективного чтения из stdin.
;
; ВЗАИМОДЕЙСТВИЕ С ПОЛЬЗОВАТЕЛЕМ:
;   1. Вывод: "Enter a value for variable 'a': "
;   2. Чтение и парсинг значения для 'a'
;   3. Вывод: "Enter a value for variable 'b': "
;   4. Чтение и парсинг значения для 'b'
;   5. При ошибке - вывод диагностики в stderr и exit(1)
;
; ДИАПАЗОН: 0 до 65535 для каждого числа
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     uint16_t a, b;
;     read_unsigned_input_vars(&a, &b);
;     // a и b содержат введённые значения
;
;   Assembly:
;     section .bss
;       var_a resw 1
;       var_b resw 1
;     section .text
;       lea rdi, [var_a]
;       lea rsi, [var_b]
;       call read_unsigned_input_vars
;
; @param  rdi  Адрес первой 16-битной переменной (uint16_t *a)
;         rsi  Адрес второй 16-битной переменной (uint16_t *b)
; @return none (результаты сохраняются по переданным адресам)
; @uses   rax, rbx, rcx, rdx, rdi, rsi, r12, r13
; @calls  layer2_read_string_unsigned, parse_uint16
; @exit   Код 1 при ошибке ввода (формат/переполнение/буфер)
;
; @see layer2_read_string_unsigned - внутренняя буферизация
; @see parse_uint16 - валидация и преобразование
; ----------------------------------------------------------------------------
read_unsigned_input_vars:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13

    mov r12, rdi                ; Сохранение адреса переменной 'a'
    mov r13, rsi                ; Сохранение адреса переменной 'b'

; --- Чтение первого числа (переменная 'a') ---
    ; Вывод приглашения
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    lea rsi, [msg_a]
    mov rdx, len_a
    syscall

    call layer2_read_string_unsigned ; Чтение строки в parse_temp_buffer

    lea rdi, [parse_temp_buffer]     ; Адрес строки для парсинга
    call parse_uint16                ; Парсинг введённого значения

    test rdx, rdx                    ; Проверка кода ошибки (0 = успех)
    jnz .handle_error                ; Если ошибка - переход к обработке

    mov word [r12], ax               ; Сохранение значения в *a (младшие 16 бит)

; --- Чтение второго числа (переменная 'b') ---
    ; Вывод приглашения
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    lea rsi, [msg_b]
    mov rdx, len_b
    syscall

    call layer2_read_string_unsigned ; Чтение строки в parse_temp_buffer

    lea rdi, [parse_temp_buffer]     ; Адрес строки для парсинга
    call parse_uint16                ; Парсинг введённого значения

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

    mov word [r13], ax               ; Сохранение значения в *b

    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

; --- Обработка ошибок парсинга ---
.handle_error:
    push rdx                    ; Сохранение кода ошибки
    
    ; syscall: write(stderr, string, length)
    mov rax, 1
    mov rdi, 2

    pop rbx                     ; Восстановление кода ошибки в rbx
    cmp rbx, 1                  ; Проверка: код ошибки = 1 (неверный формат)?
    je .print_format
    cmp rbx, 2                  ; Проверка: код ошибки = 2 (переполнение)?
    je .print_overflow
    cmp rbx, 3                  ; Проверка: код ошибки = 3 (буфер)?
    je .print_buffer

.print_format:
    lea rsi, [error_format_msg]   ; Адрес сообщения об ошибке формата
    mov rdx, error_format_len     ; Длина сообщения
    jmp .do_print

.print_overflow:
    lea rsi, [error_overflow_msg] ; Адрес сообщения о переполнении
    mov rdx, error_overflow_len   ; Длина сообщения
    jmp .do_print

.print_buffer:
    lea rsi, [error_buffer_msg]   ; Адрес сообщения о переполнении буфера
    mov rdx, error_buffer_len     ; Длина сообщения

.do_print:
    syscall
    
    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

; ----------------------------------------------------------------------------
; print_unsigned_output_var
;
; Выводит 32-битное целое число в stdout в десятичном формате
; с префиксом "Result = " и переводом строки. Поддерживает отрицательные
; числа (для результатов арифметических операций, например вычитания).
;
; АЛГОРИТМ:
;   1. Обработка специального случая (ноль)
;   2. Определение знака и взятие модуля (для отрицательных результатов)
;   3. Конвертация справа налево: число % 10 → ASCII
;   4. Добавление знака '-' при необходимости
;   5. Вывод префикса, числа и перевода строки
;
; ДИАПАЗОН: -2147483648 до 2147483647 (int32_t)
; ФОРМАТ ВЫВОДА: "Result = <число>\n"
;
; ПРИМЕРЫ:
;   print_unsigned_output_var(0)      → "Result = 0\n"
;   print_unsigned_output_var(12345)  → "Result = 12345\n"
;   print_unsigned_output_var(-100)   → "Result = -100\n"
;
; ПРИМЕЧАНИЕ:
;   Хотя функция работает с unsigned, она принимает signed int32
;   для корректной обработки результатов операций (например, a - b,
;   где a < b даст отрицательный результат).
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     int32_t result = 12345;
;     print_unsigned_output_var(result);
;
;   Assembly:
;     mov edi, 12345
;     call print_unsigned_output_var
;
; @param  edi  Значение для вывода (int32_t)
; @return none
; @uses   rax, rbx, rcx, rdx, rdi, rsi, r12, r13
;
; @complexity O(log₁₀(n)), где n - абсолютное значение числа
; @memory     O(1), использует фиксированный буфер
; ----------------------------------------------------------------------------
print_unsigned_output_var:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13

    movsxd rax, edi             ; Знаковое расширение int32 → int64

    mov r12, 10                       ; Делитель = 10 (основание системы счисления)
    lea r13, [parse_temp_buffer + 63] ; r13 = указатель на конец буфера
    mov byte [r13], 0                 ; Установка null-терминатора

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

; --- Обработка знака числа ---
.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 перед делением
    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 .print                   ; Если положительное - переход к выводу
    dec r13                     ; Смещение указателя назад
    mov byte [r13], '-'         ; Запись символа минуса

; --- Вывод результата ---
.print:
    ; Вывод префикса "Result = "
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    lea rsi, [msg_res]
    mov rdx, len_res
    syscall

    ; Вывод преобразованного числа
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    mov rsi, r13                        ; Адрес начала числовой строки
    lea rdx, [parse_temp_buffer + 63]   ; Адрес конца буфера
    sub rdx, r13                        ; Вычисление длины строки (конец - начало)
    syscall

    ; Вывод символа новой строки
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    lea rsi, [newline]
    mov rdx, 1
    syscall

    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

; ============================================================================
; ОБЁРТКИ СОВМЕСТИМОСТИ (для программ со старым интерфейсом)
; ============================================================================

; Объявление слабых внешних ссылок (weak symbols)
; Если программа не определяет эти переменные, их адрес будет 0
extern a:weak               ; uint16_t - первая входная переменная
extern b:weak               ; uint16_t - вторая входная переменная
extern output:weak          ; int32_t - выходная переменная для результата

; ----------------------------------------------------------------------------
; read_unsigned_input
;
; Обёртка совместимости для чтения во внешние глобальные переменные 'a' и 'b'.
; Проверяет наличие символов через механизм weak linking.
;
; ТРЕБОВАНИЯ:
;   Вызывающая программа должна определить:
;     global a
;     global b
;     section .bss
;       a resw 1
;       b resw 1
;
; ДИАПАЗОН: 0 до 65535 для каждого числа
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   Old-style программа:
;     extern read_unsigned_input
;     global a, b
;     section .bss
;       a resw 1
;       b resw 1
;     section .text
;       call read_unsigned_input  ; Читает в a и b
;
; @param  none (читает в extern a, b)
; @return none
; @uses   rax, rdi, rsi (через вызов read_unsigned_input_vars)
; @calls  read_unsigned_input_vars
; @exit   Код 1 при ошибке ввода или отсутствии переменных a, b
;
; @see read_unsigned_input_vars - основная реализация
; ----------------------------------------------------------------------------
read_unsigned_input:
    push rbp
    mov rbp, rsp

; --- Проверка наличия символа 'a' ---
    mov rax, a                    ; Загрузка адреса переменной 'a'
    test rax, rax                 ; Проверка: адрес равен 0? (символ не определён)
    jz .missing_symbols           ; Если да - переход к обработке ошибки

; --- Проверка наличия символа 'b' ---
    mov rax, b                    ; Загрузка адреса переменной 'b'
    test rax, rax                 ; Проверка: адрес равен 0? (символ не определён)
    jz .missing_symbols           ; Если да - переход к обработке ошибки

; --- Вызов основной функции с адресами глобальных переменных ---
    lea rdi, [a]                  ; Загрузка адреса переменной 'a' в rdi
    lea rsi, [b]                  ; Загрузка адреса переменной 'b' в rsi
    call read_unsigned_input_vars ; Вызов функции чтения

    pop rbp
    ret

; --- Обработка отсутствия требуемых переменных ---
.missing_symbols:
    ; Вывод сообщения об ошибке
    ; syscall: write(stderr, string, length)
    mov rax, 1
    mov rdi, 2
    lea rsi, [err_compat]
    mov rdx, err_compat_len
    syscall
    
    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

; ----------------------------------------------------------------------------
; print_unsigned_output
;
; Обёртка совместимости для вывода внешней глобальной переменной 'output'.
; Проверяет наличие символа через механизм weak linking.
;
; ТРЕБОВАНИЯ:
;   Вызывающая программа должна определить:
;     global output
;     section .bss
;       output resd 1
;
; ДИАПАЗОН: -2147483648 до 2147483647 (int32_t)
; ФОРМАТ ВЫВОДА: "Result = <число>\n"
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   Old-style программа:
;     extern print_unsigned_output
;     global output
;     section .bss
;       output resd 1
;     section .text
;       mov dword [output], 12345
;       call print_unsigned_output  ; Выводит: Result = 12345
;
; @param  none (читает из extern output)
; @return none
; @uses   rax, rdi (через вызов print_unsigned_output_var)
; @calls  print_unsigned_output_var
; @exit   Код 1 при отсутствии переменной output
;
; @see print_unsigned_output_var - основная реализация
; ----------------------------------------------------------------------------
print_unsigned_output:
    push rbp
    mov rbp, rsp

; --- Проверка наличия символа 'output' ---
    mov rax, output                ; Загрузка адреса переменной 'output'
    test rax, rax                  ; Проверка: адрес равен 0? (символ не определён)
    jz .missing_symbols_out        ; Если да - переход к обработке ошибки

; --- Загрузка значения и вызов основной функции ---
    mov edi, [output]              ; Загрузка значения переменной output в edi
    call print_unsigned_output_var ; Вызов функции вывода

    pop rbp
    ret

; --- Обработка отсутствия переменной output ---
.missing_symbols_out:
    ; syscall: write(stderr, string, length)
    mov rax, 1
    mov rdi, 2
    lea rsi, [err_compat]
    mov rdx, err_compat_len
    syscall
    
    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

; ----------------------------------------------------------------------------
; layer2_read_string_unsigned
;
; Внутренняя функция буферизованного чтения строки из stdin до символа LF.
; Использует внутреннюю буферизацию для минимизации системных вызовов.
; Результат сохраняется в parse_temp_buffer с null-терминатором.
;
; АЛГОРИТМ:
;   1. Очистка выходного буфера
;   2. Цикл чтения:
;      - Проверка наличия данных в буфере
;      - При необходимости: системный вызов read() для новой порции
;      - Копирование символа в выходной буфер
;      - Проверка на LF (конец строки)
;   3. Установка null-терминатора
;
; БУФЕРИЗАЦИЯ:
;   Внутренний буфер: 128 байт
;   Выходной буфер: 64 байта
;   Минимизирует количество syscall для повышения производительности
;
; @param  none
; @return none (результат в parse_temp_buffer)
; @uses   rax, rbx, rcx, rdx, rdi, rsi
; @exit   Код 1 при EOF без данных
;
; @complexity O(n), где n - длина строки
; @calls      syscall read(stdin)
; ----------------------------------------------------------------------------
layer2_read_string_unsigned:
    push rbx
    push rcx
    push rdi

; --- Очистка выходного буфера ---
    lea rdi, [parse_temp_buffer] ; Адрес буфера для очистки
    mov rcx, 64                  ; Количество байт для очистки
    xor al, al                   ; Значение для заполнения (0)
    rep stosb                    ; Повторение stosb rcx раз (заполнение нулями)

    xor rbx, rbx                 ; rbx = позиция записи в parse_temp_buffer (0)

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

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

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

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

; --- Извлечение символа из буфера ---
.has_data:
    mov rcx, [io_interactive_pos]              ; Загрузка текущей позиции чтения
    mov al, byte [io_interactive_buffer + rcx] ; Чтение символа из буфера

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

    ; Проверка 1: Достигнут ли лимит буфера parse_temp_buffer?
    cmp rbx, 63                  ; Проверка: буфер заполнен (осталось место для null)?
    jge .skip_write              ; Если да - НЕ записываем, но продолжаем читать

    ; Запись символа в выходной буфер (только если есть место)
    lea rdx, [parse_temp_buffer] ; Загрузка базового адреса выходного буфера
    mov byte [rdx + rbx], al     ; Запись символа по адресу [база + смещение]
    inc rbx                      ; Увеличение позиции записи

.skip_write:
    ; Проверка 2: Конец строки (LF)?
    cmp al, 10                  ; Проверка: LF (line feed)?
    je .done                    ; Если да - завершение чтения

    ; Продолжение чтения следующего символа (даже если буфер полон)
    jmp .read_char

; --- Обработка EOF без данных ---
.eof:
    test rbx, rbx               ; Проверка: были ли прочитаны данные?
    jnz .done                   ; Если да - завершение нормально (частичные данные)

    ; EOF без данных - это ошибка
    ; syscall: write(stderr, string, length)
    mov rax, 1
    mov rdi, 2
    lea rsi, [error_format_msg]
    mov rdx, error_format_len
    syscall

    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

; --- Успешное завершение ---
.done:
    pop rdi
    pop rcx
    pop rbx
    ret

io_float.asm — Числа с плавающей точкой

Модуль для float (IEEE 754) и int32_t. Поддерживает дробную часть через точку, использует стек FPU (x87).

📄 io_float.asm — полный код модуля
; ============================================================================
; io_float.asm
;
; Модуль ввода-вывода для чисел с плавающей точкой (float) и целых (int32).
; Обеспечивает безопасное чтение и вывод чисел с проверкой формата.
;
; АРХИТЕКТУРА:
;
;   СЛОЙ 1: Чистые функции парсинга
;     • parse_float - Парсинг строки в float (IEEE 754 single precision)
;     • parse_int32 - Парсинг строки в знаковое 32-битное число
;     • Не выполняют I/O операций, только обработка данных
;     • Возвращают значения через регистры
;
;   СЛОЙ 2: Интерактивный ввод-вывод (stdin/stdout)
;     • read_float_var, read_int_var - работа через параметры
;     • print_float_var, print_int_var - вывод через параметры
;     • read_float, read_int - обёртки для совместимости
;     • Буферизованное чтение, форматированный вывод
;
; ОСОБЕННОСТИ РАБОТЫ С FLOAT:
;   • Поддержка целой и дробной частей
;   • Точность вывода: 6 знаков после запятой
;   • Использование FPU (x87) для вычислений
;   • Формат IEEE 754 single precision (32 бита)
;   • Вывод в формате: [-]целая.дробная (hex: 0xXXXXXXXX)
;
; СОВМЕСТИМОСТЬ:
;   Поддерживает старый интерфейс через функции без префиксов
;
; ПРОИЗВОДИТЕЛЬНОСТЬ:
;   • Сложность парсинга: O(n), где n - количество цифр
;   • Память: O(1), используются фиксированные буферы
;   • Буферизация I/O минимизирует системные вызовы
; ============================================================================

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

section .data
    ; Константы FPU для вычислений с плавающей точкой
    io_const_ten dd 10.0                ; Константа 10.0 для умножения/деления
    io_const_one dd 1.0                 ; Константа 1.0 для вычисления степени

    ; Сообщения для форматированного вывода
    io_newline db 10, 0                 ; Символ новой строки с null-терминатором
    io_msg_hex db ' (hex: 0x', 0        ; Префикс для hex-представления
    io_msg_hex_end db ')', 0            ; Суффикс для hex-представления

    ; Сообщения об ошибках парсинга
    error_format_msg db "ERROR: Invalid number format", 10
    error_format_len equ $ - error_format_msg
    error_overflow_msg db "ERROR: Number overflow", 10
    error_overflow_len equ $ - error_overflow_msg

section .bss
    ; Буферы для работы всех слоёв
    parse_temp_buffer resb 64           ; Общий буфер для парсинга строк

    ; Буферы интерактивного режима (Слой 2)
    io_interactive_buffer resb 128      ; Буфер чтения из stdin
    io_interactive_pos resq 1           ; Текущая позиция чтения в буфере
    io_interactive_size resq 1          ; Количество непрочитанных байт в буфере

section .text
    ; Экспорт функций всех слоёв
    global parse_float              ; Слой 1: чистая функция парсинга float
    global parse_int32              ; Слой 1: чистая функция парсинга int32
    global read_float_var           ; Слой 2: чтение float через параметры
    global read_int_var             ; Слой 2: чтение int32 через параметры
    global print_float_var          ; Слой 2: вывод float через параметры
    global print_int_var            ; Слой 2: вывод int32 через параметры
    global read_float               ; Слой 2: обёртка совместимости для float
    global read_int                 ; Слой 2: обёртка совместимости для int32
    global print_float_from_eax     ; Слой 2: вывод float из регистра
    global print_string             ; Утилита: вывод строки
    global print_hex_32             ; Утилита: вывод 32-битного hex

; ============================================================================
; КОНСТАНТЫ
; ============================================================================
FPU_TRUNC_MODE  equ 0x0C00          ; Режим усечения FPU (округление к нулю)
BUFFER_SIZE     equ 64              ; Размер основного буфера
FRAC_DIGITS     equ 6               ; Количество знаков дробной части

; ============================================================================
; СЛОЙ 1: ФУНКЦИИ ПАРСИНГА
; ============================================================================

; ----------------------------------------------------------------------------
; parse_float
;
; Парсит текстовую строку в число с плавающей точкой (IEEE 754 single).
; Поддерживает знак '-', целую и дробную части, пропускает пробелы.
;
; АЛГОРИТМ:
;   1. Пропуск начальных пробелов
;   2. Обработка знака (только '-', плюс не требуется)
;   3. Посимвольный парсинг:
;      - До точки: накопление целой части (r10)
;      - После точки: накопление дробной части (r12) и счётчика цифр (r11)
;   4. Конвертация через FPU:
;      - Целая часть → FPU stack
;      - Дробная часть / 10^(кол-во цифр) → FPU stack
;      - Сложение
;      - Применение знака
;   5. Сохранение битового представления
;
; ФОРМАТ ЧИСЛА:
;   [пробелы][-]цифры[.цифры][пробел/LF/CR/null]
;
; ПРИМЕРЫ:
;   "  -3.14 "  → -3.14 (rdx=0)
;   "123.456"   → 123.456 (rdx=0)
;   "0.5"       → 0.5 (rdx=0)
;   "42"        → 42.0 (rdx=0, дробная часть опциональна)
;   "abc"       → 0 (rdx=1, неверный формат)
;
; @param  rdi  Указатель на null-терминированную строку
; @return eax  Битовое представление float (IEEE 754)
;         rdx  Код ошибки:
;              0 = успех
;              1 = неверный формат (нет цифр, недопустимые символы)
;              2 = переполнение (арифметическое переполнение при вычислениях)
; @uses   r8, r9, r10, r11, r12, r13, r14, FPU stack
;
; @complexity O(n), где n - длина входной строки
; @memory     O(1) + использование FPU stack
;
; @note Использует x87 FPU для точных вычислений с плавающей точкой
; ----------------------------------------------------------------------------
parse_float:
    push rbp
    mov rbp, rsp
    sub rsp, 32                 ; Локальное пространство для FPU операций
    push r8
    push r9
    push r10
    push r11
    push r12
    push r13
    push r14

    mov rcx, 0                  ; rcx = индекс текущего символа в строке
    xor r8, r8                  ; r8 = флаг знака (0 = положительное, 1 = отрицательное)
    xor r9, r9                  ; r9 = флаг точки (0 = ещё не встречена, 1 = уже встречена)
    xor r10, r10                ; r10 = накопитель целой части
    xor r11, r11                ; r11 = счётчик цифр дробной части
    xor r12, r12                ; r12 = накопитель дробной части
    xor r14, r14                ; r14 = счётчик цифр (валидация)

; --- Пропуск начальных пробелов ---
.skip_spaces:
    movzx rax, byte [rdi + rcx] ; Загрузка текущего символа с нулевым расширением
    cmp al, ' '                 ; Проверка: пробел?
    jne .check_sign             ; Если нет - переход к проверке знака
    inc rcx                     ; Переход к следующему символу
    jmp .skip_spaces            ; Продолжение пропуска пробелов

; --- Обработка знака числа ---
.check_sign:
    cmp byte [rdi + rcx], '-'   ; Проверка: минус?
    jne .parse                  ; Если нет - переход к парсингу
    mov r8, 1                   ; Установка флага отрицательного числа
    inc rcx                     ; Пропуск символа '-'

; --- Главный цикл парсинга цифр ---
; Инвариант: r10 = целая часть, r12 = дробная часть (без десятичной точки)
.parse:
    movzx rax, byte [rdi + rcx] ; Загрузка текущего символа
    call io_is_end_char         ; Проверка: символ окончания (LF/CR/пробел/null)?
    je .check_valid             ; Если да - завершение парсинга
    
    cmp al, '.'                 ; Проверка: десятичная точка?
    je .dot                     ; Если да - обработка точки
    
    ; Валидация символа как цифры
    cmp al, '0'                 ; Проверка: символ < '0'?
    jl .error_format            ; Если да - ошибка формата
    cmp al, '9'                 ; Проверка: символ > '9'?
    jg .error_format            ; Если да - ошибка формата
    
    inc r14                     ; Увеличение счётчика валидных цифр
    
    sub al, '0'                 ; Преобразование ASCII ('0'-'9') в число (0-9)
    movzx rax, al               ; Нулевое расширение до 64 бит
    
    cmp r9, 0                   ; Проверка: уже встретили точку?
    je .integer                 ; Если нет - это цифра целой части

; --- Обработка цифры дробной части ---
.fractional:
    imul r12, 10                ; r12 = r12 * 10 (сдвиг разряда)
    jo .error_overflow          ; Проверка переполнения при умножении
    add r12, rax                ; Добавление текущей цифры
    jo .error_overflow          ; Проверка переполнения при сложении
    inc r11                     ; Увеличение счётчика цифр дробной части
    jmp .next                   ; Переход к следующему символу

; --- Обработка цифры целой части ---
.integer:
    imul r10, 10                ; r10 = r10 * 10 (сдвиг разряда)
    jo .error_overflow          ; Проверка переполнения при умножении
    add r10, rax                ; Добавление текущей цифры
    jo .error_overflow          ; Проверка переполнения при сложении

.next:
    inc rcx                     ; Переход к следующему символу
    jmp .parse                  ; Продолжение парсинга

; --- Обработка десятичной точки ---
.dot:
    cmp r9, 1                   ; Проверка: точка уже встречалась?
    je .error_format            ; Если да - ошибка (две точки недопустимы)
    mov r9, 1                   ; Установка флага точки
    inc rcx                     ; Пропуск символа '.'
    jmp .parse                  ; Продолжение парсинга

; --- Проверка валидности числа (хотя бы одна цифра) ---
.check_valid:
    test r14, r14               ; Проверка: были ли обработаны цифры?
    jz .error_format            ; Если нет - ошибка формата

; --- Пропуск завершающих пробелов ---
.check_trailing:
    movzx rax, byte [rdi + rcx] ; Загрузка текущего символа
    cmp al, 0                   ; Проверка: null-терминатор?
    je .convert                 ; Если да - переход к конвертации
    cmp al, 10                  ; Проверка: LF?
    je .convert                 ; Если да - переход к конвертации
    cmp al, 13                  ; Проверка: CR?
    je .convert                 ; Если да - переход к конвертации
    cmp al, ' '                 ; Проверка: пробел?
    jne .error_format           ; Если другой символ - ошибка формата
    inc rcx                     ; Пропуск пробела
    jmp .check_trailing         ; Продолжение проверки

; --- Конвертация через FPU ---
; Алгоритм: float = целая_часть + (дробная_часть / 10^количество_цифр)
.convert:
    ; Загрузка целой части в FPU
    mov [rbp-8], r10            ; Сохранение целой части в памяти
    fild qword [rbp-8]          ; ST(0) = целая часть (int64 → float)
    
    cmp r11, 0                  ; Проверка: есть ли дробная часть?
    je .sign                    ; Если нет - переход к применению знака
    
    ; Загрузка дробной части в FPU
    mov [rbp-16], r12           ; Сохранение дробной части в памяти
    fild qword [rbp-16]         ; ST(0) = дробная часть, ST(1) = целая часть
    
    ; Вычисление делителя: 10^r11 (количество цифр дробной части)
    mov [rbp-24], r11           ; Сохранение количества цифр
    fild qword [rbp-24]         ; ST(0) = r11, ST(1) = дробная, ST(2) = целая
    fld dword [io_const_ten]    ; ST(0) = 10.0, ST(1) = r11, ST(2) = дробная, ST(3) = целая
    fxch st1                    ; ST(0) = r11, ST(1) = 10.0, ST(2) = дробная, ST(3) = целая
    
    ; Цикл возведения в степень: 10^r11
    mov r13, r11                ; r13 = счётчик итераций
    fld dword [io_const_one]    ; ST(0) = 1.0 (аккумулятор), ST(1) = r11, ST(2) = 10.0, ...
.pow:
    cmp r13, 0                  ; Проверка: остались ли итерации?
    je .pow_done                ; Если нет - завершение возведения в степень
    fmul st0, st2               ; ST(0) = ST(0) * 10.0 (умножение аккумулятора)
    dec r13                     ; Уменьшение счётчика
    jmp .pow                    ; Продолжение цикла

.pow_done:
    ; Стек FPU: ST(0) = 10^r11, ST(1) = r11, ST(2) = 10.0, ST(3) = дробная, ST(4) = целая
    fxch st3                    ; ST(0) = дробная, ST(1) = r11, ST(2) = 10.0, ST(3) = 10^r11, ST(4) = целая
    fdiv st0, st3               ; ST(0) = дробная / 10^r11 (нормализация дробной части)
    fxch st3                    ; ST(0) = 10^r11, ST(1) = r11, ST(2) = 10.0, ST(3) = дробная/10^r11, ST(4) = целая
    fstp st0                    ; Удаление 10^r11 из стека
    fstp st0                    ; Удаление r11 из стека
    fstp st0                    ; Удаление 10.0 из стека

    ; Стек FPU: ST(0) = дробная/10^r11, ST(1) = целая
    faddp st1, st0              ; ST(0) = целая + дробная/10^r11 (финальное значение)

; --- Применение знака ---
.sign:
    cmp r8, 1                   ; Проверка флага знака
    jne .done                   ; Если положительное - переход к сохранению
    fchs                        ; Изменение знака числа в FPU (ST(0) = -ST(0))

; --- Сохранение результата ---
.done:
    fstp dword [rbp-28]         ; Сохранение float из FPU в память
    mov eax, [rbp-28]           ; Загрузка битового представления в EAX
    xor rdx, rdx                ; Установка кода успеха (0)
    jmp .return                 ; Возврат из функции

; --- Обработка ошибок ---
.error_overflow:
    xor eax, eax                ; Результат = 0
    mov rdx, 2                  ; Код ошибки = 2 (переполнение)
    jmp .return

.error_format:
    xor eax, eax                ; Результат = 0
    mov rdx, 1                  ; Код ошибки = 1 (неверный формат)

.return:
    pop r14
    pop r13
    pop r12
    pop r11
    pop r10
    pop r9
    pop r8
    mov rsp, rbp
    pop rbp
    ret

; ----------------------------------------------------------------------------
; parse_int32
;
; Парсит текстовую строку в знаковое 32-битное целое число.
; Поддерживает знак '-', ведущие и завершающие пробелы.
; Выполняет проверку на переполнение int32_t.
;
; АЛГОРИТМ:
;   1. Пропуск начальных пробелов
;   2. Обработка знака (только '-')
;   3. Посимвольный парсинг цифр: result = result*10 + digit
;   4. Проверка на переполнение после каждой операции
;   5. Применение знака и валидация
;
; ДОПУСТИМЫЙ ДИАПАЗОН: -2147483648 до 2147483647
; ФОРМАТЫ: [пробелы][-]цифры[пробел/LF/CR/null]
;
; ПРИМЕРЫ:
;   "  -12345 " → -12345 (rdx=0)
;   "2147483647" → 2147483647 (rdx=0)
;   "2147483648" → 0 (rdx=2, переполнение)
;   "abc"        → 0 (rdx=1, неверный формат)
;
; @param  rdi  Указатель на null-терминированную строку
; @return eax  Распарсенное число (знаковое 32-битное)
;         rdx  Код ошибки:
;              0 = успех
;              1 = неверный формат
;              2 = переполнение
; @uses   rbx, rcx, r8, r12, r14
;
; @complexity O(n), где n - длина входной строки
; @memory     O(1), не использует динамическую память
; ----------------------------------------------------------------------------
parse_int32:
    push rbp
    mov rbp, rsp
    push r8
    push r12
    push r13
    push r14
    
    xor rcx, rcx                ; rcx = индекс текущего символа
    xor rax, rax                ; rax = аккумулятор результата
    xor r8, r8                  ; r8 = флаг знака (0 = положит., 1 = отрицат.)
    xor r14, r14                ; r14 = счётчик обработанных цифр

; --- Пропуск начальных пробелов ---
.skip_spaces:
    movzx rbx, byte [rdi + rcx] ; Загрузка текущего символа
    cmp bl, ' '                 ; Проверка: пробел?
    jne .check_sign             ; Если нет - переход к проверке знака
    inc rcx                     ; Переход к следующему символу
    jmp .skip_spaces            ; Продолжение пропуска

; --- Обработка знака ---
.check_sign:
    cmp byte [rdi + rcx], '-'   ; Проверка: минус?
    jne .parse                  ; Если нет - переход к парсингу
    mov r8, 1                   ; Установка флага отрицательного числа
    inc rcx                     ; Пропуск символа '-'

; --- Главный цикл парсинга цифр ---
; Инвариант: rax содержит частично собранное число
.parse:
    movzx rbx, byte [rdi + rcx] ; Загрузка текущего символа
    
    ; Проверка символов окончания числа
    cmp bl, 10                  ; Проверка: LF?
    je .check_valid             ; Если да - завершение парсинга
    cmp bl, 0                   ; Проверка: null-терминатор?
    je .check_valid             ; Если да - завершение парсинга
    cmp bl, 32                  ; Проверка: пробел?
    je .check_valid             ; Если да - завершение парсинга
    cmp bl, 13                  ; Проверка: CR?
    je .check_valid             ; Если да - завершение парсинга
    
    ; Валидация символа как цифры
    cmp bl, '0'                 ; Проверка: символ < '0'?
    jl .error_format            ; Если да - ошибка формата
    cmp bl, '9'                 ; Проверка: символ > '9'?
    jg .error_format            ; Если да - ошибка формата
    
    inc r14                     ; Увеличение счётчика цифр
    
    sub bl, '0'                 ; Преобразование ASCII в число
    movzx rbx, bl               ; Нулевое расширение
    
    mov r12, rax                ; Сохранение предыдущего значения для проверки
    imul rax, 10                ; rax = rax * 10
    jo .error_overflow          ; Проверка переполнения при умножении
    add rax, rbx                ; Добавление текущей цифры
    jo .error_overflow          ; Проверка переполнения при сложении
    
    inc rcx                     ; Переход к следующему символу
    jmp .parse                  ; Продолжение парсинга

; --- Проверка валидности (хотя бы одна цифра) ---
.check_valid:
    test r14, r14               ; Проверка: были ли обработаны цифры?
    jz .error_format            ; Если нет - ошибка формата

; --- Пропуск завершающих пробелов ---
.check_trailing:
    movzx rbx, byte [rdi + rcx] ; Загрузка текущего символа
    cmp bl, 0                   ; Проверка: null-терминатор?
    je .done                    ; Если да - завершение
    cmp bl, 10                  ; Проверка: LF?
    je .done                    ; Если да - завершение
    cmp bl, 13                  ; Проверка: CR?
    je .done                    ; Если да - завершение
    cmp bl, ' '                 ; Проверка: пробел?
    jne .error_format           ; Если другой символ - ошибка
    inc rcx                     ; Пропуск пробела
    jmp .check_trailing         ; Продолжение проверки

; --- Применение знака и валидация ---
.done:
    test rax, rax               ; Проверка знака результата
    js .overflow_check_neg      ; Если отрицательный - особая проверка
    
    cmp r8, 1                   ; Проверка флага знака
    jne .ret_ok                 ; Если должно быть положительным - готово
    neg rax                     ; Применение отрицательного знака
    jmp .ret_ok

; --- Проверка переполнения для отрицательных чисел ---
; Отрицательное значение в rax допустимо только если был знак минус
.overflow_check_neg:
    cmp r8, 1                   ; Проверка: был ли знак минус?
    je .ret_ok                  ; Если да - допустимо (INT_MIN = -2147483648)
    jmp .error_overflow         ; Если нет - переполнение

.ret_ok:
    xor rdx, rdx                ; Установка кода успеха (0)
    jmp .return

; --- Обработка ошибок ---
.error_overflow:
    xor eax, eax                ; Результат = 0
    mov rdx, 2                  ; Код ошибки = 2 (переполнение)
    jmp .return

.error_format:
    xor eax, eax                ; Результат = 0
    mov rdx, 1                  ; Код ошибки = 1 (неверный формат)

.return:
    pop r14
    pop r13
    pop r12
    pop r8
    pop rbp
    ret

; ============================================================================
; СЛОЙ 2: ИНТЕРАКТИВНЫЙ ВВОД-ВЫВОД
; ============================================================================

; ----------------------------------------------------------------------------
; read_float_var
;
; Читает число с плавающей точкой из стандартного ввода в переменную по адресу.
; Использует буферизованное чтение для эффективности.
;
; ВЗАИМОДЕЙСТВИЕ:
;   Ожидает ввод от пользователя (stdin), читает строку до LF,
;   парсит её через parse_float, сохраняет результат по указанному адресу.
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     float value;
;     read_float_var(&value);
;     // value содержит введённое число
;
;   Assembly:
;     section .bss
;       my_float resd 1
;     section .text
;       lea rdi, [my_float]
;       call read_float_var
;
; @param  rdi  Адрес переменной для сохранения результата (float *)
; @return none (результат сохраняется по переданному адресу)
; @uses   rax, rbx, rcx, rdx, rdi
; @calls  layer2_read_string_float, parse_float
; @exit   Код 1 при ошибке парсинга (формат/переполнение)
;
; @see layer2_read_string_float - внутренняя буферизация
; @see parse_float - валидация и преобразование
; ----------------------------------------------------------------------------
read_float_var:
    push rbp
    mov rbp, rsp
    sub rsp, 16                   ; Выделяем место на стеке для локальной переменной
    push rbx

    mov [rbp-8], rdi              ; 1. СОХРАНЯЕМ АДРЕС назначения в локальную переменную
                                  ; (защита от изменений регистров в вызываемых функциях)

    call layer2_read_string_float ; Чтение строки в parse_temp_buffer

    lea rdi, [parse_temp_buffer]  ; Адрес строки для парсинга
    call parse_float              ; Парсинг введённого значения
    
    test rdx, rdx                 ; Проверка кода ошибки (0 = успех)
    jnz .handle_error             ; Если ошибка - переход к обработке
    
    mov rbx, [rbp-8]              ; 2. ВОССТАНАВЛИВАЕМ АДРЕС из локальной переменной
    mov [rbx], eax                ; 3. Запись битового представления float

    pop rbx
    mov rsp, rbp
    pop rbp
    ret

; --- Обработка ошибок парсинга ---
.handle_error:
    cmp rdx, 1                    ; Проверка: код ошибки = 1 (формат)?
    je .print_format
    lea rsi, [error_overflow_msg] ; Адрес сообщения о переполнении
    mov rdx, error_overflow_len   ; Длина сообщения
    jmp .do_print
.print_format:
    lea rsi, [error_format_msg]   ; Адрес сообщения об ошибке формата
    mov rdx, error_format_len     ; Длина сообщения
.do_print:
    ; syscall: write(stderr, string, length)
    mov rax, 1
    mov rdi, 2
    syscall
    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

; ----------------------------------------------------------------------------
; read_int_var
;
; Читает знаковое 32-битное целое число из стандартного ввода в переменную.
; Использует буферизованное чтение для эффективности.
;
; ВЗАИМОДЕЙСТВИЕ:
;   Ожидает ввод от пользователя (stdin), читает строку до LF,
;   парсит её через parse_int32, сохраняет результат по указанному адресу.
;
; ДИАПАЗОН: -2147483648 до 2147483647
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     int32_t value;
;     read_int_var(&value);
;     // value содержит введённое число
;
;   Assembly:
;     section .bss
;       my_int resd 1
;     section .text
;       lea rdi, [my_int]
;       call read_int_var
;
; @param  rdi  Адрес переменной для сохранения результата (int32_t *)
; @return none (результат сохраняется по переданному адресу)
; @uses   rax, rbx, rcx, rdx, rdi
; @calls  layer2_read_string_float, parse_int32
; @exit   Код 1 при ошибке парсинга (формат/переполнение)
;
; @see layer2_read_string_float - внутренняя буферизация
; @see parse_int32 - валидация и преобразование
; ----------------------------------------------------------------------------
read_int_var:
    push rbp
    mov rbp, rsp
    sub rsp, 16                   ; Выделяем место на стеке
    push rbx

    mov [rbp-8], rdi              ; 1. СОХРАНЯЕМ АДРЕС назначения в локальную переменную

    call layer2_read_string_float ; Чтение строки в parse_temp_buffer

    lea rdi, [parse_temp_buffer]  ; Адрес строки для парсинга
    call parse_int32              ; Парсинг введённого значения
    
    test rdx, rdx                 ; Проверка кода ошибки (0 = успех)
    jnz .handle_error             ; Если ошибка - переход к обработке
    
    mov rbx, [rbp-8]              ; 2. ВОССТАНАВЛИВАЕМ АДРЕС
    mov [rbx], eax                ; 3. Запись значения (32 бита)

    pop rbx
    mov rsp, rbp
    pop rbp
    ret

; --- Обработка ошибок парсинга ---
.handle_error:
    cmp rdx, 1                    ; Проверка: код ошибки = 1 (формат)?
    je .print_format
    lea rsi, [error_overflow_msg] ; Адрес сообщения о переполнении
    mov rdx, error_overflow_len   ; Длина сообщения
    jmp .do_print
.print_format:
    lea rsi, [error_format_msg]   ; Адрес сообщения об ошибке формата
    mov rdx, error_format_len     ; Длина сообщения
.do_print:
    ; syscall: write(stderr, string, length)
    mov rax, 1
    mov rdi, 2
    syscall

    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

; ----------------------------------------------------------------------------
; print_float_var
;
; Выводит число с плавающей точкой в stdout в формате:
; [-]целая.дробная (hex: 0xXXXXXXXX)\n
;
; АЛГОРИТМ:
;   1. Загрузка float в FPU
;   2. Определение знака и вывод '-' при необходимости
;   3. Отделение целой части через FPU truncation
;   4. Вычисление дробной части (исходное - целая)
;   5. Вывод целой части
;   6. Вывод точки
;   7. Вывод дробной части (6 знаков)
;   8. Вывод hex-представления
;
; ФОРМАТ ВЫВОДА: [-]целая.дробная (hex: 0xXXXXXXXX)\n
; ТОЧНОСТЬ: 6 знаков после запятой (FRAC_DIGITS)
;
; ПРИМЕРЫ:
;   print_float_var(0x40490FDB) → "3.141593 (hex: 0x40490FDB)\n"  (π)
;   print_float_var(0xC0000000) → "-2.000000 (hex: 0xC0000000)\n"
;   print_float_var(0x00000000) → "0.000000 (hex: 0x00000000)\n"
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     float pi = 3.14159f;
;     print_float_var(*(uint32_t*)&pi);
;
;   Assembly:
;     mov edi, 0x40490FDB    ; Битовое представление π
;     call print_float_var
;
; @param  edi  Битовое представление float (IEEE 754)
; @return none
; @uses   rax, rbx, r12, r13, r14, r15, FPU stack
;
; @complexity O(1), фиксированная точность вывода
; @memory     O(1), использует фиксированные буферы
;
; @note Использует FPU для разделения на целую и дробную части
; ----------------------------------------------------------------------------
print_float_var:
    push rbp
    mov rbp, rsp
    sub rsp, 64                 ; Локальное пространство для FPU и временных данных
    push rbx
    push r12
    push r13
    push r14
    push r15

    mov r12d, edi                     ; Сохранение битового представления для hex-вывода
    mov [rbp-4], edi                  ; Сохранение в памяти для загрузки в FPU
    fld dword [rbp-4]                 ; Загрузка float в FPU: ST(0) = число
    
    ; Проверка знака числа через FPU
    ftst                              ; Сравнение ST(0) с 0.0
    fstsw ax                          ; Копирование FPU status word в AX
    sahf                              ; Загрузка AH в флаги процессора
    jae .positive                     ; Если >= 0 - переход к обработке положительного
    
    ; Вывод знака минус
    mov byte [parse_temp_buffer], '-' ; Подготовка символа '-'
    call io_print_char                ; Вывод символа
    fabs                              ; Взятие модуля: ST(0) = |ST(0)|

; --- Разделение на целую и дробную части ---
.positive:
    fld st0                     ; Дублирование: ST(0) = число, ST(1) = число
    
    ; Усечение до целой части (truncation)
    push rax
    sub rsp, 8
    fnstcw [rsp]                ; Сохранение текущего control word FPU
    mov ax, [rsp]               ; Загрузка control word
    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
    
; --- Вывод компонентов ---
    call pf_int_part            ; Вывод целой части
    call pf_dec_point           ; Вывод десятичной точки '.'
    call pf_frac_part           ; Вывод дробной части (6 знаков)
    call print_hex_32_internal  ; Вывод hex-представления
    
    ; Вывод символа новой строки
    lea rdi, [io_newline]
    call print_string
    
    pop r15
    pop r14
    pop r13
    pop r12
    pop rbx
    mov rsp, rbp
    pop rbp
    ret

; ----------------------------------------------------------------------------
; print_int_var
;
; Выводит знаковое 32-битное целое число в stdout в десятичном формате
; с переводом строки.
;
; АЛГОРИТМ:
;   1. Обработка специального случая (ноль)
;   2. Определение знака и взятие модуля
;   3. Конвертация справа налево: число % 10 → ASCII
;   4. Добавление знака '-' при необходимости
;   5. Вывод числа и перевода строки
;
; ДИАПАЗОН: -2147483648 до 2147483647 (int32_t)
; ФОРМАТ ВЫВОДА: "<число>\n"
;
; ПРИМЕРЫ:
;   print_int_var(0)       → "0\n"
;   print_int_var(12345)   → "12345\n"
;   print_int_var(-9876)   → "-9876\n"
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   C-style:
;     int32_t value = -12345;
;     print_int_var(value);
;
;   Assembly:
;     mov edi, -12345
;     call print_int_var
;
; @param  edi  Значение для вывода (int32_t)
; @return none
; @uses   rax, rbx, rcx, rdx, rdi, rsi, r12, r13
;
; @complexity O(log₁₀(n)), где n - абсолютное значение числа
; @memory     O(1), использует фиксированный буфер
; ----------------------------------------------------------------------------
print_int_var:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13
    
    movsxd rax, edi             ; Знаковое расширение int32 → int64
    
    mov r12, 10                 ; Делитель = 10 (основание системы счисления)
    lea r13, [parse_temp_buffer + 63] ; r13 = указатель на конец буфера
    mov byte [r13], 0           ; Установка null-терминатора
    
; --- Специальная обработка нуля ---
    test rax, rax               ; Проверка: число равно нулю?
    jnz .check_sign             ; Если нет - обработка знака
    dec r13                     ; Смещение указателя назад
    mov byte [r13], '0'         ; Запись символа '0'
    jmp .print                  ; Переход к выводу

; --- Обработка знака числа ---
.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 перед делением
    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 .print                   ; Если положительное - переход к выводу
    dec r13                     ; Смещение указателя назад
    mov byte [r13], '-'         ; Запись символа минуса

; --- Вывод результата ---
.print:
    ; Вывод числа
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    mov rsi, r13                ; Адрес начала числовой строки
    lea rdx, [parse_temp_buffer + 63] ; Адрес конца буфера
    sub rdx, r13                ; Вычисление длины строки (конец - начало)
    syscall
    
    ; Вывод символа новой строки
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    lea rsi, [io_newline]
    mov rdx, 1
    syscall
    
    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

; ============================================================================
; ОБЁРТКИ СОВМЕСТИМОСТИ
; ============================================================================

; ----------------------------------------------------------------------------
; read_float (совместимость)
;
; Обёртка совместимости для чтения float без передачи адреса.
; Использует локальную переменную в стеке.
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   Assembly:
;     call read_float
;     ; EAX содержит битовое представление float
;
; @param  none
; @return eax  Битовое представление прочитанного float
; @uses   rax, rdi (через вызов read_float_var)
; @calls  read_float_var
;
; @see read_float_var - основная реализация
; ----------------------------------------------------------------------------
read_float:
    push rbp
    mov rbp, rsp
    sub rsp, 16
    lea rdi, [rbp-4]
    call read_float_var
    mov eax, [rbp-4]
    mov rsp, rbp
    pop rbp
    ret

; ----------------------------------------------------------------------------
; read_int (совместимость)
;
; Обёртка совместимости для чтения int32 без передачи адреса.
; Использует локальную переменную в стеке.
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   Assembly:
;     call read_int
;     ; EAX содержит прочитанное int32
;
; @param  none
; @return eax  Прочитанное значение int32
; @uses   rax, rdi (через вызов read_int_var)
; @calls  read_int_var
;
; @see read_int_var - основная реализация
; ----------------------------------------------------------------------------
read_int:
    push rbp
    mov rbp, rsp
    sub rsp, 16
    lea rdi, [rbp-4]
    call read_int_var
    mov eax, [rbp-4]
    mov rsp, rbp
    pop rbp
    ret

; ----------------------------------------------------------------------------
; print_float_from_eax (совместимость)
;
; Обёртка совместимости для вывода float из регистра EAX.
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   Assembly:
;     mov eax, 0x40490FDB    ; π
;     call print_float_from_eax
;
; @param  eax  Битовое представление float
; @return none
; @uses   edi (через вызов print_float_var)
; @calls  print_float_var
;
; @see print_float_var - основная реализация
; ----------------------------------------------------------------------------
print_float_from_eax:
    mov edi, eax
    call print_float_var
    ret

; ----------------------------------------------------------------------------
; layer2_read_string_float
;
; Внутренняя функция буферизованного чтения строки из stdin до символа LF.
; Использует внутреннюю буферизацию для минимизации системных вызовов.
; Результат сохраняется в parse_temp_buffer с null-терминатором.
;
; АЛГОРИТМ:
;   1. Очистка выходного буфера
;   2. Цикл чтения:
;      - Проверка наличия данных в буфере
;      - При необходимости: системный вызов read() для новой порции
;      - Копирование символа в выходной буфер
;      - Проверка на LF (конец строки)
;   3. Установка null-терминатора
;
; БУФЕРИЗАЦИЯ:
;   Внутренний буфер: 128 байт
;   Выходной буфер: 64 байта
;   Минимизирует количество syscall для повышения производительности
;
; @param  none
; @return none (результат в parse_temp_buffer)
; @uses   rax, rbx, rcx, rdx, rdi, rsi
; @exit   Код 1 при EOF без данных
;
; @complexity O(n), где n - длина строки
; @calls      syscall read(stdin)
; @internal   Используется внутри read_float_var и read_int_var
; ----------------------------------------------------------------------------
layer2_read_string_float:
    push rbx
    push rcx
    push rdi
    
; --- Очистка выходного буфера ---
    lea rdi, [parse_temp_buffer] ; Адрес буфера для очистки
    mov rcx, 64                  ; Количество байт для очистки
    xor al, al                   ; Значение для заполнения (0)
    rep stosb                    ; Повторение stosb rcx раз (заполнение нулями)
    
    xor rbx, rbx                 ; rbx = позиция записи в parse_temp_buffer (0)

; --- Цикл чтения символов ---
.read_char:
    ; Проверка наличия данных в буфере
    mov rax, [io_interactive_size]      ; Загрузка количества доступных байт
    test rax, rax                       ; Проверка: есть ли данные в буфере?
    jnz .has_data                       ; Если да - переход к чтению из буфера
    
    ; Буфер пуст - системный вызов
    ; syscall: read(stdin, buffer, count)
    xor rax, rax
    xor rdi, rdi
    lea rsi, [io_interactive_buffer]    ; Адрес буфера для чтения
    mov rdx, 128                        ; Количество байт для чтения
    syscall
    
    test rax, rax                       ; Проверка результата (rax = количество прочитанных байт)
    jle .eof                            ; Если <= 0 (EOF или ошибка) - обработка
    
    mov [io_interactive_size], rax      ; Сохранение количества прочитанных байт
    mov qword [io_interactive_pos], 0   ; Сброс позиции чтения в начало буфера

; --- Извлечение символа из буфера ---
.has_data:
    mov rcx, [io_interactive_pos]              ; Загрузка текущей позиции чтения
    mov al, byte [io_interactive_buffer + rcx] ; Чтение символа из буфера
    
    inc qword [io_interactive_pos]             ; Увеличение позиции чтения
    dec qword [io_interactive_size]            ; Уменьшение количества доступных байт
    
    ; Запись символа в выходной буфер
    lea rdx, [parse_temp_buffer]               ; Загрузка базового адреса выходного буфера
    mov byte [rdx + rbx], al                   ; Запись символа по адресу [база + смещение]
    inc rbx                                    ; Увеличение позиции записи
    
; --- Проверка на конец строки ---
    cmp al, 10                  ; Проверка: LF (line feed)?
    je .done                    ; Если да - завершение чтения
    
    cmp rbx, 63                 ; Проверка: буфер заполнен (осталось место для null)?
    jge .done                   ; Если да - завершение чтения
    
    jmp .read_char              ; Продолжение чтения следующего символа

; --- Обработка EOF без данных ---
.eof:
    test rbx, rbx               ; Проверка: были ли прочитаны данные?
    jnz .done                   ; Если да - завершение нормально (частичные данные)
    
    ; EOF без данных - это ошибка
    ; syscall: write(stderr, string, length)
    mov rax, 1
    mov rdi, 2                  ; stderr
    lea rsi, [error_format_msg]
    mov rdx, error_format_len
    syscall
    
    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

; --- Успешное завершение ---
.done:
    pop rdi
    pop rcx
    pop rbx
    ret

; ============================================================================
; ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
; ============================================================================

; ----------------------------------------------------------------------------
; pf_int_part
;
; Внутренняя функция для вывода целой части float.
; Вызывается из print_float_var.
;
; АЛГОРИТМ:
;   1. Специальная обработка нуля
;   2. Конвертация справа налево в локальный буфер
;   3. Вывод слева направо через io_print_char
;
; @param  r13d Целая часть числа (int32)
; @uses   rax, rbx, rdx, r14, r15
; @calls  io_print_char
; @internal Используется только внутри print_float_var
; ----------------------------------------------------------------------------
pf_int_part:
    mov eax, r13d               ; Загрузка целой части
    test eax, eax               ; Проверка: равно нулю?
    jnz .convert                ; Если нет - конвертация
    mov byte [parse_temp_buffer], '0' ; Если ноль - подготовка '0'
    call io_print_char          ; Вывод символа '0'
    ret

.convert:
    lea r14, [rbp-32]           ; r14 = адрес локального буфера
    xor r15, r15                ; r15 = позиция записи (начинаем с 0)

.loop:
    xor edx, edx                ; Обнуление EDX перед делением
    mov ebx, 10                 ; Делитель = 10
    div ebx                     ; EAX = EAX / 10, EDX = EAX % 10 (цифра)
    add dl, '0'                 ; Преобразование цифры в ASCII
    mov [r14 + r15], dl         ; Запись цифры в локальный буфер
    inc r15                     ; Увеличение позиции
    test eax, eax               ; Проверка: остались ли цифры?
    jnz .loop                   ; Если да - продолжение

; --- Вывод цифр слева направо (обратный порядок записи) ---
.print:
    dec r15                     ; Переход к последней записанной цифре
    movzx eax, byte [r14 + r15] ; Загрузка цифры
    mov [parse_temp_buffer], al ; Подготовка для вывода
    call io_print_char          ; Вывод символа
    test r15, r15               ; Проверка: остались ли цифры?
    jnz .print                  ; Если да - продолжение вывода
    ret

; ----------------------------------------------------------------------------
; pf_dec_point
;
; Внутренняя функция для вывода десятичной точки.
; Вызывается из print_float_var.
;
; @uses   parse_temp_buffer
; @calls  io_print_char
; @internal Используется только внутри print_float_var
; ----------------------------------------------------------------------------
pf_dec_point:
    mov byte [parse_temp_buffer], '.' ; Подготовка символа '.'
    call io_print_char                ; Вывод символа
    ret

; ----------------------------------------------------------------------------
; pf_frac_part
;
; Внутренняя функция для вывода дробной части float (6 знаков).
; Вызывается из print_float_var.
;
; АЛГОРИТМ:
;   1. Загрузка дробной части в FPU
;   2. Цикл 6 итераций:
;      - Умножение на 10
;      - Усечение для получения текущей цифры
;      - Вывод цифры
;      - Удаление целой части
;
; @param  [rbp-12] Дробная часть числа (float)
; @uses   rax, r15, FPU stack
; @calls  io_print_char
; @internal Используется только внутри print_float_var
;
; @note Использует FPU для точных вычислений
; ----------------------------------------------------------------------------
pf_frac_part:
    fld dword [rbp-12]          ; Загрузка дробной части в FPU
    mov r15, FRAC_DIGITS        ; r15 = 6 (количество знаков)

.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
    fnstcw [rsp]                ; Сохранение control word FPU
    mov ax, [rsp]
    or ax, FPU_TRUNC_MODE       ; Установка режима усечения
    mov [rsp+4], ax
    fldcw [rsp+4]               ; Загрузка режима усечения
    frndint                     ; Усечение до целого
    fldcw [rsp]                 ; Восстановление исходного control word
    add rsp, 8
    pop rax
    
    fist dword [rbp-20]         ; Сохранение текущей цифры
    mov eax, [rbp-20]           ; Загрузка цифры в EAX
    
    ; Валидация цифры (должна быть 0-9)
    cmp eax, 0                  ; Проверка: меньше 0?
    jl .zero                    ; Если да - использовать '0'
    cmp eax, 9                  ; Проверка: больше 9?
    jg .zero                    ; Если да - использовать '0'
    add al, '0'                 ; Преобразование в ASCII
    jmp .out

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

.out:
    mov [parse_temp_buffer], al ; Подготовка для вывода
    call io_print_char          ; Вывод символа
    
    fsubp st1, st0              ; Удаление целой части цифры из дробного остатка
    dec r15                     ; Уменьшение счётчика цифр
    jnz .loop                   ; Если остались цифры - продолжение
    
    fstp st0                    ; Удаление остатка из стека FPU
    ret

; ----------------------------------------------------------------------------
; print_hex_32_internal
;
; Внутренняя функция для вывода hex-представления float.
; Вызывается из print_float_var.
; Формат: " (hex: 0xXXXXXXXX)"
;
; @param  r12d Битовое представление float для hex-вывода
; @uses   rax, rdi
; @calls  print_string, print_hex_32
; @internal Используется только внутри print_float_var
; ----------------------------------------------------------------------------
print_hex_32_internal:
    lea rdi, [io_msg_hex]       ; Загрузка адреса " (hex: 0x"
    call print_string           ; Вывод префикса
    mov eax, r12d               ; Загрузка битового представления
    call print_hex_32           ; Вывод hex-значения
    lea rdi, [io_msg_hex_end]   ; Загрузка адреса ")"
    call print_string           ; Вывод суффикса
    ret

; ----------------------------------------------------------------------------
; print_hex_32
;
; Выводит 32-битное значение в шестнадцатеричном формате (8 символов).
; Публичная утилита, может использоваться отдельно.
;
; ФОРМАТ ВЫВОДА: XXXXXXXX (заглавные буквы A-F)
;
; ПРИМЕРЫ:
;   print_hex_32(0x40490FDB) → "40490FDB"
;   print_hex_32(0x00000000) → "00000000"
;   print_hex_32(0xFFFFFFFF) → "FFFFFFFF"
;
; @param  eax  Значение для вывода
; @return none
; @uses   rbx, rcx
; @calls  io_print_char
;
; @complexity O(1), всегда 8 символов
; ----------------------------------------------------------------------------
print_hex_32:
    push rbp
    mov rbp, rsp
    push rbx
    push rcx

    mov rbx, rax                ; Сохранение значения в RBX
    mov rcx, 8                  ; Счётчик: 8 hex-цифр (32 бита / 4)

.loop:
    rol ebx, 4                  ; Циклический сдвиг влево на 4 бита (1 hex-цифра)
    mov eax, ebx                ; Копирование для извлечения младших 4 бит
    and eax, 0xF                ; Маскирование: оставить только младшие 4 бита
    cmp al, 9                   ; Проверка: цифра 0-9 или A-F?
    jle .digit                  ; Если 0-9 - переход к обработке цифры
    add al, 'A' - 10            ; Преобразование 10-15 → 'A'-'F'
    jmp .out

.digit:
    add al, '0'                 ; Преобразование 0-9 → '0'-'9'

.out:
    mov [parse_temp_buffer], al ; Подготовка для вывода
    push rcx                    ; Сохранение счётчика
    call io_print_char          ; Вывод символа
    pop rcx                     ; Восстановление счётчика
    dec rcx                     ; Уменьшение счётчика
    jnz .loop                   ; Если остались цифры - продолжение

    pop rcx
    pop rbx
    pop rbp
    ret

; ----------------------------------------------------------------------------
; print_string
;
; Выводит null-терминированную строку в stdout.
; Публичная утилита, может использоваться отдельно.
;
; ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
;   Assembly:
;     section .data
;       msg db "Hello, World!", 0
;     section .text
;       lea rdi, [msg]
;       call print_string
;
; @param  rdi  Адрес null-терминированной строки
; @return none
; @uses   rax, rdx, rsi
;
; @complexity O(n), где n - длина строки
; ----------------------------------------------------------------------------
print_string:
    push rbp
    mov rbp, rsp
    push rdi                    ; Сохранение адреса строки
    
    xor rdx, rdx                ; rdx = счётчик длины (начинаем с 0)

; --- Вычисление длины строки ---
.len:
    cmp byte [rdi + rdx], 0     ; Проверка: null-терминатор?
    je .do_print                ; Если да - переход к выводу
    inc rdx                     ; Увеличение счётчика
    jmp .len                    ; Продолжение подсчёта

; --- Вывод строки ---
.do_print:
    pop rsi                     ; Восстановление адреса строки в RSI (для syscall)

    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    syscall
    
    pop rbp
    ret

; ----------------------------------------------------------------------------
; io_print_char
;
; Выводит один символ из parse_temp_buffer в stdout.
; Внутренняя утилита для вспомогательных функций.
;
; @param  [parse_temp_buffer] Символ для вывода
; @return none
; @uses   rax, rdi, rsi, rdx
; @internal Используется вспомогательными функциями
; ----------------------------------------------------------------------------
io_print_char:
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    lea rsi, [parse_temp_buffer] ; Адрес символа
    mov rdx, 1
    syscall
    ret

; ----------------------------------------------------------------------------
; io_is_end_char
;
; Проверяет, является ли символ в AL символом окончания числа.
; Символы окончания: LF (10), null (0), пробел (32), CR (13).
;
; @param  al   Символ для проверки
; @return ZF   Установлен, если символ является окончанием
; @uses   Только флаги (не изменяет регистры)
; @internal Используется в функциях парсинга
; ----------------------------------------------------------------------------
io_is_end_char:
    cmp al, 10                  ; Проверка: LF (line feed)?
    je .yes                     ; Если да - установка ZF и возврат
    cmp al, 0                   ; Проверка: null-терминатор?
    je .yes                     ; Если да - установка ZF и возврат
    cmp al, 32                  ; Проверка: пробел?
    je .yes                     ; Если да - установка ZF и возврат
    cmp al, 13                  ; Проверка: CR (carriage return)?
    je .yes                     ; Если да - установка ZF и возврат
    ret                         ; Возврат с ZF = 0 (не символ окончания)

.yes:
    cmp al, al                  ; Установка ZF = 1 (гарантированное равенство)
    ret                         ; Возврат с ZF = 1 (символ окончания)

Makefile

📄 Makefile — правила сборки
.PHONY: all build_signed build_unsigned build_float clean prepare_dirs

all: build_signed build_unsigned build_float

prepare_dirs:
	@mkdir -p build bin

build_signed: prepare_dirs
	nasm -f elf64 -g -F dwarf src/io_signed.asm -o build/io_signed.o
	nasm -f elf64 -g -F dwarf src/compute_signed.asm -o build/compute_signed.o
	ld build/compute_signed.o build/io_signed.o -o bin/signed

build_unsigned: prepare_dirs
	nasm -f elf64 -g -F dwarf src/io_unsigned.asm -o build/io_unsigned.o
	nasm -f elf64 -g -F dwarf src/compute_unsigned.asm -o build/compute_unsigned.o
	ld build/compute_unsigned.o build/io_unsigned.o -o bin/unsigned

build_float: prepare_dirs
	nasm -f elf64 -g -F dwarf src/io_float.asm -o build/io_float.o
	nasm -f elf64 -g -F dwarf src/compute_float.asm -o build/compute_float.o
	ld build/compute_float.o build/io_float.o -o bin/float

clean:
	rm -rf build/*.o bin/*

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

Концепция: Разделение парсинга и I/O

Проблема:

Программа растёт — появляются новые источники данных (файлы, сокеты, аргументы командной строки). Наивный подход приводит к дублированию кода парсинга в каждой функции чтения.

Сценарий 1: Монолитный подход (до рефакторинга)

┌─────────────────────────────────────────────────────────────┐
│  ТРЕБОВАНИЕ: Читать числа из stdin                          │
├─────────────────────────────────────────────────────────────┤
│  read_number_stdin:                                         │
│    1. syscall read(0, buffer, 128)    ← I/O операция        │
│    2. Пропуск пробелов                ← Парсинг (20 строк)  │
│    3. Обработка знака                                       │
│    4. Цикл по цифрам                  ← Парсинг (30 строк)  │
│    5. Проверка переполнения           ← Парсинг (10 строк)  │
│    6. Возврат результата                                    │
│                                                             │
│  Итого: 60 строк (10 I/O + 50 парсинга)                     │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  НОВОЕ ТРЕБОВАНИЕ: Читать числа из файла                    │
├─────────────────────────────────────────────────────────────┤
│  read_number_file:                                          │
│    1. syscall read(fd, buffer, 128)  ← ДРУГОЙ I/O (3 строки)│
│    2. Пропуск пробелов               ← КОПИЯ (20 строк)     │
│    3. Обработка знака                ← КОПИЯ (30 строк)     │
│    4. Цикл по цифрам                 ← КОПИЯ (10 строк)     │
│    5. Проверка переполнения          ← КОПИЯ                │
│    6. Возврат результата                                    │
│                                                             │
│  Проблема: 50 строк парсинга ДУБЛИРУЮТСЯ!                   │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  ЕЩЁ ОДНО ТРЕБОВАНИЕ: Парсить из буфера в памяти            │
├─────────────────────────────────────────────────────────────┤
│  parse_from_buffer:                                         │
│    1. БЕЗ I/O (данные уже в памяти)                         │
│    2. Пропуск пробелов               ← КОПИЯ № 3 (20 строк) │
│    3. Обработка знака                ← КОПИЯ № 3 (30 строк) │
│    4. Цикл по цифрам                 ← КОПИЯ № 3 (10 строк) │
│    5. Проверка переполнения          ← КОПИЯ № 3            │
│                                                             │
│  Итого: 50 строк × 3 функции = 150 строк парсинга!          │
│  Баг в обработке переполнения → исправлять в 3 местах       │
└─────────────────────────────────────────────────────────────┘

Последствия дублирования:

Проблема Пример
Синхронизация Исправили баг в read_number_stdin, забыли в read_number_file
Поддержка Изменение алгоритма → редактировать N функций
Тестирование Нужно писать тесты для каждой копии
Размер бинарника 150 строк вместо 50 + 3 обёртки

Сценарий 2: Двухслойная архитектура (после рефакторинга)

┌─────────────────────────────────────────────────────────────┐
│  СЛОЙ 1: Чистая функция парсинга (ОДИН РАЗ)                 │
├─────────────────────────────────────────────────────────────┤
│  parse_int16(rdi = указатель на строку):                    │
│    1. Пропуск пробелов                                      │
│    2. Обработка знака                                       │
│    3. Цикл по цифрам                                        │
│    4. Проверка переполнения                                 │
│    5. Возврат: rax = число, rdx = код ошибки                │
│                                                             │
│  Характеристики:                                            │
│    • Детерминированная (один вход → один выход)             │
│    • БЕЗ побочных эффектов (нет syscall, нет вывода)        │
│    • Тестируемая (не требует эмуляции I/O)                  │
│                                                             │
│  Итого: 50 строк (НАПИСАНО ОДИН РАЗ)                        │
└─────────────────────────────────────────────────────────────┘
            ⠀⠀⠀⠀↓ используется всеми обёртками ↓
┌─────────────────────────────────────────────────────────────┐
│  СЛОЙ 2: Обёртки I/O (по 5-10 строк каждая)                 │
├─────────────────────────────────────────────────────────────┤
│  read_number_stdin:                                         │
│    1. syscall read(0, buffer, 128)   ← Специфика I/O        │
│    2. lea rdi, [buffer]                                     │
│    3. call parse_int16               ← Переиспользование!   │
│    4. ret                                                   │
│                                                             │
│  read_number_file:                                          │
│    1. syscall read(fd, buffer, 128)  ← Другой FD            │
│    2. lea rdi, [buffer]                                     │
│    3. call parse_int16               ← Тот же парсер!       │
│    4. ret                                                   │
│                                                             │
│  parse_from_buffer:                                         │
│    1. ; rdi уже указывает на данные  ← БЕЗ I/O              │
│    2. call parse_int16               ← Прямой вызов         │
│    3. ret                                                   │
│                                                             │
│  Итого: 50 строк парсинга + 3×5 обёрток = 65 строк          │
│  Экономия: 150 - 65 = 85 строк (57%!)                       │
└─────────────────────────────────────────────────────────────┘

Сценарий 3: Исправление бага

┌─────────────────────────────────────────────────────────────┐
│  БАГ: Неправильная проверка переполнения для граничного     │
│       случая (32767 для int16_t)                            │
├─────────────────────────────────────────────────────────────┤
│  МОНОЛИТНЫЙ ПОДХОД:                                         │
│    ❌ Исправить в read_number_stdin (строка 45)             │
│    ❌ Исправить в read_number_file (строка 45)              │
│    ❌ Исправить в parse_from_buffer (строка 45)             │
│    ❌ Проверить консистентность всех трёх версий            │
│    ⏱️ Время: 30-60 минут                                    │
│                                                             │
│  ДВУХСЛОЙНЫЙ ПОДХОД:                                        │
│    ✅ Исправить в parse_int16 (строка 45)                   │
│    ✅ Все обёртки автоматически получают исправление        │
│    ⏱️ Время: 5-10 минут                                     │
└─────────────────────────────────────────────────────────────┘

Характеристики чистых функций (Слой 1)

Свойство Описание Преимущество
Детерминированность Одинаковый вход → одинаковый выход Предсказуемое поведение, легко тестировать
Отсутствие побочных эффектов Не читает stdin, не пишет stderr Можно вызывать многократно без последствий
Независимость от контекста Только rdi на входе, rax/rdx на выходе Переиспользование в любом месте
Тестируемость Можно протестировать без syscall Юнит-тесты на уровне функций

Концепция: Weak symbols для двух интерфейсов

Проблема:

Модуль должен поддерживать два способа использования:

  1. Legacy API: Программа определяет global a, b, output, модуль использует их напрямую
  2. Modern API: Программа передаёт адреса через регистры rdi, rsi

Решение:

Механизм weak symbols — если символ объявлен как extern a:weak:

  • Программа определила global a → адрес a ≠ 0 (символ существует)
  • Программа НЕ определила a → адрес a = 0 (NULL, символ отсутствует)

Сценарий 1: Программа со старым интерфейсом

┌─────────────────────────────────────────────────────────────┐
│  ПРОГРАММА В СТАРОМ СТИЛЕ (Legacy API)                      │
├─────────────────────────────────────────────────────────────┤
│  section .bss                                               │
│      global a, b, output    ← Глобальные символы объявлены  │
│      a resw 1                                               │
│      b resw 1                                               │
│      output resd 1                                          │
│                                                             │
│  section .text                                              │
│      call read_signed_input    ← Вызов обёртки              │
│         ↓                                                   │
├─────────────────────────────────────────────────────────────┤
│  МОДУЛЬ io_signed.asm                                       │
├─────────────────────────────────────────────────────────────┤
│  read_signed_input:                                         │
│      mov rax, a              ← Проверка существования       │
│      test rax, rax           ← rax ≠ 0?                     │
│      jz .missing_symbols     ← Если 0 → ошибка              │
│                                                             │
│      lea rdi, [a]            ← Передача адресов             │
│      lea rsi, [b]            ← в новый API                  │
│      call read_signed_input_vars                            │
│      ret                                                    │
│                              ✅ Работает!                   │
└─────────────────────────────────────────────────────────────┘

Сценарий 2: Программа с новым интерфейсом

┌─────────────────────────────────────────────────────────────┐
│  ПРОГРАММА В НОВОМ СТИЛЕ (Modern API)                       │
├─────────────────────────────────────────────────────────────┤
│  section .bss                                               │
│      my_x resw 1             ← Произвольные имена           │
│      my_y resw 1             ← (не a, b, output)            │
│      result resd 1                                          │
│                                                             │
│  section .text                                              │
│      lea rdi, [my_x]         ← Адреса передаются вручную    │
│      lea rsi, [my_y]                                        │
│      call read_signed_input_vars  ← Прямой вызов            │
│         ↓                                                   │
├─────────────────────────────────────────────────────────────┤
│  МОДУЛЬ io_signed.asm                                       │
├─────────────────────────────────────────────────────────────┤
│  read_signed_input_vars:                                    │
│      ; rdi = адрес первой переменной                        │
│      ; rsi = адрес второй переменной                        │
│      ; Чтение напрямую через параметры                      │
│      ...                                                    │
│      mov word [rdi], ax      ← Сохранение по адресу из RDI  │
│      ret                                                    │
│                              ✅ Работает!                   │
└─────────────────────────────────────────────────────────────┘

Сценарий 3: Ошибка — символы не определены

┌─────────────────────────────────────────────────────────────┐
│  ПРОГРАММА БЕЗ ГЛОБАЛЬНЫХ СИМВОЛОВ (ошибка)                 │
├─────────────────────────────────────────────────────────────┤
│  section .bss                                               │
│      ; НЕТ объявлений a, b, output                          │
│                                                             │
│  section .text                                              │
│      call read_signed_input    ← Попытка вызова Legacy API  │
│         ↓                                                   │
├─────────────────────────────────────────────────────────────┤
│  МОДУЛЬ io_signed.asm                                       │
├─────────────────────────────────────────────────────────────┤
│  read_signed_input:                                         │
│      mov rax, a              ← Загрузка адреса weak symbol  │
│      test rax, rax           ← rax = 0 (NULL!)              │
│      jz .missing_symbols     ← Переход к ошибке             │
│                                                             │
│  .missing_symbols:                                          │
│      ; Вывод в stderr:                                      │
│      ; "CRITICAL ERROR: Legacy wrapper called but           │
│      ;  'a', 'b' or 'output' not defined!"                  │
│      mov rax, 60             ← sys_exit                     │
│      mov rdi, 1              ← Код возврата = 1             │
│      syscall                                                │
│                              ❌ Программа завершена!        │
└─────────────────────────────────────────────────────────────┘

⚠️ ВАЖНО: Платформенная специфичность

Механизм weak symbols в представленном виде работает только на Linux (формат ELF + GNU Linker). На других платформах:

  • macOS (Mach-O): Требуется директива .weak_definition вместо extern name:weak
  • Windows (PE/COFF): Нет прямой поддержки weak symbols в стандартном линкере MSVC
  • BSD: Аналогично Linux, но с нюансами в некоторых версиях ld

Для кроссплатформенного кода рекомендуется:

  • Использовать только новый API (передача адресов через параметры)
  • Применять условную компиляцию через макросы препроцессора NASM:
    %ifdef LINUX
        extern a:weak
    %elifdef MACOS
        extern a
        .weak_definition a
    %endif
    

Преимущества подхода

Аспект Legacy API Modern API
Совместимость ✅ Работает со старым кодом Требует переписывания программы
Гибкость ⚠️ Фиксированные имена переменных ✅ Любые имена переменных
Переносимость ❌ Только Linux/ELF ✅ Работает везде
Безопасность ⚠️ Глобальные символы ✅ Явная передача данных
Множественные наборы ❌ Только одна тройка a,b,output ✅ Неограниченное количество

Рекомендация: Используйте Modern API для новых проектов, Legacy API оставьте для обратной совместимости.


Концепция: Буферизация ввода

Проблема:

Системный вызов read() — дорогая операция: переключение контекста в ядро, копирование данных, возврат в userspace. Стоимость ~1000-2000 тактов CPU.

Сценарий 1: Побайтовое чтение (наивный подход)

┌─────────────────────────────────────────────────────────────┐
│  ПОЛЬЗОВАТЕЛЬ ВВОДИТ: "100 -200 300\n" (14 символов)        │
├─────────────────────────────────────────────────────────────┤
│  Программа читает ПО ОДНОМУ БАЙТУ:                          │
│                                                             │
│  syscall read(0, &buffer[0], 1)  → '1'    ~1000 тактов      │
│  syscall read(0, &buffer[1], 1)  → '0'    ~1000 тактов      │
│  syscall read(0, &buffer[2], 1)  → '0'    ~1000 тактов      │
│  syscall read(0, &buffer[3], 1)  → ' '    ~1000 тактов      │
│  syscall read(0, &buffer[4], 1)  → '-'    ~1000 тактов      │
│  syscall read(0, &buffer[5], 1)  → '2'    ~1000 тактов      │
│  ... (ещё 8 syscall)                                        │
│                                                             │
│  Итого: 14 syscall × 1000 тактов = ~14000 тактов            │
│  При частоте 3 GHz: ~4.7 микросекунды                       │
└─────────────────────────────────────────────────────────────┘

Профиль выполнения:

  • User mode: 10% времени (парсинг)
  • Kernel mode: 90% времени (syscall overhead)

Сценарий 2: Буферизованное чтение (оптимизация)

┌─────────────────────────────────────────────────────────────┐
│  ТОТ ЖЕ ВВОД: "100 -200 300\n" (14 символов)                │
├─────────────────────────────────────────────────────────────┤
│  Программа читает БЛОКОМ:                                   │
│                                                             │
│  syscall read(0, io_buffer, 128)  → 14 байт    ~1000 тактов │
│                                                             │
│  Ядро копирует ВСЕ 14 байт за одну операцию:                │
│  ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐  │
│  │'1'│'0'│'0'│' '│'-'│'2'│'0'│'0'│' '│'3'│'0'│'0'│\n │...│  │
│  └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘  │
│    ↑                                                        │
│    io_interactive_buffer[128]                               │
│                                                             │
│  Далее: чтение из ПАМЯТИ (не syscall!):                     │
│    mov al, [io_buffer + 0]   → '1'    ~1 такт               │
│    mov al, [io_buffer + 1]   → '0'    ~1 такт               │
│    mov al, [io_buffer + 2]   → '0'    ~1 такт               │
│    ... (ещё 11 обращений к памяти)                          │
│                                                             │
│  Итого: 1 syscall (1000 тактов) + 14 чтений (14 тактов)     │
│        = ~1014 тактов                                       │
│  При частоте 3 GHz: ~0.34 микросекунды                      │
│                                                             │
│  УСКОРЕНИЕ: 14000 / 1014 ≈ ×13.8                            │
└─────────────────────────────────────────────────────────────┘

Архитектура буферизации

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

┌─────────────────────────────────────────────────────────────┐
│  ЛОГИКА ЧТЕНИЯ ОДНОГО СИМВОЛА                               │
├─────────────────────────────────────────────────────────────┤
│  .read_char:                                                │
│    ┌─────────────────────────────────┐                      │
│    │ ПРОВЕРКА: Есть данные в буфере? │                      │
│    │ cmp [io_interactive_size], 0    │                      │
│    └─────────────────────────────────┘                      │
│            │                                                │
│     ┌──────┴──────┐                                         │
│     │ БУФ  ПУСТОЙ │ БУФЕР СОДЕРЖИТ ДАННЫЕ                   │
│     ▼             ▼                                         │
│  ┌─────────┐  ┌───────────────────────────┐                 │
│  │ syscall │  │ Извлечь символ из буфера: │                 │
│  │ read    │  │ pos = [io_interactive_pos]│                 │
│  │ (128    │  │ al = [io_buffer + pos]    │                 │
│  │  байт)  │  │ pos++; size--             │                 │
│  └─────────┘  └───────────────────────────┘                 │
│       │             │                                       │
│       └─────┬───────┘                                       │
│             ▼                                               │
│    Символ получен → продолжение парсинга                    │
└─────────────────────────────────────────────────────────────┘

Пример: чтение трёх чисел

┌─────────────────────────────────────────────────────────────┐
│  Ввод: "10 20 30\n"                                         │
├─────────────────────────────────────────────────────────────┤
│  ВЫЗОВ 1: read_signed_input()                               │
│    • Буфер пуст → syscall read(0, buffer, 128)              │
│      Получено: "10 20 30\n" (9 байт)                        │
│      io_interactive_size = 9                                │
│    • Парсинг '1', '0' из буфера (pos: 0→2, size: 9→7)       │
│    • Встретили ' ' → конец числа                            │
│    • Результат: 10                                          │
│                                                             │
│  ВЫЗОВ 2: read_signed_input()                               │
│    • Буфер НЕ пуст (size=7) → БЕЗ syscall!                  │
│    • Парсинг '2', '0' из буфера (pos: 3→5, size: 7→5)       │
│    • Встретили ' ' → конец числа                            │
│    • Результат: 20                                          │
│                                                             │
│  ВЫЗОВ 3: read_signed_input()                               │
│    • Буфер НЕ пуст (size=5) → БЕЗ syscall!                  │
│    • Парсинг '3', '0' из буфера (pos: 6→8, size: 5→3)       │
│    • Встретили '\n' → конец числа                           │
│    • Результат: 30                                          │
│                                                             │
│  Итого: 3 числа прочитаны с ОДНИМ syscall!                  │
└─────────────────────────────────────────────────────────────┘

Сравнение подходов

Метрика Побайтовое Буферизованное Улучшение
Syscall для “10 20 30” 9 1 ×9
Syscall для 100 чисел ~300 ~3-5 ×60-100
Тактов CPU ~300000 ~5000 ×60
L1 cache miss Высокий Низкий Данные в кэше

Концепция: Трёхуровневая обработка ошибок

Проблема:

В ассемблере нет механизма исключений (try/catch). Ошибки нужно обрабатывать явно, но смешивание детектирования и сообщений об ошибках усложняет код.

Архитектура обработки

╔══════════════════════════════════════════════════════════╗
║  СЛОЙ 1: ДЕТЕКТИРОВАНИЕ ОШИБКИ (parse_int16)             ║
║  ────────────────────────────────────────────────────────║
║  Задача: Определить ЧТО пошло не так                     ║
║                                                          ║
║  Возвращает КОД ошибки в RDX:                            ║
║    • 0 = SUCCESS (число в RAX валидно)                   ║
║    • 1 = FORMAT_ERROR (некорректный символ)              ║
║    • 2 = OVERFLOW (число вне диапазона)                  ║
║    • 3 = BUFFER_OVERFLOW (строка слишком длинная)        ║
║                                                          ║
║  НЕ делает:                                              ║
║    • НЕ выводит сообщения в stderr                       ║
║    • НЕ завершает программу                              ║
║    • НЕ зависит от языка интерфейса (русский/английский) ║
╚══════════════════════════════════════════════════════════╝
                    ⠀⠀⠀⠀⠀⠀↓ вызов
╔══════════════════════════════════════════════════════════╗
║  СЛОЙ 2: ИНТЕРПРЕТАЦИЯ ОШИБКИ (read_signed_input_vars)   ║
║  ────────────────────────────────────────────────────────║
║  Задача: Сообщить пользователю, что делать               ║
║                                                          ║
║  test rdx, rdx         ; Проверка кода                   ║
║  jnz .handle_error                                       ║
║                                                          ║
║  .handle_error:                                          ║
║    cmp rdx, 1 → stderr: "ERROR: Invalid number format"   ║
║    cmp rdx, 2 → stderr: "ERROR: Number overflow..."      ║
║    cmp rdx, 3 → stderr: "ERROR: Input too long..."       ║
║                                                          ║
║  Затем: exit(1)                                          ║
╚══════════════════════════════════════════════════════════╝
                       ⠀⠀⠀↓ вызов
╔══════════════════════════════════════════════════════════╗
║  СЛОЙ 3: ИСПОЛЬЗОВАНИЕ (compute_signed.asm)              ║
║  ────────────────────────────────────────────────────────║
║  Задача: Выполнить бизнес-логику                         ║
║                                                          ║
║  call read_signed_input                                  ║
║  ; Если вернулся сюда → данные корректны                 ║
║  ; Если ошибка → программа уже завершена слоем 2         ║
║                                                          ║
║  Программист НЕ пишет обработку ошибок I/O!              ║
╚══════════════════════════════════════════════════════════╝

Сценарий 1: Успешный ввод

┌─────────────────────────────────────────────────────────────┐
│  ПОЛЬЗОВАТЕЛЬ ВВОДИТ: "42"                                  │
├─────────────────────────────────────────────────────────────┤
│  СЛОЙ 2: read_signed_input_vars                             │
│    1. Чтение из stdin → "42"                                │
│    2. Вызов: call parse_int16                               │
│       ↓                                                     │
│  ┌─────────────────────────────────────────┐                │
│  │ СЛОЙ 1: parse_int16                     │                │
│  │   • Проверка '4' → цифра ✅             │                │
│  │   • Проверка '2' → цифра ✅             │                │
│  │   • 42 в диапазоне [-32768..32767] ✅   │                │
│  │   • Возврат: rax=42, rdx=0              │                │
│  └─────────────────────────────────────────┘                │
│       ↓                                                     │
│  СЛОЙ 2 (продолжение):                                      │
│    3. Проверка: test rdx, rdx → rdx=0 (успех!)              │
│    4. Сохранение: mov [a], ax                               │
│    5. Возврат в программу                                   │
│       ↓                                                     │
│  СЛОЙ 3: compute_signed.asm                                 │
│    • Продолжение выполнения с корректными данными           │
└─────────────────────────────────────────────────────────────┘

Сценарий 2: Ошибка формата

┌─────────────────────────────────────────────────────────────┐
│  ПОЛЬЗОВАТЕЛЬ ВВОДИТ: "12a45"                               │
├─────────────────────────────────────────────────────────────┤
│  СЛОЙ 2: read_signed_input_vars                             │
│    1. Чтение из stdin → "12a45"                             │
│    2. Вызов: call parse_int16                               │
│       ↓                                                     │
│  ┌─────────────────────────────────────────┐                │
│  │ СЛОЙ 1: parse_int16                     │                │
│  │   • Проверка '1' → цифра ✅             │                │
│  │   • Проверка '2' → цифра ✅             │                │
│  │   • Проверка 'a' → НЕ ЦИФРА ❌          │                │
│  │     cmp dl, '0'; jb .error_format       │                │
│  │     cmp dl, '9'; ja .error_format       │                │
│  │   • Возврат: rdx=1 (FORMAT_ERROR)       │                │
│  └─────────────────────────────────────────┘                │
│       ↓                                                     │
│  СЛОЙ 2 (продолжение):                                      │
│    3. Проверка: test rdx, rdx → rdx≠0 (ошибка!)             │
│    4. Сравнение: cmp rdx, 1 → да, формат                    │
│    5. Вывод в stderr:                                       │
│       "ERROR: Invalid number format"                        │
│    6. Завершение: exit(1)                                   │
│       ↓                                                     │
│  СЛОЙ 3: НЕ ДОСТИГАЕТСЯ (программа завершена)               │
└─────────────────────────────────────────────────────────────┘

Сценарий 3: Переполнение

┌─────────────────────────────────────────────────────────────┐
│  ПОЛЬЗОВАТЕЛЬ ВВОДИТ: "99999" (для int16_t)                 │
├─────────────────────────────────────────────────────────────┤
│  СЛОЙ 2: read_signed_input_vars                             │
│    1. Чтение из stdin → "99999"                             │
│    2. Вызов: call parse_int16                               │
│       ↓                                                     │
│  ┌─────────────────────────────────────────┐                │
│  │ СЛОЙ 1: parse_int16                     │                │
│  │   Парсинг по цифрам:                    │                │
│  │   • rax = 0                             │                │
│  │   • rax = 0*10 + 9 = 9     ✅           │                │
│  │   • rax = 9*10 + 9 = 99    ✅           │                │
│  │   • rax = 99*10 + 9 = 999  ✅           │                │
│  │   • rax = 999*10 + 9 = 9999 ✅          │                │
│  │   • rax = 9999*10 + 9 = 99999           │                │
│  │     Проверка: 99999 > 32767 ❌          │                │
│  │   • Возврат: rdx=2 (OVERFLOW)           │                │
│  └─────────────────────────────────────────┘                │
│       ↓                                                     │
│  СЛОЙ 2 (продолжение):                                      │
│    3. Проверка: test rdx, rdx → rdx≠0 (ошибка!)             │
│    4. Сравнение: cmp rdx, 2 → да, overflow                  │
│    5. Вывод в stderr:                                       │
│       "ERROR: Number overflow (must be -32768 to 32767)"    │
│    6. Завершение: exit(1)                                   │
└─────────────────────────────────────────────────────────────┘

Сценарий 4: Слишком длинный ввод

┌─────────────────────────────────────────────────────────────┐
│  ПОЛЬЗОВАТЕЛЬ ВВОДИТ: "1234567890..." (100 символов)        │
├─────────────────────────────────────────────────────────────┤
│  СЛОЙ 2: layer2_read_string_signed                          │
│    Цикл чтения символов:                                    │
│    • rcx = 0 (счётчик)                                      │
│    • Символ 1: rcx=1  ✅                                    │
│    • Символ 2: rcx=2  ✅                                    │
│    • ...                                                    │
│    • Символ 30: rcx=30 ✅                                   │
│    • Символ 31: rcx=31                                      │
│      ┌─────────────────────────────┐                        │
│      │ ПРОВЕРКА: cmp rcx, 30       │                        │
│      │           jg .error_buffer  │                        │
│      │ → Переход к ошибке!         │                        │
│      └─────────────────────────────┘                        │
│       ↓                                                     │
│  .error_buffer:                                             │
│    • БЕЗ вызова parse_int16 (данные некорректны)            │
│    • Прямой переход к обработке ошибки                      │
│    • Вывод в stderr:                                        │
│      "ERROR: Input too long (max 30 characters)"            │
│    • Завершение: exit(1)                                    │
└─────────────────────────────────────────────────────────────┘

Преимущества подхода

Принцип Реализация Польза
Разделение ответственности Слой 1 детектирует, Слой 2 сообщает Чистый код, переиспользование
Fail-fast Ошибка → немедленный exit(1) Нет “зомби-состояний” программы
Локализация Сообщения только в слое 2 Легко перевести на другой язык
Тестируемость Слой 1 тестируется отдельно Можно проверить все коды ошибок

Концепция: Signed vs Unsigned — разные инструкции

Проблема:

Процессор x86-64 интерпретирует биты по-разному в зависимости от контекста signed/unsigned. Один и тот же битовый паттерн требует разных инструкций для корректной обработки.

Пример: 0xFF9C означает разные числа

┌─────────────────────────────────────────────────────────────┐
│  ПАМЯТЬ: 0xFF9C (16 бит)                                    │
├─────────────────────────────────────────────────────────────┤
│  Бинарное: 1111111110011100                                 │
│            ↑ знаковый бит                                   │
│                                                             │
│  ИНТЕРПРЕТАЦИЯ SIGNED (int16_t):                            │
│    • Старший бит = 1 → отрицательное                        │
│    • Дополнительный код: -100                               │
│                                                             │
│  ИНТЕРПРЕТАЦИЯ UNSIGNED (uint16_t):                         │
│    • Все биты = значение                                    │
│    • Результат: 65436                                       │
│                                                             │
│  ОДНИ БИТЫ, РАЗНЫЕ ЧИСЛА!                                   │
└─────────────────────────────────────────────────────────────┘

Критичные различия в инструкциях:

Операция Signed Unsigned Ошибка при смешивании
Расширение 16→32 movsx eax, word [x] movzx eax, word [x] -100 → 65436
Подготовка к делению cdq (копия знака) xor edx, edx (обнуление) Деление мусора
Деление idiv div -100/2 = 32668 вместо -50
Сравнение “меньше” jl (less) jb (below) -1 < 1000? Разные ответы

Почему модули разделены:

  • io_signed.asm использует movsx, cdq, idiv, jl/jg
  • io_unsigned.asm использует movzx, xor edx, div, jb/ja
  • Смешивание инструкций приводит к тонким багам, которые проявляются только на граничных значениях

📘 Примечание: Подробная шпаргалка по всем регистрам, флагам, директивам памяти и System V ABI — в статье «Шпаргалка NASM x86-64: Регистры, Инструкции, Syscalls (Linux)».


Концепция: Стек FPU

Проблема:

Float/double требуют другой модели вычислений. Вместо регистров произвольного доступа (RAX, RBX, RCX) используется стек из 8 регистров ST(0)..ST(7).

Сравнение моделей

┌─────────────────────────────────────────────────────────────┐
│  ЦЕЛЫЕ: Регистровая модель (произвольный доступ)            │
├─────────────────────────────────────────────────────────────┤
│  mov eax, [a]              ; EAX = a                        │
│  add eax, [b]              ; EAX = a + b                    │
│  imul eax, [c]             ; EAX = (a+b) × c                │
│                                                             │
│  Операнды: любые регистры (EAX, EBX, ECX...)                │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  FLOAT: Стековая модель (LIFO)                              │
├─────────────────────────────────────────────────────────────┤
│  fld dword [a]             ; Стек: [a]                      │
│  fld dword [b]             ; Стек: [b, a]        ← Push     │
│  faddp st1, st0            ; Стек: [a+b]         ← Pop      │
│  fld dword [c]             ; Стек: [c, a+b]                 │
│  fmulp st1, st0            ; Стек: [(a+b)×c]     ← Pop      │
│  fstp dword [result]       ; Стек: []            ← Pop      │
│                                                             │
│  Операнды: только вершина стека ST(0), ST(1)                │
└─────────────────────────────────────────────────────────────┘

Ключевые отличия:

Аспект Целые (RAX/RBX) Float (FPU)
Доступ Произвольный Только вершина стека
Операции add eax, ebx faddp st1, st0 (всегда ST)
Управление Простое Нужно отслеживать глубину стека
Ошибка Перезапись регистра Stack overflow (8 уровней)

Почему это важно для модулей:

  • Парсинг дробной части: пошаговое умножение на 0.1, 0.01… → стековые операции естественны
  • Вывод: деление на 10 в цикле → fdiv + fld работают со стеком
  • Нужно следить за балансом Push/Pop, иначе переполнение стека на 9-м элементе

📘 Примечание: Работа с вещественными числами (float/double) подробно разобрана в «FPU в NASM: Вещественные числа, Синусы и Точность».


Концепция: Многоуровневая защита от ошибок ввода

Проблема:

При низкоуровневом программировании отсутствуют автоматические проверки компилятора, характерные для высокоуровневых языков. Некорректный ввод может привести к:

  • Переполнению буфера (запись за границы памяти)
  • Переполнению при вычислениях (некорректный результат)
  • Десинхронизации состояния программы

Решение:

Модули реализуют трёхэшелонную защиту на разных стадиях обработки данных.

Эшелон 1: Ограничение размера ввода

┌─────────────────────────────────────────────────────┐
│  ПОЛЬЗОВАТЕЛЬ ВВОДИТ ДАННЫЕ                         │
├─────────────────────────────────────────────────────┤
│  "12345678901234567890..." (100 символов)           │
│         ↓                                           │
│  ┌───────────────────────────────┐                  │
│  │ ПРОВЕРКА СЧЁТЧИКА СИМВОЛОВ    │                  │
│  │ if (count > 30) → ERROR       │                  │
│  └───────────────────────────────┘                  │
│         ↓                                           │
│  ✅ Первые 30 символов → в буфер                    │
│  ❌ Остальные 70 → отклонены с кодом ошибки 3       │
└─────────────────────────────────────────────────────┘

Логика:

  • Каждый прочитанный символ увеличивает счётчик rcx
  • До записи в буфер проверяется: cmp rcx, 30; jg .error_buffer
  • Если лимит превышен — функция завершается до записи, возвращая код ошибки

Защищает от: Переполнения буфера, классической уязвимости buffer overflow.

Эшелон 2: Валидация формата данных

┌─────────────────────────────────────────────────────┐
│  БУФЕР СОДЕРЖИТ: "12a45"                            │
├─────────────────────────────────────────────────────┤
│  Символ за символом:                                │
│                                                     │
│  '1' → ASCII 0x31 → cmp с '0'-'9' → ✅ OK           │
│  '2' → ASCII 0x32 → cmp с '0'-'9' → ✅ OK           │
│  'a' → ASCII 0x61 → cmp с '0'-'9' → ❌ НЕ ЦИФРА     │
│         ↓                                           │
│  ┌─────────────────────────────────┐                │
│  │ ОСТАНОВКА ПАРСИНГА              │                │
│  │ Возврат: rdx = 1 (формат)       │                │
│  └─────────────────────────────────┘                │
│         ↓                                           │
│  Вызывающий код получает ошибку и выводит:          │
│  "ERROR: Invalid number format"                     │
└─────────────────────────────────────────────────────┘

Логика:

  • Каждый символ проверяется: cmp dl, '0'; jb .error и cmp dl, '9'; ja .error
  • Если символ вне диапазона '0'-'9' (и не знак/пробел в начале) — немедленная остановка
  • Проверяется наличие хотя бы одной цифры: test r14, r14; jz .error_format

Защищает от: Обработки мусорных данных, которые могут привести к непредсказуемому поведению.

Эшелон 3: Проверка переполнения при вычислениях

┌─────────────────────────────────────────────────────┐
│  ПАРСИНГ ЧИСЛА: "32768" (для int16_t)               │
├─────────────────────────────────────────────────────┤
│  Итерация 1: rax = 0                                │
│    rax = rax * 10 + 3 = 3      ✅ В диапазоне       │
│                                                     │
│  Итерация 2: rax = 3                                │
│    rax = rax * 10 + 2 = 32     ✅ В диапазоне       │
│                                                     │
│  ...                                                │
│                                                     │
│  Итерация 5: rax = 3276                             │
│    ┌──────────────────────────────┐                 │
│    │ ГРАНИЧНЫЙ СЛУЧАЙ             │                 │
│    │ rax == 3276 (32767/10)       │                 │
│    │ Следующая цифра: 8           │                 │
│    │ Максимум для int16: 7        │                 │
│    │ 8 > 7 → OVERFLOW             │                 │
│    └──────────────────────────────┘                 │
│         ↓                                           │
│  Возврат: rdx = 2 (overflow)                        │
│  Программа НЕ пытается сохранить некорректное число │
└─────────────────────────────────────────────────────┘

Логика:

  • Перед каждым imul rax, rax, 10 проверяется: не превысит ли результат диапазон
  • Для граничных случаев (3276, 6553) проверяется последняя цифра отдельно
  • Для операций используется флаг OF (overflow): jo .error_overflow

Защищает от: Тихого переполнения (silent overflow), когда некорректное число принимается как валидное.

Архитектура обработки ошибок

┌─────────────────────────────────────────────────────┐
│  СЛОЙ 1: Функция парсинга (parse_int16)             │
├─────────────────────────────────────────────────────┤
│  Входные данные: строка (rdi)                       │
│  Выходные данные: rax = число, rdx = код ошибки     │
│                                                     │
│  Коды возврата:                                     │
│    rdx = 0 → ✅ Успех                               │
│    rdx = 1 → ❌ Неверный формат                     │
│    rdx = 2 → ❌ Переполнение диапазона              │
│    rdx = 3 → ❌ Слишком длинная строка              │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│  СЛОЙ 2: Обработчик ошибок (read_signed_input_vars) │
├─────────────────────────────────────────────────────┤
│  Проверка: test rdx, rdx; jnz .handle_error         │
│                                                     │
│  .handle_error:                                     │
│    cmp rdx, 1 → "ERROR: Invalid number format"      │
│    cmp rdx, 2 → "ERROR: Number overflow..."         │
│    cmp rdx, 3 → "ERROR: Input too long..."          │
│                                                     │
│  Вывод в stderr + exit(1)                           │
└─────────────────────────────────────────────────────┘

Принципы:

  1. Разделение ответственности: Слой 1 детектирует ошибку, Слой 2 сообщает пользователю
  2. Нулевая толерантность: Любая ошибка приводит к немедленному завершению (fail-fast)
  3. Информативность: Коды ошибок позволяют точно определить причину отказа

Дополнительная защита: Размещение буферов

┌─────────────────────────────────────────────────────┐
│  СТЕК (Stack)                                       │
├─────────────────────────────────────────────────────┤
│  Return Address  ← Критичный для безопасности!      │
│  Локальные переменные функций                       │
│  ...                                                │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│  .bss СЕГМЕНТ (неинициализированные данные)         │
├─────────────────────────────────────────────────────┤
│  parse_temp_buffer: resb 64   ← БУФЕРЫ ЗДЕСЬ        │
│  io_interactive_buffer: resb 128                    │
│  ...                                                │
└─────────────────────────────────────────────────────┘

Почему это важно:

  • Буферы в .bss физически отделены от стека
  • Переполнение буфера не затронет return address функций
  • Даже при теоретической ошибке проверки — программа упадёт с segfault, а не выполнит произвольный код

Концепция “Defense in Depth”

Модули реализуют принцип эшелонированной обороны:

Уровень Что проверяется Действие при нарушении
Периметр Длина ввода (≤30 символов) Отклонение с кодом 3
Валидация Формат символов (‘0’-‘9’, знак) Отклонение с кодом 1
Диапазон Границы типа данных Отклонение с кодом 2
Физический Размещение в .bss, не на стеке Segfault вместо RCE

Результат: Даже если одна проверка по ошибке пропущена, следующие слои предотвратят эксплуатацию.

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

Пример со знаковыми числами (io_signed)

Задача: Вычислить результат в зависимости от соотношения $a$ и $b$:

\[ result = \begin{cases} \frac{a}{b} + 100, & a < b \\ 32, & a > b \\ \frac{a^2}{b}, & a = b \end{cases} \]

Демонстрация:

$ ./bin/signed
Enter a value for variable 'a': -100
Enter a value for variable 'b': -50
Result = 102

# a = -100, b = -50
# -100 < -50? ДА (signed сравнение)
# -100 / -50 = 2
# 2 + 100 = 102 ✓
📄 compute_signed.asm
; ============================================================================
; compute_signed.asm
;
; Демонстрационная программа вычисления условного выражения со знаковыми
; 16-битными целыми числами. Читает два int16_t, вычисляет результат
; в зависимости от соотношения a и b, и выводит результат.
;
; НАЗНАЧЕНИЕ:
;   - Демонстрация условной логики с signed 16-bit integers
;   - Обработка деления на ноль с диагностикой в stderr
;   - Использование знаковых инструкций (jl/jg, cdq, idiv)
;
; АЛГОРИТМ:
;   1. Чтение двух int16_t из stdin
;   2. Вычисление в зависимости от соотношения:
;      - a < b  → result = a / b + 100
;      - a == b → result = a² / b
;      - a > b  → result = 32
;   3. Вывод результата в десятичном формате
;
; ЗАВИСИМОСТИ:
;   io_signed.asm:
;     - read_signed_input   (чтение двух int16_t)
;     - print_signed_output (вывод int32_t в десятичном формате)
; ============================================================================

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

; Импорт функций из модуля io_signed.asm
extern read_signed_input                ; Чтение: заполняет глобальные a, b
extern print_signed_output              ; Вывод: читает глобальный output

section .bss
    ; Глобальные переменные (экспортируются для io_signed.asm)
    global a, b, output
    a resw 1                            ; Первое int16_t число
    b resw 1                            ; Второе int16_t число
    output resd 1                       ; Результат int32_t (32 бита достаточно для всех случаев)

section .data
    ; Сообщение об ошибке деления на ноль
    error_zero db "Arithmetic error: Can't divide by zero!", 10
    error_zero_len equ $ - error_zero

section .text
    global _start

; ----------------------------------------------------------------------------
; _start
;
; Точка входа. Выполняет чтение, условное вычисление и вывод результата.
;
; ЭТАПЫ:
;   1. Чтение двух int16_t через read_signed_input
;   2. Сравнение a и b (знаковое: jl/jg)
;   3. Вычисление по соответствующей ветке:
;      - a < b:  result = a / b + 100 (знаковое деление cdq + idiv)
;      - a == b: result = a² / b
;      - a > b:  result = 32 (константа)
;   4. Вывод через print_signed_output
;   5. Завершение с exit(0)
;
; ОБРАБОТКА ОШИБОК:
;   - Деление на ноль → stderr + exit(1)
; ----------------------------------------------------------------------------
_start:
    ; Чтение входных данных
    call read_signed_input              ; Заполняет глобальные переменные a, b

    ; Загрузка a в ax для сравнения
    xor eax, eax                        ; Обнуление регистра (очистка старших битов)
    mov ax, [a]

    ; Сравнение a и b (знаковое)
    cmp ax, [b]
    jl .a_less_b                        ; Переход, если a < b (signed less)
    jg .a_greater_b                     ; Переход, если a > b (signed greater)

    ; === Случай: a == b ===
    ; Результат: a² / b
    movsx eax, word [a]
    imul eax, eax                       ; EAX = a * a (знаковое умножение 32-бит)
    movsx ecx, word [b]

    ; Проверка деления на ноль
    test ecx, ecx                       ; Проверка: b == 0?
    jz .division_by_zero                ; Если да - ошибка

    cdq                                 ; Расширение EAX до EDX:EAX (подготовка к делению)
    idiv ecx                            ; EAX = EAX / ECX (знаковое деление)
    mov [output], eax
    jmp .end_if

.a_less_b:
    ; === Случай: a < b ===
    ; Результат: a / b + 100
    movsx eax, word [a]
    movsx ecx, word [b]

    ; Проверка деления на ноль
    test ecx, ecx                       ; Проверка: b == 0?
    jz .division_by_zero                ; Если да - ошибка

    cdq                                 ; Расширение EAX до EDX:EAX
    idiv ecx                            ; EAX = EAX / ECX (знаковое деление)
    add eax, 100                        ; EAX = EAX + 100
    mov [output], eax
    jmp .end_if

.a_greater_b:
    ; === Случай: a > b ===
    ; Результат: 32 (константа)
    mov dword [output], 32
    jmp .end_if

.division_by_zero:
    ; === Обработка ошибки деления на ноль ===
    ; Вывод сообщения об ошибке в stderr
    ; syscall: write(stderr, string, length)
    mov rax, 1
    mov rdi, 2
    lea rsi, [error_zero]
    mov rdx, error_zero_len
    syscall

    ; Завершение программы с кодом ошибки
    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

.end_if:
    ; Вывод результата
    call print_signed_output            ; Выводит глобальную переменную output

    ; Корректное завершение программы
    ; syscall: exit(0)
    mov rax, 60
    xor rdi, rdi
    syscall

Пример с беззнаковыми числами (io_unsigned)

Задача: Та же формула, но для беззнаковых:

\[ result = \begin{cases} \frac{a}{b} + 100, & a < b \\ 32, & a > b \\ \frac{a^2}{b}, & a = b \end{cases} \]

Демонстрация:

$ ./bin/unsigned
Enter a value for variable 'a': 60000
Enter a value for variable 'b': 100
Result = 32

# 60000 > 100? ДА → результат 32 ✓
📄 compute_unsigned.asm
; ============================================================================
; compute_unsigned.asm
;
; Демонстрационная программа вычисления условного выражения с беззнаковыми
; 16-битными целыми числами. Читает два uint16_t, вычисляет результат
; в зависимости от соотношения a и b, и выводит результат.
;
; НАЗНАЧЕНИЕ:
;   - Демонстрация условной логики с unsigned 16-bit integers
;   - Обработка деления на ноль с диагностикой в stderr
;   - Использование беззнаковых инструкций (jb/ja, movzx, div)
;
; АЛГОРИТМ:
;   1. Чтение двух uint16_t из stdin
;   2. Вычисление в зависимости от соотношения:
;      - a < b  → result = a / b + 100
;      - a == b → result = a² / b
;      - a > b  → result = 32
;   3. Вывод результата в десятичном формате
; ЗАВИСИМОСТИ:
;   io_unsigned.asm:
;     - read_unsigned_input   (чтение двух uint16_t)
;     - print_unsigned_output (вывод int32_t в десятичном формате)
;
; КРИТИЧНЫЕ ОТЛИЧИЯ ОТ SIGNED:
;   - movzx вместо movsx (нулевое расширение)
;   - jb/ja вместо jl/jg (беззнаковое сравнение)
;   - xor edx, edx перед div (обнуление старших битов, не cdq!)
;   - mul вместо imul (беззнаковое умножение)
; ============================================================================

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

; Импорт функций из модуля io_unsigned.asm
extern read_unsigned_input              ; Чтение: заполняет глобальные a, b
extern print_unsigned_output            ; Вывод: читает глобальный output

section .bss
    ; Глобальные переменные (экспортируются для io_unsigned.asm)
    global a, b, output
    a resw 1                            ; Первое uint16_t число
    b resw 1                            ; Второе uint16_t число
    output resd 1                       ; Результат int32_t (32 бита достаточно)

section .data
    ; Сообщение об ошибке деления на ноль
    error_zero db "Arithmetic error: Can't divide by zero!", 10
    error_zero_len equ $ - error_zero

section .text
    global _start

; ----------------------------------------------------------------------------
; _start
;
; Точка входа. Выполняет чтение, условное вычисление и вывод результата.
;
; ЭТАПЫ:
;   1. Чтение двух uint16_t через read_unsigned_input
;   2. Сравнение a и b (беззнаковое: jb/ja)
;   3. Вычисление по соответствующей ветке:
;      - a < b:  result = a / b + 100 (беззнаковое деление xor edx, edx + div)
;      - a == b: result = a² / b
;      - a > b:  result = 32 (константа)
;   4. Вывод через print_unsigned_output
;   5. Завершение с exit(0)
;
; ОБРАБОТКА ОШИБОК:
;   - Деление на ноль → stderr + exit(1)
; ----------------------------------------------------------------------------
_start:
    ; Чтение входных данных
    call read_unsigned_input            ; Заполняет глобальные переменные a, b

    ; Загрузка a в ax для сравнения
    mov ax, [a]

    ; Сравнение a и b (беззнаковое!)
    cmp ax, [b]
    jb .a_less_b                        ; Переход, если a < b (unsigned below)
    ja .a_greater_b                     ; Переход, если a > b (unsigned above)

    ; === Случай: a == b ===
    ; Результат: a² / b
    movzx eax, word [a]
    imul eax, eax                       ; EAX = EAX * EAX (работает для unsigned!)
                                        ; Для uint16²: max = 65535² = 4,294,836,225
                                        ; Помещается в 32 бита, результат корректен

    movzx ecx, word [b]

    ; Проверка деления на ноль
    test ecx, ecx                       ; Проверка: b == 0?
    jz .division_by_zero                ; Если да - ошибка

    ; Беззнаковое деление (КРИТИЧНО: не cdq, а xor!)
    xor edx, edx                        ; Обнуление EDX (для беззнакового деления!)
    div ecx                             ; EAX = EDX:EAX / ECX (беззнаковое деление)
    mov [output], eax
    jmp .end_if

.a_less_b:
    ; === Случай: a < b ===
    ; Результат: a / b + 100
    movzx eax, word [a]
    movzx ecx, word [b]

    ; Проверка деления на ноль
    test ecx, ecx                       ; Проверка: b == 0?
    jz .division_by_zero                ; Если да - ошибка

    xor edx, edx                        ; Обнуление EDX перед делением (ОБЯЗАТЕЛЬНО!)
    div ecx                             ; EAX = EAX / ECX (беззнаковое деление)
    add eax, 100                        ; EAX = EAX + 100
    mov [output], eax
    jmp .end_if

.a_greater_b:
    ; === Случай: a > b ===
    ; Результат: 32 (константа)
    mov dword [output], 32
    jmp .end_if

.division_by_zero:
    ; === Обработка ошибки деления на ноль ===
    ; Вывод сообщения об ошибке в stderr
    ; syscall: write(stderr, string, length)
    mov rax, 1
    mov rdi, 2
    lea rsi, [error_zero]
    mov rdx, error_zero_len
    syscall

    ; Завершение программы с кодом ошибки
    ; syscall: exit(1)
    mov rax, 60
    mov rdi, 1
    syscall

.end_if:
    ; Вывод результата
    call print_unsigned_output          ; Выводит глобальную переменную output

    ; Корректное завершение программы
    ; syscall: exit(0)
    mov rax, 60
    xor rdi, rdi
    syscall

Пример с float (io_float)

Задача: Вычислить сложное выражение:

\[ result = \frac{b \times 7 + \frac{64}{a}}{31 - \frac{c \times b}{2}}, \]

где $a$, $b$ — вещественные числа (float), $c$ — целое число (int32)

Демонстрация:

$ ./bin/float
Введите a (float): 8.0
Введите b (float): 2.5
Введите c (int): 10

Результаты вычисления:
Числитель (n) = 25.500000 (hex: 0x41CC0000)
Знаменатель (d) = 18.500000 (hex: 0x41940000)
Результат (res) = 1.378378 (hex: 0x3FB05B06)
📄 compute_float.asm
; ============================================================================
; compute_float.asm
;
; Демонстрационная программа вычислений с вещественными числами (float) и
; целыми числами (int32_t) с использованием FPU x87.
;
; НАЗНАЧЕНИЕ:
;   Вычисляет значение выражения:
;   RESULT = (b * 7 + 64 / a) / (31 - c * b / 2)
;
; АЛГОРИТМ:
;   1. Ввод значений a (float), b (float), c (int32).
;   2. Проверка a != 0 (деление на ноль).
;   3. Вычисление числителя: n = b*7 + 64/a.
;   4. Вычисление знаменателя: d = 31 - c*b/2.
;   5. Проверка d != 0 (деление на ноль с учетом epsilon).
;   6. Финальное деление: res = n / d.
;   7. Вывод результатов в десятичном и hex формате.
;
; ОБРАБОТКА ОШИБОК:
;   • Деление на ноль при a = 0: вывод сообщения, завершение программы
;   • Деление на ноль при d ≈ 0: вывод сообщения, завершение программы
;   • Epsilon для сравнения с нулём: 1.0e-6
;
; СТРУКТУРА ВЫЧИСЛЕНИЙ:
;   Числитель:      n = b*7 + 64/a
;   Знаменатель:    d = 31 - c*b/2
;   Результат:      res = n / d
;
; ЗАВИСИМОСТИ:
;   io_float.asm:
;     - read_float         : Чтение вещественного числа из stdin
;     - read_int           : Чтение целого числа из stdin
;     - print_string       : Вывод null-терминированной строки
;     - print_float_from_eax : Вывод float в формате [-]целая.дробная (hex: ...)
;
; ПРОИЗВОДИТЕЛЬНОСТЬ:
;   • Сложность: O(1), фиксированное количество операций
;   • Память: O(1), используются только статические переменные
;   • FPU операции: ~15 инструкций для основного вычисления
; ============================================================================

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

; --- Импорт внешних функций из io_float.asm ---
extern read_float                       ; Чтение float из stdin → EAX
extern read_int                         ; Чтение int32 из stdin → EAX
extern print_string                     ; Вывод null-терминированной строки
extern print_float_from_eax             ; Вывод float из EAX

section .bss
    ; --- Переменные для вычислений ---
    var_a resd 1                        ; Входная переменная a (float)
    var_b resd 1                        ; Входная переменная b (float)
    var_c resd 1                        ; Входная переменная c (int32)
    result_numerator resd 1             ; Промежуточный результат: числитель n
    result_denominator resd 1           ; Промежуточный результат: знаменатель d
    result_final resd 1                 ; Финальный результат: res = n / d

section .data
    ; --- Приглашения для ввода ---
    prompt_a db 'Введите a (float): ', 0
    prompt_b db 'Введите b (float): ', 0
    prompt_c db 'Введите c (int): ', 0
    
    ; --- Сообщения для вывода результатов ---
    result_header db 10, 'Результаты вычисления:', 10, 0
    msg_numerator db 'Числитель (n) = ', 0
    msg_denominator db 'Знаменатель (d) = ', 0
    msg_result db 'Результат (res) = ', 0
    
    ; --- Сообщения об ошибках ---
    error_div_a db 10, 'ОШИБКА: Деление на ноль (a=0)!', 10, 0
    error_div_d db 10, 'ОШИБКА: Знаменатель равен нулю!', 10, 0
    
    ; --- Константы для вычислений (float) ---
    const_two dd 2.0                    ; Константа 2.0
    const_seven dd 7.0                  ; Константа 7.0
    const_thirtyone dd 31.0             ; Константа 31.0
    const_sixtyfour dd 64.0             ; Константа 64.0
    const_epsilon dd 1.0e-6             ; Epsilon для сравнения с нулём (10⁻⁶)

section .text
    global _start

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

; ----------------------------------------------------------------------------
; _start
;
; Точка входа программы. Выполняет последовательный ввод данных,
; проверку корректности, вычисления и вывод результатов.
;
; ПОСЛЕДОВАТЕЛЬНОСТЬ ВЫПОЛНЕНИЯ:
;   1. Ввод значений a, b, c с интерактивными приглашениями
;   2. Валидация: проверка a != 0
;   3. Вычисление числителя: n = b*7 + 64/a
;   4. Вычисление знаменателя: d = 31 - c*b/2
;   5. Валидация: проверка |d| > epsilon
;   6. Финальное деление: res = n / d
;   7. Форматированный вывод всех результатов
;   8. Завершение программы с кодом 0 (успех) или после ошибки
;
; ОБРАБОТКА ОШИБОК:
;   • a = 0: вывод error_div_a, завершение программы
;   • |d| < epsilon: вывод error_div_d, завершение программы
;   • Ошибки парсинга ввода обрабатываются в io_float.asm (exit code 1)
;
; @param  none (интерактивный ввод из stdin)
; @return exit code 0 при успехе, никогда не возвращается
; @uses   rax, rdi, FPU stack, все регистры через вызовы функций
; @calls  print_string, read_float, read_int, calculate_numerator,
;         calculate_denominator, print_float_from_eax
;
; @complexity O(1), фиксированная последовательность операций
; ----------------------------------------------------------------------------
_start:
    ; ========================================================================
    ; ФАЗА 1: ВВОД ДАННЫХ
    ; ========================================================================
    
    ; --- Ввод переменной a (float) ---
    lea rdi, [prompt_a]         ; Загрузка адреса приглашения "Введите a (float): "
    call print_string           ; Вывод приглашения
    call read_float             ; Чтение float из stdin → EAX (битовое представление)
    mov [var_a], eax            ; Сохранение a в памяти

    ; --- Ввод переменной b (float) ---
    lea rdi, [prompt_b]         ; Загрузка адреса приглашения "Введите b (float): "
    call print_string           ; Вывод приглашения
    call read_float             ; Чтение float из stdin → EAX
    mov [var_b], eax            ; Сохранение b в памяти

    ; --- Ввод переменной c (int32) ---
    lea rdi, [prompt_c]         ; Загрузка адреса приглашения "Введите c (int): "
    call print_string           ; Вывод приглашения
    call read_int               ; Чтение int32 из stdin → EAX
    mov [var_c], eax            ; Сохранение c в памяти

    ; ========================================================================
    ; ФАЗА 2: ВАЛИДАЦИЯ ВХОДНЫХ ДАННЫХ
    ; ========================================================================
    
    ; --- Проверка: a != 0 (предотвращение деления на ноль в числителе) ---
    fld dword [var_a]           ; Загрузка a в FPU: ST(0) = a
    ftst                        ; Сравнение ST(0) с 0.0 (установка флагов FPU)
    fstsw ax                    ; Копирование FPU status word в AX
    sahf                        ; Загрузка AH в процессорные флаги (ZF, CF, PF)
    fstp st0                    ; Удаление a из стека FPU (очистка)
    je .error_a_zero            ; Если ZF=1 (a == 0) - переход к обработке ошибки

    ; ========================================================================
    ; ФАЗА 3: ВЫЧИСЛЕНИЯ
    ; ========================================================================
    
    ; --- Вычисление числителя: n = b*7 + 64/a ---
    call calculate_numerator        ; Вычисление числителя → EAX
    mov [result_numerator], eax     ; Сохранение результата n

    ; --- Вычисление знаменателя: d = 31 - c*b/2 ---
    call calculate_denominator      ; Вычисление знаменателя → EAX
    mov [result_denominator], eax   ; Сохранение результата d

    ; --- Проверка: |d| > epsilon (предотвращение деления на ноль) ---
    ; Алгоритм: |d| < epsilon ⟺ деление на ноль
    fld dword [result_denominator]  ; ST(0) = d
    fabs                            ; ST(0) = |d| (взятие абсолютного значения)
    fld dword [const_epsilon]       ; ST(0) = epsilon (1.0e-6), ST(1) = |d|
    fcomip st0, st1                 ; Сравнение: epsilon с |d|, установка флагов, pop
                                    ; CF устанавливается, если ST(0) < ST(1), т.е. epsilon < |d|
    fstp st0                        ; Удаление |d| из стека FPU
    jae .error_denom_zero           ; Если CF=0 (epsilon >= |d|) - ошибка деления на ноль

    ; --- Финальное деление: res = n / d ---
    fld dword [result_numerator]    ; ST(0) = n
    fld dword [result_denominator]  ; ST(0) = d, ST(1) = n
    fdivp st1, st0                  ; ST(0) = n / d, pop d
    fstp dword [result_final]       ; Сохранение результата res в памяти

    ; ========================================================================
    ; ФАЗА 4: ВЫВОД РЕЗУЛЬТАТОВ
    ; ========================================================================
    
    ; --- Вывод заголовка результатов ---
    lea rdi, [result_header]        ; Загрузка адреса "\nРезультаты вычисления:\n"
    call print_string               ; Вывод заголовка

    ; --- Вывод числителя ---
    lea rdi, [msg_numerator]        ; Загрузка адреса "Числитель (n) = "
    call print_string               ; Вывод метки
    mov eax, [result_numerator]     ; Загрузка битового представления n
    call print_float_from_eax       ; Вывод n в формате [-]целая.дробная (hex: ...)

    ; --- Вывод знаменателя ---
    lea rdi, [msg_denominator]      ; Загрузка адреса "Знаменатель (d) = "
    call print_string               ; Вывод метки
    mov eax, [result_denominator]   ; Загрузка битового представления d
    call print_float_from_eax       ; Вывод d в формате [-]целая.дробная (hex: ...)

    ; --- Вывод финального результата ---
    lea rdi, [msg_result]           ; Загрузка адреса "Результат (res) = "
    call print_string               ; Вывод метки
    mov eax, [result_final]         ; Загрузка битового представления res
    call print_float_from_eax       ; Вывод res в формате [-]целая.дробная (hex: ...)

    jmp .exit                       ; Переход к успешному завершению программы

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

; --- Обработка ошибки: деление на ноль при a = 0 ---
.error_a_zero:
    lea rdi, [error_div_a]      ; Загрузка адреса сообщения об ошибке
    call print_string           ; Вывод: "ОШИБКА: Деление на ноль (a=0)!"
    jmp .exit                   ; Переход к завершению программы

; --- Обработка ошибки: знаменатель близок к нулю ---
.error_denom_zero:
    lea rdi, [error_div_d]      ; Загрузка адреса сообщения об ошибке
    call print_string           ; Вывод: "ОШИБКА: Знаменатель равен нулю!"
    jmp .exit                   ; Переход к завершению программы

; --- Завершение программы ---
.exit:
    mov rax, 60
    xor rdi, rdi
    syscall

; ============================================================================
; ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ВЫЧИСЛЕНИЙ
; ============================================================================

; ----------------------------------------------------------------------------
; calculate_numerator
;
; Вычисляет числитель выражения по формуле: n = b * 7 + 64 / a
;
; АЛГОРИТМ:
;   1. Загрузка b и 7 в FPU stack
;   2. Умножение: temp1 = b * 7
;   3. Загрузка 64 и a в FPU stack
;   4. Деление: temp2 = 64 / a
;   5. Сложение: n = temp1 + temp2
;   6. Сохранение результата в памяти
;   7. Возврат битового представления через EAX
;
; СОСТОЯНИЕ FPU:
;   До вызова:  (пустой стек)
;   После вызова: (пустой стек, результат в EAX)
;
; ПРИМЕРЫ:
;   a=2.0, b=3.0 → n = 3*7 + 64/2 = 21 + 32 = 53.0
;   a=4.0, b=1.5 → n = 1.5*7 + 64/4 = 10.5 + 16 = 26.5
;
; @param  none (использует глобальные переменные var_a, var_b)
; @return eax  Битовое представление результата n (IEEE 754 float)
; @uses   rbp, rsp, FPU stack
;
; @complexity O(1), фиксированное количество FPU операций
; @memory     O(1), использует только локальное пространство в стеке
;
; @note Предполагается, что a != 0 (проверяется в _start)
; ----------------------------------------------------------------------------
calculate_numerator:
    push rbp
    mov rbp, rsp
    sub rsp, 16                 ; Выделение локального пространства для промежуточных данных
    
    ; --- Вычисление: temp1 = b * 7 ---
    fld dword [var_b]           ; ST(0) = b
    fld dword [const_seven]     ; ST(0) = 7.0, ST(1) = b
    fmulp st1, st0              ; ST(0) = b * 7 (умножение и pop 7.0)
    
    ; --- Вычисление: temp2 = 64 / a ---
    fld dword [const_sixtyfour] ; ST(0) = 64.0, ST(1) = b*7
    fld dword [var_a]           ; ST(0) = a, ST(1) = 64.0, ST(2) = b*7
    fdivp st1, st0              ; ST(0) = 64/a, ST(1) = b*7 (деление и pop a)
    
    ; --- Вычисление: n = temp1 + temp2 ---
    faddp st1, st0              ; ST(0) = b*7 + 64/a (сложение и pop temp2)
    
    ; --- Сохранение результата ---
    fstp dword [rbp-4]          ; Сохранение ST(0) в локальную переменную, pop
    mov eax, [rbp-4]            ; Загрузка битового представления в EAX
    
    mov rsp, rbp                ; Восстановление указателя стека
    pop rbp
    ret

; ----------------------------------------------------------------------------
; calculate_denominator
;
; Вычисляет знаменатель выражения по формуле: d = 31 - c * b / 2
;
; АЛГОРИТМ:
;   1. Преобразование c (int32) в float через fild
;   2. Загрузка b в FPU stack
;   3. Умножение: temp1 = c * b
;   4. Деление: temp2 = temp1 / 2
;   5. Загрузка 31 в FPU stack
;   6. Вычитание: d = 31 - temp2
;   7. Сохранение результата в памяти
;   8. Возврат битового представления через EAX
;
; СОСТОЯНИЕ FPU:
;   До вызова:  (пустой стек)
;   После вызова: (пустой стек, результат в EAX)
;
; ПРИМЕРЫ:
;   b=3.0, c=5 → d = 31 - 5*3/2 = 31 - 15/2 = 31 - 7.5 = 23.5
;   b=2.0, c=10 → d = 31 - 10*2/2 = 31 - 10 = 21.0
;
; @param  none (использует глобальные переменные var_b, var_c)
; @return eax  Битовое представление результата d (IEEE 754 float)
; @uses   rbp, rsp, FPU stack
;
; @complexity O(1), фиксированное количество FPU операций
; @memory     O(1), использует только локальное пространство в стеке
;
; @note Результат может быть близок к нулю, что проверяется в _start
; ----------------------------------------------------------------------------
calculate_denominator:
    push rbp
    mov rbp, rsp
    sub rsp, 16                 ; Выделение локального пространства для промежуточных данных
    
    ; --- Вычисление: temp1 = c * b ---
    fild dword [var_c]          ; ST(0) = c (преобразование int32 → float)
    fld dword [var_b]           ; ST(0) = b, ST(1) = c
    fmulp st1, st0              ; ST(0) = c * b (умножение и pop b)
    
    ; --- Вычисление: temp2 = (c*b) / 2 ---
    fld dword [const_two]       ; ST(0) = 2.0, ST(1) = c*b
    fdivp st1, st0              ; ST(0) = (c*b)/2 (деление и pop 2.0)
    
    ; --- Вычисление: d = 31 - temp2 ---
    fld dword [const_thirtyone] ; ST(0) = 31.0, ST(1) = (c*b)/2
    fxch st1                    ; ST(0) = (c*b)/2, ST(1) = 31.0 (обмен для корректного вычитания)
    fsubp st1, st0              ; ST(0) = 31 - (c*b)/2 (вычитание и pop temp2)
                                ; fsubp: ST(1) = ST(1) - ST(0), затем pop ST(0)
    
    ; --- Сохранение результата ---
    fstp dword [rbp-4]          ; Сохранение ST(0) в локальную переменную, pop
    mov eax, [rbp-4]            ; Загрузка битового представления в EAX
    
    mov rsp, rbp                ; Восстановление указателя стека
    pop rbp
    ret

🔧 5. Расширение модулей под другие типы данных

Справочная таблица: Типы данных и их характеристики

Полная таблица констант для всех стандартных типов:

Тип MIN MAX MAX/10 MAX%10 Размер Длина буфера Директива .bss
int8 -128 127 12 7 1 байт 5 resb 1
uint8 0 255 25 5 1 байт 4 resb 1
int16 -32768 32767 3276 7 2 байта 7 resw 1
uint16 0 65535 6553 5 2 байта 6 resw 1
int32 -2147483648 2147483647 214748364 7 4 байта 12 resd 1
uint32 0 4294967295 429496729 5 4 байта 11 resd 1
int64 -9223372036854775808 9223372036854775807 ¹ 7 8 байт 21 resq 1
uint64 0 18446744073709551615 ¹ 5 8 байт 21 resq 1
float ~-3.4×10³⁸ ~3.4×10³⁸ 4 байта 32 resd 1
double ~-1.7×10³⁰⁸ ~1.7×10³⁰⁸ 8 байт 64 resq 1

¹ Для 64-битных типов используйте переменные в .data:

section .data
    MAX_INT64_DIV10 dq 922337203685477580
    MAX_UINT64_DIV10 dq 1844674407370955161

Как читать таблицу:

  • MIN/MAX — границы допустимых значений
  • MAX/10 — граница проверки переполнения при умножении на 10
  • MAX%10 — максимальная последняя цифра при граничном значении
  • Длина буфера — размер строкового представления с учётом знака и \0

Матрица инструкций по типам

Целочисленные типы (знаковые и беззнаковые):

Размер Регистр Загрузка (signed) Загрузка (unsigned) Сохранение Умножение Деление Сравнение
8 бит AL movsx ax, byte [x] movzx ax, byte [x] mov byte [x], al imul / mul idiv / div jl/jg / jb/ja
16 бит AX movsx eax, word [x] movzx eax, word [x] mov word [x], ax imul / mul idiv / div jl/jg / jb/ja
32 бит EAX movsxd rax, dword [x] mov eax, dword [x] mov dword [x], eax imul / mul idiv / div jl/jg / jb/ja
64 бит RAX mov rax, qword [x] mov rax, qword [x] mov qword [x], rax imul / mul idiv / div jl/jg / jb/ja

⚠️ Критическое различие: Для знаковых используйте imul/idiv/jl/jg, для беззнаковых — mul/div/jb/ja.

Специфика 64-битных типов (int64/uint64):

Операция int64 (знаковый) uint64 (беззнаковый) Пояснение
Константы MAX/MIN section .data
MAX_INT64 dq 9223372036854775807
section .data
MAX_UINT64 dq 18446744073709551615
Immediate-операнды ограничены 32 битами
Умножение 64×64 imul qword [b] → RDX:RAX mul qword [b] → RDX:RAX Результат всегда 128 бит
Проверка overflow (умножение) test rdx, rdx; jnz overflow test rdx, rdx; jnz overflow Если RDX≠0 — переполнение
Проверка overflow (сложение) add rax, rbx; jo overflow add rax, rbx; jc overflow OF для signed, CF для unsigned
Сравнение с константой mov rbx, [MAX_INT64]
cmp rax, rbx; jg overflow
mov rbx, [MAX_UINT64]
cmp rax, rbx; ja overflow
jg для signed, ja для unsigned
Функция вывода 128-бит print_int128_output(rdi, rsi) print_uint128_output(rdi, rsi) Разные (обработка знака)

⚠️ Ограничение x86-64: Immediate-операнды в cmp, add, mov ограничены 32 битами (signed: -2³¹..2³¹-1). Для int64/uint64 обязательно использовать константы в .data через dq.

Вещественные типы:

Тип Размер Директива Загрузка Сохранение Стек FPU
float 32 бита resd 1 fld dword [x] fstp dword [x] ST(0)
double 64 бита resq 1 fld qword [x] fstp qword [x] ST(0)

📘 Примечание: Подробная шпаргалка по всем регистрам, флагам, директивам памяти и System V ABI — в статье «Шпаргалка NASM x86-64: Регистры, Инструкции, Syscalls (Linux)».


Требования к функциям вывода

Тип Результат умножения Специальная функция? Пример вызова
int8, uint8, int16, uint16 Помещается в 16/32 бита ❌ Нет print_signed_output_var(edi)
int32, uint32 Помещается в 32/64 бита ❌ Нет print_unsigned_output_var(edi)
int64 128 бит (RDX:RAX) Да print_int128_output(rdi, rsi)
uint64 128 бит (RDX:RAX) Да print_uint128_output(rdi, rsi)
float, double Вещественный ⚠️ Зависит от формата print_float_output_var(xmm0)

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

Математическое обоснование:

  • uint16 × uint16 max = 65535² = 4,294,836,225 → помещается в uint32 ✅
  • uint32 × uint32 max = (2³²-1)² = 18,446,744,065,119,617,025 → помещается в uint64 ✅
  • uint64 × uint64 max = (2⁶⁴-1)² = 2¹²⁸ - 2⁶⁵ + 1 ≈ 3.4×10³⁸требует uint128 ⚠️
  • int64 × int64 max = (2⁶³-1)² ≈ 8.5×10³⁷требует int128 ⚠️

📘 Примечание: Работа с вещественными числами (float/double) подробно разобрана в «FPU в NASM: Вещественные числа, Синусы и Точность».


Пошаговый чеклист адаптации под новые типы данных

Модули спроектированы так, чтобы адаптация под другой тип данных требовала точечных изменений в 4-5 местах. Ниже — универсальная инструкция на примере адаптации io_unsigned.asm с uint16 → uint64 (входные данные: 0..65535 → 0..18446744073709551615).

Ключевая идея: Для uint16/int16 все проверки границ выполняются через immediate-значения прямо в инструкциях (cmp rax, 65535). Для uint64/int64 значения не помещаются в immediate-операнды (лимит 32 бита), поэтому их обязательно нужно выносить в section .data через директиву dq.

Критическое отличие: Для int64/uint64 также требуется проверка переполнения при арифметических операциях через флаги процессора (CF/OF), так как умножение и сложение могут незаметно переполниться.

Шаг 1: Добавление констант диапазона в `section .data` (только для 64-бит)

1.1 Текущий код uint16 (без констант)

В io_unsigned.asm нет констант MAX/MAX_DIV10 в section .data. Проверки выполняются напрямую:

; io_unsigned.asm (текущий код uint16)
section .data
    ; Сообщения об ошибках парсинга
    error_format_msg db "ERROR: Invalid number format", 10
    error_format_len equ $ - error_format_msg
    error_overflow_msg db "ERROR: Number overflow (must be 0 to 65535)", 10
    error_overflow_len equ $ - error_overflow_msg

section .text
parse_uint16:
    ; ...
    ; Предварительная проверка переполнения перед умножением
    ; Для uint16_t: max/10 = 65535/10 = 6553 (остаток 5)
    cmp rax, 6553               ; Проверка граничного случая
    jg .check_last_digit        ; Если превышено - особая обработка последней цифры

    ; Вычисление: result = result * 10 + digit
    imul rax, rax, 10           ; rax = rax * 10 (сдвиг разряда)
    sub dl, '0'                 ; Преобразование ASCII ('0'-'9') в число (0-9)
    movzx rdx, dl               ; Нулевое расширение до 64 бит
    add rax, rdx                ; Добавление текущей цифры к результату

    ; Проверка границы uint16_t (максимум 65535)
    cmp rax, 65535              ; Проверка: результат превысил максимум?
    jg .error_overflow          ; Если да - ошибка переполнения

1.2 Адаптация для uint64 (с константами)

Для uint64 значения не помещаются в immediate-операнды:

❌ Невозможно для uint64
; Это НЕ СКОМПИЛИРУЕТСЯ!
cmp rax, 18446744073709551615  ; Ошибка: значение > 2³²-1
✅ Правильный подход
; io_uint64.asm (адаптированный код)
section .data
    ; Константы диапазона для uint64
    MAX_UINT64 dq 18446744073709551615
    MAX_UINT64_DIV10 dq 1844674407370955161

section .text
parse_uint64:
    ; ...
    ; Проверка на граничный случай
    mov rbx, qword [MAX_UINT64_DIV10]
    cmp rax, rbx
    ja .check_last_digit        ; Беззнаковое сравнение!
    je .check_last_digit        ; ВАЖНО: также обрабатываем равенство!

⚠️ Критично:

  • Используйте директиву dq для 64-битных констант, не equ!
  • Все сравнения должны быть беззнаковыми (ja вместо jg) для uint64!
  • Для int64 используйте знаковые (jg вместо ja)!

1.3 Обновление сообщений об ошибках

uint16
section .data
    error_overflow_msg db "ERROR: Number overflow (must be 0 to 65535)", 10
    error_overflow_len equ $ - error_overflow_msg
uint64
section .data
    error_overflow_msg db "ERROR: Number overflow (must be 0 to 18446744073709551615)", 10
    error_overflow_len equ $ - error_overflow_msg
Шаг 2: Расширение буферов и переменных в `section .bss`

2.1 Текущий код uint16

; io_unsigned.asm
section .bss
    ; Буферы для работы всех слоёв
    parse_temp_buffer resb 64           ; Общий буфер для парсинга строк

    ; Буферы интерактивного режима (Слой 2)
    io_interactive_buffer resb 128      ; Буфер чтения из stdin
    io_interactive_pos resq 1           ; Текущая позиция чтения в буфере
    io_interactive_size resq 1          ; Количество непрочитанных байт в буфере

Расчёт: uint16 max = 65535 (5 цифр) + 1 null = 6 байт минимум. Буфер 64 байта — с большим запасом.

2.2 Адаптация для uint64

; io_uint64.asm
section .bss
    ; Буферы для работы всех слоёв
    parse_temp_buffer resb 64           ; Общий буфер для парсинга строк
                                        ; uint64: максимум 20 цифр + null = 21 байт
                                        ; 64 байта всё ещё достаточно (запас остаётся)

    ; Буферы интерактивного режима (Слой 2)
    io_interactive_buffer resb 128      ; Буфер чтения из stdin
    io_interactive_pos resq 1           ; Текущая позиция чтения в буфере
    io_interactive_size resq 1          ; Количество непрочитанных байт в буфере

⚠️ Важно: Буфер parse_temp_buffer resb 64 достаточен для uint64/int64 (20 цифр + знак + запас). Менять размер не требуется.

2.3 КРИТИЧНО: Переменные программы для 64-битных типов

uint16 (программа)
; compute_unsigned.asm (текущий код)
section .bss
    global a, b, output
    a resw 1         ; 16-битная переменная
    b resw 1         ; 16-битная переменная
    output resd 1    ; 32-битный результат ✅ ДОСТАТОЧНО

Почему работает:
uint16 × uint16 максимум = 65535 × 65535 = 4,294,836,225 → помещается в uint32 (32 бита)

uint64 (адаптация)
; compute_unsigned64.asm (адаптированная программа)
section .bss
    global a, b, output_low, output_high
    a resq 1              ; 64-битная переменная
    b resq 1              ; 64-битная переменная
    output_low resq 1     ; Младшие 64 бита результата (биты 0-63)
    output_high resq 1    ; Старшие 64 бита результата (биты 64-127)

Почему обязательно:
uint64 × uint64 максимум = (2⁶⁴-1)² = 2¹²⁸ - 2⁶⁵ + 1 ≈ 3.4×10³⁸НЕ помещается в uint64!

Инструкция mul автоматически возвращает 128-битный результат в паре регистров RDX:RAX:

; Умножение a × b → RDX:RAX (128 бит)
mov rax, [a]          ; RAX = первый операнд
mul qword [b]         ; RDX:RAX = RAX × [b] (128 бит автоматически!)
                      ; RDX = старшие 64 бита
                      ; RAX = младшие 64 бита

; Сохранение результата
mov [output_low], rax          ; Младшие 64 бита
mov [output_high], rdx         ; Старшие 64 бита

⚠️ Без этого: Использование output resq 1 приведёт к потере старших 64 бит результата.

⚠️ Для int64: Используйте imul qword [b] вместо mul. Результат также 128 бит в RDX:RAX.

Шаг 3: Замена инструкций в функции `parse_*` (слой 1)

3.1 Текущий код uint16 — основной цикл парсинга

; parse_uint16 (фрагмент из io_unsigned.asm)
; --- Главный цикл парсинга цифр ---
; Инвариант цикла: rax содержит частично собранное число
.parse_digits:
    movzx rdx, byte [r15]       ; Загрузка текущего символа

    ; Проверка символов окончания числа (null/newline/carriage return/пробел)
    test dl, dl                 ; Проверка: null-терминатор (0)?
    jz .finalize                ; Если да - завершение парсинга
    cmp dl, 10                  ; Проверка: LF (line feed)?
    je .finalize                ; Если да - завершение парсинга
    cmp dl, 13                  ; Проверка: CR (carriage return)?
    je .finalize                ; Если да - завершение парсинга
    cmp dl, ' '                 ; Проверка: пробел?
    je .finalize                ; Если да - завершение парсинга

    inc rcx                     ; Увеличение счётчика символов
    cmp rcx, 30                 ; Проверка на превышение лимита длины
    jg .error_buffer            ; Если превышено - ошибка переполнения буфера

    ; Валидация символа как цифры (ASCII '0'-'9')
    cmp dl, '0'                 ; Проверка: символ < '0'?
    jb .error_format            ; Если да - это не цифра, ошибка формата
    cmp dl, '9'                 ; Проверка: символ > '9'?
    ja .error_format            ; Если да - это не цифра, ошибка формата

    inc r14                     ; Увеличение счётчика обработанных цифр

    ; Предварительная проверка переполнения перед умножением
    ; Для uint16_t: max/10 = 65535/10 = 6553 (остаток 5)
    cmp rax, 6553               ; Проверка граничного случая
    jg .check_last_digit        ; Если превышено - особая обработка последней цифры

    ; Вычисление: result = result * 10 + digit
    imul rax, rax, 10           ; rax = rax * 10 (сдвиг разряда)
    sub dl, '0'                 ; Преобразование ASCII ('0'-'9') в число (0-9)
    movzx rdx, dl               ; Нулевое расширение до 64 бит
    add rax, rdx                ; Добавление текущей цифры к результату

    ; Проверка границы uint16_t (максимум 65535)
    cmp rax, 65535              ; Проверка: результат превысил максимум?
    jg .error_overflow          ; Если да - ошибка переполнения

    inc r15                     ; Переход к следующему символу
    jmp .parse_digits           ; Продолжение парсинга

3.2 Адаптация для uint64 — КРИТИЧНЫЕ ИЗМЕНЕНИЯ

; parse_uint64 (адаптированный код из io_uint64.asm)
; --- Главный цикл парсинга цифр ---
; Инвариант цикла: rax содержит частично собранное число
.parse_digits:
    movzx rdx, byte [r15]       ; Загрузка текущего символа

    ; Проверка символов окончания числа (null/newline/carriage return/пробел)
    test dl, dl                 ; Проверка: null-терминатор (0)?
    jz .finalize                ; Если да - завершение парсинга
    cmp dl, 10                  ; Проверка: LF (line feed)?
    je .finalize                ; Если да - завершение парсинга
    cmp dl, 13                  ; Проверка: CR (carriage return)?
    je .finalize                ; Если да - завершение парсинга
    cmp dl, ' '                 ; Проверка: пробел?
    je .finalize                ; Если да - завершение парсинга

    inc rcx                     ; Увеличение счётчика символов
    cmp rcx, 30                 ; Проверка на превышение лимита длины
    jg .error_buffer            ; Если превышено - ошибка переполнения буфера

    ; Валидация символа как цифры (ASCII '0'-'9')
    cmp dl, '0'                 ; Проверка: символ < '0'?
    jb .error_format            ; Если да - это не цифра, ошибка формата
    cmp dl, '9'                 ; Проверка: символ > '9'?
    ja .error_format            ; Если да - это не цифра, ошибка формата

    inc r14                     ; Увеличение счётчика обработанных цифр

    ; Проверка на граничный случай (для uint64: max/10 = 1844674407370955161)
    mov rbx, qword [MAX_UINT64_DIV10]
    cmp rax, rbx
    ja .check_last_digit        ; Беззнаковое сравнение: если больше - граничный случай
    je .check_last_digit        ; ВАЖНО: также обрабатываем равенство!

    ; Основной путь: умножение с проверкой переполнения
    ; Используем mul для корректной проверки CF
    push rdx                    ; Сохранение rdx (содержит текущую цифру)
    mov rbx, rax                ; rbx = текущее накопленное значение
    mov rax, 10                 ; rax = делитель
    mul rbx                     ; rdx:rax = rbx * 10 (беззнаковое умножение)
    test rdx, rdx               ; Проверка старших 64 бит результата
    jnz .error_overflow_pop     ; Если старшие биты != 0 - переполнение 64 бит!
    pop rdx                     ; Восстановление текущей цифры
    
    ; Добавление текущей цифры к результату
    sub dl, '0'                 ; Преобразование ASCII ('0'-'9') в число (0-9)
    movzx rbx, dl               ; Нулевое расширение цифры до 64 бит
    add rax, rbx                ; Добавление цифры к накопленному значению
    jc .error_overflow          ; Проверка флага переноса (CF): переполнение при сложении!

    ; Проверка границы uint64_t (максимум 18446744073709551615)
    mov rbx, qword [MAX_UINT64]
    cmp rax, rbx
    ja .error_overflow          ; Беззнаковое сравнение: если больше - ошибка

    inc r15                     ; Переход к следующему символу
    jmp .parse_digits           ; Продолжение парсинга

; --- Обработка ошибки переполнения с очисткой стека ---
.error_overflow_pop:
    pop rdx                     ; Восстановление стека перед переходом к ошибке
    jmp .error_overflow

Почему это необходимо:

  1. mul вместо imul (для uint64): Беззнаковое умножение автоматически помещает старшие 64 бита результата в rdx. Если они не нулевые — произошло переполнение!
  2. test rdx, rdx: Проверка, что умножение не вышло за пределы 64 бит
  3. jc после add (для uint64): Флаг CF (carry flag) сигнализирует о переполнении при беззнаковом сложении
  4. ja вместо jg (для uint64): Беззнаковые сравнения обязательны

⚠️ Для int64: Используйте jo вместо jc после add (overflow flag вместо carry flag) и jg вместо ja для сравнений.

3.3 Обработка граничного случая (последняя цифра)

uint16
; --- Обработка граничного случая: rax == 6553 ---
; Требуется особая обработка последней цифры для предотвращения переполнения
.check_last_digit:
    cmp rax, 6553               ; Проверка: точное равенство граничному значению?
    jne .error_overflow         ; Если больше - гарантированное переполнение

    movzx rdx, byte [r15]       ; Загрузка последней цифры
    sub dl, '0'                 ; Преобразование ASCII в числовое значение

    ; Для uint16_t: последняя цифра ≤ 5 (65535 = 6553*10 + 5)
    cmp dl, 5                   ; Проверка последней цифры
    jg .error_overflow          ; Если > 5 - переполнение (65535 max)

    imul rax, rax, 10           ; Умножение на 10 (безопасно, т.к. проверено)
    movzx rdx, dl               ; Нулевое расширение последней цифры
    add rax, rdx                ; Добавление последней цифры

    ; Дополнительная проверка на всякий случай
    cmp rax, 65535              ; Проверка финального результата
    jg .error_overflow          ; Если превышено - ошибка

    inc r15                     ; Переход к следующему символу
    jmp .parse_digits           ; Продолжение парсинга (может быть пробел/LF)
uint64
; --- Обработка граничного случая: rax == MAX_DIV10 или близко к нему ---
; Требуется особая обработка последней цифры для предотвращения переполнения
.check_last_digit:
    mov rbx, qword [MAX_UINT64_DIV10]
    cmp rax, rbx
    ja .error_overflow          ; Если больше MAX/10 - гарантированное переполнение

    movzx rdx, byte [r15]       ; Загрузка последней цифры
    sub dl, '0'                 ; Преобразование ASCII в числовое значение
    cmp dl, 9                   ; Проверка: цифра в диапазоне 0-9?
    ja .error_format            ; Если нет - ошибка формата

    ; Проверка последней цифры только при точном равенстве rax == MAX_DIV10
    cmp rax, rbx                ; rax == MAX_UINT64_DIV10?
    jne .do_last_calc           ; Если нет - можно безопасно вычислять
    cmp dl, 5                   ; Для uint64: последняя цифра ≤ 5 (18446744073709551615 = ...161*10 + 5)
    ja .error_overflow          ; Если > 5 - переполнение

.do_last_calc:
    imul rax, rax, 10           ; Умножение на 10 (безопасно, т.к. проверено)
    movzx rbx, dl               ; Нулевое расширение последней цифры
    add rax, rbx                ; Добавление последней цифры

    ; Финальная проверка результата
    mov rbx, qword [MAX_UINT64]
    cmp rax, rbx
    ja .error_overflow          ; Беззнаковое сравнение: если больше MAX - ошибка

    inc r15                     ; Переход к следующему символу
    jmp .parse_digits           ; Продолжение парсинга (может быть пробел/LF)
Шаг 4: Обновление функций `read_*_input_vars` (слой 2)

4.1 Текущий код uint16 — сохранение результата

; read_unsigned_input_vars (фрагмент из io_unsigned.asm)
read_unsigned_input_vars:
    ; ...
    call layer2_read_string_unsigned ; Чтение строки в parse_temp_buffer

    lea rdi, [parse_temp_buffer]     ; Адрес строки для парсинга
    call parse_uint16                ; Парсинг введённого значения

    test rdx, rdx                    ; Проверка кода ошибки (0 = успех)
    jnz .handle_error                ; Если ошибка - переход к обработке

    mov word [r12], ax               ; Сохранение значения в *a (младшие 16 бит)

4.2 Адаптация для uint64

; read_uint64_input_vars (адаптация из io_uint64.asm)
read_uint64_input_vars:
    ; ...
    call layer2_read_string_uint64  ; Чтение строки в parse_temp_buffer

    lea rdi, [parse_temp_buffer]    ; Адрес строки для парсинга
    call parse_uint64               ; Парсинг введённого значения

    test rdx, rdx                   ; Проверка кода ошибки (0 = успех)
    jnz .handle_error               ; Если ошибка - переход к обработке

    mov qword [r12], rax            ; Сохранение значения в *a (64 бита)
Шаг 5: Вывод 128-битного результата (новая функция)

5.1 Проблема: стандартные функции не работают для 128-бит

Текущая функция print_unsigned_output_var из io_unsigned.asm принимает один регистр (edi) и выводит максимум 32-битное значение. Для результата умножения 64×64 бит нужна новая функция, работающая с двумя 64-битными значениями.

5.2 Готовая функция print_uint128_output

Добавьте в ваш адаптированный модуль io_uint64.asm:

; ----------------------------------------------------------------------------
; print_uint128_output
;
; Выводит 128-битное целое число в stdout в десятичном формате
; с префиксом "Result = " и переводом строки. Поддерживает только
; беззнаковые числа (uint128).
;
; АЛГОРИТМ:
;   1. Обработка специального случая (ноль)
;   2. Конвертация справа налево через многоразрядное деление на 10
;   3. Вывод префикса, числа и перевода строки
;
; ДИАПАЗОН: 0 до 340282366920938463463374607431768211455 (2¹²⁸-1)
; ФОРМАТ ВЫВОДА: "Result = <число>\n"
;
; ПРИМЕРЫ:
;   print_uint128_output(0, 0)      → "Result = 0\n"
;   print_uint128_output(200, 0)    → "Result = 200\n"
;   print_uint128_output(MAX, MAX)  → "Result = 340282366920938463463374607431768211455\n"
;
; @param  rdi  Младшие 64 бита (output_low)
;         rsi  Старшие 64 бита (output_high)
; @return none
; @uses   rax, rbx, rcx, rdx, rdi, rsi, r12, r13, r14, r15
;
; @complexity O(log₁₀(n)), где n - значение числа
; @memory     O(1), использует фиксированный буфер
; ----------------------------------------------------------------------------
print_uint128_output:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13
    push r14
    push r15

    mov r14, rdi                      ; r14 = младшие 64 бита (output_low)
    mov r15, rsi                      ; r15 = старшие 64 бита (output_high)
    
    mov r12, 10                       ; Делитель = 10 (основание системы счисления)
    lea r13, [parse_temp_buffer + 63] ; r13 = указатель на конец буфера
    mov byte [r13], 0                 ; Установка null-терминатора

; --- Специальная обработка нуля ---
    mov rax, r14                ; Загрузка младших 64 бит
    or rax, r15                 ; Объединение с старшими 64 битами
    test rax, rax               ; Проверка: число равно нулю?
    jnz .convert                ; Если нет - переход к конвертации
    
    dec r13                     ; Смещение указателя назад
    mov byte [r13], '0'         ; Запись символа '0'
    jmp .print                  ; Переход к выводу

; --- Конвертация 128-битного числа в строку (справа налево) ---
; Инвариант цикла: r13 указывает на следующую позицию для записи
.convert:
    ; Деление 128-битного числа на 10
    ; Алгоритм: (high * 2^64 + low) / 10
    
    ; Шаг 1: Делим старшую часть
    mov rax, r15                ; rax = старшие 64 бита
    xor rdx, rdx                ; Обнуление rdx перед делением
    div r12                     ; rax = high / 10, rdx = high % 10 (остаток)
    mov r15, rax                ; Сохранение нового значения старших 64 бит
    
    ; Шаг 2: Объединяем остаток с младшей частью и делим
    mov rax, r14                ; rax = младшие 64 бита
    ; rdx уже содержит остаток от деления старших 64 бит
    div r12                     ; rax = (rdx*2^64 + low) / 10, rdx = остаток (цифра 0-9)
    mov r14, rax                ; Сохранение нового значения младших 64 бит
    
    ; Шаг 3: Сохраняем полученную цифру в буфер
    dec r13                     ; Смещение указателя назад
    add dl, '0'                 ; Преобразование цифры (0-9) в ASCII ('0'-'9')
    mov [r13], dl               ; Запись ASCII-символа в буфер
    
    ; Проверка: остались ли ещё цифры?
    mov rax, r14                ; Загрузка младших 64 бит
    or rax, r15                 ; Объединение с старшими 64 битами
    test rax, rax               ; Проверка: результат равен нулю?
    jnz .convert                ; Если нет - продолжение конвертации

; --- Вывод результата ---
.print:
    ; Вывод префикса "Result = "
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    lea rsi, [msg_res]
    mov rdx, len_res
    syscall

    ; Вывод преобразованного числа
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    mov rsi, r13                        ; Адрес начала числовой строки
    lea rdx, [parse_temp_buffer + 63]   ; Адрес конца буфера
    sub rdx, r13                        ; Вычисление длины строки (конец - начало)
    syscall

    ; Вывод символа новой строки
    ; syscall: write(stdout, string, length)
    mov rax, 1
    mov rdi, 1
    lea rsi, [newline]
    mov rdx, 1
    syscall

    pop r15
    pop r14
    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

⚠️ Для int64: Требуется отдельная функция print_int128_output, которая обрабатывает знак перед вызовом алгоритма деления.

5.3 Использование в программе

; ============================================================================
; compute_unsigned64.asm
;
; Демонстрационная программа умножения беззнаковых 64-битных чисел.
; Читает два uint64, вычисляет произведение (128-бит) и выводит результат.
;
; НАЗНАЧЕНИЕ:
;   - Демонстрация умножения 64×64 → 128 бит (RDX:RAX)
;   - Вывод результатов, не помещающихся в один регистр
;   - Использование современного API с передачей адресов
;
; АЛГОРИТМ:
;   1. Чтение двух uint64 из stdin
;   2. Умножение: a × b → RDX:RAX (128 бит)
;   3. Сохранение в output_low/output_high
;   4. Вывод результата в десятичном формате
;
; ПРИМЕР:
;   $ ./compute_unsigned64
;   Enter a value for variable 'a': 18446744073709551615
;   Enter a value for variable 'b': 2
;   Result = 36893488147419103230
;
; ЗАВИСИМОСТИ:
;   io_uint64.asm:
;     - read_uint64_input_vars   (чтение двух uint64)
;     - print_uint128_output     (вывод 128-бит в десятичном формате)
;
; СООТВЕТСТВИЕ ТАБЛИЦЕ 5.1:
;   Тип: uint64, Директива: resq 1, Результат умножения: 128 бит (resq 2)
; ============================================================================

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

; Импорт функций из модуля io_uint64.asm
extern read_uint64_input_vars           ; Чтение: rdi=&a, rsi=&b
extern print_uint128_output             ; Вывод: rdi=low (значение), rsi=high (значение)

section .bss
    ; Локальные переменные (не экспортируются)
    a resq 1                            ; Первое uint64 число
    b resq 1                            ; Второе uint64 число
    
    output_low resq 1                   ; Младшие 64 бита результата (биты 0-63)
    output_high resq 1                  ; Старшие 64 бита результата (биты 64-127)

section .text
    global _start

; ----------------------------------------------------------------------------
; _start
;
; Точка входа. Выполняет чтение, умножение и вывод результата.
;
; ЭТАПЫ:
;   1. Чтение двух uint64 через read_uint64_input_vars
;   2. Умножение: mul qword [b] → RDX:RAX (128-битный результат)
;      - RDX = старшие 64 бита
;      - RAX = младшие 64 бита
;   3. Вывод через print_uint128_output
;   4. Завершение с exit(0)
;
; МАТЕМАТИКА:
;   Результат = output_high × 2⁶⁴ + output_low
;   Пример: (2⁶⁴-1) × 2 = 2⁶⁵ - 2
;           = 36893488147419103230
;           output_high = 1
;           output_low  = 18446744073709551614
; ----------------------------------------------------------------------------
_start:
    ; Чтение входных данных (modern API)
    lea rdi, [a]                        ; rdi = адрес переменной a
    lea rsi, [b]                        ; rsi = адрес переменной b
    call read_uint64_input_vars         ; Чтение из stdin
    
    ; Вычисление произведения (128-бит)
    mov rax, [a]                        ; RAX = первый операнд (64 бита)
    mul qword [b]                       ; RDX:RAX = RAX × [b] (128 бит автоматически!)
                                        ; mul автоматически использует rdx:rax для 128-битного результата
    
    ; Сохранение результата
    mov [output_low], rax               ; Младшие 64 бита (биты 0-63)
    mov [output_high], rdx              ; Старшие 64 бита (биты 64-127)
    
    ; Вывод результата в десятичном формате
    mov rdi, [output_low]               ; rdi = младшие 64 бита (ЗНАЧЕНИЕ!)
    mov rsi, [output_high]              ; rsi = старшие 64 бита (ЗНАЧЕНИЕ!)
    call print_uint128_output           ; Форматированный вывод
    
    ; Завершение программы
    ; syscall: exit(0)
    mov rax, 60
    xor rdi, rdi
    syscall

Сводная таблица изменений

Компонент uint16 (текущий) uint64 (адаптация) int64 (адаптация) Комментарий
Константы диапазона cmp rax, 65535 (immediate) mov rbx, [MAX_UINT64]
cmp rax, rbx
mov rbx, [MAX_INT64]
cmp rax, rbx
Для 64-бит нужны константы в .data через dq
Тип сравнения jg (uint: неверно!) ja (беззнаковое) jg (знаковое) КРИТИЧНО различать!
Проверка overflow (умножение) Не требуется mul + test rdx, rdx imul + test rdx, rdx Одинаково для обоих
Проверка overflow (сложение) Не требуется add + jc (CF флаг) add + jo (OF флаг) Разные флаги!
Буфер парсинга resb 64 resb 64 resb 64 Достаточен, изменять не нужно
Входные переменные resw 1 (16 бит) resq 1 (64 бит) resq 1 (64 бит) В программе, не в модуле
Выходная переменная output resd 1 (32 бита) output_low resq 1
output_high resq 1
output_low resq 1
output_high resq 1
uint128/int128 обязателен
Сохранение результата mov word [r12], ax mov qword [r12], rax mov qword [r12], rax В read_*_input_vars
Вывод результата print_unsigned_output_var print_uint128_output print_int128_output Разные функции для signed/unsigned

Частые ошибки при адаптации

Проблема Причина Решение
error: invalid combination of opcode and operands Попытка использовать immediate > 2³²-1 в cmp Вынести константу в .data через dq, загружать в регистр
error: symbol 'MAX_UINT64' undefined Константа объявлена через equ вместо dq Используйте dq для 64-битных значений
Segfault при записи результата Переменная объявлена как resw вместо resq Проверьте .bss в программе (не в модуле!)
Тихое переполнение (неверный результат) Используется output resq 1 вместо двух qword Для int64/uint64 нужны output_low и output_high
Неверный вывод больших чисел Используется print_unsigned_output_var для 128-бит Используйте print_uint128_output из Шага 5
Числа > MAX принимаются без ошибки Отсутствует проверка переполнения через флаги Используйте mul/imul + test rdx, rdx и jc/jo после add
Знаковое сравнение для беззнаковых Используется jg вместо ja для uint64 Замените все jg/jl на ja/jb для uint
Беззнаковое сравнение для знаковых Используется ja вместо jg для int64 Замените все ja/jb на jg/jl для int
Неверный флаг overflow при сложении Используется jc для int64 или jo для uint64 CF для unsigned, OF для signed

📘 Примечание: Диагностика типичных проблем (segfault, overflow, неверные флаги) описана в «Топ ошибок в NASM: Почему падает Segfault и неверные расчёты». Для отладки с breakpoints и step-by-step см. «Отладка ASM в VS Code: Настройка GDB и визуальный интерфейс».

✅ Заключение

Представленные модули I/O демонстрируют, что чистый ассемблер без libc может быть не только возможным, но и практичным решением для определённых задач. Двухслойная архитектура обеспечивает баланс между гибкостью и безопасностью: слой 1 предоставляет переиспользуемые функции парсинга, слой 2 — интерфейс для стандартного ввода-вывода.

Ключевые преимущества:

  • Полная автономность — нет зависимостей от версий libc
  • Детерминированность — одинаковое поведение на любом Linux x86-64
  • Понимание механики — каждая проверка и валидация прозрачна

Ограничения:

  • Только Linux x86-64 (System V ABI)
  • Базовые типы данных (расширение требует ручной адаптации)
  • Отсутствие локализации и сложного форматирования

Модули готовы к использованию в учебных проектах, минималистичных утилитах и embedded-системах, где важны размер бинарника и отсутствие внешних зависимостей.

💜

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

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

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