Skip to main content

Структура программы и память

Интересный учебник

Еще интересный ресурс

Типы памяти

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

Оперативная память (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 движках

    Математических пакетах

    Игровых движках