Вчера отловил неприятный баг. Система рушилась при сборке мусора: NIL pointer dereference. С трудом нашли рецепт воспроизведения, потом я дихотомически локализовал сбой путём добавления oberonRTS.Collect и отладочного вывода в файл. Далее пришлось зарыться в дизассемблер-отладчик. (Огромное спасибо компании Data Rescue за бесплатную версию IDA Pro!) Это с моим-то знанием ассемблера... плохим.
Проблема, слава богу, оказалась не в рантайме XDS, а в нашем коде. Точнее, в неосторожном обращении с низкоуровневыми средствами. Есть такая процедура, которая в произвольный буфер памяти складывает данные:
Код:
ItemToBuffer (VAR buff: ARRAY OF SYSTEM.BYTE; ...);
(* Помещаем данные в buff либо читаем из buff, в зависимости от прочих параметров процедуры. *)
А в другом модуле она используется:
Код:
VAR mem: Common.Buffer;
BEGIN
NEW (mem, GetSize (item));
...
ItemToBuffer (mem, ...);
Код, естественно, сильно упрощён. На самом деле mem - это поле объекта, объявление его далеко и не видно, а тип Common.Buffer - ещё дальше, в другом модуле. И тип этот такой: POINTER TO ARRAY OF SYSTEM.BYTE. ItemToBuffer - тоже метод объекта и находится в третьем модуле. То, что вся эта информация далеко разнесена по разным местам, делает проблему неочевидной. А то, что низкоуровневый формальный параметр типа ARRAY OF SYSTEM.BYTE совместим с ЛЮБЫМ фактическим параметром, заставляет компилятор пропускать нашу ошибку и считать, что POINTER TO ARRAY OF SYSTEM.BYTE и просто ARRAY OF SYSTEM.BYTE - это практически одно и то же. Даже предупреждения не было.
А правильно было написать вот так:
Код:
ItemToBuffer (mem^, ...);
Как видите, ошибка всего в один символ, а в результате младшие два байта указателя затирались нулями, и сборщик мусора ломался в какой-то случайный момент времени после порчи памяти.
svn blame показал, что с момента внесения ошибки до момента устранения прошло... Готовы?..
1 год и 9 месяцев. Это ошибка в библиотеке общего назначения, используемой несколькими коммерческими приложениями. 21 месяц подряд приложения закрывались с ошибкой в случайный момент времени без видимых причин, и никак невозможно было связать поломку ни с действия пользователя, ни с недавними изменениями в приложениях или библиотеке.
Отсюда несколько выводов:
- Низкоуровневые средства нельзя размазывать по системе и использовать без чёткого обозначения красными флажками. В идеале, тонкая прослойка между кодом низкого и высокого уровня должна быть перекрыта мощным тестовым набором.
- Сборку мусора необходимо проводить регулярно - например, в idle loop - хотя бы для проверки целостности памяти. Хорошее время для очистки памяти - обработчик закрытия окна, когда внимание пользователя отвлечено переключением к очередной задаче. В случае сбоя пользователь не сможет сказать, что именно он сделал, но наверняка вспомнит окно, с которым только что закончил работать.
- Даже на такое низкоуровневое средство компилятор должен был бы ругаться. Шутка ли - спутать ARRAY и POINTER TO ARRAY! Указатель вообще не должен в неявном виде приводиться к неуказательному типу, это же прямой путь к адресной арифметике. Надо привести - есть SYSTEM.VAL, SYSTEM.ADR, и прочие прелести модуля SYSTEM.
- Дизассемблеры нужны не только кракерам, но и высокоуровневым программистам.