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

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

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

 Типы памяти 

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

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

 Данные (Data) Стек (Stack) Код (Code) В Assembler можно использовать сегментирование для организации памяти. 

 Стек Стек — структура данных, в которой операции записи и чтения выполняются по принципу “последним пришёл — первым ушёл” (LIFO). Стек используется для хранения локальных переменных, адресов возврата при вызове функций, а также для управления процессом вызова процедур. 

 Куча Куча (heap) — это область памяти, в которой динамически выделяются блоки памяти во время выполнения программы. Работа с кучей требует явного управления памятью (например, с помощью системных вызовов операционной системы). 

 Структура программы 

 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 

 Примерный результат выполнения: 

 Code (.text):   0x400500 Data (.data):   0x600800   Heap (break):   0x1ae20000 Stack (rsp):    0x7fffffffe010 Stack - Heap:   2147358720 bytes  (примерно 2GB) 

 Выделение и освобождение стека:  

 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 

 Выделение и освобождение кучи:  

 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 

 Если куча и стек встречаются, то SEGFAULT или ENOMEM при попытке выделить память. Предотвращение столкновений: 

 # Посмотреть текущие лимиты

ulimit -a

# Ограничить стек для теста

ulimit -s 1024  # 1MB стек

./program

# Ограничить кучу

ulimit -d 65536  # 64MB куча

# Просмотр карты памяти для процесса

cat /proc/.../maps 

 Проверка в коде:  

 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 

 Динамическое выделение памяти 

 Выделяется через системные вызовы или стандартные библиотеки (например, 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:  

 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 

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

 

     Всегда проверяйте успешность выделения памяти 

     Выравнивайте запросы по границам страниц (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) или динамическое генерирование кода. 

 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 флага будет SEGFAULT:  

 ; Без этого - 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): 

 

 Память не может быть одновременно записываемой и исполняемой 

 Нужно сначала записать код, потом сделать исполняемым 

 

 

 section .data

 code_bytes db 0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3 ; mov eax,1; ret

section .text

global _start

_start:

 ; Выделяем память с правами READ|WRITE

 mov rax, 9 ; mmap

 xor rdi, rdi ; адрес

 mov rsi, 4096 ; размер

 mov rdx, 0x3 ; PROT_READ | PROT_WRITE (но НЕ PROT_EXEC!)

 mov r10, 0x22 ; MAP_PRIVATE | MAP_ANONYMOUS

 mov r8, -1

 xor r9, r9

 syscall

 mov rbx, rax ; сохраняем адрес

 

 ; Копируем код в выделенную память

 mov rdi, rax

 mov rsi, code_bytes

 mov rcx, 6

 rep movsb

 

 ; Пытаемся выполнить код - ЭТО ВЫЗОВЕТ SEGFAULT!

 ; call rbx ; ← segmentation fault!

 

 ; Сначала меняем права на READ|EXECUTE

 mov rax, 10 ; mprotect

 mov rdi, rbx ; адрес

 mov rsi, 4096 ; размер

 mov rdx, 0x5 ; PROT_READ | PROT_EXECUTE

 syscall

 

 ; Теперь можно выполнять

 call rbx ; работает!