Введение
Эта статья представляет собой практическое руководство по преобразованию C-функций в ассемблерный код NASM для 64-битного Linux. Для каждой функции будет рассмотрено:
- C-версию — исходный код на C
- Прямую NASM-версию — буквальный перевод на ассемблер
- Оптимизированную NASM-версию — улучшенный вариант с использованием возможностей архитектуры
- Пояснения — концептуальное объяснение идей и подходов
Основы соглашений о вызовах (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
int add(int a, int b) {
return a + b;
}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
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;
}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
int max(int a, int b) {
if (a > b) {
return a;
}
return b;
}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
int clamp(int value, int min, int max) {
if (value < min) return min;
if (value > max) return max;
return value;
}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: Оптимизированная версия применяет два условных перемещения:
- Предполагаем
value < min, загружаемmin - Если
value >= min, заменяем наvalue - Сравниваем результат с
max - Если результат
> max, заменяем наmax
Преимущество безветвевого кода: В цикле с непредсказуемыми данными эта версия даёт стабильную производительность, так как процессор не тратит ресурсы на предсказание ветвлений.
🔁 3. Циклы
Функция sum_array
int sum_array(int* array, int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += array[i];
}
return sum;
}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
int find_element(int* array, int size, int value) {
for (int i = 0; i < size; i++) {
if (array[i] == value) {
return i;
}
}
return -1;
}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
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;
}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-битные пересылки используют возможности шины данных полностью.
Двухфазный алгоритм:
- Основная фаза: копируем максимальное количество полных 8-байтовых блоков
- Завершающая фаза: копируем оставшиеся 0-7 байт побайтно
Вычисление остатка: and r8, 7 эквивалентно size % 8, но выполняется за 1 такт вместо десятков для операции деления.
📝 5. Строковые функции
Функция my_strlen
size_t my_strlen(const char* str) {
size_t len = 0;
while (str[len] != '\0') {
len++;
}
return len;
}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
retSIMD-версия
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
int my_strcmp(const char* s1, const char* s2) {
while (*s1 && (*s1 == *s2)) {
s1++;
s2++;
}
return (unsigned char)*s1 -
(unsigned char)*s2;
}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
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;
}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 * 5lea 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
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;
}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Пояснение концепции
Типы ошибок: Функция различает три класса ошибок:
- NULL-указатель (error = 1) — защита от segfault
- Отсутствие цифр (error = 2) — строки типа "" или “-” некорректны
- Переполнение (error = 3) — результат не помещается в int32
Проверка переполнения в процессе: Используем 64-битную арифметику для накопления результата, проверяя границы INT_MAX на каждой итерации. Это позволяет обнаружить переполнение до того, как оно произойдёт.
Оптимизация проверки знака через SETE: Вместо нескольких условных переходов используем инструкцию sete (set if equal):
- Создаём две маски: для ‘-’ и ‘+’
- Объединяем через
or - Получаем единый флаг наличия знака
Функция hex_to_int
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;
}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
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;
}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
Пояснение концепции
Алгоритм преобразования: Число преобразуется в строку в три этапа:
- Обработка знака — для отрицательных (только base 10) добавляем ‘-’
- Преобразование в обратном порядке — делим на base, остаток даёт очередную цифру справа налево
- Разворот строки — меняем порядок символов на правильный
Почему в обратном порядке: Деление на base естественным образом даёт цифры справа налево (младшие разряды первыми). Проще записать их так, затем развернуть, чем пытаться вычислить количество цифр заранее.
Оптимизация выбора символа через SBB: Прямая версия использует условный переход для выбора между цифрой (‘0’-‘9’) и буквой (‘a’-‘z’). Оптимизированная версия использует sbb (subtract with borrow):
sbb r8d, r8dсоздаёт маску: 0xFFFFFFFF если был borrow, иначе 0andс маской даёт смещение для букв или 0 для цифр- Избегаем условного перехода на каждой цифре
Функция itoa_base10
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;
}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
#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);
}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— номер системного вызова writerdi = 1— файловый дескриптор (STDOUT)rsi— указатель на данныеrdx— количество байт
Встроенное вычисление длины: Функция сама находит нулевой терминатор, вычисляет длину через указательную арифметику и вызывает syscall.
Функция print_int
#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);
}
}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
#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;
}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
#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);
}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
#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;
}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— добавлять в конец
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
#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;
}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
#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;
}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
#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;
}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Пояснение концепции
Чтение всего файла в память: Загружаем файл целиком для обработки. Алгоритм:
- Открываем файл
- Узнаём размер через
lseek(fd, 0, SEEK_END) - Выделяем память через
malloc(size + 1) - Читаем данные через
pread(syscall 17) - Добавляем ‘\0’ в конец
Преимущество pread: Атомарная операция чтения с указанной позиции — один системный вызов вместо двух (lseek + read).
Функция write_entire_file
#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;
}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
#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;
}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
#include <unistd.h>
int file_exists(const char* filename) {
return access(filename, F_OK) == 0 ? 1 : 0;
}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
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;
}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
unsigned long long factorial(unsigned int n) {
unsigned long long result = 1;
for (unsigned int i = 2; i <= n; i++) {
result *= i;
}
return result;
}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для арифметики быстрееimulcmovустраняет ветвления- SIMD для параллельной обработки данных
- Быстрое деление через магические константы
СИСТЕМНЫЕ ВЫЗОВЫ
- Минимальные обертки над syscalls
- Прямой доступ к ядру Linux
- Проверка ошибок через возвращаемое значение
ДЛЯ НАЧИНАЮЩИХ:
- Начинайте с прямых версий
- Используйте отладчик (GDB)
- Сравнивайте с C-кодом
ДЛЯ ПРОДВИНУТЫХ:
- Изучайте таблицы инструкций Intel/AMD
- Измеряйте производительность
- Используйте профилировщики (perf, valgrind)
ИНСТРУМЕНТЫ:
# Компиляция
nasm -f elf64 -g -F dwarf file.asm
ld file.o -o program
# Отладка
gdb ./program
# Дизассемблирование
objdump -d -M intel program