20 мая 2018 г.

Дело о случайных вылетах

Один начинающий программист спросил, почему вылетает с Access Violation очень простой и, что интереснее, явно корректный код.

Примечание: слово "случайных" в заголовке означает, что вылет происходит в непредсказуемом месте - разном, в зависимости от приложения. Но в рамках одного конкретного приложения вылет всегда детерминирован (и, следовательно, воспроизводим).

Я не буду приводить код целиком, потому что, во-первых, он не имеет отношения к вопросу, во-вторых, он многократно менялся. Автор вопроса показал себя мастером скрывать данные и давать неточные показания.

Методом термо-ректального допроса были установлены следующие начальные условия:
  • С Access Violation вылетает простой вызов ShowMessage(SomeVariable);
  • Сообщение: 'Access Violation at 0x0000056B: read of address 0x0000056B'.
  • В SomeVariable - много текста.
  • В новом проекте код работает успешно.
Звучит знакомо? Возможно ли, что это очередная проблема с ShowMessage, реализованным через Task Dialogs?

После просьбы проверить работу с манифестом ("темами") и без, было выдано последнее начальное условие, заменившее собой последний пункт:
  • С Access Violation вылетает простой вызов ShowMessage(SomeVariable);
  • Сообщение: 'Access Violation at 0x0000056B: read of address 0x0000056B'.
  • В SomeVariable - много текста.
  • Вылетает при добавлении в проект штатной VCL-темы ("скина"), и не вылетает при не-скинованном приложении.

Ваши варианты? Листайте ниже для ответа.



















Как всегда, начинаем анализ с класса и сообщения исключения.
  • Исключение имеет класс EAccessViolation, что говорит о том, что это - проблема с доступом к памяти.
  • В частности: ошибка чтения ("read").
  • Заметим, что адрес $0000056B, который мы пытаемся прочитать, находится недалеко от нуля, т.е. начинаем подозревать попытку прочитать что-то по адресу nil (да, nil равен нулю, но если мы читаем что-то по смещению, например, поле объекта, равного nil, то как раз и получится небольшой, но при этом не нулевой адрес).
  • Ещё можно заметить, что исключение возникло по адресу $0000056B - это явно нижние 64 Кб (меньше $FFFF) из зарезервированного (недоступного) блока памяти. По идее, по этому адресу должен находится код, который возбудил исключение. Откуда взялся код в зарезервированном пространстве?
  • Наконец, слон в комнате: адрес исключения ($0000056B) в точности равен адресу, который нельзя прочитать ($0000056B).
Всё это означает, что произошёл переход управления "в космос": на некоторые мусорные данные, которые указывают на недоступную память. Как такое может произойти?

Есть два наиболее частых случаях. Во-первых, вы можете вызвать функцию по процедурной переменной, которой не присвоено корректное значение, например:
type
  TSomeProc = procedure(const ASomeArg: Integer); stdcall;
var
  SomeProc: TSomeProc;
begin
  SomeProc := GetProcAddress(SomeLib, 'SomeProc');
  SomeProc(42);
В случае если в библиотеке нет такой функции, то GetProcAddress вернёт nil. А поскольку код никак не проверяет на ошибки, то мы попытаемся вызвать функцию по nil, что приведёт к "Access Violation at 0x00000000: read of address 0x00000000". Да, это не наш случай, т.к. адрес в нашем случае - не nil. Поэтому, возможно и что-то такое:
if SomeCondition then
  SomeProc := GetProcAddress(SomeLib, 'SomeProc')
else if SomeOtherCondition then
  SomeProc := GetProcAddress(SomeOtherLib, 'SomeProc');
SomeProc(42);
Если ложны оба условия, то SomeProc будет не инициализирована, т.е. в ней останется какое-то мусорное значение от предыдущего выполнения - которое как раз может быть равно $56B.

Во-вторых, вызов по "левому" указателю может произойти когда вы выходите из подпрограммы - по адресу возврата, сохранённому в стеке. Поскольку адрес возврата хранится в стеке, то его можно испортить, допустив ошибку переполнения буфера, например:
var
  Buffer: array[0..255] of Char;
begin
  Move(Some1KbVariable, Buffer, 1024);
  // или:
  for X := 0 to 1023 do 
    Buffer[X] := ' '; // в предположении, что Range Check отключен
end; // - вылет тут
Мы пишем данные за пределы буфера Buffer. За буфером лежат другие локальные переменные, а после них - стековый фрейм с адресом возврата. Таким образом мы затрём адрес возврата, поэтому когда функция закончит работу и попытается вернуть управление вызывающему, на самом деле, произойдёт переход на "случайное" мусорное место.

Итак, что же может пойти не так при вызове ShowMessage?

ShowMessage - это функция RTL Delphi, т.е. она не импортируется из другой DLL. Однако, мы знаем, что это просто переходник к MessageBox или Task Dialogs - оба варианта являются API Windows и импортируются из соответствующих функций. Можно предположить, что MessageBox импортируется статически (через external), поскольку MessageBox есть во всех версиях Windows, а Task Dialogs - динамически, поскольку есть только на Windows Vista и выше.

Таким образом, предварительно можно подозревать баг в RTL Delphi при импорте API Task Dialogs.

Давайте, наконец, нажмём кнопку Break в окне уведомления отладчика об исключении и посмотрим стек вызовов:


Мягко говоря, информации не много. Отладчик Delphi не способен реконструировать стек вызовов. Есть два способа решить эту проблему: простой и сложный.

Способ сложный: раскрутить стек вручную. Для начала, несложно сообразить, что раз мы вызвали недоступную память, то ни одной команды вызываемой подпрограммы выполнено не было. Следовательно, последней успешно выполненной командой была инструкция CALL. Иными словами, последнее значение, записанное в стек - это адрес возврата. Смотрим:


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

Мы видим, что в нашем случае происходит вызов GDIFlush из TBitmap.GetScanLine. Это несколько странно по двум причинам:
  1. Работа с графикой не соответствует нашим ожиданиям (мы ищем ошибку импорта Task Dialogs).
  2. Откуда берётся ошибка вызова в этом случае?
  3. Если вы были очень внимательны, то увидели наше проблемное число $56B на снимке выше. Как на него произошёл переход? (признаюсь, я заметил это только во время написания статьи, а не во время отладки)
Вероятнее всего, проблема сложнее, чем нам показалось вначале. Для лучшего понимания проблемы нужно реконструировать стек вызовов полностью. Для этого нужно смещаться вверх по окну стека и проверять каждое значение через Follow to near code:


Как видим, TBitmap.GetScanLine вызывается из StyleUtils.CreateRegionDataFromBitmap. Это - второй элемент в стеке вызова. Продолжая двигаться по стеку, вы реконструируете стек вызовов.

(Разумеется, при некотором опыте вы сможете на глаз определять является ли значение на стеке адресом возврата или нет, а также использовать фреймы, чтобы пропускать области стека.)

Как мы видим, ручной разбор стека - возможный, но крайне трудоёмкий способ.

Способ простой: используйте утилиту Threads Snapshot из состава EurekaLog. Если у вас есть EurekaLog - то утилита, скорее всего, уже стоит. Если у вас нет EurekaLog, то на сайте можно скачать бесплатный набор утилит. Просто запустите её, тыркните в ваше тестовое приложение и сохраните отчёт в файл. Разумеется, при этом ваше приложение должно стоять на паузе в отладчике, чтобы стек не менялся.

Вот, что обнаружила Threads Snapshot:
  • Vcl.Graphics.TBitmap.GetScanline
  • Vcl.Styles.CreateRegionDataFromBitmap
  • Vcl.Styles.CreateRegionFromBitmap
  • Vcl.Styles.TSeBitmapObject.CreateRegion
  • Vcl.Styles.TSeStyleObject.GetRegion
  • Vcl.Styles.TSeStyle.WindowGetRegion
  • Vcl.Styles.TCustomStyle.DoGetElementRegion
  • Vcl.Themes.TCustomStyleServices.GetElementRegion
  • Vcl.Forms.TFormStyleHook.GetRegion
  • Vcl.Controls.TWinControl.WndProc
  • Vcl.Forms.TFormStyleHook.WMNCCalcSize
  • Vcl.Forms.TFormStyleHook.ChangeSize
  • Vcl.Themes.TStyleHook.CallDefaultProc
  • Vcl.Forms.TFormStyleHook.WMWindowPosChanging
  • Vcl.Themes.TStyleHook.WndProc
  • Vcl.Themes.TMouseTrackControlStyleHook.WndProc
  • Vcl.Forms.TFormStyleHook.WndProc
  • Vcl.Themes.TStyleHook.HandleMessage
  • Vcl.Styles.TStyleEngine.HandleMessage
  • Vcl.Themes.TStyleManager.HandleMessage
  • Vcl.Controls.TWinControl.DoHandleStyleMessage
  • Vcl.Controls.TWinControl.WndProc
  • Vcl.Forms.TCustomForm.WndProc
  • Vcl.Controls.TWinControl.MainWndProc
  • System.Classes.StdWndProc
  • user32._InternalCallWinProc
  • user32.InternalCallWinProc
  • win32u.NtUserSystemParametersInfo
  • user32.RealSystemParametersInfoW
  • uxtheme.ThemeSystemParametersInfoW
  • user32.DispatchClientMessage
  • win32u.NtUserSetWindowPos
  • Vcl.Controls.TWinControl.SetBounds
  • Vcl.Controls.TWinControl.GetClientRect
  • Vcl.Forms.TCustomForm.GetClientRect
  • Vcl.Controls.TControl.SetClientSize
  • Vcl.Controls.TControl.SetClientHeight
  • Vcl.Forms.TCustomForm.SetClientHeight
  • Vcl.Dialogs.CreateMessageDialog
  • Vcl.Dialogs.MessageDlgPosHelp
  • Vcl.Dialogs.ShowMessagePos
  • Vcl.Dialogs.ShowMessage
  • Unit1.TForm1.Button1Click
  • Vcl.Controls.TControl.Click
  • Vcl.StdCtrls.TCustomButton.Click
  • Vcl.StdCtrls.TCustomButton.CNCommand
  • Vcl.Controls.TControl.WndProc
  • user32._InternalCallWinProc
  • user32.InternalCallWinProc
  • user32.UserCallWinProcCheckWow
  • System.TObject.Dispatch
  • Vcl.Themes.TStyleHook.WndProc
  • Vcl.Themes.TMouseTrackControlStyleHook.WndProc
  • Vcl.StdCtrls.TButtonStyleHook.WndProc
  • Vcl.Themes.TStyleHook.HandleMessage
  • Vcl.Styles.TStyleEngine.HandleMessage
  • Vcl.Themes.TStyleManager.HandleMessage
  • Vcl.Controls.TWinControl.DoHandleStyleMessage
  • Vcl.Controls.TWinControl.WndProc
  • Vcl.StdCtrls.TButtonControl.WndProc
  • Vcl.Controls.TControl.Perform
  • Vcl.Controls.DoControlMsg
  • Vcl.Controls.TWinControl.WMCommand
  • Vcl.Forms.TCustomForm.WMCommand
  • Vcl.Controls.TControl.WndProc
  • comctl32.Button_WndProc
  • user32.CallWindowProcW
  • System.TObject.Dispatch
  • Vcl.Themes.TStyleHook.WndProc
  • Vcl.Themes.TMouseTrackControlStyleHook.WndProc
  • Vcl.Forms.TFormStyleHook.WndProc
  • Vcl.Themes.TStyleHook.HandleMessage
  • Vcl.Styles.TStyleEngine.HandleMessage
  • Vcl.Themes.TStyleManager.HandleMessage
  • Vcl.Controls.TWinControl.DoHandleStyleMessage
  • Vcl.Controls.TWinControl.WndProc
  • Vcl.Forms.TCustomForm.WndProc
  • Vcl.Controls.TWinControl.MainWndProc
  • System.Classes.StdWndProc
  • user32._InternalCallWinProc
  • user32.InternalCallWinProc
  • user32.SendMessageWorker
  • user32.SendMessageW
  • Vcl.StdCtrls.TButtonStyleHook.DoClick
  • Vcl.Themes.TStyleHook.Invalidate
  • Vcl.StdCtrls.TButtonStyleHook.WMLButtonUp
  • Vcl.Themes.TStyleHook.WndProc
  • Vcl.Themes.TMouseTrackControlStyleHook.WndProc
  • Vcl.StdCtrls.TButtonStyleHook.WndProc
  • Vcl.Themes.TStyleHook.HandleMessage
  • Vcl.Styles.TStyleEngine.HandleMessage
  • Vcl.Themes.TStyleManager.HandleMessage
  • Vcl.Controls.TWinControl.DoHandleStyleMessage
  • Vcl.Controls.TWinControl.WndProc
  • Vcl.StdCtrls.TButtonControl.WndProc
  • Vcl.Controls.TWinControl.MainWndProc
  • System.Classes.StdWndProc
  • user32._InternalCallWinProc
  • user32.InternalCallWinProc
  • user32.PeekMessageW
  • user32.DispatchMessageW
  • Vcl.Forms.TApplication.ProcessMessage
  • Vcl.Forms.TApplication.HandleMessage
  • Vcl.Forms.TApplication.Run
  • Project1.Initialization
  • KERNEL32.BaseThreadInitThunk
Это ж... жёваный крот! Огромный стек (представьте, сколько бы вы его реконструировали вручную)...

(заметьте, что это - RAW-трассировка, т.е. возможно присутствие ложно-положительных вызовов).

Итак, из стека сразу видно, что проблема - не в импорте API, т.к. в стеке есть вызов Vcl.Dialogs.CreateMessageDialog - это альтернативная реализация, без Task Dialogs. Кроме того, на вершине стека - вызовы кода, ответственного за VCL стили. Т.е. проблема не там, где мы предположили.

Почему, кстати, не используются Task Dialogs? Вспомним условие:
function MessageDlgPosHelp(const Msg: string; DlgType: TMsgDlgType;
  Buttons: TMsgDlgButtons; HelpCtx: Longint; X, Y: Integer;
  const HelpFileName: string): Integer;
begin
  if TOSVersion.Check(6) and UseLatestCommonDialogs and
     IsNewCommonCtrl and StyleServices.IsSystemStyle then
    Result := DoTaskMessageDlgPosHelp('', Msg, DlgType, Buttons,
      HelpCtx, X, Y, HelpFileName)
  else
    Result := DoMessageDlgPosHelp(CreateMessageDialog(Msg, DlgType, Buttons),
      HelpCtx, X, Y, HelpFileName);
end;
Несложно увидеть, что Task Dialogs отключаются при использовании VCL стилей.

Смотря на стек далее, мы видим, что я ошибся: ShowMessage (по крайней мере, в последних версиях Delphi) реализована вовсе не через функцию MessageBox. Вместо этого она вызывает CreateMessageDialog - которая создаёт форму для показа сообщения. Иными словами, наша проблема, похоже, вообще не имеет отношения к API Windows и заключается только в коде RTL / VCL Delphi.

Ну раз проблема где-то в коде RTL / VCL и VCL стилях, то давайте же посмотрим, что там с вызовом GDIFlush из TBitmap.GetScanLine: почему он проваливается? Мы знаем место вылета, поэтому можем поставить точку останова на вызов GDIFlush. Однако это не сильно нам поможет: TBitmap.GetScanLine вызывается очень часто. В том числе и внутри вызова ShowMessage.

Простейший способ, что я нашёл, заключается в следующем: просто запустите программу и позвольте ей вылететь. Как только она вылетела - попробуйте повторить вызов GDIFlush. Напомню, что мы вылетаем с Access Violation при попытке выполнить код по адресу $0000056B. При этом на вершине стека находится адрес возврата к вызову GDIFlush из TBitmap.GetScanLine. Поскольку сейчас возникло исключение, то это означает, что нормальное выполнение программы будет прервано и управление получит обработчик исключения. Мы можем попробовать переписать код так:
try
  ShowMessage(SomeVariable);
except
  GdiFlush;
end;
И установить точку останова на вызов GDIFlush из блока except.

Однако когда вы попробуете запустить этот код, а затем продолжить выполнение после первого (ожидаемого) вылета (внутри ShowMessage), то получите вот это:


Ух... Обработчик исключений RTL попытался создать объект Delphi (EAccessViolation) для аппаратного исключения, чтобы вызвать блок except - и ему это не удалось! Возникло ещё одно исключение Access Violation, очень похожее на первое, хотя и по иному адресу. Что-то серьёзно пошло не так.

На самом деле, нам здорово повезло, поскольку функция GetExceptionObject вызывается всего один раз за выполнение программы и как раз после первого и перед вторым исключениями. Поэтому можно смело ставить точку останова на её начало (или даже на строку "11: E := CreateAVObject;"). И дальше у нас есть широкие возможности отладки:
  1. Можно, как мы и планировали ранее, попытаться повторно вызвать GDIFlush.
  2. А можно просто продолжить выполнение дальше (по шагам), следя за поведением программы. Здесь мы исходим из того, что эти два исключения, вероятно, вызваны одной и той же проблемой, поэтому не имеет значения какое мы будем исследовать.
Давайте посмотрим на оба способа. Для начала нам нужно узнать адрес вызова GDIFlush. Для этого нам нужно перезапустить программу и вылететь первый раз, затем проследовать по адресу на вершине стека:


Запомним это место. Можно просто установить точку останова (в отключенном состоянии). А можно просто выписать адрес на листочек.

Теперь продолжим выполнение программы и остановимся в функции GetExceptionObject (где мы поставили точку останова) - до второго вылета. Теперь открываем CPU отладчик и используем функцию Go to address в поле кода (не памяти! не стека!):


Это перебросит нас прямо на наш вызов GDIFlush. Теперь мы можем использовать команду New EIP:


Заметьте, что, в отличие от команды Go to address, для New EIP щёлкать правой кнопкой нужно именно на той команде, которую мы хотим выполнить следующей. Данная операция изменит значение регистра EIP (RIP - для 64-битной программы) - программного счётчика, т.е. указателя на машинную инструкцию, которую нужно выполнить. В частности, если в вашей версии Delphi нет команды New EIP, то вы можете её эмулировать просто изменив значение регистра EIP на нужный вам адрес. Кстати, это будет ещё и быстрее: не надо предварительно переходить на указанный адрес.

Так или иначе, сейчас мы стоим на вызове GDIFlush и готовы его выполнить.

Мы не пойдём дальше по этой ветке, а вместо этого рассмотрим второй вариант. Перезапустим программу, дождёмся первого вылета, продолжим выполнение до точки останова в GetExceptionObject. Далее, (если вы ставили точку останова на начало функции) пройдём до вызова CreateAVObject. Теперь мы готовы его выполнить. Да, это оказалось существенно проще, чем возня с EIP, но надо понимать, что далеко не всегда будет второй идентичный вылет, который можно отлаживать. Чаще всего вам придётся возвращаться назад, меняя EIP.

В любом случае, начнём пошаговое выполнение. Будем использовать самый детальный режим: F7 (Step Into). Оказывается, что в CreateAVObject мы успешно входим. Проходим её по шагам (через F8 / Step Over), пока не наткнёмся на строку, возбуждающую второе исключение:


Что это? Простое присвоение уводит выполнение программы на случайное место? Как такое может быть?

Ответ несложно увидеть, если использовать машинный отладчик:


Оказывается, что SReadAccess - это не константа, а ресурсная строка (resourcestring). Т.е. она загружается из ресурса. Delphi позволяет обращаться к ней напрямую, не используя явно никаких функций загрузки. А сама загрузка происходит скрыто, под капотом RTL, за счёт магии компилятора.

Собственно, заходя далее по F7 в LoadResString, мы видим, что она вылетает при попытке вызвать функцию WinAPI LoadString:


Итак, просуммируем: ShowMessage вызывает VCL код для создания формы для окна сообщения. При включенных темах происходит вызов TBitmap.GetScanLine, который не может вызвать GDIFlush. Происходит вылет, управление получает RTL код, который пытается создать Delphi-обёртку над аппаратным исключением, но проваливается при попытке загрузить строку из ресурса. Возникает второе исключение, после чего цикл повторяется бесконечно до исчерпания стека, а после него - и фатального вылета.

Вызовы GDIFlush и LoadString в нашем случае идентичны: это вызовы WinAPI, которые вылетают. Расследовать дальше можно любой из них. Я буду работать с LoadString.

Воспроизведём ситуацию, встанем перед проблемным вызовом LoadString:


Пока всё нормально. Это прямой безусловный вызов. Провалиться он не должен. Нажимаем F7:


Что это такое?
Вспомним, что мы вызываем не просто функцию, а функцию WinAPI. Это означает, что эта функция импортируется из внешней DLL. Как компилятор может вызвать функцию, адреса которой он не знает (на этапе компиляции)? Ведь точный адрес этой функции будет известен только при выполнении программы, когда будет загружена DLL, будет определён её адрес и адрес функции в ней. Очевидно, что для этого нужно использовать какую-то относительную адресацию: "вызови функцию по этому адресу", используя адрес-заглушку, который будет изменён на реальный адрес, как только он станет известен в run-time.

Если подумать ещё немного, то станет понятно, почему мы видим два вызова (первый - прямой, второй - опосредованный). Ведь если вставлять адрес при каждом вызове WinAPI функции в вашем коде, то после загрузки DLL загрузчику придётся перелопатить миллион вызовов в вашем коде, исправляя адрес в каждом из них. Если же вместо этого сделать заглушку-переходник с адресом, который нужно исправлять, а все вызовы этой функции направить на переходник, то исправлять придётся только один адрес: в заглушке-переходнике. Все же "миллионы вызовов" останутся неизменными.
Итак, сейчас мы стоим на вызове по косвенному адресу. Я думаю, несложно сообразить, что тут кроется возможность для вылета. Давайте посмотрим, что же там лежит по этому адресу. Для этого используем команду Go to address в окне памяти (не кода! не стека!):


Увидим (не забываем переключиться в группировку по DWORD для лучшей читаемости):


Вот, собственно, и причина вылета. Мы вызываем функцию по указателю, в котором записан мусор. Кстати, обратите внимание и на соседние адреса тоже. Все они должны принадлежать заглушкам для вызова внешних функций из DLL. Но вместо адресов кода там лежат явно левые значения: $148F, $56B и т.д.

Если у вас ещё глаза на лоб не полезли ("как такое может быть?"), то давайте подумаем: очевидно, что эти значения - это не нормально, они не могут быть записаны туда загрузчиком ОС. Также очевидно, что и GDIFlush и LoadString вызываются очень часто - и все предыдущие вызовы были успешны. Т.е. изначально по этим адресам записаны всё же правильные значения. Откуда можно сделать вывод, что в какой-то момент времени (внутри вызова ShowMessage) какой-то код как-то испортил эти значения.

Очень много вопросов. Но как мы можем найти этот код, ведь он выполнялся где-то ранее и с моментом вылета не связан никак? К счастью, у нас ситуация простая: имеем 100% воспроизводимую проблему, по известному и неизменному адресу кто-то пишет. Ничего не напоминает? Конечно же, точку останова на данные. Ставим. Для этого открываем окно точек останова, щёлкаем правой: Add - Data Breakpoint:


Вводим адрес, по которому мы обнаружили порчу данных и не забываем указывать размер этих данных (в нашем случае данные - это указатель, т.е. 4 байта в 32-битной программе, 8 - в 64-битной). Перезапускаем программу.

Заметьте, что при перезапуске все адресные точки останова отключаются! Поэтому не забывайте их включать до вызова проблемного кода.

Итак, жмём на кнопку, и отладчик немедленно останавливает нас в этом месте:


Вот, собственно, и проблема. Подсвеченная строка - виновник. Именно она портит данные. А почему она это делает? Потому что она пишет в глобальную переменную, которая является статическим (это важно) массивом. Более того, она не проверяет индекс (как явно, через if, так и неявно, через {$R+}) - поэтому при слишком большом количестве элементов мы получаем банальное переполнение буфера. Подпрограмма CreateRegionDataFromBitmap перезаписывает данные, находящиеся за буфером Rts. Ну и так получается, что глобальные переменные находятся не так далеко от адресов для импорта. Соответственно, если мы будем слишком сильно вылезать за пределы глобального статического массива, то рано или поздно залезем в таблицу адресов для импорта.

Собственно, теперь несложно увидеть, что мусорные данные, на которые происходили вызовы, являются просто-напросто координатами прямоугольников.

Вот и всё, тайна раскрыта. Причина - баг в VCL Delphi, выход за границы массива.

Мораль истории: не используйте статические массивы для буферов. У компилятора Delphi нет отладочных средств для контроля их переполнения. Используйте динамические массивы. Отладочный менеджер памяти может вставлять отладочные маркеры до и после массива, контролируя переполнение по их отсутствию.

5 комментариев :

  1. Спасибо за статью, как всегда очень познавательно

    ОтветитьУдалить
  2. Спасибо за статью.
    А.П.
    Known bag is not a Bag. This is a Feature. Я(а, может не я?).

    ОтветитьУдалить
  3. доброго дня, столкнулся почти с такой же проблемой, только ошибка возникает при закрытии приложения, то есть нужная подпрограмма отработала успешно. если интересно - напишите электронный адрес, а то на сайте не нашел - скину исходники.

    ОтветитьУдалить
  4. Спасибо за статью.
    А какие ещё попадались причины багов со "скинами" VCL?

    А то в своих программах столкнулся с отчётами от пользователей вида: Access violation at address 0061E9CA in module 'Pr.exe'. Read of address FFFFFFFC.
    > Скриншот стека вызовов <

    Пока не знаю даже как воспроизвести, так как на сколько я понял они вылазят когда программа работает в нагрузке по потокам, и пользователь что-то делает с главной формой. И вообще непонятно куда копать.

    ОтветитьУдалить
    Ответы
    1. Возможно, у вас нет синхронизации потоков. Возможно, вы обращаетесь к VCL из фонового потока. Я не могу сказать, не видя кода программы. Дизассемблер бы в студию.

      Удалить

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

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

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

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

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

Примечание. Отправлять комментарии могут только участники этого блога.