4 февраля 2021 г.

Добавление EurekaLog в программу вызывает EOutOfResources (Out of system resources)

К нам обратился человек, который пожаловался на то, что его приложение работало нормально, пока он не добавил в него EurekaLog. После включения в проекте EurekaLog стало появляться исключение Out of system resources. Исключение возбуждалось вспомогательной функцией OutOfResources из модуля Vcl.Graphics.

Стек вызова для исключения выглядел следующий образом:
  • Vcl.Graphics.OutOfResources
  • Vcl.Graphics.GDIError
  • Vcl.Graphics.GDICheck
  • Vcl.Graphics.TransparentStretchBlt
  • Vcl.Graphics.TBitmap.Draw
  • Vcl.Graphics.TCanvas.Draw
  • SomeComponent.TSomeDBGrid.DrawCell
  • Vcl.Grids.DrawCells
  • Vcl.Grids.TCustomGrid.Paint
  • Vcl.Controls.TCustomControl.PaintWindow
  • Vcl.Controls.TWinControl.PaintHandler
  • Vcl.Controls.TWinControl.WMPrintClient
  • ...

Само исключение возбуждается такой функцией:
procedure OutOfResources;
begin
  raise EOutOfResources.Create(SOutOfResources);
end;
Которая в свою очередь вызывается из:
procedure GDIError;
const
  BufSize = 256;
var
  ErrorCode: Integer;
  Buf: array [Byte] of Char;
begin
  ErrorCode := GetLastError;
  if (ErrorCode <> 0) and (FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nil, ErrorCode, LOCALE_USER_DEFAULT, Buf, BufSize, nil) <> 0) then
    raise EOutOfResources.Create(Buf)
  else
    OutOfResources;
end;

function GDICheck(Value: THandle): THandle;
begin
  if Value = 0 then
    GDIError;
  Result := Value;
end;

Заметьте, что такая реализация в VCL имеет проблему: вне зависимоси от ошибки возбуждается исключение класса EOutOfResources, даже если ошибка не равна ERROR_NOT_ENOUGH_MEMORY, ERROR_NO_SYSTEM_RESOURCES (или аналогичной). Логичнее было бы возбуждать что-то типа EInvalidGraphicOperation в общем случае и возбуждать EOutOfResources только для ошибок подобного типа.

Из полного баг-отчёта EurekaLog было видно, что показатели памяти и описателей находятся в разумной норме, т.е. проблема не в нехватке памяти. Откуда следует, что (вероятнее всего) GetLastError вернула 0. В самом деле, строка в TransparentStretchBlt, которая проваливает проверку GDICheck выглядит следующим образом:
MemBmp := GDICheck(CreateCompatibleBitmap(SrcDC, SrcW, SrcH));
Из документации видно, что функция CreateCompatibleBitmap не устанавливает значение GetLastError при неудаче.

Впрочем, у функции не так много причин завершиться неудачей: либо ей переданы неверные аргументы, либо ей нехватает памяти для создания bitmap. Заметьте, что нехватка памяти также возможна, если в SrcW и SrcH находится "мусор", который "слишком большой". Таким образом, хотя мы не знаем точную причину неудачи CreateCompatibleBitmap, но мы можем предположить, что проблема - в аргументах.

Значения SrcDC, SrcW и SrcH являются параметрами функции и приходят в неё из TBitmap.Draw:
TransparentStretchBlt
  (ACanvas.FHandle, Left, Top, Right - Left, Bottom - Top, 
   Canvas.FHandle { SrcDC }, 0, 0, 
   FDIB.dsbm.bmWidth { SrcW }, FDIB.dsbm.bmHeight { SrcH }, 
   MaskDC, 0, 0);
Где Canvas - это поле FCanvas bitmap-а, создаваемое по запросу, а FDIB - поле из FImage: TBitmapImage. Таким образом, все параметры (SrcDC, SrcW и SrcH) приходят в функцию TransparentStretchBlt из полей объекта класса TBitmap.

Следовательно, TBitmap, который пытается рисовать TSomeDBGrid.DrawCell, повреждён. Поскольку исключения не происходит без EurekaLog, но происходит с EurekaLog, то содержимое памяти TBitmap меняется при включении EurekaLog. Наиболее вероятное объяснение такого поведения: ошибка типа "use after free". Без отладочных инструментов в программе код может обратиться к уже удалённому TBitmap и "успешно" выполнить с ним операцию - поскольку память освобождённых объектов не удаляется физически, а лишь помечается как "свободная", без изменения её содежимого. При добавлении в программу EurekaLog её конфигурация по умолчанию включает проверки памяти, которые стирают память при её освобождении.

Проверить эту гипотезу можно изменив настройку "When memory is released" в положение "Do nothing". Если после этого исключение EOutOfResources пропадёт, то в коде имеется ошибка вида "use after free". Наиболее вероятна ошибка в коде SomeComponent, но есть небольшой ненулевой шанс, что клиент нашёл ошибку в VCL.

К сожалению, мы не получили ответа от клиента.

Комментариев нет :

Отправка комментария

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.