Язык ассемблера - Управление памятью

Управление памятью — важная часть системного программирования. В языке ассемблера можно динамически выделять и освобождать память с помощью системных вызовов, напрямую управлять адресами памяти и обеспечивать эффективное управление памятью.


Схема распределения памяти программы

Запущенная программа распределена в памяти следующим образом:

程序内存布局图

Схема распределения памяти процесса Linux выглядит следующим образом:

高地址 (0xFFFFFFFF)
+----------------------+
|    内核空间           |  用户程序不可访问
+----------------------+  (0xC0000000)
|    栈 (Stack)        |  <-- ESP 指向栈顶
|    向下增长           |
+----------------------+
|                      |
|    内存映射区域        |  mmap 分配的区域
|                      |
+----------------------+
|    堆 (Heap)         |  <-- brk/sbrk 管理
|    向上增长           |
+----------------------+
|    BSS 段 (.bss)     |  未初始化全局变量
+----------------------+
|    数据段 (.data)     |  已初始化全局变量
+----------------------+
|    代码段 (.text)     |  程序指令(只读)
+----------------------+
低地址 (0x08048000)

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

sys_brk(Системный вызов номер 45) путём измененияграницы сегмента данных программы (program break)для выделения/освобождения памяти.

program break — это конечная позиция сегмента данных (включая .data, .bss и кучу). Программа не может обращаться к памяти выше неё.

Способ вызоваОписаниеВозвращаемое значение
ebx = 0Получить текущий адрес breakEAX = текущий адрес границы
ebx = 新地址Установить новый адрес границыEAX = новый адрес границы (при ошибке возвращает исходный адрес)

Пример

; Путь к файлу: brk_demo.asm
; Использовать sys_brk для динамического выделения памяти

section .data
    alloc_msg db 'Память успешно выделена по адресу: 0x'
    alloc_len equ $ - alloc_msg
    newline db 0xA

section .bss
    orig_break resd 1            ; Сохранить исходный адрес границы

section .text
    global _start

_start:
    ; 1. Получить текущую границу программы
    mov eax, 45                  ; sys_brk
    mov ebx, 0                   ; ebx=0 означает запросить текущую границу
    int 0x80
    mov [orig_break], eax        ; Сохранить исходный адрес границы

    ; 2. Выделить 4096 байт (4KB) памяти
    mov ebx, eax                 ; Текущий адрес границы
    add ebx, 4096                ; Увеличить на 4096 байт
    mov eax, 45                  ; sys_brk
    int 0x80
    ; EAX = новый адрес границы (т.е. конец выделенной области)

    ; 3. Новый выделенный участок памяти находится между orig_break и eax-1
    ; Вы можете безопасно читать и записывать в эту область памяти
    mov ebx, [orig_break]        ; Получить начальный адрес выделенной области
    mov dword [ebx], 42          ; Записать 42 в новую память
    mov eax, [ebx]               ; Прочитать обратно

    ; 4. Освободить память: вернуть границу в исходное положение
    mov eax, 45                  ; sys_brk
    mov ebx, [orig_break]        ; Восстановить исходную границу
    int 0x80

    mov eax, 1
    mov ebx, 0
    int 0x80

sys_brk может только изменять границу непрерывного сегмента данных и не может освобождать память в середине. Чтобы освободить блок выделенной памяти, необходимо сначала освободить всю память, выделенную после него. Именно поэтому современные программы чаще используют mmap или библиотечные функции malloc.


Использование mmap для выделения памяти (рекомендуется)

sys_mmap2(Системный вызов номер 192) более гибкий; он может независимо выделять и освобождать несколько блоков памяти:

Пример

; Путь к файлу: mmap_demo.asm
; Использование mmap для выделения памяти, которую можно освобождать независимо

section .data
    ; константы, связанные с mmap
    PROT_READ equ 1
    PROT_WRITE equ 2
    MAP_PRIVATE equ 2
    MAP_ANONYMOUS equ 0x20

section .bss
    mem_block resd 1             ; Сохранить адрес выделенной памяти

section .text
    global _start

_start:
    ; вызов mmap: выделить 4096 байт анонимной памяти
    mov eax, 192                 ; sys_mmap2
    mov ebx, 0                   ; Пусть ядро выберет адрес
    mov ecx, 4096                ; Размер выделения: 4KB
    mov edx, PROT_READ | PROT_WRITE  ; Доступно для чтения и записи
    mov esi, MAP_PRIVATE | MAP_ANONYMOUS  ; Приватное анонимное отображение
    mov edi, -1                  ; Файловый дескриптор (используйте -1 для анонимного отображения)
    mov ebp, 0                   ; Смещение (используйте 0 для анонимного отображения)
    int 0x80
    ; EAX = адрес выделенной памяти (возвращает отрицательное значение при ошибке)

    cmp eax, -4096               ; Проверить, не произошла ли ошибка
    ja  mmap_failed              ; Если значение между -4095 и -1, значит произошла ошибка

    mov [mem_block], eax         ; Сохранить адрес памяти

    ; Использовать выделенную память: записать данные
    mov ebx, [mem_block]
    mov dword [ebx], 0x12345678  ; Записать 4 байта
    mov dword [ebx + 4], 'runo'  ; Записать "runo"
    mov dword [ebx + 8], 'ob!!'  ; Записать "ob!!"

    ; Освободить память: munmap
    mov eax, 91                  ; sys_munmap
    mov ebx, [mem_block]         ; Адрес памяти
    mov ecx, 4096                ; Размер освобождения
    int 0x80

    jmp exit

mmap_failed:
    ; Обработать ошибки...

exit:
    mov eax, 1
    mov ebx, 0
    int 0x80

Операции чтения/записи памяти

В языке ассемблера все обращения к памяти выполняются черезmovкоманды в сочетании с квадратными скобками:

Пример

; Основные операции чтения/записи памяти

section .data
    var1 db 0x55                ; 1 byte
    var2 dw 0x1234              ; 2 bytes
    var3 dd 0x12345678          ; 4 bytes

section .text
    global _start

_start:
    ; Чтение памяти разных размеров
    mov al, [var1]              ; Чтение 1 byte: al = 0x55
    mov ax, [var2]              ; Чтение 2 bytes: ax = 0x1234
    mov eax, [var3]             ; Чтение 4 bytes: eax = 0x12345678

    ; Запись в память разных размеров
    mov byte [var1], 0xAA       ; Записать 1 байт
    mov word [var2], 0xABCD     ; Записать 2 байта
    mov dword [var3], 0xDEADBEEF ; Записать 4 байта

    ; Доступ к памяти через указатели
    mov ebx, var3               ; ebx указывает на var3
    mov eax, [ebx]              ; Косвенно прочитать значение var3
    add dword [ebx], 1          ; var3 = var3 + 1

    mov eax, 1
    mov ebx, 0
    int 0x80

Правила безопасности при операциях с памятью

При работе с памятью в ассемблере нет "страховки"; помните о следующих правилах:

ПравилоОписаниеПоследствие нарушения
Не допускайте доступа за границыЧтение/запись не более размера переменной/буфераПовреждение данных, ошибка сегментации
Не читайте неинициализированную памятьЗначения в секции .bss не определеныНепредсказуемые результаты
Не читайте/записывайте по недопустимым адресамУбедитесь, что указатель указывает на допустимую памятьОшибка сегментации
Не записывайте в память только для чтенияСекция .text и область констант не доступны для записиОшибка сегментации
Выравнивание адресаОбращения к слову используют чётные адреса; обращения к двойному слову используют кратные 4Снижение производительности или ошибка шины

Пример

; Примеры распространённых ошибок памяти (предупреждение: приведут к сбоям!)

section .data
    small_buf db 0, 0, 0, 0     ; Только 4 байта

section .text
    global _start

_start:
    ; Опасная операция 1: запись за границы
    ; mov dword [small_buf + 3], 0x12345678
    ; Это перезаписывает 3 байта данных после small_buf!

    ; Опасная операция 2: запись в секцию кода (только для чтения)
    ; mov dword [_start], 0x90 ; Попытка изменить код приведёт к ошибке сегментации

    ; Опасная операция 3: нулевой указатель / недопустимый адрес
    ; mov eax,
    ; mov

    ; Безопасный подход: всегда работать в пределах выделенного диапазона памяти
    mov dword [small_buf], 'runo'  ; Корректно: работать в пределах 4 bytes

    mov eax, 1
    mov ebx, 0
    int 0x80

Управление памятью стека

Стек — это ещё одна важная область памяти, управляемая инструкциями PUSH/POP и регистром ESP:

Пример

; Комплексный пример операций со стеком

section .data
    original_esp dd 0

section .text
    global _start

_start:
    mov [original_esp], esp      ; Сохранить исходный указатель стека

    ; PUSH: поместить в стек (ESP -= 4, записать данные)
    push dword 100               ; Поместить 100
    ; ESP = ESP - 4, [ESP] = 100

    push dword 200               ; Поместить 200
    push dword 300               ; Поместить 300 в стек

    ; Структура стека:
    ; ESP+0: 300
    ; ESP+4: 200
    ; ESP+8: 100

    ; POP: извлечь из стека (прочитать данные, ESP += 4)
    pop eax                      ; eax = 300, ESP += 4
    pop ebx                      ; ebx = 200, ESP += 4
    pop ecx                      ; ecx = 100, ESP += 4

    ; Выделить локальное пространство в стеке
    sub esp, 256                 ; Выделить 256 байт в стеке
    ; Теперь 256 байт от ESP до ESP+255 можно безопасно использовать
    mov dword [esp], 42          ; Записать 42 в локальную область
    add esp, 256                 ; Освободить локальную область

    ; Убедитесь, что ESP возвращается в исходное положение!
    mov eax, 1
    mov ebx, 0
    int 0x80

Самое важное правило для операций со стеком:PUSH и POP должны быть парными. Перед возвратом из функции вы должны убедиться, что ESP возвращается в правильное положение; иначе RET извлечёт неверный адрес возврата, что приведёт к падению программы или выполнению произвольного кода. Это одна из самых фатальных ошибок в программировании на ассемблере.


Сравнение методов управления памятью

СхемаГибкостьСложностьСценарий применения
Глобальные переменные (.data/.bss)Низкая (фиксировано во время компиляции)Самый низкийДанные фиксированного размера
Выделение в стеке (sub esp)Средняя (динамически внутри функции)НизкаяЛокальные переменные функции
brk/sbrkСредний (может только расти/сжиматься)СредняяНепрерывная область памяти
mmapВысокий (произвольное выделение/освобождение)ВысокаяКогда требуется гибкое управление памятью
Другие расширения