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 Программа. 

Карта памяти:

ПрограммаАдреса для изученияв памяти:

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 куча

# Просмотр карты памяти для процесса cat /proc/.../maps

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 Базовое использование 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-аллокаторы)

Работа с сегментами памяти.

section .data
    msg db "Hello from allocated memory in x64!", 10
    msg_len equ $ - msg

section .bss
    allocated_ptr resq 1  ; 64-битный указатель

section .text
global _start

_start:
    ; Выделяем 2 страницы памяти с помощью mmap
    mov rax, 9          ; sys_mmap
    xor rdi, rdi        ; адрес = NULL (ядро выбирает)
    mov rsi, 8192       ; размер: 2 страницы (8192 байта)
    mov rdx, 0x7        ; PROT_READ | PROT_WRITE | PROT_EXEC
    mov r10, 0x22       ; MAP_PRIVATE | MAP_ANONYMOUS
    mov r8, -1          ; без файла
    xor r9, r9          ; смещение = 0
    syscall
    
    test rax, rax
    js error            ; если ошибка (отрицательное значение)
    
    mov [allocated_ptr], rax
    
    ; Копируем строку в выделенную память
    mov rdi, rax        ; назначение
    mov rsi, msg        ; источник
    mov rcx, msg_len    ; длина
    rep movsb           ; копируем байты
    
    ; Выводим строку из выделенной памяти
    mov rax, 1          ; sys_write
    mov rdi, 1          ; stdout
    mov rsi, [allocated_ptr]
    mov rdx, msg_len
    syscall
    
    ; Освобождаем память
    mov rax, 11         ; sys_munmap
    mov rdi, [allocated_ptr]
    mov rsi, 8192
    syscall
    
    ; Выход
    mov rax, 60         ; sys_exit
    xor rdi, rdi        ; код 0
    syscall

error:
    ; Обработка ошибки
    mov rax, 60         ; sys_exit
    mov rdi, 1          ; код ошибки 1
    syscall

Работа с сегментами считается сложнее, но эффективнее в контексте выделения и освобождения.

ТеперьВыполнение укода васиз есть полноценная система для работы с кучей в ассемблере!
Отлично! А подскажи пожалуйста, можно ли в кучу записать код и выполнить его?

Да, это возможно, но это продвинутая и потенциально опасная техника!кучи. Это называется 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

Важные моменты безопасности

Более1. сложныйmprotect пример:обязателен, генерациябез функцииPROT_EXEC сложения
nasmфлага будет SEGFAULT: 

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
    
    ; Делаем память исполняемой
 SEGFAULT! mov rax, 10                 ; sys_mprotect
  mov rdi, jit_code
    and rdi, ~0xFFF
 code_addr mov rsi, 4096
size   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 движках

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

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