При разработке программ на чистом ассемблере NASM с меткой _start неизбежно возникает вопрос: как организовать ввод-вывод без libc? Эта статья описывает три готовых модуля (signed/unsigned/float) с двухслойной архитектурой, которая разделяет парсинг строк и операции I/O.
Статья объясняет архитектурные концепции, лежащие в основе модулей: почему выбрана такая структура, какие проблемы она решает, и как адаптировать модули под свои задачи.
🚀 1. Введение и мотивация
Зачем отказываться от libc?
Когда вы используете scanf/printf из libc, программа становится зависимой от внешней библиотеки:
$ 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
- Поведение зависит от версии библиотеки
$ 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 ║
╚══════════════════════════════════════════════════════════╝Что даёт такое разделение:
- Переиспользование. Один парсер для stdin, файлов, буферов — код написан один раз
- Тестируемость. Чистые функции слоя 1 можно тестировать изолированно
- Отладка. Ошибка в парсинге → слой 1; данные не читаются → слой 2
- Расширяемость. Новый источник данных = новая обёртка слоя 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 для двух интерфейсов
Проблема:
Модуль должен поддерживать два способа использования:
- Legacy API: Программа определяет
global a, b, output, модуль использует их напрямую - 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/jgio_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 детектирует ошибку, Слой 2 сообщает пользователю
- Нулевая толерантность: Любая ошибка приводит к немедленному завершению (fail-fast)
- Информативность: Коды ошибок позволяют точно определить причину отказа
Дополнительная защита: Размещение буферов
┌─────────────────────────────────────────────────────┐
│ СТЕК (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 .dataMAX_INT64 dq 9223372036854775807 |
section .dataMAX_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-операнды:
; Это НЕ СКОМПИЛИРУЕТСЯ!
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 Обновление сообщений об ошибках
section .data
error_overflow_msg db "ERROR: Number overflow (must be 0 to 65535)", 10
error_overflow_len equ $ - error_overflow_msgsection .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-битных типов
; 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 бита)
; 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
Почему это необходимо:
mulвместоimul(для uint64): Беззнаковое умножение автоматически помещает старшие 64 бита результата вrdx. Если они не нулевые — произошло переполнение!test rdx, rdx: Проверка, что умножение не вышло за пределы 64 битjcпослеadd(для uint64): Флаг CF (carry flag) сигнализирует о переполнении при беззнаковом сложенииjaвместоjg(для uint64): Беззнаковые сравнения обязательны
⚠️ Для int64: Используйте
joвместоjcпослеadd(overflow flag вместо carry flag) иjgвместоjaдля сравнений.
3.3 Обработка граничного случая (последняя цифра)
; --- Обработка граничного случая: 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); --- Обработка граничного случая: 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 1output_high resq 1 |
output_low resq 1output_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-системах, где важны размер бинарника и отсутствие внешних зависимостей.