Байки о несовместимых приложениях

:

Это отрывки из бесплатной главы из бумажной книги «The Old New Thing». Они ничему не пытаются научить; это просто короткие забавные эпизоды из будней борцов за совместимость новых версий Windows со старыми приложениями.

Изменяем номер версии Windows


Изменить номер версии, который Windows сообщает программам, не просто, как кажется. Например, некоторые программы проверяют номер версии так:
UINT Ver = GetVersion();
UINT MajorVersion = LOBYTE(uVer);
UINT MinorVersion = HIBYTE(uVer);
if (MajorVersion < 3 || MinorVersion < 10) {
    Error("This program requires Windows 3.1");
}

Представьте себе, как этот код отработает на Windows 95, у которой номер версии 4.0. Вторая проверка срабатывает из-за того, что 0 меньше 10.
И ладно бы программа просто выводила сообщение об ошибке и завершалась. Многие рушились: очевидно было, что их поведение на «неподдерживаемых» версиях Windows никогда не тестировалось.

Таких программ оказалось настолько много, что мы перестали исправлять их по одной, и просто изменили возвращаемый номер версии с 4.0 на 3.95.

Программы для MS-DOS тоже не все гладко реагировали на изменение версии ОС. Это удивительно потому, что к тому времени у MS-DOS уже были десятки выпущенных версий и под-версий, и разработчикам пора было бы уже научиться проверять номер версии. Но например, один пакет программ использовал номер версии DOS как индекс в таблице функций: по собственной функции для каждой вышедшей версии. В таблице было пять функций: от MS-DOS 1.x до 5.x. Когда программа запускалась под MS-DOS 6.0, она вызывала адрес за пределами таблицы, и рушилась.

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

Сообщаем разработчикам о баге в их программе


Конкретно этот разговор вымышлен, но похожие по сути разговоры случались не единожды.

—Алло?
—Здравствуйте, это такой-то, из отдела совместимости Microsoft Windows. Мы нашли в вашей программе баг, из-за которого она не работала. Мы были вынуждены добавить в Windows 95 код для обхода этого бага, чтобы ваша программа продолжала работать.
—Замечательно, большое вам спасибо! До свидания. ( Короткие гудки.)
(Набираем номер снова.)
—Алло?
—Алло, ээ… Вы даже не хотите узнать, какой был баг?
—Какая разница? Вы же всё исправили. Спасибо! Что бы мы без вас делали!
—Но, ээ, код в обход бага будет работать только с текущей версией вашей программы. Когда вы выпустите следующую версию, она не заработает.
—Вот оно как?.. Ну, подождите, я свяжу вас с нашими программистами.

Разработчики совершенно не обращают на вас внимания, пока вы не упомянете, что их программа перестанет работать.

Когда я занимался обратной совместимостью с программами для MS-DOS (в основном это были игры), часто бывало, что я звонил сообщить разработчикам, что их программа не работает под Windows 95. Многие разработчики отвечали просто «Так и есть, мы не работаем с Windows.»

Напоминаем пользователям действовать строго по инструкции


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

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

Ставим телегу впереди лошади


Иногда в программах обнаруживаются такие жуткие баги, что непонятно, как они вообще работают.

Установщик одной популярной программы пытается заменить системный файл собственной версией. Он заменяет файл, даже если эта собственная версия старее, чем уже установленная в системе: установщик не заморачивается проверкой версий.
Отлично, система защиты файлов Windows внимательно следит за такими программами, и она восстановит правильную версию, как только установка завершится. Проблема не в этом.

Если системный файл, который установщик пытается заменить, занят, то установщик перезаписывает его с опцией MOVEFILE_DELAY_UNTIL_REBOOT — чтобы занятый файл заменился при перезагрузке системы:

// Код упрощен для ясности
MoveFileEx("sysfile.new", "sysfile.dll", MOVEFILE_DELAY_UNTIL_REBOOT);
CopyFile("D:\\CDROM\\INSTALL\\sysfile.dll", "sysfile.new");

Так и есть — программа пытается копировать файл, которого ещё не существует!
Этот код действительно работал на Windows NT, потому что там функция MoveFileEx, когда её вызывали с MOVEFILE_DELAY_UNTIL_REBOOT, не проверяла, существует ли копируемый файл. В бета-версии Windows 2000 мы добавили более строгую проверку параметров; теперь MoveFileEx возвращала ошибку. Установщик считал эту ошибку фатальной, и прекращал установку.

Пришлось в Windows 2000 разрешить копирование несуществующих файлов с опцией MOVEFILE_DELAY_UNTIL_REBOOT. MoveFileEx рапортует об успешном выполнении — в надежде, что ко времени перезагрузки копируемый файл действительно появится.

Самый странный способ проверки успешности вызова


Понятия не имею, о чём думали разработчики одной мультимедиа-программы, которая, проверяя успех вызова мультимедиа-функций, не сравнивала возвращаемый MMRESULT с MMSYSERR_NOERROR, а получала для номера ошибки текстовое описание, и затем сравнивала эту строку с «The specified command complete successfully.»
На самом деле, она сравнивала только первые шестнадцать символов с «The specified co» — наверное, кто-то просматривал код и решил оптимизировать это место.
В одной из версий Windows, когда мы немного перефразировали это сообщение, программа перестала работать. Нет нужды говорить, что на не-английских версиях Windows она никогда не работала.

Более странный, чем самый странный, способ проверки успешности вызова


Что бы вы думали? Есть ещё более странный способ. Как минимум, прошлая программа работала с возвращаемым кодом ошибки. Другая программа, которая пользовалась MCI для проигрывания видеороликов, полностью игнорировала значение, возвращаемое вызовом MCIWndOpen. Вместо этого она читала заголовок MCI-окна, и сравнивала его со строкой «No Device», чтобы определить, удалось ли открытие файла.

В Windows 95 заголовок MCI-окна при открытии файла устанавливался с небольшой задержкой. Теперь та программа не могла открыть видеоролики: она проверяла заголовок окна слишком рано.

Как допустить ошибку в функции из одной строчки


Однажды ко мне в офис зашёл сотрудник, и спросил: «Эй, хочешь звуковую карту?» Своей звуковой карты у меня не было, так что я согласился.

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

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

void FAR PASCAL midiCallback(NPPORTALLOC pPortAlloc, WORD msg,
                             DWORD dwParam1, DWORD dwParm2) {
  if (pPostAlloc->dwCallback)
    DriverCallBack(pPortalloc->dwCallback, HIWORD(pPortalloc->dwFlags),
                   pPortalloc->hMidi, msg, dwParam1, dwParam2);
}
Она вызывается системой при аппаратном прерывании.

Вот так выглядела эта функция в том драйвере:

void FAR PASCAL midiCallback(NPPORTALLOC pPortAlloc, WORD msg,
                             DWORD dwParam1, DWORD dwParm2) {
  char szBuf[80];
  if (pPostAlloc->dwCallback) {
    wsprintf(szBuf, " Dc(hMidi=%X,wMsg=%X)", pPortalloc->hMidi, msg);
#ifdef DEBUG
    OutputDebugString(szBuf);
#endif
    DriverCallBack(pPortalloc->dwCallback, HIWORD(pPortalloc->dwFlags),
                   pPortalloc->hMidi, msg, dwParam1, dwParam2);
  }
}

Они не только оставили в окончательной версии драйвера остатки отладочного кода; во время аппаратного прерывания они вызывают функцию, которую в этот момент вызывать нельзя. Если wsprintf выгружена из памяти, то система во время аппаратного прерывания получает новое прерывание «отсутствующий сегмент», — и всё, конец. Даже если повезло, и wsprintf находится в памяти — эта функция изменяет верхнее слово 32-битных регистров, а обработчик аппаратных прерываний сохраняет только нижнее слово. Это означает, что прерванный код ждёт весьма грубое пробуждение, когда обработка прерывания завершится.

Несмотря на всё это, звуковая карта получила награду одного популярного компьютерного журнала.
Но этим несправедливость не ограничилась: производитель карты хотел, чтобы в Windows 95 вошла новая версия их драйвера. И что же мы увидели в новой версии? Тот самый баг, рушащий систему — тот самый, который мы показали их программистам несколько месяцев назад. Как можно догадаться, новая версия драйвера в Windows 95 не вошла.

Редактор звуков и назойливый паразит


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

Первая проблема была банальной: программа запускала Редактор звуков по имени его 16-битной версии, SOUNDREC.EXE. В Windows 95 Редактор звуков был 32-битным, и назывался SNDREC32.EXE — так же, как в Windows NT. Мы переименовали файл обратно в 16-битное имя, и полагали, что теперь программа заработает.

Но она не заработала. Успешно запустив редактор, программа ищет его окно по заголовку. Сначала она пробует найти английский заголовок, потом итальянский (программа разрабатывалась для итальянцев). Не так уж и плохо: многие разработчики вовсе не задумывались о поддержке локализованных выпусков Windows. Правда, итальянцы, живущие, скажем, во Франции, воспользоваться этой программой не смогут; но это не наша забота.
В Windows 95 заголовок Редактора звуков поменялся: мы добавили туда имя открытого файла. Теперь программа не могла найти окно редактора, который запустила.

Тогда мы поменяли заголовок 16-битного Редактора звуков, чтобы он в точности совпадал с заголовком, бывшим в Windows 3.1. Наконец-то программа запустилась. Мы проследили за ней, и обнаружили, что она никогда не пользуется запущенным редактором! Зачем же она его запускала?

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

Но если обратить внимание в момент запуска программы, можно заметить, как окно Редактора звуков мелькает и сразу же пропадает, оставив лишь полосочку слева экрана. Программисты не хотели, чтоб окно редактора было видно. Но они не знали, как сделать окно невидимым, поэтому они сделали всё, что было в их силах: «утащили» окно за пределы экрана. Ну, почти за пределы. Они не умели даже переместить окно программно, поэтому они имитировали клики и движения мыши, как будто перетаскивают окно за заголовок. Когда на экране остаётся только узкая полоска, которую мышью уже не утащить дальше влево, программисты считают свою задачу выполненной: «Мы убрали окно за экран — наверное, никто его не заметит.»



Советую глянуть заодно мой прошлый перевод — Любимый «железный» баг