Сообщений 0    Оценка 0        Оценить  
Система Orphus

О реализации контроля целостности структуры «кучи» при выделении памяти

Автор: Караваев Дмитрий Юрьевич
Перевод: Фамилия Имя Отчество
Источник: Название источника где статья была опубликована впервые
Материал предоставил: Фамилия Имя Отчество
Опубликовано: 30.12.2013
Исправлено: 10.12.2016
Версия текста: 1.1
Введение
Организация «кучи» в рассматриваемой системе программирования
Исходный текст системной подпрограммы управления памятью
Реализация оператора выделения памяти
Реализация оператора освобождения памяти
Реализация контроля целостности памяти
Пример работы средств контроля
Заключение
Литература

Введение

Операторы выделения и освобождения памяти из «кучи» являются широко используемыми языковыми возможностями наряду с присваиваниями, условиями и т.п. В последние десятилетия имеется тенденция к усложнению реализации этих операторов, что связано с попытками исключить в принципе возможность нарушения программистом внутренней структуры «кучи» в результате ошибочных действий при явном выделении и освобождении памяти в программе. В некоторых языках программист уже не может сам явно выделять память, а для ее освобождения от ненужных объектов используется встроенная программа «сборки мусора». Однако платой за такое повышение надежности является снижение эффективности при распределении памяти, усложнение внутренних структур и малая предсказуемость времени отклика в программе из-за нерегулярной работы «сборщика мусора» [1].

Автор статьи в течение многих лет использовал систему программирования на языке PL/1 [2], где реализована наиболее простая и наименее надежная схема управления памятью, почти полностью возлагающая на программиста ответственность за правильность при выделении и освобождении памяти. В реальных проектах, особенно больших, ошибки при управлении памятью возникали и оказывались очень болезненными, поскольку трудно диагностировались. Но с другой стороны, не хотелось из-за этого отказываться от свободного манипулирования памятью в программе, позволяющего удобно и эффективно решать поставленные задачи. Компромисс был найден в добавлении к встроенному в язык аппарату управления памятью несложных средств контроля целостности внутренней структуры «кучи». Такие средства позволили постоянно проверять отсутствие каких-либо нарушений при распределении памяти и в противном случае сообщать об этом сигналом ошибки. В процессе контроля становилась доступной информация, позволяющая определить место нарушения, а поэтому часто и его причину.

Организация «кучи» в рассматриваемой системе программирования

Основой внутренней структуры «кучи», как, вероятно, и во многих других системах программирования, является связанный список, поскольку это наиболее удобная в данном случае организация памяти. Поскольку работа идет в среде Win32, под один адрес в связанном списке выделяется 4 байта. Так как не используется «сборка мусора», достаточно самой минимальной информации внутри «кучи»: адреса очередного блока и признака, занят он или свободен.

Однако, поскольку при освобождении памяти принята стратегия автоматического слияния двух подряд идущих свободных блоков, связанный список «кучи» сделан двунаправленным, т.е. в начале каждого блока есть ссылка и на начало предыдущего блока в списке, и на начало следующего блока. Такой двойной связанный список позволяет при освобождении памяти работать со смежными блоками без необходимости просмотра всего списка с начала для поиска предыдущего блока. В самом первом блоке ссылка на предыдущий блок устанавливается в ноль, и в самом последнем блоке ссылка на следующий блок устанавливается в ноль.

Для признака занятости блока используется младший бит ссылки на предыдущий блок. Предполагается, что сам адрес блока всегда кратен двум, и этот бит не используется для адресации. Таким образом, память можно выделять лишь порциями байт, кратными двум. Это несущественное ограничение, особенно учитывая частое требование выравнивания блока памяти на границу двойного слова. Зато тогда для признака занятости блока не требуется дополнительных байт и все накладные расходы на один блок в «куче» составляют 8 байт: ссылка на предыдущий блок (младший бит – признак занятости) и ссылка на следующий блок.

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

Исходный текст системной подпрограммы управления памятью

Ниже приведен исходный текст подпрограммы управления и контроля памяти из системной библиотеки для рассматриваемого языка PL/1. Возможно, для журнальной статьи этот текст на ассемблере для процессора IA-32 несколько велик. Однако автор не предлагает читателям, даже хорошо знакомым с ассемблером, самим разбираться в этом тексте. Его объяснение имеется далее в статье. Цель приведения текста – показать, что небольшим числом команд (т.е. просто и поэтому эффективно) реализован весь аппарат управления памятью, причем значительная часть – это даже не собственно выделение/освобождение памяти, а вспомогательные средства контроля целостности «кучи».

      EXTRN ?SIGC:N

;------------------- ОБНУЛЯТЬ ПОСЛЕ КАЖДОГО ALLOCATE ------------------

PUBLIC ?ALLOC_ZR:
      MOVDS:N0001,033H      ;ИСПРАВЛЯЕМ RET НА XOREAX,EAXRET

;----------------- НЕ ОБНУЛЯТЬ ПОСЛЕ КАЖДОГО ALLOCATE -----------------

PUBLIC ?ALLOC_NZ:
      MOVDS:N0001,0C3H      ;ИСПРАВЛЯЕМ XOREAX,EAX НА RETRET

;--------------------- ALLOCATE (ВЫДЕЛЕНИЕ ПАМЯТИ) --------------------

PUBLIC ?ALLOP:                 ;ВЫЗЫВАЕТСЯ ИЗ ПРОГРАММ PL/1
      MOVECX,OFFSET ?BEGIN  ;АДРЕС НАЧАЛА СВЯЗАННОГО СПИСКА

;---- ДОСТАЛИ АДРЕС НАЧАЛА "КУЧИ" PL/1 ----

      MOVEAX,[ECX]+0        ;НАЧАЛО СВЯЗАННОГО СПИСКА
      MOV   ?START_MEM,EAX     ;ЭТО И НАЧАЛО БУДУЩЕЙ ПРОВЕРКИ
      XORESI,ESIMOVEAX,[ECX]+4        ;ПЕРВЫЙ СВОБОДНЫЙ БЛОК В "КУЧЕ"

;---- ИСХОДНОЕ СОСТОЯНИЕ ПРИ ПРОВЕРКИ ЦЕЛОСТНОСТИ "КУЧИ" ----

      MOV   ?ОБЪЕМ_МЕМ,ESI     ;ПОКА НЕТ ПРОВЕРКИ СУММАРНОГО ОБЪЕМА

;---- ВЫРАВНИВАНИЕ ЗАКАЗАННОГО ЧИСЛА БАЙТ НА СЛОВО ----

      INCEBXANDBL,NOT 1           ;ВЫРОВНИЛИ НА ГРАНИЦУ СЛОВА

;---- ВСТАЕМ НА ПЕРВЫЙ СВОБОДНЫЙ БЛОК "КУЧИ" ----

      XCHGEBX,EAX            ;НАЧАЛО СПИСКА ПАМЯТИ

;---- ЦИКЛ ПОИСКА ПОДХОДЯЩЕГО СВОБОДНОГО БЛОКА "КУЧИ" ----

M0001:TEST  B PTR [EBX],1      ;ЭТОТ БЛОК СВОБОДЕН ?
      JZ    M0003              ;ДА - ПРОВЕРЯЕМ ДЛИНУ

M0002:MOVEBX,[EBX]+4        ;АДРЕС СЛЕДУЮЩЕГО БЛОКА
      OREBX,EBX            ;ЕЩЕ ЕСТЬ БЛОКИ ?
      JNZ   M0001              ;ДА - ПРОДОЛЖАЕМ ПОИСК

;---- НЕ НАЙДЕНО ПОДХОДЯЩЕГО БЛОКА ----

      MOVAX,0700H           ;УСТАНАВЛИВАЕМ КОД (7,0)
      MOVEDX,OFFSET ОШИБ2   ;"ВСЯ ПАМЯТЬ ИСЧЕРПАНА"
      JMPS  M0005              ;ВЫДАЕМ СИГНАЛ ОШИБКИ

;---- ВЫЧИСЛЕНИЕ ДЛИНЫ СВОБОДНОГО БЛОКА ----

M0003:MOVEDX,[EBX]+4        ;АДРЕС СЛЕДУЮЩЕГО БЛОКА
      LEAECX,[EBX]+8        ;АДРЕС НАЧАЛА ДАННЫХ В БЛОКЕ
      SUBEDX,ECX            ;ДЛИНА ПОЛЕЗНОГО МЕСТА В БЛОКЕ
      JB    M0006              ;НЕ МОЖЕТ БЫТЬ ОТРИЦАТЕЛЬНОЙ

      ORESI,ESI            ;ЕЩЕ НЕТ ПЕРВОГО СВОБОДНОГО ?
      CMOVZESI,EBX            ;ЗАПОМНИЛИ ПЕРВЫЙ СВОБОДНЫЙ БЛОК

;---- ПРОВЕРКА, ХВАТАЕТ ЛИ ДЛИНЫ БЛОКА ----

      SUBEDX,EAX            ;ВЫЧИТАЕМ СКОЛЬКО НАДО
      JB    M0002              ;ЭТОТ БЛОК СЛИШКОМ МАЛ
      INC   B PTR [EBX]        ;ОТМЕТИЛИ, ЧТО ТЕПЕРЬ БЛОК ЗАНЯТ
      CMPEDX,16             ;ОСТАЛОСЬ МАЛО СВОБОДНЫХ БАЙТ ?
      JB    @                  ;ТОГДА НЕ СОЗДАЕМ НОВЫХ БЛОКОВ

;---- РАЗБИВАЕМ НАЙДЕННЫЙ БЛОК НА ЗАНЯТЫЙ И НОВЫЙ ----

      ADDECX,EAX            ;НАЧАЛО НОВОГО СВОБОДНОГО БЛОКА
      MOVEDX,[EBX]+4        ;НАЧАЛО СЛЕДУЮЩЕГО БЛОКА
      MOV   [ECX]+0,EBX        ;ПРЕДЫДУЩИЙ В НОВОМ БЛОКЕ
      MOV   [ECX]+4,EDX        ;СЛЕДУЮЩИЙ В НОВОМ БЛОКЕ
      MOV   [EBX]+4,ECX        ;СЛЕДУЮЩИЙ В НАЙДЕННОМ БЛОКЕ
      RCRCL,1               ;ВЫБРОСИЛИ ПРИЗНАК ЗАНЯТОСТИ
      RCR   B PTR [EDX]+0,1    ;ДОСТАЛИ ПРИЗНАК ЗАНЯТОСТИ
      RCLCL,1               ;СОХРАНИЛИ ПРИЗНАК ЗАНЯТОСТИ СЛЕДУЮЩЕГО
      MOV   [EDX]+0,ECX        ;АДРЕС НОВОГО БЛОКА КАК ПРЕДЫДУЩИЙ

;---- ВЫХОД С АДРЕСОМ ПАМЯТИ ----

@:    ADDEBX,8              ;НАЧАЛО ПАМЯТИ ПОЛЬЗОВАТЕЛЯ В БЛОКЕ
      MOV   ?BEGIN+4,ESI       ;НОВЫЙ АДРЕС ПЕРВОГО СВОБОДНОГО
      XCHGECX,EAX            ;ДЛИНА БЛОКА ДЛЯ ВОЗМОЖНОГО ОБНУЛЕНИЯ

;---- СРАЗУ ВЫХОД ИЛИ ЕЩЕ ОБНУЛЕНИЕ ВСЕГО ВЫДЕЛЕННОГО БЛОКА ----

N0001 DB    0C3H               ;\RETDB    0C0H               ;/ИЛИ XOREAX,EAX

;---- ОБНУЛЕНИЕ ALLOCATE, ЕСЛИ ЗАКАЗАНО ----

      MOVEDI,EBX            ;НАЧАЛО ВЫДЕЛЕННОГО БЛОКА
      TESTCL,2
      JZ    @
      STOSW
@:    SHRECX,2
      REPSTOSD              ;САМО ОБНУЛЕНИЕ ДВОЙНЫМИ СЛОВАМИ
      RET

;---- ВЫВОД ОШИБКИ В СВЯЗАННОМ СПИСКЕ БЛОКОВ ----

M0004:MOVAX,2500H           ;УСТАНОВИЛИ КОД (37,0)
      MOVEDX,OFFSET ОШИБ1   ;"ПАМЯТЬ КУЧИ РАЗРУШЕНА"
M0005:CALL  ?SIGC              ;ВЫДАЕМ СИГНАЛ ОШИБКИ

;---- В СЛУЧАЕ ОШИБКИ НАЧИНАЕМ ПОЛНУЮ ПРОВЕРКУ ----

M0006:JMP   ?TEST_MEM          ;ПРОВЕРКА И ПОИСК НАЧАЛА НАРУШЕНИЯ

;--------------------- FREE (ОСВОБОЖДЕНИЕ ПАМЯТИ) ---------------------

PUBLIC ?FREOP:                 ;ВЫЗЫВАЕТСЯ ИЗ ПРОГРАММ PL/1
      MOVECX,OFFSET ?BEGIN  ;АДРЕС НАЧАЛА СПИСКА

;---- ПРОВЕРКА УКАЗАТЕЛЯ НА NULL ----

      OREBX,EBX            ;ЗАДАН ПУСТОЙ УКАЗАТЕЛЬ ?
      JZ    M0007              ;ТОГДА НИЧЕГО НЕ ДЕЛАЕМ

;---- ПРОВЕРКА, ЧТО АДРЕС ВНУТРИ СПИСКА БЛОКОВ ----

      MOVEAX,[ECX]+0        ;НАЧАЛО СПИСКА БЛОКОВ
      CMPEBX,EAX            ;МЕНЬШЕ НИЖНЕЙ ГРАНИЦЫ ?
      JB    M0004              ;ДА - НЕВЕРНЫЙ АДРЕС
      CMPEBX,?MAX_MEMORY    ;БОЛЬШЕ ВЕРХНЕЙ ГРАНИЦЫ ?
      MOVEDX,OFFSET ?START_MEM
      JAE   M0004              ;ДА - НЕВЕРНЫЙ АДРЕС

;---- ПОДГОТОВКА К ВОЗМОЖНЫМ ПРОВЕРКАМ ЦЕЛОСТНОСТИ ----

      MOV   [EDX]+0,EAXMOV   D PTR [EDX]+4,0    ;ПРОВЕРКА БУДЕТ С НАЧАЛА
      SUBEBX,8              ;АДРЕС НАЧАЛА ПАМЯТИ В БЛОКЕ

;---- КОРРЕКТИРОВКА АДРЕСА ПЕРВОГО СВОБОДНОГО БЛОКА ----

      CMP   [ECX]+4,EBXJB    @
      MOV   [ECX]+4,EBX

;---- ИСХОДНОЕ ПОЛОЖЕНИЕ ПРИ ПРОВЕРКЕ ЦЕЛОСТНОСТИ "КУЧИ" ----

@:    MOVEDX,[EBX]+0        ;АДРЕС ПРЕДЫДУЩЕГО БЛОКА
      MOVECX,[EBX]+4        ;АДРЕС СЛЕДУЮЩЕГО БЛОКА
      TESTDL,1               ;ПРАВИЛЬНЫЙ (ЗАНЯТЫЙ) АДРЕС ?
      JZ    M0006              ;НЕ МОЖЕТ БЫТЬ УЖЕ СВОБОДНЫМ

;---- ОТМЕЧАЕМ БЛОК КАК СВОБОДНЫЙ ----

      DECEDX                ;СНЯЛИ ПРИЗНАК ЗАНЯТОСТИ
      MOV   [EBX]+0,DL         ;ПОМЕСТИЛИ АДРЕС НА МЕСТО

;---- ПРОВЕРКА АДРЕСОВ ПРЕДЫДУЩЕГО И СЛЕДУЮЩЕГО БЛОКОВ ----

      CMPEDX,EBX            ;ПРЕДЫДУЩИЙ ДОЛЖЕН БЫТЬ МЕНЬШЕ
      JAE   M0006              ;ИНАЧЕ ОШИБКА
      CMPEBX,ECX            ;СЛЕДУЮЩИЙ ДОЛЖЕН БЫТЬ БОЛЬШЕ
      JAE   M0006              ;ИНАЧЕ ОШИБКА

;---- ПРОВЕРКА, НУЖНО ЛИ СКЛЕИВАТЬ С ПРЕДЫДУЩИМ ----

      OREDX,EDX            ;ПРЕДЫДУЩЕГО БЛОКА НЕТ ?
      JZ    @                  ;НЕ СКЛЕИВАЕМ
      TEST  B PTR [EDX]+0,1    ;ПРЕДЫДУЩИЙ БЛОК ЗАНЯТ ?
      JNZ   @                  ;ЗАНЯТ - НЕ СКЛЕИВАЕМ

;---- СКЛЕЙКА С ПРЕДЫДУЩИМ БЛОКОМ ----

      MOVAL,[ECX]+0         ;ДОСТАЛИ ПРИЗНАК ЗАНЯТОСТИ СЛЕДУЮЩЕГО
      ANDEAX,1              ;ВЫДЕЛИЛИ ПРИЗНАК ЗАНЯТОСТИ
      OREAX,EDX            ;УСТАНОВИЛИ ПРИЗНАК ЗАНЯТОСТИ
      MOV   [ECX]+0,EAX        ;ТЕПЕРЬ ЗДЕСЬ ПРЕДЫДУЩИЙ
      MOV   [EDX]+4,ECX        ;ТЕПЕРЬ ЗДЕСЬ СЛЕДУЮЩИЙ

;---- ПРОВЕРКА, НУЖНО ЛИ СКЛЕИВАТЬ СО СЛЕДУЮЩИМ ----

@:    TEST  B PTR [ECX]+0,1    ;СЛЕДУЮЩИЙ БЛОК СВОБОДЕН ?
      JNZ   M0007              ;БЛОК ЗАНЯТ - НЕ СКЛЕИВАЕМ

;---- СКЛЕЙКА СО СЛЕДУЮЩИМ БЛОКОМ ----

      MOVEBX,[ECX]+4        ;АДРЕС СЛЕДУЮЩЕГО В СЛЕДУЮЩЕМ
      MOVECX,[ECX]+0        ;АДРЕС ПРЕДЫДУЩЕГО В СЛЕДУЮЩЕМ
      TEST  B PTR [EBX],1      ;И СЛЕДУЮЩИЙ СВОБОДЕН ?
      JZ    M0006              ;ЭТОГО НЕ МОЖЕТ БЫТЬ
      MOV   [EBX]+0,ECX        ;ТЕПЕРЬ УКАЗЫВАЕТ НА ПРЕДЫДУЩИЙ
      INC   B PTR [EBX]+0      ;И ЗАНЯТЫЙ
      MOV   [ECX]+4,EBX        ;НОВЫЙ СЛЕДУЮЩИЙ
M0007:RET

;---------------- ПОЛНАЯ ПРОВЕРКА ЦЕЛОСТНОСТИ ПАМЯТИ ------------------

PUBLIC ?TEST_MEM:              ;МОЖЕТ ВЫЗЫВАТЬСЯ В ПРОГРАММЕ
      MOVEBX,?BEGIN         ;НАЧАЛО СПИСКА БЛОКОВ
      MOVEAX,[EBX]+0        ;АДРЕС ПЕРВОГО БЛОКА
      XOREBP,EBP            ;МАКСИМАЛЬНОЕ ЧИСЛО ПРОВЕРОК
      XOREDX,EDX            ;ПОКА НЕТ ОБЪЕМА ПАМЯТИ
      MOVESI,[EBX]+4        ;АДРЕС СЛЕДУЮЩЕГО БЛОКА
      CMPEAX,1              ;НАЧАЛО ИЛИ НОЛЬ (СВОБОДЕН) ИЛИ 1
      JA    ERROR              ;ЛЮБОЕ ДРУГОЕ ЗНАЧЕНИЕ - ОШИБКА
      JMPS  TST1               ;ПРИСТУПАЕМ К ЦИКЛУ ПРОВЕРОК

;--------------- ЧАСТИЧНАЯ ПРОВЕРКА ЦЕЛОСТНОСТИ ПАМЯТИ ----------------

PUBLIC ?TEST_MEM_KVANT:        ;МОЖЕТ ВЫЗЫВАТЬСЯ В ПРОГРАММЕ
      MOVEBX,?START_MEM     ;НАЧАЛО СПИСКА ПАМЯТИ
      MOVEDX,?ОБЪЕМ_MEM     ;ТЕКУЩИЙ ОБЪЕМ ПАМЯТИ
      OREBX,EBX            ;ЕЩЕ НИ РАЗУ НЕ ПРОВЕРЯЛИ ?
      JNZ   @
      MOVEBX,?BEGIN
      MOV   ?START_MEM,EBX     ;ТОГДА НА НАЧАЛО ПРОВЕРКИ
@:    MOVEAX,[EBX]+0        ;ПРЕДЫДУЩИЙ ВСЕГДА НОЛЬ
      MOVEBP,100            ;НЕ БОЛЕЕ 100 ПРОВЕРОК ЗА РАЗ
      MOVESI,[EBX]+4        ;АДРЕС СЛЕДУЮЩЕГО ЭЛЕМЕНТА
TST1: XORECX,ECX            ;МАКСИМАЛЬНОЕ ЧИСЛО ЦИКЛОВ

;---- ЦИКЛ ПО БЛОКАМ СВЯЗАННОГО СПИСКА "КУЧИ" ----

TST2: MOVEDI,EBX            ;ЗАПОМНИЛИ ПРЕДЫДУЩИЙ
      SUBEBX,[ESI]+0        ;АДРЕСА ДОЛЖНЫ СОВПАДАТЬ
      JZ    @                  ;ДА, СОВПАДАЮТ
      INCEBX                ;ИЛИ ОТЛИЧАТЬСЯ НА 1 (ПРИЗНАК ЗАНЯТОСТИ)
      JNZ   ERROR              ;АДРЕС ИСПОРЧЕН
@:    OREAX,[ESI]+0        ;СУММА ДВУХ ПРИЗНАКОВ
      TESTAL,1               ;ОБА СВОБОДНЫЕ ?
      JZ    ERROR              ;НЕ ДОЛЖНО БЫТЬ ДВУХ ПОДРЯД СВОБОДНЫХ
      MOVEBX,ESI            ;ЗАПОМНИЛИ ТЕКУЩИЙ
      MOVEAX,[ESI]+0        ;АДРЕС ПРЕДЫДУЩЕГО БЛОКА
      MOVESI,[ESI]+4        ;АДРЕС СЛЕДУЮЩЕГО БЛОКА
      ORESI,ESI            ;КОНЕЦ ВСЕГО СПИСКА ?
      JZ    ВЫХОД              ;ДА, ВЫХОДИМ
      ADDEDX,ESI            ;\УЧЛИ ОБЪЕМ
      SUBEDX,EBX            ;/ЭТОГО УЧАСТКА
      CMPESI,?MAX_MEMORY    ;ВЫСКОЧИЛИ ЗА ПРЕДЕЛЫ ПАМЯТИ ?
      JA    ERROR              ;ВЫСКОЧИЛИ - ОШИБКА
      DECEBP                ;ЗАКОНЧИЛАСЬ ПОРЦИЯ ?
      JZ    ВЫХ1               ;ВЫХОДИМ ИЗ ОЧЕРЕДНОЙ ПОРЦИИ
      LOOP  TST2               ;ЗАЩИТА ОТ БЕСКОНЕЧНОГО ЗАЦИКЛИВАНИЯ

;---- НАЙДЕН СЛУЧАЙ "ПАМЯТЬ РАЗРУШЕНА" ----

ERROR:MOV   ?ERR_MEM,EDI       ;ПОСЛЕДНИЙ ПРАВИЛЬНЫЙ АДРЕС
      MOVEBX,EDI            ;ЗАПОМНИЛИ ЕГО

;---- ВЫДЕЛИЛИ МЕСТО В СТЕКЕ ДЛЯ СООБЩЕНИЯ ----

      SUBESP,10+LENGTH ОШИБ3
      PUSH  8
      POPECXMOVEDI,ESP

;---- ЗАПИСАЛИ ДЛИНУ СТРОКИ СООБЩЕНИЯ ОБ ОШИБКЕ ----

      MOVAL,LENGTH ОШИБ3+8
      STOSB

;---- ФОРМИРУЕМ ПОСЛЕДНИЙ ПРАВИЛЬНЫЙ АДРЕС КАК ТЕКСТ ----

M0008:SHLDEAX,EBX,4
      SHLEBX,4
      ANDAL,0FH
      ADDAL,'0'CMPAL,'9'JBE   @
      ADDAL,7
@:    STOSBLOOP  M0008

;---- ДОПИСЫВАЕМ ПОЯСНЕНИЕ "ПАМЯТЬ РАЗРУШЕНА" ----

      MOVCL,LENGTH ОШИБ3
      MOVESI,OFFSET ОШИБ3
      REPMOVSB

;---- ВЫХОДИМ С КОДОМ ОШИБКИ 38 (ПАМЯТЬ РАЗРУШЕНА) ----

      MOVAX,2600H           ;УСТАНОВИЛИ КОД (38,0)
      MOVEDX,ESP            ;КОММЕНТАРИЙ С АДРЕСОМ
      JMP   ?SIGC              ;ВЫДАЕМ СИГНАЛ ОШИБКИ

;---- ЦЕЛОСТНОСТЬ ПРОВЕРЕНА, ПРОВЕРЯЕМ ЭТАЛОННЫЙ ОБЪЕМ ПАМЯТИ ----

ВЫХОД:MOVECX,OFFSET ?ЭТАЛОН_MEM
      MOVEBX,?BEGIN
      CMP   D PTR [ECX],0      ;УЖЕ ЕСТЬ ЭТАЛОН ?
      JNZ   @                  ;ДА, УЖЕ ЕСТЬ

;---- ОДИН РАЗ ПОСЛЕ ALLOCATE/FREE ЗАПОМНИЛИ ЭТАЛОННЫЙ ОБЪЕМ ----

      MOV   [ECX],EDX

;---- СОБСТВЕННО ПРОВЕРКА ПОЛНОГО ОБЪЕМА ----

@:    SUBEDX,[ECX]
      JNZ   ERROR              ;ОБЪЕМ ПАМЯТИ ВДРУГ ИЗМЕНИЛСЯ

;---- ЗАПОМНИЛИ НАЧАЛО ОЧЕРЕДНОЙ ПОРЦИИ ПРОВЕРОК ----

ВЫХ1: MOVEBP,OFFSET ?START_MEM
      MOV   [EBP]+0,EBXMOV   [EBP]+4,EDXXORECX,ECX

;---- ЕСЛИ ДОШЛИ ДО КОНЦА - ПРОВЕРКИ НАЧИНАЕМ ОПЯТЬ С НАЧАЛА ----

      ANDEBX,NOT 1
      MOVEBX,[EBX]+4
      CMP   [EBX]+4,ECXJNZ   @

;---- ИСХОДНОЕ СОСТОЯНИЕ ПРОВЕРКИ С НАЧАЛА ----

      MOVEAX,?BEGIN
      MOV   [EBP]+0,EAXMOV   [EBP]+4,ECX
@:    RET

      DSEG

EXTRN ?BEGIN:D,?MAX_MEMORY:D ;НАЧАЛО И МАКСИМАЛЬНЫЙ АДРЕС ПАМЯТИ

;---- ТЕКУЩИЙ АДРЕС ПОРЦИИ ПРИ ПРОВЕРКЕ ЦЕЛОСТНОСТИ ----

?START_MEM  DD 0

;---- ТЕКУЩИЙ ОБЪЕМ ПРОВЕРЕННЫХ БЛОКОВ ПРИ ПРОВЕРКЕ ЦЕЛОСТНОСТИ ----

?ОБЪЕМ_МЕМ  DD 0

;---- ЭТАЛОННЫЙ ОБЪЕМ ВСЕХ БЛОКОВ ПРИ ПРОВЕРКЕ ЦЕЛОСТНОСТИ ----

?ЭТАЛОН_МЕМ DD 0
?ERR_MEM    DD 0

;---- ТЕКСТЫ СООБЩЕНИЙ ОБ ОШИБКАХ РАСПРЕДЕЛЕНИЯ ПАМЯТИ ----

ОШИБ1 DBLENGTH ОШИБ1-1,'ПАМЯТЬ КУЧИ РАЗРУШЕНА'
ОШИБ2 DBLENGTH ОШИБ2-1,'ВСЯ ПАМЯТЬ ИСЧЕРПАНА'
ОШИБ3 DB' ПАМЯТЬ РАЗРУШЕНА'

Реализация оператора выделения памяти

Реализация оператора ALLOCATE (подпрограмма ?ALLOP), т.е. оператора выделения памяти, проста: по связанному списку «кучи» ищется первый свободный блок с размером не меньше заданного. Найденный блок разбивается на два: заданного размера, помечаемый теперь как занятый, и свободный остаток. Если остаток очень мал (менее 16 байт), разбивка блока не производится. Программисту возвращается адрес памяти, отстоящий на 8 байт от начала выделенного блока, в эти байты записываются служебные ссылки на предыдущий и следующий блоки.

Поиск начинается с запомненного ранее адреса первого свободного блока, а не вообще с начала всего связанного списка. Такая особенность обусловлена тем, что в реальных проектах, особенно при выделении памяти для одинаковых по размеру объектов, память в «куче» часто выделяется «плотно» без неиспользуемых участков, и неэффективно каждый раз при поиске проходить по непрерывно занятому участку «кучи», постоянно растущему от ее начала.

К деталям, не имеющим отношения к теме статьи, относятся вспомогательные процедуры ?ALLOC_ZR и ?ALLOC_NZ, включающие и выключающие режим обнуления выделенной памяти. Автоматическое обнуление выделенной памяти позволяет не задавать явно ее инициализацию в программе, однако иногда затраты на ненужное обнуление больших блоков памяти велики, поэтому и добавлена возможность включения/выключения обнуления с помощью вызова данных процедур.

Реализация оператора освобождения памяти

Реализация оператора FREE (подпрограмма ?FREOP), т.е. оператора освобождения памяти, состоит в том, что помечается блок заданного адреса как опять свободный и, если предыдущий или следующий блоки тоже свободны, этот блок «сливается» со смежным, чтобы предотвратить фрагментацию свободной памяти в «куче».

Как было описано в [2], в точке обращения в программе к оператору FREE в указатель автоматически записывается нулевое значение для уменьшения риска «висячих ссылок».

Реализация контроля целостности памяти

Некоторые проверки правильности адресов имеются внутри подпрограмм ?ALLOP и ?FREOP и выполняются всегда, но, конечно, этого недостаточно для надежной работы. Поэтому добавлены две служебные подпрограммы ?TEST_MEM и ?TEST_MEM_KVANT без параметров, которые требуют явного вызова в программе и предназначены только для контроля целостности «кучи». Разница между ними в том, что первая проводит полную проверку всей «кучи», а вторая – только частичную (не более 100 очередных блоков за одно обращение). Поэтому время выполнения ?TEST_MEM_KVANT невелико и не превышает некоторой величины, тогда как длительность работы ?TEST_MEM зависит от числа блоков в «куче» и в общем случае не определена.

Предполагается, что ?TEST_MEM используется в отладочных режимах, в случаях обработки ошибок и т.п. Тогда как использование ?TEST_MEM_KVANT рассчитано и на длительную «штатную» эксплуатацию в отлаженных программах, при этом вызов подпрограммы можно поместить в некий всегда выполняющийся «главный» цикл программы, например, в цикл ожидания сообщений. Каждый раз, когда в программе происходит обращение к операторам ALLOCATE/FREE, «текущая» работа ?TEST_MEM_KVANT и уже накопленные результаты отменяются, проверка начинается с начала, поскольку структура «кучи» изменилась.

Контроль целостности «кучи» основан на проверках всех ограничений структуры, разумности адресов, а также на подсчете общего объема всех занятых и свободных блоков в «куче». Эта величина однократно выделяется программе при создании «кучи» с помощью подпрограммы Windows API VirtualAlloc и, естественно, не может самопроизвольно измениться при работе с «кучей».

В принципе, при ошибках распределения памяти структура «кучи» может быть испорчена любым образом. Непредсказуемо могут быть испорчены ссылки на блоки, однако, вероятность, что при этом не нарушится монотонное возрастание адресов, отсутствие двух смежных свободных блоков и т.д. мала. И уж совсем исчезающе мала вероятность, что нарушения ссылок внутри «кучи» не изменят суммарный размер всех занятых и свободных блоков. Хотя, конечно, теоретически могут быть случаи «порчи» памяти и без нарушения внутренней структуры «кучи».

Пример работы средств контроля

Простейший пример, моделирующий «порчу» памяти:

TEST:PROC MAIN;
DCL
  A(100)          FIXED(31) BASED(P),
  P               PTR,
  ?TEST_MEM       ENTRY;
ALLOCATE(100) SET(P);
A=0;
FREE A;
END TEST;

В данном примере для моделирования характерной ошибки распределения памяти вместо оператора ALLOCATE A; записан оператор явного выделения блока размером 100 байт из «кучи» с занесением адреса в указатель P. В этом случае следующий оператор A=0; разрушит структуру «кучи», поскольку массив А должен занимать 400 байт, а в используемом для него по умолчанию указателе P записан адрес блока размером всего 100 байт.

Запуск программы даст неопределенную ошибку (рис.1).


Рис. 1. Неопределенная ошибка вследствие нарушения структуры «кучи».

Однако, если после оператора A=0; добавить вызов подпрограммы ?TEST_MEM, результат запуска программы изменится. На рис. 2. показан результат такого запуска программы в режиме интерактивной отладки.


Рис. 2. Результат выполнения с вызовом ?TEST_MEM в режиме интерактивной отладки.

Во-первых, эффект неопределенной ошибки изменился на более конкретную причину «память разрушена», поскольку оператор A=0; затер нулями в «куче» начало следующего блока со ссылками и алгоритм ?TEST_MEM обнаружил это. Во-вторых, имеется подсказка в виде комментария к ошибке со значением 01B4B590. Это последний адрес в связанном списке «кучи», когда контроль целостности еще не находил ошибок. В режиме интерактивной отладки директивой «d .p» было запрошено значение указателя P. Оно оказалось на 8 байт больше начала блока, где обнаружено нарушение структуры. Это подсказка на возможную причину нарушения: до блока по адресу, хранящемуся в указателе P, в структуре «кучи» все еще было нормально. Поэтому можно предположить, что обращение именно к этому объекту и «разрушило память».

Заключение

Такие средства контроля при распределении памяти, конечно, не могут обеспечить полную защиту от ошибочных действий в программе. Однако, при всей простоте и даже примитивности этого контроля, он дал хорошие результаты и позволил существенно улучшить отладку и эксплуатацию программ, защитив от часто встречающихся ошибок. Мало того, при многолетнем сопровождении больших проектов не было зафиксировано ни одного реального случая, когда нарушение целостности структуры «кучи» прошло бы при таком контроле незамеченным и вызвало бы в дальнейшем неопределенные эффекты в программе. И это достигнуто ценой лишь нескольких десятков команд, добавленных в системную библиотеку, и не приведших к ощутимой потере эффективности при управлении памятью.

Литература

  1. Т. Пратт «Языки программирования: разработка и реализация» глава 7 управление памятью. Издательство «Мир» Москва, 1979
  2. Д.Ю. Караваев «К вопросу о совершенствовании языка программирования» RSDN Magazine #4 2011


    Сообщений 0    Оценка 0        Оценить