Nikita Mandrykin

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

Назад

Связь C и NASM: Конвенции вызовов (ABI) и Оптимизация

Введение

Эта статья представляет собой практическое руководство по преобразованию C-функций в ассемблерный код NASM для 64-битного Linux. Для каждой функции будет рассмотрено:

  1. C-версию — исходный код на C
  2. Прямую NASM-версию — буквальный перевод на ассемблер
  3. Оптимизированную NASM-версию — улучшенный вариант с использованием возможностей архитектуры
  4. Пояснения — концептуальное объяснение идей и подходов

Основы соглашений о вызовах (Calling Convention)

В x86-64 System V ABI (используется в Linux) применяются следующие правила:

  • Параметры функций передаются через регистры: rdi, rsi, rdx, rcx, r8, r9
  • Возвращаемое значение помещается в регистр rax
  • Сохраняемые регистры: rbx, rbp, r12-r15 (функция должна восстановить их перед возвратом)

📘 Справка: Полная таблица сохраняемых регистров и аргументов System V ABI доступна в «Шпаргалке NASM».

➕ 1. Простые арифметические функции

Функция add

C-версия
int add(int a, int b) {
    return a + b;
}
Оптимизированная NASM
section .text
    global add

add:
    ; Параметры: rdi = a, rsi = b
    lea eax, [rdi + rsi]
    ret
Прямая NASM-версия
section .text
    global add

add:
    push rbp
    mov rbp, rsp
    
    mov eax, edi
    add eax, esi
    
    pop rbp
    ret

Пояснение концепции

Стековый фрейм: Прямая версия создаёт стековый фрейм (push rbp; mov rbp, rsp), хотя для такой простой функции это избыточно. Стековый фрейм нужен для отладки и доступа к локальным переменным, но здесь их нет.

Инструкция LEA как арифметическая: Оптимизированная версия использует lea (Load Effective Address) — инструкцию для вычисления адресов, но её можно применять для обычной арифметики. Преимущества:

  • Не обращается к памяти — работает только с регистрами
  • Не изменяет флаги процессора
  • Выполняется за 1 такт
  • Не требует стекового фрейма

Выбор регистров: Используем eax (32-битный) вместо rax (64-битный), так как функция возвращает int. Это автоматически обнуляет старшие 32 бита rax согласно архитектуре x86-64.


Функция safe_multiply

C-версия
int safe_multiply(int a, int b, int* overflow) {
    long long result = (long long)a * b;
    if (result > INT_MAX || result < INT_MIN) {
        *overflow = 1;
        return 0;
    }
    *overflow = 0;
    return (int)result;
}
Оптимизированная NASM
section .text
    global safe_multiply

safe_multiply:
    ; Параметры: rdi = a, rsi = b, rdx = overflow*
    movsxd rax, edi
    movsxd rcx, esi
    imul rax, rcx
    
    ; Проверка через арифметический сдвиг
    mov rcx, rax
    sar rcx, 31
    add rcx, 1
    cmp rcx, 1
    ja .overflow
    
    mov dword [rdx], 0
    ret
    
.overflow:
    mov dword [rdx], 1
    xor eax, eax
    ret
Прямая NASM-версия
section .text
    global safe_multiply

safe_multiply:
    push rbp
    mov rbp, rsp
    
    movsxd rax, edi
    movsxd rcx, esi
    imul rax, rcx
    
    mov r8, 0x7FFFFFFF
    mov r9, 0xFFFFFFFF80000000
    
    cmp rax, r8
    jg .overflow
    
    cmp rax, r9
    jl .overflow
    
    mov dword [rdx], 0
    mov eax, ecx
    pop rbp
    ret
    
.overflow:
    mov dword [rdx], 1
    xor eax, eax
    pop rbp
    ret

Пояснение концепции

Проблема переполнения: При умножении двух 32-битных чисел результат может не поместиться в 32 бита. Например, 2000000000 × 2 = 4000000000 превышает INT_MAX (2147483647).

Знаковое расширение: Инструкция movsxd расширяет 32-битное signed число до 64 бит, сохраняя знак. Это позволяет выполнить умножение в 64-битном пространстве без потери данных.

Математический трюк для проверки: Вместо явного сравнения с INT_MAX и INT_MIN, оптимизированная версия использует свойство знакового представления:

  • Если 64-битное число помещается в 32-битное signed, то биты 31-63 являются копиями бита 31
  • sar rcx, 31 превращает это в 0x00000000 (для положительных) или 0xFFFFFFFF (для отрицательных)
  • Добавление 1 даёт 1 для обоих допустимых случаев
  • Любое другое значение означает переполнение

Это экономит загрузку констант и дополнительные сравнения.

❓ 2. Условные конструкции

Функция max

C-версия
int max(int a, int b) {
    if (a > b) {
        return a;
    }
    return b;
}
Оптимизированная NASM
section .text
    global max

max:
    ; Параметры: rdi = a, rsi = b
    mov eax, edi
    cmp edi, esi
    cmovle eax, esi
    ret
Прямая NASM-версия
section .text
    global max

max:
    push rbp
    mov rbp, rsp
    
    mov eax, edi
    cmp edi, esi
    jg .return_a
    
    mov eax, esi
    
.return_a:
    pop rbp
    ret

Пояснение концепции

Цена условных переходов: Прямая версия использует jmp, который требует предсказания ветвлений процессором. При промахе предсказателя происходит сброс конвейера — потеря 15-20 тактов.

Условное перемещение: Инструкция cmov (conditional move) выполняет перемещение только при выполнении условия, но сама инструкция выполняется всегда. Это устраняет ветвление:

  • Детерминированное время выполнения (1-2 такта)
  • Конвейер процессора не блокируется
  • Не требуется предсказание

Паттерн использования: Оптимистично предполагаем результат (загружаем a), затем условно заменяем его на b, если условие истинно.

Когда НЕ использовать: Если вычисление обоих вариантов дорого, то классический подход с jmp может быть лучше, так как выполняется только одна ветка.


Функция clamp

C-версия
int clamp(int value, int min, int max) {
    if (value < min) return min;
    if (value > max) return max;
    return value;
}
Оптимизированная NASM
section .text
    global clamp

clamp:
    ; Параметры: rdi = value, rsi = min, rdx = max
    mov eax, esi
    cmp edi, esi
    cmovge eax, edi
    cmp eax, edx
    cmovg eax, edx
    ret
Прямая NASM-версия
section .text
    global clamp

clamp:
    push rbp
    mov rbp, rsp
    
    mov eax, edi
    
    cmp edi, esi
    jl .return_min
    
    cmp edi, edx
    jg .return_max
    
    pop rbp
    ret
    
.return_min:
    mov eax, esi
    pop rbp
    ret
    
.return_max:
    mov eax, edx
    pop rbp
    ret

Пояснение концепции

Цепочка ограничений: Функция clamp ограничивает значение между минимумом и максимумом — это композиция двух операций: min(max(value, min_val), max_val).

Последовательное применение cmov: Оптимизированная версия применяет два условных перемещения:

  1. Предполагаем value < min, загружаем min
  2. Если value >= min, заменяем на value
  3. Сравниваем результат с max
  4. Если результат > max, заменяем на max

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

🔁 3. Циклы

Функция sum_array

C-версия
int sum_array(int* array, int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
        sum += array[i];
    }
    return sum;
}
Оптимизированная NASM
section .text
    global sum_array

sum_array:
    ; Параметры: rdi = array, rsi = size
    xor eax, eax
    test esi, esi
    jz .end
    
    lea rcx, [rdi + rsi*4]
    
.loop:
    add eax, [rdi]
    add rdi, 4
    cmp rdi, rcx
    jne .loop
    
.end:
    ret
Прямая NASM-версия
section .text
    global sum_array

sum_array:
    push rbp
    mov rbp, rsp
    
    xor eax, eax
    xor ecx, ecx
    
.loop:
    cmp ecx, esi
    jge .end
    
    add eax, [rdi + rcx*4]
    inc ecx
    jmp .loop
    
.end:
    pop rbp
    ret

Пояснение концепции

Индексная vs указательная арифметика: Прямая версия использует индекс i и вычисляет адрес через [rdi + rcx*4] на каждой итерации. Оптимизированная версия инкрементирует указатель напрямую.

Предвычисление границы: Вместо сравнения счётчика с размером, вычисляем адрес конца массива один раз: array + size*4. Затем в цикле сравниваем указатель с этой границей.

Экономия инструкций: На каждой итерации экономим:

  • Вычисление rcx*4 (масштабирование индекса)
  • Сложную адресацию заменяем на простую [rdi]
  • Освобождается регистр, ранее занятый счётчиком

Инструкция LEA для адресной арифметики: lea rcx, [rdi + rsi*4] вычисляет array + size*4 за одну операцию, используя аппаратные возможности адресации.


Функция find_element

C-версия
int find_element(int* array, int size, int value) {
    for (int i = 0; i < size; i++) {
        if (array[i] == value) {
            return i;
        }
    }
    return -1;
}
Оптимизированная NASM
section .text
    global find_element

find_element:
    ; Параметры: rdi = array, rsi = size, rdx = value
    xor eax, eax
    test esi, esi
    jz .not_found
    
.loop:
    cmp [rdi], edx
    je .found
    
    add rdi, 4
    inc eax
    dec esi
    jnz .loop
    
.not_found:
    mov eax, -1
    
.found:
    ret
Прямая NASM-версия
section .text
    global find_element

find_element:
    push rbp
    mov rbp, rsp
    
    xor ecx, ecx
    
.loop:
    cmp ecx, esi
    jge .not_found
    
    cmp dword [rdi + rcx*4], edx
    je .found
    
    inc ecx
    jmp .loop
    
.found:
    mov eax, ecx
    pop rbp
    ret
    
.not_found:
    mov eax, -1
    pop rbp
    ret

Пояснение концепции

Двойное назначение регистра: В оптимизированной версии eax одновременно служит счётчиком индекса и возвращаемым значением. Это устраняет необходимость копирования результата перед возвратом.

Использование флагов от dec: Инструкция dec esi автоматически устанавливает флаг нуля (ZF), когда результат становится равным нулю. Это позволяет использовать jnz без отдельного cmp, экономя инструкцию на каждой итерации.

Обратный отсчёт размера: Вместо сравнения счётчика с размером, уменьшаем сам размер до нуля. Это естественнее сочетается с проверкой флагов.

📋 4. Работа с массивами

Функция my_memcpy

C-версия
void* my_memcpy(void* dest, const void* src, 
                size_t n) {
    char* d = (char*)dest;
    const char* s = (const char*)src;
    for (size_t i = 0; i < n; i++) {
        d[i] = s[i];
    }
    return dest;
}
Оптимизированная NASM
section .text
    global my_memcpy

my_memcpy:
    ; Параметры: rdi = dest, rsi = src, rdx = n
    mov rax, rdi
    mov rcx, rdx
    test rcx, rcx
    jz .end
    
    ; Копировать по 8 байт
    mov r8, rcx
    shr rcx, 3
    jz .copy_bytes
    
.copy_qwords:
    mov r9, [rsi]
    mov [rdi], r9
    add rsi, 8
    add rdi, 8
    dec rcx
    jnz .copy_qwords
    
.copy_bytes:
    and r8, 7
    jz .end
    
.copy_byte_loop:
    mov cl, [rsi]
    mov [rdi], cl
    inc rsi
    inc rdi
    dec r8
    jnz .copy_byte_loop
    
.end:
    ret
Прямая NASM-версия
section .text
    global my_memcpy

my_memcpy:
    push rbp
    mov rbp, rsp
    push rdi
    
    xor rcx, rcx
    
.loop:
    cmp rcx, rdx
    jge .end
    
    mov al, [rsi + rcx]
    mov [rdi + rcx], al
    
    inc rcx
    jmp .loop
    
.end:
    pop rax
    pop rbp
    ret

Пояснение концепции

Блочное копирование: Побайтовое копирование неэффективно, так как обращение к памяти — медленная операция. Копирование блоками по 8 байт (qword) уменьшает количество обращений к памяти в 8 раз.

Выравнивание данных: Современные процессоры эффективнее работают с данными, выровненными по границам слов (8 байт для 64-битных систем). 64-битные пересылки используют возможности шины данных полностью.

Двухфазный алгоритм:

  1. Основная фаза: копируем максимальное количество полных 8-байтовых блоков
  2. Завершающая фаза: копируем оставшиеся 0-7 байт побайтно

Вычисление остатка: and r8, 7 эквивалентно size % 8, но выполняется за 1 такт вместо десятков для операции деления.

📝 5. Строковые функции

Функция my_strlen

C-версия
size_t my_strlen(const char* str) {
    size_t len = 0;
    while (str[len] != '\0') {
        len++;
    }
    return len;
}
Оптимизированная NASM
section .text
    global my_strlen

my_strlen:
    ; Параметр: rdi = str
    mov rax, rdi
    
.loop:
    cmp byte [rdi], 0
    je .end
    inc rdi
    jmp .loop
    
.end:
    sub rdi, rax
    mov rax, rdi
    ret
SIMD-версия
section .text
    global my_strlen

my_strlen:
    mov rax, rdi
    
.loop:
    movdqu xmm0, [rdi]
    pxor xmm1, xmm1
    pcmpeqb xmm0, xmm1
    pmovmskb edx, xmm0
    test edx, edx
    jnz .found_zero
    
    add rdi, 16
    jmp .loop
    
.found_zero:
    bsf edx, edx
    add rdi, rdx
    sub rdi, rax
    mov rax, rdi
    ret

Концепция SIMD: Обрабатывает 16 байт параллельно с помощью SSE-инструкций. pcmpeqb сравнивает все 16 байт одновременно, pmovmskb создаёт битовую маску, bsf находит позицию первого нуля. В ~4-8 раз быстрее на длинных строках (>64 байта).

Пояснение концепции

Указательная арифметика: Сохраняем начальный адрес строки, перемещаем указатель до нулевого байта, вычисляем разность. Это элегантнее, чем ведение отдельного счётчика.

SIMD-подход: Параллельная обработка использует векторные регистры (XMM) для проверки 16 байт за раз:

  • movdqu загружает 16 байт невыровненных данных
  • pcmpeqb выполняет 16 сравнений одновременно
  • pmovmskb превращает результаты в битовую маску
  • bsf (bit scan forward) находит первый установленный бит

Когда использовать SIMD: Для больших массивов данных. На коротких строках (<16 символов) overhead не окупается — простая версия может быть быстрее.


Функция my_strcmp

C-версия
int my_strcmp(const char* s1, const char* s2) {
    while (*s1 && (*s1 == *s2)) {
        s1++;
        s2++;
    }
    return (unsigned char)*s1 - 
           (unsigned char)*s2;
}
Оптимизированная NASM
section .text
    global my_strcmp

my_strcmp:
    ; Параметры: rdi = s1, rsi = s2
.loop:
    mov al, [rdi]
    mov cl, [rsi]
    
    cmp al, cl
    jne .different
    
    test al, al
    jz .equal
    
    inc rdi
    inc rsi
    jmp .loop
    
.different:
    movzx eax, al
    movzx ecx, cl
    sub eax, ecx
    ret
    
.equal:
    xor eax, eax
    ret
Прямая NASM-версия
section .text
    global my_strcmp

my_strcmp:
    push rbp
    mov rbp, rsp
    
.loop:
    mov al, [rdi]
    mov cl, [rsi]
    
    test al, al
    jz .end_compare
    
    cmp al, cl
    jne .end_compare
    
    inc rdi
    inc rsi
    jmp .loop
    
.end_compare:
    movzx eax, al
    movzx ecx, cl
    sub eax, ecx
    
    pop rbp
    ret

Пояснение концепции

Оптимизация порядка проверок: Оптимизированная версия сначала сравнивает символы, затем проверяет конец строки. Это быстрее для типичного случая, когда строки различаются где-то в середине.

Принцип ранней проверки: Располагайте наиболее вероятные условия первыми. Для strcmp более вероятно, что строки различаются (рано выходим из функции), чем что они одинаковы до конца.

Расширение с нулями: movzx (move with zero extension) необходимо, чтобы корректно обработать символы как беззнаковые значения при вычислении разности.

🔢 6. Преобразование строк в числа

Функция atoi

C-версия
int atoi(const char* str) {
    int result = 0;
    int sign = 1;
    
    while (*str == ' ' || *str == '\t') {
        str++;
    }
    
    if (*str == '-') {
        sign = -1;
        str++;
    } else if (*str == '+') {
        str++;
    }
    
    while (*str >= '0' && *str <= '9') {
        result = result * 10 + (*str - '0');
        str++;
    }
    
    return result * sign;
}
Оптимизированная NASM
section .text
    global atoi

atoi:
    ; Параметр: rdi = str
    xor eax, eax
    mov ecx, 1
    
.skip_spaces:
    movzx edx, byte [rdi]
    cmp dl, ' '
    je .next_space
    cmp dl, 9
    jne .check_sign
.next_space:
    inc rdi
    jmp .skip_spaces
    
.check_sign:
    cmp dl, '-'
    jne .check_plus
    neg ecx
    inc rdi
    movzx edx, byte [rdi]
    jmp .convert_loop
    
.check_plus:
    cmp dl, '+'
    jne .convert_loop
    inc rdi
    movzx edx, byte [rdi]
    
.convert_loop:
    sub dl, '0'
    cmp dl, 9
    ja .end_convert
    
    lea eax, [rax*4 + rax]
    lea eax, [rax*2 + rdx]
    
    inc rdi
    movzx edx, byte [rdi]
    jmp .convert_loop
    
.end_convert:
    imul eax, ecx
    ret
Прямая NASM-версия
section .text
    global atoi

atoi:
    push rbp
    mov rbp, rsp
    
    xor eax, eax
    mov ecx, 1
    
.skip_spaces:
    mov dl, [rdi]
    cmp dl, ' '
    je .space
    cmp dl, 9
    jne .check_sign
.space:
    inc rdi
    jmp .skip_spaces
    
.check_sign:
    cmp dl, '-'
    jne .check_plus
    mov ecx, -1
    inc rdi
    jmp .convert
    
.check_plus:
    cmp dl, '+'
    jne .convert
    inc rdi
    
.convert:
    mov dl, [rdi]
    sub dl, '0'
    cmp dl, 9
    ja .done
    
    imul eax, 10
    movzx edx, dl
    add eax, edx
    
    inc rdi
    jmp .convert
    
.done:
    imul eax, ecx
    pop rbp
    ret

Пояснение концепции

Быстрое умножение на 10: Прямая версия использует imul eax, 10, что занимает 3-4 такта. Оптимизированная версия заменяет это на две инструкции lea:

  • lea eax, [rax*4 + rax] вычисляет rax * 5
  • lea eax, [rax*2 + rdx] удваивает результат и добавляет цифру
  • Итого: (rax * 5) * 2 + digit = rax * 10 + digit

Использование LEA для умножения: Инструкция lea может выполнять сложные вычисления за 1 такт: [base + index*scale + offset], где scale может быть 1, 2, 4 или 8.

Проверка цифры одним сравнением: sub dl, '0'; cmp dl, 9 — вычитаем ‘0’ сразу, получая значение цифры. Если символ не цифра, результат будет >9 (или отрицательный), что ловится одним сравнением.


Функция safe_atoi

C-версия
int safe_atoi(const char* str, int* result, 
              int* error) {
    *error = 0;
    *result = 0;
    
    if (!str) {
        *error = 1;
        return -1;
    }
    
    long long temp = 0;
    int sign = 1;
    
    while (*str == ' ' || *str == '\t') {
        str++;
    }
    
    if (*str == '-') {
        sign = -1;
        str++;
    } else if (*str == '+') {
        str++;
    }
    
    if (*str < '0' || *str > '9') {
        *error = 2;
        return -1;
    }
    
    while (*str >= '0' && *str <= '9') {
        temp = temp * 10 + (*str - '0');
        if (temp > 0x7FFFFFFF) {
            *error = 3;
            return -1;
        }
        str++;
    }
    
    temp *= sign;
    
    if (temp < INT_MIN || temp > INT_MAX) {
        *error = 3;
        return -1;
    }
    
    *result = (int)temp;
    return 0;
}
Оптимизированная NASM
section .text
    global safe_atoi

safe_atoi:
    push rbx
    push r12
    
    mov dword [rsi], 0
    mov dword [rdx], 0
    
    test rdi, rdi
    jz .error_null
    
    mov r12, rdx
    xor eax, eax
    xor ecx, ecx
    
.skip_spaces:
    movzx ebx, byte [rdi]
    cmp bl, ' '
    je .space_found
    cmp bl, 9
    jne .check_sign
.space_found:
    inc rdi
    jmp .skip_spaces
    
.check_sign:
    xor edx, edx
    cmp bl, '-'
    sete dl
    cmp bl, '+'
    sete dh
    or dl, dh
    test dl, dl
    jz .first_digit
    
    cmp bl, '-'
    jne .skip_sign
    neg ecx
.skip_sign:
    inc rdi
    movzx ebx, byte [rdi]
    
.first_digit:
    sub bl, '0'
    cmp bl, 9
    ja .error_no_digits
    
    movzx eax, bl
    inc rdi
    
.convert_loop:
    movzx ebx, byte [rdi]
    sub bl, '0'
    cmp bl, 9
    ja .apply_sign
    
    lea rax, [rax*4 + rax]
    lea rax, [rax*2 + rbx]
    
    cmp rax, 0x7FFFFFFF
    ja .error_overflow
    
    inc rdi
    jmp .convert_loop
    
.apply_sign:
    test ecx, ecx
    jns .store_result
    neg rax
    
    cmp rax, -2147483648
    jl .error_overflow
    
.store_result:
    mov [rsi], eax
    xor eax, eax
    pop r12
    pop rbx
    ret
    
.error_null:
    mov dword [rdx], 1
    mov eax, -1
    pop r12
    pop rbx
    ret
    
.error_no_digits:
    mov dword [r12], 2
    mov eax, -1
    pop r12
    pop rbx
    ret
    
.error_overflow:
    mov dword [r12], 3
    mov eax, -1
    pop r12
    pop rbx
    ret

Пояснение концепции

Типы ошибок: Функция различает три класса ошибок:

  1. NULL-указатель (error = 1) — защита от segfault
  2. Отсутствие цифр (error = 2) — строки типа "" или “-” некорректны
  3. Переполнение (error = 3) — результат не помещается в int32

Проверка переполнения в процессе: Используем 64-битную арифметику для накопления результата, проверяя границы INT_MAX на каждой итерации. Это позволяет обнаружить переполнение до того, как оно произойдёт.

Оптимизация проверки знака через SETE: Вместо нескольких условных переходов используем инструкцию sete (set if equal):

  • Создаём две маски: для ‘-’ и ‘+’
  • Объединяем через or
  • Получаем единый флаг наличия знака

Функция hex_to_int

C-версия
int hex_to_int(const char* hex_str) {
    int result = 0;
    
    if (hex_str[0] == '0' && 
        (hex_str[1] == 'x' || hex_str[1] == 'X')) {
        hex_str += 2;
    }
    
    while (*hex_str) {
        char c = *hex_str;
        int digit;
        
        if (c >= '0' && c <= '9') {
            digit = c - '0';
        } else if (c >= 'a' && c <= 'f') {
            digit = c - 'a' + 10;
        } else if (c >= 'A' && c <= 'F') {
            digit = c - 'A' + 10;
        } else {
            break;
        }
        
        result = (result << 4) | digit;
        hex_str++;
    }
    
    return result;
}
Оптимизированная NASM
section .text
    global hex_to_int

hex_to_int:
    xor eax, eax
    
    cmp word [rdi], 0x7830
    je .skip_prefix
    cmp word [rdi], 0x5830
    jne .convert
    
.skip_prefix:
    add rdi, 2
    
.convert:
    movzx ecx, byte [rdi]
    test cl, cl
    jz .done
    
    or cl, 0x20
    
    sub cl, '0'
    cmp cl, 9
    jbe .valid_digit
    
    sub cl, 'a' - '0' - 10
    cmp cl, 15
    ja .done
    
.valid_digit:
    shl eax, 4
    or al, cl
    
    inc rdi
    jmp .convert
    
.done:
    ret
Версия с таблицей поиска
section .data
align 16
hex_table:
    times '0' db 0xFF
    db 0,1,2,3,4,5,6,7,8,9
    times ('A'-'9'-1) db 0xFF
    db 10,11,12,13,14,15
    times ('a'-'F'-1) db 0xFF
    db 10,11,12,13,14,15
    times (256-'f'-1) db 0xFF

section .text
    global hex_to_int

hex_to_int:
    xor eax, eax
    
    cmp word [rdi], 0x7830
    je .skip
    cmp word [rdi], 0x5830
    jne .loop
.skip:
    add rdi, 2
    
.loop:
    movzx ecx, byte [rdi]
    test cl, cl
    jz .done
    
    lea rdx, [rel hex_table]
    movzx ecx, byte [rdx + rcx]
    cmp cl, 0xFF
    je .done
    
    shl eax, 4
    or al, cl
    
    inc rdi
    jmp .loop
    
.done:
    ret

Пояснение концепции

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

  • Бит 0x20 различает верхний и нижний регистр в ASCII
  • Для ‘A’ (0x41): 0x41 | 0x20 = 0x61 (‘a’)
  • Для цифр операция не меняет значение

Проверка префикса за одну операцию: cmp word [rdi], 0x7830 сравнивает два байта одновременно. 0x7830 — это “0x” в little-endian (младший байт первым).

Версия с таблицей поиска: Предвычисленный массив из 256 элементов, где каждый символ сопоставлен значению (0-15 или 0xFF для недопустимых). Один поиск в памяти вместо цепочки сравнений — самый быстрый метод для часто вызываемых функций.


Функция itoa

C-версия
char* itoa(int value, char* str, int base) {
    char* ptr = str;
    char* ptr1 = str;
    char tmp_char;
    int tmp_value;
    
    if (base < 2 || base > 36) {
        *str = '\0';
        return str;
    }
    
    if (value < 0 && base == 10) {
        *ptr++ = '-';
        value = -value;
        ptr1++;
    }
    
    do {
        tmp_value = value % base;
        *ptr++ = (tmp_value < 10) ? 
                 tmp_value + '0' : 
                 tmp_value - 10 + 'a';
        value /= base;
    } while (value);
    
    *ptr-- = '\0';
    
    while (ptr1 < ptr) {
        tmp_char = *ptr;
        *ptr-- = *ptr1;
        *ptr1++ = tmp_char;
    }
    
    return str;
}
Оптимизированная NASM
section .text
    global itoa

itoa:
    push rbx
    push r12
    push r13
    
    mov ecx, edx
    xor eax, eax
    cmp ecx, 2
    jl .invalid
    cmp ecx, 36
    jg .invalid
    
    mov r12, rsi
    mov eax, edi
    mov r13, rsi
    
    test eax, eax
    jns .positive
    cmp ecx, 10
    jne .positive
    
    mov byte [rsi], '-'
    inc rsi
    inc r13
    neg eax
    
.positive:
    mov rbx, rsi
    
.convert:
    xor edx, edx
    div ecx
    
    cmp edx, 10
    sbb r8d, r8d
    and r8d, 'a' - '0' - 10
    add edx, '0'
    add edx, r8d
    
    mov [rbx], dl
    inc rbx
    
    test eax, eax
    jnz .convert
    
    mov byte [rbx], 0
    dec rbx
    
.reverse:
    cmp r13, rbx
    jge .done
    
    mov al, [r13]
    mov dl, [rbx]
    mov [rbx], al
    mov [r13], dl
    
    inc r13
    dec rbx
    jmp .reverse
    
.invalid:
    mov byte [rsi], 0
    
.done:
    mov rax, r12
    pop r13
    pop r12
    pop rbx
    ret
Прямая NASM-версия
section .text
    global itoa

itoa:
    push rbp
    mov rbp, rsp
    push rbx
    push r12
    push r13
    
    mov ecx, edx
    cmp ecx, 2
    jl .invalid
    cmp ecx, 36
    jg .invalid
    
    mov r12, rsi
    mov eax, edi
    mov r13, rsi
    
    test eax, eax
    jns .positive
    cmp ecx, 10
    jne .positive
    
    mov byte [rsi], '-'
    inc rsi
    inc r13
    neg eax
    
.positive:
    mov rbx, rsi
    
.convert:
    xor edx, edx
    div ecx
    
    cmp edx, 10
    jl .digit
    
    add edx, 'a' - 10
    jmp .store
    
.digit:
    add edx, '0'
    
.store:
    mov [rbx], dl
    inc rbx
    
    test eax, eax
    jnz .convert
    
    mov byte [rbx], 0
    dec rbx
    
.reverse:
    cmp r13, rbx
    jge .done
    
    mov al, [r13]
    mov dl, [rbx]
    mov [rbx], al
    mov [r13], dl
    
    inc r13
    dec rbx
    jmp .reverse
    
.invalid:
    mov byte [rsi], 0
    
.done:
    mov rax, r12
    pop r13
    pop r12
    pop rbx
    pop rbp
    ret

Пояснение концепции

Алгоритм преобразования: Число преобразуется в строку в три этапа:

  1. Обработка знака — для отрицательных (только base 10) добавляем ‘-’
  2. Преобразование в обратном порядке — делим на base, остаток даёт очередную цифру справа налево
  3. Разворот строки — меняем порядок символов на правильный

Почему в обратном порядке: Деление на base естественным образом даёт цифры справа налево (младшие разряды первыми). Проще записать их так, затем развернуть, чем пытаться вычислить количество цифр заранее.

Оптимизация выбора символа через SBB: Прямая версия использует условный переход для выбора между цифрой (‘0’-‘9’) и буквой (‘a’-‘z’). Оптимизированная версия использует sbb (subtract with borrow):

  • sbb r8d, r8d создаёт маску: 0xFFFFFFFF если был borrow, иначе 0
  • and с маской даёт смещение для букв или 0 для цифр
  • Избегаем условного перехода на каждой цифре

Функция itoa_base10

C-версия
char* itoa_base10(int value, char* str) {
    char* ptr = str;
    char* ptr1 = str;
    char tmp_char;
    
    if (value < 0) {
        *ptr++ = '-';
        value = -value;
        ptr1++;
    }
    
    if (value == 0) {
        *ptr++ = '0';
        *ptr = '\0';
        return str;
    }
    
    while (value) {
        *ptr++ = '0' + (value % 10);
        value /= 10;
    }
    
    *ptr-- = '\0';
    
    while (ptr1 < ptr) {
        tmp_char = *ptr;
        *ptr-- = *ptr1;
        *ptr1++ = tmp_char;
    }
    
    return str;
}
Оптимизированная NASM
section .text
    global itoa_base10

itoa_base10:
    push rbx
    push r12
    
    mov eax, edi
    mov r12, rsi
    
    mov ecx, eax
    sar ecx, 31
    xor eax, ecx
    sub eax, ecx
    
    test ecx, ecx
    jz .positive
    
    mov byte [rsi], '-'
    inc rsi
    
.positive:
    mov rbx, rsi
    mov r8d, eax
    
    test eax, eax
    jnz .convert
    
    mov byte [rbx], '0'
    inc rbx
    mov byte [rbx], 0
    mov rax, r12
    pop r12
    pop rbx
    ret
    
.convert:
    mov eax, r8d
    mov edx, 0xCCCCCCCD
    mul edx
    shr edx, 3
    
    lea eax, [rdx + rdx*4]
    lea eax, [rax*2]
    sub r8d, eax
    
    add r8b, '0'
    mov [rbx], r8b
    inc rbx
    
    mov r8d, edx
    test r8d, r8d
    jnz .convert
    
    mov byte [rbx], 0
    dec rbx
    
.reverse:
    cmp rsi, rbx
    jge .done_reverse
    
    mov al, [rsi]
    mov dl, [rbx]
    mov [rbx], al
    mov [rsi], dl
    
    inc rsi
    dec rbx
    jmp .reverse
    
.done_reverse:
    mov rax, r12
    pop r12
    pop rbx
    ret

Пояснение концепции

Специализация для base 10: Вместо универсальной функции itoa создаём оптимизированную версию только для десятичной системы. Это позволяет применить быстрое деление.

Магическое число для деления на 10: Формула (n * 0xCCCCCCCD) >> 35 эквивалентна n / 10 для 32-битных чисел:

  • Это работает благодаря математике модульной арифметики
  • 0xCCCCCCCD — обратное к 10 по модулю 2^32
  • Умножение + сдвиг занимает 2-3 такта против 15-20 для div

Обработка знака через маску: Вместо условного перехода используем арифметический трюк для вычисления модуля:

mov ecx, eax
sar ecx, 31      ; ecx = маска знака (все 1 или все 0)
xor eax, ecx     ; Инверсия для отрицательных
sub eax, ecx     ; +1 для отрицательных = дополнение до двух

⌨️ 7. Ввод/вывод в консоли

Функция print_str

C-версия
#include <unistd.h>

void print_str(const char* str) {
    const char* end = str;
    while (*end) end++;
    
    size_t len = end - str;
    write(1, str, len);
}
Оптимизированная NASM
section .text
    global print_str

print_str:
    test rdi, rdi
    jz .done
    
    mov rsi, rdi
    
.find_end:
    cmp byte [rdi], 0
    je .print
    inc rdi
    jmp .find_end
    
.print:
    mov rdx, rdi
    sub rdx, rsi
    jz .done
    
    mov rax, 1
    mov rdi, 1
    syscall
    
.done:
    ret

Пояснение концепции

Системный вызов write: Простейшая обёртка над syscall write(1, str, len). В x86-64 Linux параметры передаются через регистры:

  • rax = 1 — номер системного вызова write
  • rdi = 1 — файловый дескриптор (STDOUT)
  • rsi — указатель на данные
  • rdx — количество байт

Встроенное вычисление длины: Функция сама находит нулевой терминатор, вычисляет длину через указательную арифметику и вызывает syscall.


Функция print_int

C-версия
#include <unistd.h>

void print_int(int num) {
    char buffer[12];
    int i = 0;
    int is_negative = 0;
    
    if (num < 0) {
        is_negative = 1;
        num = -num;
    }
    
    if (num == 0) {
        buffer[i++] = '0';
    } else {
        while (num > 0) {
            buffer[i++] = '0' + (num % 10);
            num /= 10;
        }
    }
    
    if (is_negative) {
        buffer[i++] = '-';
    }
    
    for (int j = i - 1; j >= 0; j--) {
        write(1, &buffer[j], 1);
    }
}
Оптимизированная NASM
section .bss
    buffer resb 12

section .text
    global print_int

print_int:
    push rbx
    
    mov eax, edi
    lea rbx, [rel buffer + 11]
    mov byte [rbx], 10
    dec rbx
    
    mov ecx, eax
    sar ecx, 31
    xor eax, ecx
    sub eax, ecx
    
    mov r8d, eax
    
.convert:
    mov eax, r8d
    mov edx, 0xCCCCCCCD
    mul edx
    shr edx, 3
    
    lea eax, [rdx + rdx*4]
    lea eax, [rax*2]
    sub r8d, eax
    
    add r8b, '0'
    mov [rbx], r8b
    dec rbx
    
    mov r8d, edx
    test r8d, r8d
    jnz .convert
    
    test ecx, ecx
    jz .print
    mov byte [rbx], '-'
    dec rbx
    
.print:
    inc rbx
    
    lea rdx, [rel buffer + 12]
    sub rdx, rbx
    
    mov rax, 1
    mov rdi, 1
    mov rsi, rbx
    syscall
    
    pop rbx
    ret
Прямая NASM-версия
section .bss
    buffer resb 12

section .text
    global print_int

print_int:
    push rbp
    mov rbp, rsp
    push rbx
    
    mov eax, edi
    lea rbx, [rel buffer + 11]
    mov byte [rbx], 10
    dec rbx
    
    test eax, eax
    jns .positive
    
    neg eax
    mov r9d, 1
    jmp .convert
    
.positive:
    xor r9d, r9d
    
.convert:
    xor edx, edx
    mov ecx, 10
    div ecx
    
    add dl, '0'
    mov [rbx], dl
    dec rbx
    
    test eax, eax
    jnz .convert
    
    test r9d, r9d
    jz .print
    mov byte [rbx], '-'
    dec rbx
    
.print:
    inc rbx
    
    lea rdx, [rel buffer + 12]
    sub rdx, rbx
    
    mov rax, 1
    mov rdi, 1
    mov rsi, rbx
    syscall
    
    pop rbx
    pop rbp
    ret

Пояснение концепции

Быстрое деление на 10: Прямая версия использует инструкцию div, которая занимает 15-20 тактов. Оптимизированная версия применяет магическую константу: экономия ~10-15 тактов на каждую цифру.

Обратное заполнение буфера: Цифры записываются справа налево, начиная с конца буфера. Это естественно для алгоритма деления и избегает необходимости разворота.


Функция read_int

C-версия
#include <unistd.h>

int read_int(void) {
    char buffer[32];
    int bytes_read = read(0, buffer, 31);
    
    if (bytes_read <= 0) return 0;
    
    buffer[bytes_read] = '\0';
    
    int result = 0;
    int sign = 1;
    int i = 0;
    
    while (buffer[i] == ' ' || 
           buffer[i] == '\t' || 
           buffer[i] == '\n') {
        i++;
    }
    
    if (buffer[i] == '-') {
        sign = -1;
        i++;
    } else if (buffer[i] == '+') {
        i++;
    }
    
    while (buffer[i] >= '0' && 
           buffer[i] <= '9') {
        result = result * 10 + 
                 (buffer[i] - '0');
        i++;
    }
    
    return result * sign;
}
Оптимизированная NASM
section .bss
    buffer resb 32

section .text
    global read_int

read_int:
    xor eax, eax
    xor edi, edi
    lea rsi, [rel buffer]
    mov edx, 31
    syscall
    
    test eax, eax
    jle .error
    
    mov byte [rsi + rax], 0
    
    xor eax, eax
    xor ecx, ecx
    
.skip_ws:
    lodsb
    cmp al, ' '
    je .skip_ws
    cmp al, 9
    je .skip_ws
    cmp al, 10
    je .skip_ws
    
    cmp al, '-'
    jne .check_plus
    mov ecx, 1
    lodsb
    jmp .parse
    
.check_plus:
    cmp al, '+'
    jne .parse
    lodsb
    
.parse:
    sub al, '0'
    cmp al, 9
    ja .apply_sign
    
    lea edx, [rax*4 + rax]
    lea eax, [rdx*2 + rax]
    
    lodsb
    jmp .parse
    
.apply_sign:
    test ecx, ecx
    jz .done
    neg eax
    
.done:
    ret
    
.error:
    xor eax, eax
    ret

Пояснение концепции

Чтение с консоли: Системный вызов read(0, buffer, 31) читает до 31 байта из STDIN. Возвращаемое значение — количество прочитанных байт или -1 при ошибке.

Использование LODSB: Инструкция lodsb автоматически загружает байт из [rsi] в al и инкрементирует rsi. Это экономит инструкцию inc rsi на каждой итерации.


Функция print_hex

C-версия
#include <unistd.h>

void print_hex(unsigned int num) {
    char buffer[11] = "0x";
    int i = 2;
    
    if (num == 0) {
        buffer[i++] = '0';
    } else {
        int shift = 28;
        while (shift >= 0 && 
               ((num >> shift) & 0xF) == 0) {
            shift -= 4;
        }
        
        while (shift >= 0) {
            int digit = (num >> shift) & 0xF;
            if (digit < 10) {
                buffer[i++] = '0' + digit;
            } else {
                buffer[i++] = 'a' + digit - 10;
            }
            shift -= 4;
        }
    }
    
    buffer[i++] = '\n';
    write(1, buffer, i);
}
Оптимизированная NASM
section .bss
    buffer resb 11

section .text
    global print_hex

print_hex:
    push rbx
    
    mov eax, edi
    lea rbx, [rel buffer]
    
    mov word [rbx], 0x7830
    
    bsr ecx, eax
    jz .zero
    
    or ecx, 3
    inc ecx
    
    shr ecx, 2
    mov edx, ecx
    add edx, 3
    
    add rbx, 2
    
.convert:
    dec ecx
    mov r9d, ecx
    shl r9d, 2
    mov r10d, eax
    shr r10d, cl
    and r10d, 0xF
    
    cmp r10d, 10
    sbb r8d, r8d
    and r8d, 'a' - '0' - 10
    add r10d, '0'
    add r10d, r8d
    
    mov [rbx], r10b
    inc rbx
    
    test ecx, ecx
    jnz .convert
    
    mov byte [rbx], 10
    
    mov rax, 1
    mov rdi, 1
    lea rsi, [rel buffer]
    syscall
    
    pop rbx
    ret
    
.zero:
    mov byte [rbx + 2], '0'
    mov byte [rbx + 3], 10
    
    mov rax, 1
    mov rdi, 1
    lea rsi, [rel buffer]
    mov rdx, 4
    syscall
    
    pop rbx
    ret
Версия с фиксированным форматом
section .bss
    buffer resb 11

section .text
    global print_hex_fast

print_hex_fast:
    push rbx
    
    mov eax, edi
    lea rbx, [rel buffer]
    
    mov word [rbx], 0x7830
    add rbx, 2
    
    mov ecx, 8
    
.unroll:
    rol eax, 4
    movzx edx, al
    and edx, 0xF
    
    cmp edx, 10
    sbb r8d, r8d
    and r8d, 'a' - '0' - 10
    add edx, '0'
    add edx, r8d
    
    mov [rbx], dl
    inc rbx
    
    dec ecx
    jnz .unroll
    
    mov byte [rbx], 10
    
    mov rax, 1
    mov rdi, 1
    lea rsi, [rel buffer]
    mov rdx, 11
    syscall
    
    pop rbx
    ret

Пояснение концепции

Инструкция BSR: bsr (bit scan reverse) находит позицию старшего установленного бита. Используем её для определения количества значащих hex-цифр без ведущих нулей.

Извлечение nibble: Сдвигаем число вправо на нужное количество бит, затем маскируем младшие 4 бита через and r10d, 0xF.

Версия с фиксированным форматом: Всегда выводит 8 hex-цифр (0x00000000). Использует rol (rotate left) для циклического извлечения nibbles.


Функция prompt_and_read_int

C-версия
#include <unistd.h>

int prompt_and_read_int(const char* prompt) {
    const char* end = prompt;
    while (*end) end++;
    size_t len = end - prompt;
    
    write(1, prompt, len);
    
    char buffer[32];
    int bytes_read = read(0, buffer, 31);
    
    if (bytes_read <= 0) {
        return 0;
    }
    
    buffer[bytes_read] = '\0';
    
    int result = 0;
    int sign = 1;
    int i = 0;
    
    while (buffer[i] == ' ' || 
           buffer[i] == '\t' || 
           buffer[i] == '\n') {
        i++;
    }
    
    if (buffer[i] == '-') {
        sign = -1;
        i++;
    } else if (buffer[i] == '+') {
        i++;
    }
    
    while (buffer[i] >= '0' && 
           buffer[i] <= '9') {
        result = result * 10 + 
                 (buffer[i] - '0');
        i++;
    }
    
    return result * sign;
}
Оптимизированная NASM
section .bss
    buffer resb 32

section .text
    global prompt_and_read_int

prompt_and_read_int:
    push rbx
    
    mov rbx, rdi
    
    xor ecx, ecx
.find_len:
    cmp byte [rdi + rcx], 0
    je .print_prompt
    inc ecx
    jmp .find_len
    
.print_prompt:
    mov eax, 1
    push 1
    pop rdi
    mov rsi, rbx
    mov edx, ecx
    syscall
    
    xor eax, eax
    xor edi, edi
    lea rsi, [rel buffer]
    mov edx, 31
    syscall
    
    test eax, eax
    jle .error
    
    mov byte [rsi + rax], 0
    
    xor eax, eax
    xor ecx, ecx
    
.skip_ws:
    lodsb
    cmp al, ' '
    je .skip_ws
    cmp al, 9
    je .skip_ws
    cmp al, 10
    je .skip_ws
    
    cmp al, '-'
    jne .check_plus
    mov ecx, 1
    lodsb
    jmp .parse
    
.check_plus:
    cmp al, '+'
    jne .parse
    lodsb
    
.parse:
    sub al, '0'
    cmp al, 9
    ja .apply_sign
    
    lea edx, [rax*4 + rax]
    lea eax, [rdx*2 + rax]
    
    lodsb
    jmp .parse
    
.apply_sign:
    test ecx, ecx
    jz .done
    neg eax
    
.done:
    pop rbx
    ret
    
.error:
    xor eax, eax
    pop rbx
    ret

Пояснение концепции

Комбинированная функция: Объединяет вывод строки-приглашения и чтение числа в одну удобную функцию. Типичное использование в интерактивных программах.

Повторное использование кода: Внутри применяется логика из print_str и read_int, но объединённая в единый интерфейс.

📂 8. Работа с файлами

Основные операции

Системные вызовы

Открытие файла:

int fd = open("file.txt", O_RDONLY);

Константы для флагов:

  • O_RDONLY = 0 — только чтение
  • O_WRONLY = 1 — только запись
  • O_RDWR = 2 — чтение и запись
  • O_CREAT = 64 — создать если не существует
  • O_TRUNC = 512 — обрезать до нуля
  • O_APPEND = 1024 — добавлять в конец
Реализация в NASM
section .text
    global open_file
    global read_from_file
    global write_to_file
    global close_file

open_file:
    mov rax, 2
    syscall
    ret

read_from_file:
    xor eax, eax
    syscall
    ret

write_to_file:
    mov rax, 1
    syscall
    ret

close_file:
    mov rax, 3
    syscall
    ret

Функция copy_file

C-версия
#include <fcntl.h>
#include <unistd.h>

int copy_file(const char* src, 
              const char* dest) {
    char buffer[4096];
    
    int src_fd = open(src, O_RDONLY);
    if (src_fd < 0) return -1;
    
    int dest_fd = open(dest, 
                       O_WRONLY | O_CREAT | O_TRUNC, 
                       0644);
    if (dest_fd < 0) {
        close(src_fd);
        return -1;
    }
    
    ssize_t bytes_read;
    while ((bytes_read = read(src_fd, 
                              buffer, 
                              sizeof(buffer))) > 0) {
        ssize_t bytes_written = 
            write(dest_fd, buffer, bytes_read);
        if (bytes_written != bytes_read) {
            close(src_fd);
            close(dest_fd);
            return -1;
        }
    }
    
    close(src_fd);
    close(dest_fd);
    
    return (bytes_read < 0) ? -1 : 0;
}
Оптимизированная NASM
section .bss
    buffer resb 4096

section .text
    global copy_file

copy_file:
    push rbx
    push r12
    push r13
    
    mov rax, 2
    xor esi, esi
    syscall
    
    test eax, eax
    js .error
    
    mov ebx, eax
    
    mov rax, 2
    mov rdi, [rsp + 24]
    mov esi, 0x241
    mov edx, 0x1A4
    syscall
    
    test eax, eax
    js .close_src
    
    mov r12d, eax
    lea r13, [rel buffer]
    
.loop:
    xor eax, eax
    mov edi, ebx
    mov rsi, r13
    mov edx, 4096
    syscall
    
    test eax, eax
    jle .success
    
    mov edx, eax
    mov eax, 1
    mov edi, r12d
    mov rsi, r13
    syscall
    
    cmp eax, edx
    je .loop
    
    mov eax, 3
    mov edi, r12d
    syscall
    
.close_src:
    mov eax, 3
    mov edi, ebx
    syscall
    
.error:
    or eax, -1
    pop r13
    pop r12
    pop rbx
    ret
    
.success:
    mov eax, 3
    mov edi, ebx
    syscall
    
    mov eax, 3
    mov edi, r12d
    syscall
    
    xor eax, eax
    pop r13
    pop r12
    pop rbx
    ret

Пояснение концепции

Блочное копирование: Эффективное копирование файлов использует промежуточный буфер. Читаем блоками по 4KB (стандартный размер блока файловой системы), затем записываем каждый блок.

Обработка частичных операций: Системные вызовы read/write могут обработать меньше байт, чем запрошено. Поэтому проверяем bytes_written == bytes_read.

Корректное освобождение ресурсов: При любой ошибке обязательно закрываем открытые файловые дескрипторы.


Функция get_file_size

C-версия
#include <sys/stat.h>

off_t get_file_size(const char* filename) {
    struct stat st;
    
    if (stat(filename, &st) < 0) {
        return -1;
    }
    
    return st.st_size;
}
Оптимизированная NASM
section .text
    global get_file_size

get_file_size:
    sub rsp, 144
    
    mov eax, 4
    mov rsi, rsp
    syscall
    
    test eax, eax
    js .error
    
    mov rax, [rsp + 48]
    add rsp, 144
    ret
    
.error:
    add rsp, 144
    or rax, -1
    ret

Пояснение концепции

Системный вызов stat: Получает метаданные файла без его открытия. Поле st_size находится по смещению 48 байт в структуре stat.

Преимущество над lseek: Не требует открытия файла, выполняется одним системным вызовом вместо трёх (open + lseek + close).


Функция read_entire_file

C-версия
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

ssize_t read_entire_file(const char* filename, 
                         char** buffer) {
    *buffer = NULL;
    
    int fd = open(filename, O_RDONLY);
    if (fd < 0) return -1;
    
    off_t size = lseek(fd, 0, SEEK_END);
    if (size < 0) {
        close(fd);
        return -1;
    }
    
    lseek(fd, 0, SEEK_SET);
    
    *buffer = (char*)malloc(size + 1);
    if (*buffer == NULL) {
        close(fd);
        return -1;
    }
    
    ssize_t total_read = 0;
    while (total_read < size) {
        ssize_t bytes = read(fd, 
                            *buffer + total_read, 
                            size - total_read);
        if (bytes <= 0) {
            free(*buffer);
            *buffer = NULL;
            close(fd);
            return -1;
        }
        total_read += bytes;
    }
    
    (*buffer)[size] = '\0';
    close(fd);
    
    return size;
}
Оптимизированная NASM
section .text
    global read_entire_file
    extern malloc
    extern free

read_entire_file:
    push rbx
    push r12
    push r13
    push r14
    
    mov r12, rsi
    mov qword [r12], 0
    
    mov eax, 2
    xor esi, esi
    syscall
    
    test eax, eax
    js .error
    mov ebx, eax
    
    mov eax, 8
    mov edi, ebx
    xor esi, esi
    mov edx, 2
    syscall
    
    test rax, rax
    js .close_err
    mov r13, rax
    
    lea rdi, [rax + 1]
    call malloc
    
    test rax, rax
    jz .close_err
    
    mov [r12], rax
    mov r14, rax
    
    mov eax, 17
    mov edi, ebx
    mov rsi, r14
    mov rdx, r13
    xor r8, r8
    syscall
    
    cmp rax, r13
    jne .free_err
    
    mov byte [r14 + r13], 0
    
    mov eax, 3
    mov edi, ebx
    syscall
    
    mov rax, r13
    pop r14
    pop r13
    pop r12
    pop rbx
    ret
    
.free_err:
    mov rdi, r14
    call free
    mov qword [r12], 0
    
.close_err:
    mov eax, 3
    mov edi, ebx
    syscall
    
.error:
    or rax, -1
    pop r14
    pop r13
    pop r12
    pop rbx
    ret

Пояснение концепции

Чтение всего файла в память: Загружаем файл целиком для обработки. Алгоритм:

  1. Открываем файл
  2. Узнаём размер через lseek(fd, 0, SEEK_END)
  3. Выделяем память через malloc(size + 1)
  4. Читаем данные через pread (syscall 17)
  5. Добавляем ‘\0’ в конец

Преимущество pread: Атомарная операция чтения с указанной позиции — один системный вызов вместо двух (lseek + read).


Функция write_entire_file

C-версия
#include <fcntl.h>
#include <unistd.h>

ssize_t write_entire_file(const char* filename, 
                          const char* buffer, 
                          size_t size) {
    int fd = open(filename, 
                  O_WRONLY | O_CREAT | O_TRUNC, 
                  0644);
    if (fd < 0) return -1;
    
    ssize_t total_written = 0;
    while (total_written < size) {
        ssize_t bytes = write(fd, 
                             buffer + total_written, 
                             size - total_written);
        if (bytes <= 0) {
            close(fd);
            return -1;
        }
        total_written += bytes;
    }
    
    close(fd);
    return total_written;
}
Оптимизированная NASM
section .text
    global write_entire_file

write_entire_file:
    push rbx
    push r12
    push r13
    
    mov r12, rsi
    mov r13, rdx
    
    mov eax, 2
    mov esi, 0x241
    mov edx, 0x1A4
    syscall
    
    test eax, eax
    js .error
    mov ebx, eax
    
    mov eax, 1
    mov edi, ebx
    mov rsi, r12
    mov rdx, r13
    syscall
    
    cmp rax, r13
    jne .partial_write
    
    mov r8, rax
    jmp .close_file
    
.partial_write:
    test rax, rax
    jle .close_err
    
    mov r8, rax
    
.write_loop:
    cmp r8, r13
    jge .close_file
    
    mov eax, 1
    mov edi, ebx
    lea rsi, [r12 + r8]
    mov rdx, r13
    sub rdx, r8
    syscall
    
    test rax, rax
    jle .close_err
    
    add r8, rax
    jmp .write_loop
    
.close_file:
    mov eax, 3
    mov edi, ebx
    syscall
    
    mov rax, r8
    pop r13
    pop r12
    pop rbx
    ret
    
.close_err:
    mov eax, 3
    mov edi, ebx
    syscall
    
.error:
    or rax, -1
    pop r13
    pop r12
    pop rbx
    ret

Пояснение концепции

Флаги open: O_WRONLY | O_CREAT | O_TRUNC создаёт/перезаписывает файл. Внимание: все данные в существующем файле будут потеряны!

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


Функция append_to_file

C-версия
#include <fcntl.h>
#include <unistd.h>

ssize_t append_to_file(const char* filename, 
                       const char* buffer, 
                       size_t size) {
    int fd = open(filename, 
                  O_WRONLY | O_CREAT | O_APPEND, 
                  0644);
    if (fd < 0) return -1;
    
    ssize_t total_written = 0;
    while (total_written < size) {
        ssize_t bytes = write(fd, 
                             buffer + total_written, 
                             size - total_written);
        if (bytes <= 0) {
            close(fd);
            return -1;
        }
        total_written += bytes;
    }
    
    close(fd);
    return total_written;
}
Оптимизированная NASM
section .text
    global append_to_file

append_to_file:
    push rbx
    push r12
    push r13
    
    mov r12, rsi
    mov r13, rdx
    
    mov eax, 2
    mov esi, 0x441
    mov edx, 0x1A4
    syscall
    
    test eax, eax
    js .error
    mov ebx, eax
    
    mov eax, 1
    mov edi, ebx
    mov rsi, r12
    mov rdx, r13
    syscall
    
    mov r8, rax
    cmp rax, r13
    je .close
    
    test rax, rax
    jle .close_err
    
.loop:
    cmp r8, r13
    jge .close
    
    mov eax, 1
    mov edi, ebx
    lea rsi, [r12 + r8]
    mov rdx, r13
    sub rdx, r8
    syscall
    
    test rax, rax
    jle .close_err
    add r8, rax
    jmp .loop
    
.close:
    mov eax, 3
    mov edi, ebx
    syscall
    
    mov rax, r8
    pop r13
    pop r12
    pop rbx
    ret
    
.close_err:
    mov eax, 3
    mov edi, ebx
    syscall
    
.error:
    or rax, -1
    pop r13
    pop r12
    pop rbx
    ret

Пояснение концепции

Флаг O_APPEND: Гарантирует атомарное добавление в конец файла. Ядро Linux автоматически устанавливает позицию записи в конец перед каждой операцией write.

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


Функция file_exists

C-версия
#include <unistd.h>

int file_exists(const char* filename) {
    return access(filename, F_OK) == 0 ? 1 : 0;
}
Оптимизированная NASM
section .text
    global file_exists

file_exists:
    mov eax, 21
    xor esi, esi
    syscall
    
    test eax, eax
    setz al
    movzx eax, al
    ret
Функции проверки прав доступа
section .text
    global file_exists
    global file_readable
    global file_writable
    global file_executable

file_exists:
    xor esi, esi
    jmp check_access

file_readable:
    mov esi, 4
    jmp check_access

file_writable:
    mov esi, 2
    jmp check_access

file_executable:
    mov esi, 1

check_access:
    mov eax, 21
    syscall
    
    test eax, eax
    setz al
    movzx eax, al
    ret

Пояснение концепции

Системный вызов access: Проверяет доступность файла без его открытия. Режимы: F_OK (существование), R_OK (чтение), W_OK (запись), X_OK (выполнение).

Преобразование результата: access возвращает 0 при успехе, -1 при ошибке. Преобразуем в boolean через setz (set if zero).

🧠 9. Расширенные примеры

Функция power

C-версия
long long power(long long base, 
                unsigned int exp) {
    long long result = 1;
    
    while (exp > 0) {
        if (exp & 1) {
            result *= base;
        }
        base *= base;
        exp >>= 1;
    }
    
    return result;
}
Оптимизированная NASM
section .text
    global power

power:
    mov rax, 1
    test esi, esi
    jz .done
    
.loop:
    shr esi, 1
    jnc .no_mult
    
    imul rax, rdi
    
.no_mult:
    jz .done
    
    imul rdi, rdi
    jmp .loop
    
.done:
    ret
Рекурсивная версия
section .text
    global power_recursive

power_recursive:
    test esi, esi
    jz .base_case
    
    push rbx
    push r12
    
    mov rbx, rdi
    mov r12d, esi
    
    shr esi, 1
    call power_recursive
    
    imul rax, rax
    
    test r12d, 1
    jz .even
    
    imul rax, rbx
    
.even:
    pop r12
    pop rbx
    ret
    
.base_case:
    mov rax, 1
    ret

Пояснение концепции

Бинарное возведение в степень: Алгоритм основан на представлении показателя в двоичной системе. Например, x^13 = x^(1101₂) = x^8 * x^4 * x^1.

Использование флага переноса: Инструкция shr сдвигает младший бит в флаг CF. jnc проверяет этот флаг без дополнительной инструкции.

Сложность O(log n): Выполняем log₂(n) итераций вместо n умножений.


Функция factorial

C-версия
unsigned long long factorial(unsigned int n) {
    unsigned long long result = 1;
    
    for (unsigned int i = 2; i <= n; i++) {
        result *= i;
    }
    
    return result;
}
Оптимизированная NASM
section .text
    global factorial

factorial:
    cmp edi, 1
    mov rax, 1
    jbe .done
    
    mov ecx, edi
    
.loop:
    imul rax, rcx
    dec ecx
    jnz .loop
    
.done:
    ret

Пояснение концепции

Обратный отсчёт: Умножаем сверху вниз (от n до 1), что естественнее сочетается с проверкой флага нуля от dec.

Граница переполнения: factorial(20) = 2432902008176640000 — последнее значение в 64 битах.

📈 10. Комплексный пример: Программа подсчёта слов

section .data
    msg_usage db "Usage: wordcount <filename>", 10, 0
    msg_not_found db "Error: File not found", 10, 0
    msg_read_error db "Error: Cannot read file", 10, 0
    msg_result db "Words: ", 0
    msg_newline db 10, 0

section .bss
    file_buffer resq 1
    file_size resq 1
    word_count resq 1

section .text
    global _start
    extern file_exists
    extern read_entire_file
    extern print_str
    extern print_int
    extern free

_start:
    pop rax
    cmp rax, 2
    jne .usage
    
    pop rax
    pop rdi
    
    push rdi
    
    call file_exists
    test eax, eax
    jz .file_not_found
    
    pop rdi
    push rdi
    
    lea rsi, [rel file_buffer]
    call read_entire_file
    
    test rax, rax
    js .read_error
    
    mov [rel file_size], rax
    
    mov rsi, [rel file_buffer]
    mov rcx, [rel file_size]
    call count_words
    
    mov [rel word_count], rax
    
    lea rdi, [rel msg_result]
    call print_str
    
    mov rdi, [rel word_count]
    call print_int
    
    lea rdi, [rel msg_newline]
    call print_str
    
    mov rdi, [rel file_buffer]
    call free
    
    xor edi, edi
    jmp exit_program

.usage:
    lea rdi, [rel msg_usage]
    call print_str
    mov edi, 1
    jmp exit_program

.file_not_found:
    pop rdi
    lea rdi, [rel msg_not_found]
    call print_str
    mov edi, 1
    jmp exit_program

.read_error:
    pop rdi
    lea rdi, [rel msg_read_error]
    call print_str
    mov edi, 1
    jmp exit_program

exit_program:
    mov rax, 60
    syscall

count_words:
    xor eax, eax
    xor edx, edx
    
    test rcx, rcx
    jz .done
    
.loop:
    movzx ebx, byte [rsi]
    
    cmp bl, ' '
    je .whitespace
    cmp bl, 9
    je .whitespace
    cmp bl, 10
    je .whitespace
    cmp bl, 13
    je .whitespace
    
    test edx, edx
    jnz .continue
    
    inc eax
    mov edx, 1
    jmp .continue
    
.whitespace:
    xor edx, edx
    
.continue:
    inc rsi
    dec rcx
    jnz .loop
    
.done:
    ret

Пояснение программы подсчёта слов

Обработка аргументов командной строки: При запуске стек содержит argc и argv. Используем pop для извлечения имени файла.

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

Алгоритм подсчёта слов: Конечный автомат с двумя состояниями (в слове / в пробелах). При переходе из пробелов в символы инкрементируем счётчик.

🧮 11. Интерактивная программа-калькулятор

section .data
    msg_title db "Simple Calculator", 10
    msg_separator db "=================", 10, 10, 0
    msg_first db "Enter first number: ", 0
    msg_second db "Enter second number: ", 0
    msg_results db 10, "Results:", 10, "--------", 10, 0
    msg_sum db "Sum: ", 0
    msg_diff db "Difference: ", 0
    msg_prod db "Product: ", 0
    msg_quot db "Quotient: ", 0
    msg_div_zero db "Cannot divide by zero", 10, 0
    msg_newline db 10, 0

section .bss
    num1 resd 1
    num2 resd 1

section .text
    global _start
    extern print_str
    extern read_int
    extern print_int

_start:
    lea rdi, [rel msg_title]
    call print_str
    
    lea rdi, [rel msg_separator]
    call print_str
    
    lea rdi, [rel msg_first]
    call print_str
    call read_int
    mov [rel num1], eax
    
    lea rdi, [rel msg_second]
    call print_str
    call read_int
    mov [rel num2], eax
    
    lea rdi, [rel msg_results]
    call print_str
    
    lea rdi, [rel msg_sum]
    call print_str
    
    mov eax, [rel num1]
    add eax, [rel num2]
    mov edi, eax
    call print_int
    
    lea rdi, [rel msg_newline]
    call print_str
    
    lea rdi, [rel msg_diff]
    call print_str
    
    mov eax, [rel num1]
    sub eax, [rel num2]
    mov edi, eax
    call print_int
    
    lea rdi, [rel msg_newline]
    call print_str
    
    lea rdi, [rel msg_prod]
    call print_str
    
    mov eax, [rel num1]
    imul eax, [rel num2]
    mov edi, eax
    call print_int
    
    lea rdi, [rel msg_newline]
    call print_str
    
    mov eax, [rel num2]
    test eax, eax
    jz .div_by_zero
    
    lea rdi, [rel msg_quot]
    call print_str
    
    mov eax, [rel num1]
    cdq
    idiv dword [rel num2]
    mov edi, eax
    call print_int
    
    lea rdi, [rel msg_newline]
    call print_str
    
    jmp .exit
    
.div_by_zero:
    lea rdi, [rel msg_div_zero]
    call print_str
    
.exit:
    mov rax, 60
    xor edi, edi
    syscall

Пояснение программы калькулятор

Структура интерактивной программы: Типичная последовательность: вывод приглашения, чтение ввода, обработка, вывод результата.

Инструкция CDQ: Расширяет eax до edx:eax для знакового деления. Копирует знаковый бит eax во все биты edx.

Проверка деления на ноль: Критически важна перед idiv. Деление на ноль вызывает SIGFPE, что аварийно завершает программу.

✅ Заключение

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

СОГЛАШЕНИЯ О ВЫЗОВАХ

  • x86-64 System V ABI определяет порядок параметров
  • Сохранение регистров предотвращает ошибки
  • rbx, rbp, r12-r15 должны восстанавливаться

ОПТИМИЗАЦИЯ

  • lea для арифметики быстрее imul
  • cmov устраняет ветвления
  • SIMD для параллельной обработки данных
  • Быстрое деление через магические константы

СИСТЕМНЫЕ ВЫЗОВЫ

  • Минимальные обертки над syscalls
  • Прямой доступ к ядру Linux
  • Проверка ошибок через возвращаемое значение
Практические рекомендации

ДЛЯ НАЧИНАЮЩИХ:

  1. Начинайте с прямых версий
  2. Используйте отладчик (GDB)
  3. Сравнивайте с C-кодом

ДЛЯ ПРОДВИНУТЫХ:

  1. Изучайте таблицы инструкций Intel/AMD
  2. Измеряйте производительность
  3. Используйте профилировщики (perf, valgrind)

ИНСТРУМЕНТЫ:

# Компиляция
nasm -f elf64 -g -F dwarf file.asm
ld file.o -o program

# Отладка
gdb ./program

# Дизассемблирование
objdump -d -M intel program
💜

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

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

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