Структура программы и память
Типы памяти
Регистровая память Самый быстрый способ хранения данных. Процессоры имеют набор регистров, которые могут использоваться для хранения данных.
Оперативная память (RAM) Это основное место хранения данных программы. В отличие от регистров, доступ к памяти менее быстрый, но объём значительно больше. Оперативная память делится на несколько сегментов:
Данные (Data)
Стек (Stack)
Код (Code)
В Assembler можно использовать сегментирование для организации памяти.
Стек
Стек — это структура данных, в которой операции записи и чтения выполняются по принципу “последним пришёл — первым ушёл” (LIFO). Стек используется для хранения локальных переменных, адресов возврата при вызове функций, а также для управления процессом вызова процедур.
Куча
Куча (heap) — это область памяти, в которой динамически выделяются блоки памяти во время выполнения программы. В Assembler работа с кучей требует явного управления памятью (например, с помощью системных вызовов операционной системы).
Структура программы
section .data
msg db "hello, world", 0
section .bss
section .text
global main
main:
mov rax, 1
mov rdi, 1
mov rsi, msg
mov rdx, 12
syscall
mov rax, 60
mov rdi, 0
syscall
Страницы:
- .data
- .bss
- .txt
.data
Переменные: <varname> <type> <value>
Константы: <constant_name> equ <value>
.bss
Неинициализированные данные. resb - байт, resw - слово, resd - двойное слово, resq - двойное длинное слово
.txt Программа.
Вот как выглядит типичная картаКарта памяти:text
Высокие адреса0x7fffffffffff ┌─────────────────┐ │ Стек │ ← растет ВНИЗ │ (Stack) │ rsp регистр ├─────────────────┤ │ ... │ ├─────────────────┤ │ Динамические │ │ библиотеки │ ├─────────────────┤ │ Куча │ ← растет ВВЕРХ │ (Heap) │ brk указатель ├─────────────────┤ │ .bss секция │ (неинициализированные данные) ├─────────────────┤ │ .data секция │ (инициализированные данные) ├─────────────────┤ │ .rodata │ (константы) ├─────────────────┤ │ .text секция │ (код программы) │ (исполняемый) │Низкие адреса └─────────────────┘0x400000

На практике посмотрим
Создадим программуПрограмма для изучения памяти:nasm
section .data
code_msg db "Code (.text): 0x%lx", 10, 0
data_msg db "Data (.data): 0x%lx", 10, 0
heap_msg db "Heap (break): 0x%lx", 10, 0
stack_msg db "Stack (rsp): 0x%lx", 10, 0
diff_msg db "Stack - Heap: %ld bytes", 10, 0
section .text
global main
extern printf
main:
push rbp
mov rbp, rsp
; Адрес кода (самой функции main)
lea rax, [rel main]
mov rdi, code_msg
mov rsi, rax
call printf
; Адрес данных
mov rdi, data_msg
mov rsi, code_msg ; любая метка из .data
call printf
; Адрес кучи (break)
mov rax, 12 ; sys_brk
mov rdi, 0
syscall
mov rdi, heap_msg
mov rsi, rax
call printf
; Адрес стека (rsp)
mov rdi, stack_msg
mov rsi, rsp
call printf
; Разница между стеком и кучей
mov rax, 12 ; снова получаем break
mov rdi, 0
syscall
mov rbx, rax ; heap в rbx
mov rax, rsp ; stack в rax
sub rax, rbx ; stack - heap
mov rdi, diff_msg
mov rsi, rax
call printf
pop rbp
mov rax, 0
ret
Что покажет эта программа
Примерный вывод:
text
Code (.text): 0x400500
Data (.data): 0x600800
Heap (break): 0x1ae20000
Stack (rsp): 0x7fffffffe010
Stack - Heap: 2147358720 bytes (примерно 2GB)
Динамика роста
Стек (растет ВНИЗ):
nasm
section .text
global main
main:
push rbp
mov rbp, rsp
; Текущий rsp
mov r12, rsp
; Выделяем 1KB в стеке
sub rsp, 1024
; rsp УМЕНЬШИЛСЯ!
; r12 (старый rsp) > rsp (новый rsp)
; Восстанавливаем
mov rsp, rbp
pop rbp
ret
Куча (растет ВВЕРХ):
nasm
section .text
global main
main:
; Текущий break
mov rax, 12
mov rdi, 0
syscall
mov r12, rax ; сохраняем старый break
; Увеличиваем кучу на 1KB
mov rax, 12
mov rdi, r12
add rdi, 1024
syscall
mov r13, rax ; новый break
; r13 > r12 - куча ВЫРОСЛА!
ret
Что происходит при collision
Если куча и стек встречаются:
text
До столкновения:
Стек: 0x7fffffffe000 ← rsp
... (много свободного пространства)
Куча: 0x1ae20000 ← brk
После роста:
Стек: 0x7fffff000000 ← вырос ВНИЗ
Куча: 0x7fffff000000 ← вырос ВВЕРХ
↑
СТОЛКНОВЕНИЕ!
Результат: Программа получает SEGFAULT или ENOMEM при попытке выделить память.
Практический пример столкновения
nasm
section .text
global main
main:
push rbp
mov rbp, rsp
; Пытаемся исчерпать стек
mov rcx, 1000000 ; много итераций
.stack_exhaust:
push rcx ; кладем в стек
loop .stack_exhaust
; Теперь пытаемся выделить кучу
mov rax, 12 ; sys_brk
mov rdi, 0
syscall
add rdi, 1000000 ; большой запрос
mov rax, 12
syscall
; Может вернуть ошибку - нет памяти!
pop rbp
ret
Как система предотвращает столкновение
1. RLIMIT_STACK - лимит стека
bash
# Посмотреть текущие лимиты
ulimit -a
# Ограничить стек для теста
ulimit -s 1024 # 1MB стек
./program
2. RLIMIT_DATA - лимит кучи
bash
ulimit -d 65536 # 64MB куча
3. Проверка в коде:
nasm
; Безопасное выделение с проверкой
safe_stack_allocation:
mov rax, rsp
sub rax, desired_size
cmp rax, [heap_upper_bound]
jl .stack_heap_collision
; Иначе безопасно
sub rsp, desired_size
ret
.stack_heap_collision:
; Обработка нехватки памяти
mov rax, -1
ret
Реальная карта памяти
Посмотрим на реальный процесс:
bash
# Запускаем программу
./memory_layout &
# Смотрим карту памяти
cat /proc/$!/maps
# Пример вывода:
00400000-00401000 r-xp 00000000 00:00 0 [код]
00600000-00601000 r--p 00000000 00:00 0 [данные]
00601000-00602000 rw-p 00001000 00:00 0 [bss]
01ae2000-01b03000 rw-p 00000000 00:00 0 [heap]
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
Ключевые выводы
Стек растет ВНИЗ (от высоких адресов к низким)
Куча растет ВВЕРХ (от низких адресов к высоким)
Они движутся навстречу друг другу
Столкновение вызывает нехватку памяти
Система защищает лимитами (ulimit)
Динамическое выделение памяти
Выделяется через системные вызовы или стандартные библиотеки (например, malloc из libc).
Получение текущего адреса кучи (heap):
Для x32:
mov eax, 45
mov ebx, 0
int 0x80 ; В eax вернётся текущий адрес конца кучи
Для x64:
mov rax, 12
mov rdi, 0
syscall ; В rax вернётся текущий адрес конца кучи
Похоже под кучей понимается
Как использовать кучу
Базовое использование sys_brk:
nasm
section .data
success_msg db "Heap: allocated %d bytes at address 0x%lx", 10, 0
error_msg db "Heap: allocation failed!", 10, 0
section .bss
initial_break resq 1
current_break resq 1
section .text
global main
extern printf
main:
push rbp
mov rbp, rsp
; 1. Получаем текущую границу кучи
mov rax, 12 ; sys_brk
mov rdi, 0
syscall
mov [initial_break], rax
mov [current_break], rax
; 2. Выделяем 16KB памяти
mov rdi, rax
add rdi, 16384 ; 16 * 1024 = 16384 байт
mov rax, 12 ; sys_brk
syscall
; 3. Проверяем успешность
cmp rax, [current_break]
jle .error
mov [current_break], rax ; сохраняем новую границу
; 4. Используем выделенную память
mov rdi, [initial_break] ; начало выделенной области
; Записываем некоторые данные
mov byte [rdi], 'H'
mov byte [rdi + 1], 'e'
mov byte [rdi + 2], 'l'
mov byte [rdi + 3], 'l'
mov byte [rdi + 4], 'o'
mov byte [rdi + 5], 0
; Выводим информацию
mov rdi, success_msg
mov rsi, 16384 ; размер
mov rdx, [initial_break] ; адрес
mov rax, 0
call printf
jmp .exit
.error:
mov rdi, error_msg
mov rax, 0
call printf
.exit:
pop rbp
mov rax, 0
ret
Как определить текущий размер кучи
nasm
; Функция: получить размер кучи в байтах
; Возвращает: rax = размер кучи в байтах
get_heap_size:
mov rax, 12 ; sys_brk
mov rdi, 0
syscall ; rax = текущий break
; Вычисляем размер от начала программы до текущего break
; Начало кучи обычно находится в [initial_break]
sub rax, [initial_break]
ret
section .data
initial_break dq 0 ; должна быть инициализирована при старте
Когда нужно увеличивать кучу
Признаки необходимости увеличения:
Вы исчерпали текущую выделенную память
Планируете хранить большие структуры данных
Работаете с динамическими массивами
Умный менеджер кучи:
nasm
section .bss
heap_start resq 1
heap_end resq 1
heap_size resq 1
section .text
global main
; Функция выделения памяти
; rdi = требуемый размер в байтах
; возвращает rax = указатель на память или 0 при ошибке
malloc:
push rbp
mov rbp, rsp
; Проверяем, достаточно ли текущей памяти
mov rax, [heap_end]
sub rax, [heap_start]
cmp rax, rdi
jge .have_memory ; если достаточно, используем существующую
; Нужно выделить больше
call grow_heap
test rax, rax
jz .error ; если выделение не удалось
.have_memory:
mov rax, [heap_start] ; возвращаем текущий указатель
add [heap_start], rdi ; сдвигаем указатель
pop rbp
ret
.error:
xor rax, rax ; возвращаем 0 (ошибка)
pop rbp
ret
; Увеличивает кучу минимум на запрошенный размер
; rdi = минимальный размер для добавления
grow_heap:
push rbp
mov rbp, rsp
; Округляем до страниц (4KB)
add rdi, 4095
and rdi, ~4095 ; выравниваем до границы 4KB
mov rax, 12 ; sys_brk
mov rsi, [heap_end]
add rsi, rdi ; новый break = старый + размер
mov rdi, rsi
syscall
cmp rax, [heap_end]
jle .error ; если не увеличилось
mov [heap_end], rax ; обновляем конец кучи
mov rax, 1 ; успех
jmp .exit
.error:
xor rax, rax ; ошибка
.exit:
pop rbp
ret
; Инициализация кучи при старте программы
init_heap:
mov rax, 12 ; sys_brk
mov rdi, 0
syscall
mov [heap_start], rax
mov [heap_end], rax
ret
Практический пример: динамический массив
nasm
section .data
array_size equ 1000
element_size equ 8 ; 8 байт на элемент
section .text
global main
main:
push rbp
mov rbp, rsp
call init_heap ; инициализируем кучу
; Выделяем память для массива из 1000 элементов
mov rdi, array_size * element_size
call malloc
test rax, rax
jz .error
mov r15, rax ; сохраняем указатель на массив
; Заполняем массив
mov rcx, array_size
mov rbx, 0 ; индекс
.fill_loop:
mov [r15 + rbx*8], rbx ; array[i] = i
inc rbx
loop .fill_loop
; Используем массив...
jmp .exit
.error:
; Обработка ошибки выделения памяти
.exit:
pop rbp
mov rax, 0
ret
Автоматическое определение необходимости увеличения
nasm
; Функция для безопасного выделения с автоматическим увеличением
; rdi = требуемый размер
safe_malloc:
push rbp
mov rbp, rsp
; Проверяем доступную память
mov rax, [heap_end]
sub rax, [heap_start]
cmp rax, rdi
jge .alloc_ok
; Недостаточно памяти - пытаемся увеличить
push rdi
call grow_heap ; пытаемся увеличить кучу
pop rdi
test rax, rax
jz .error ; не удалось увеличить
.alloc_ok:
call malloc ; выделяем память
jmp .exit
.error:
xor rax, rax
.exit:
pop rbp
ret
Как отслеживать использование кучи
nasm
section .data
heap_info db "Heap: start=0x%lx, end=0x%lx, used=%lu bytes, free=%lu bytes", 10, 0
; Функция вывода информации о куче
print_heap_info:
push rbp
mov rbp, rsp
mov rax, [heap_end]
sub rax, [heap_start] ; rax = свободная память
mov rbx, [heap_start]
sub rbx, [initial_break] ; rbx = использованная память
mov rdi, heap_info
mov rsi, [initial_break] ; начало кучи
mov rdx, [heap_end] ; конец кучи
mov rcx, rbx ; использовано
mov r8, rax ; свободно
mov rax, 0
call printf
pop rbp
ret
Ключевые принципы
Всегда проверяйте успешность выделения памяти
Выравнивайте запросы по границам страниц (4KB)
Отслеживайте использование чтобы вовремя увеличивать кучу
Используйте умные стратегии выделения (пулы, slab-аллокаторы)
Теперь у вас есть полноценная система для работы с кучей в ассемблере!
Отлично! А подскажи пожалуйста, можно ли в кучу записать код и выполнить его?
Да, это возможно, но это продвинутая и потенциально опасная техника! Это называется JIT-компиляция (Just-In-Time) или динамическое генерирование кода.
Базовый пример выполнения кода из кучи
nasm
section .data
success_msg db "Executing code from heap!", 10, 0
after_exec_msg db "Back from JIT code! Return value: %d", 10, 0
section .bss
code_buffer resb 4096 ; буфер для кода
section .text
global main
extern printf, mprotect
main:
push rbp
mov rbp, rsp
; 1. Выделяем память под код (уже есть code_buffer)
; 2. Записываем машинный код в буфер
mov rdi, code_buffer
; Генерируем простую функцию: mov rax, 42; ret
mov byte [rdi], 0x48 ; mov rax, 42
mov byte [rdi + 1], 0xC7
mov byte [rdi + 2], 0xC0
mov byte [rdi + 3], 0x2A
mov byte [rdi + 4], 0x00
mov byte [rdi + 5], 0x00
mov byte [rdi + 6], 0x00
mov byte [rdi + 7], 0xC3 ; ret
; 3. Делаем память исполняемой
mov rax, 10 ; sys_mprotect
mov rdi, code_buffer ; адрес
and rdi, ~0xFFF ; выравниваем до границы страницы
mov rsi, 4096 ; размер
mov rdx, 7 ; PROT_READ|PROT_WRITE|PROT_EXEC
syscall
test rax, rax
jnz .error
; 4. Вызываем код из кучи!
mov rdi, success_msg
call printf
mov rax, code_buffer ; получаем указатель на функцию
call rax ; ВЫЗЫВАЕМ КОД ИЗ КУЧИ!
; 5. rax содержит возвращаемое значение (42)
mov rsi, rax
mov rdi, after_exec_msg
call printf
jmp .exit
.error:
; Обработка ошибки
.exit:
pop rbp
mov rax, 0
ret
Более сложный пример: генерация функции сложения
nasm
section .data
add_func_msg db "Generated add function: %d + %d = %d", 10, 0
section .bss
jit_code resb 128
section .text
global main
extern printf
; Генерирует функцию: int add(int a, int b) { return a + b; }
generate_add_function:
mov rdi, jit_code
; x86-64 код для: mov rax, rdi; add rax, rsi; ret
mov byte [rdi], 0x48 ; mov rax, rdi
mov byte [rdi + 1], 0x89
mov byte [rdi + 2], 0xF8
mov byte [rdi + 3], 0x48 ; add rax, rsi
mov byte [rdi + 4], 0x01
mov byte [rdi + 5], 0xF0
mov byte [rdi + 6], 0xC3 ; ret
ret
main:
push rbp
mov rbp, rsp
; Генерируем функцию сложения
call generate_add_function
; Делаем память исполняемой
mov rax, 10 ; sys_mprotect
mov rdi, jit_code
and rdi, ~0xFFF
mov rsi, 4096
mov rdx, 7 ; RWX
syscall
test rax, rax
jnz .error
; Вызываем сгенерированную функцию
mov r15, jit_code ; указатель на функцию
mov rdi, 15 ; первый аргумент
mov rsi, 27 ; второй аргумент
call r15 ; вызываем JIT-функцию!
; Выводим результат
mov rdx, rax ; результат
mov rsi, 27 ; b
mov rdi, 15 ; a
mov rcx, add_func_msg
mov rdi, rcx
call printf
jmp .exit
.error:
; Ошибка
.exit:
pop rbp
mov rax, 0
ret
Важные моменты безопасности
1. mprotect обязателен!
Без PROT_EXEC флага вы получите SEGFAULT:
nasm
; Без этого - SEGFAULT!
mov rax, 10 ; sys_mprotect
mov rdi, code_addr
mov rsi, size
mov rdx, 7 ; PROT_READ|PROT_WRITE|PROT_EXEC
syscall
2. W^X политика
В современных системах часто включена политика W^X (Write XOR Execute):
Память не может быть одновременно записываемой и исполняемой
Нужно сначала записать код, потом сделать исполняемым
nasm
; 1. Записываем код (память должна быть W)
mov byte [buffer], 0xC3 ; ret
; 2. Делаем исполняемой (память становится X)
mov rax, 10 ; mprotect
mov rdx, 5 ; PROT_READ|PROT_EXEC (без WRITE!)
syscall
; 3. Теперь можно исполнять, но нельзя изменять
Реальный пример: простой интерпретатор
nasm
section .data
op_add equ 1
op_sub equ 2
op_mul equ 3
section .bss
jit_buffer resb 4096
section .text
global main
; Генерирует код по байткоду
; rdi = байткод, rsi = длина, rdx = буфер для JIT
generate_jit:
push rbp
mov rbp, rsp
mov r8, rdx ; указатель в JIT буфере
mov r9, rdi ; указатель в байткоде
mov r10, rsi ; длина байткода
; Пролог функции
mov byte [r8], 0x55 ; push rbp
mov byte [r8 + 1], 0x48 ; mov rbp, rsp
mov byte [r8 + 2], 0x89
mov byte [r8 + 3], 0xE5
add r8, 4
; Начальное значение: mov rax, 0
mov byte [r8], 0x48 ; mov rax, 0
mov byte [r8 + 1], 0xC7
mov byte [r8 + 2], 0xC0
mov byte [r8 + 3], 0x00
mov byte [r8 + 4], 0x00
mov byte [r8 + 5], 0x00
mov byte [r8 + 6], 0x00
add r8, 7
; Генерируем код для каждого опкода
.interpret_loop:
cmp r10, 0
jle .epilog
mov al, [r9] ; текущий опкод
cmp al, op_add
je .gen_add
cmp al, op_sub
je .gen_sub
; ... другие операции
.gen_add:
; add rax, [r9+1] - следующее число
mov byte [r8], 0x48 ; add rax, immediate
mov byte [r8 + 1], 0x05
mov r11b, [r9 + 1]
mov [r8 + 2], r11b
add r8, 3
add r9, 2 ; пропускаем опкод и операнд
sub r10, 2
jmp .interpret_loop
.gen_sub:
; sub rax, [r9+1]
mov byte [r8], 0x48 ; sub rax, immediate
mov byte [r8 + 1], 0x2D
mov r11b, [r9 + 1]
mov [r8 + 2], r11b
add r8, 3
add r9, 2
sub r10, 2
jmp .interpret_loop
.epilog:
; Эпилог функции
mov byte [r8], 0x5D ; pop rbp
mov byte [r8 + 1], 0xC3 ; ret
add r8, 2
pop rbp
ret
Ограничения и предупреждения
🔴 Опасности:
Уязвимости выполнения кода
Сложность отладки
Проблемы с безопасностью памяти
🟡 Ограничения:
DEP (Data Execution Prevention) может блокировать
Требует привилегий в некоторых системах
Сложность генерации корректного машинного кода
🟢 Лучшие практики:
Используйте для высокопроизводительных вычислений
Тщательно проверяйте генерируемый код
Ограничивайте права памяти минимально необходимыми
Проверка поддержки системы
nasm
; Проверяем, можем ли мы сделать память исполняемой
test_exec_support:
mov rax, 10 ; sys_mprotect
mov rdi, test_buffer
mov rsi, 4096
mov rdx, 7 ; RWX
syscall
; Если rax = 0 - поддерживается, иначе - нет
JIT-компиляция в куче — это мощная техника, используемая в:
Виртуальных машинах (Java, .NET)
JavaScript движках
Математических пакетах
Игровых движках