воскресенье, 11 октября 2009 г.

История GlobalLock, часть 4: подглядываем в реализацию

Это перевод A history of GlobalLock, part 4: A peek at the implementation. Автор: Реймонд Чен.

В одной из наших внутренних обсуждений в mailing list-ах, кто-то опубликовал такой вопрос:
У нас есть код, который использовал DragQueryFile для выделения имён файлов. Прототип для DragQueryFile выглядит так:
UINT DragQueryFile(
HDROP hDrop,
UINT iFile,
LPTSTR lpszFile,
UINT cch
);
В нашем коде, вместо передачи HDROP в качестве первого параметра, мы передаём указатель на структуру DROPFILES. Этот код работал нормально последние несколько месяцев, пока мы не сделали какие-то изменения в протоколе примерно неделю назад.

Я знаю, что это баг, что мы должны бы передавать описатель HDROP вместо указателя, но мне просто интересно, как это вообще работало до этого. Другими словами, что определяет допустимость описателя, и почему это указатель иногда может быть использован вместо описателя?
Функция GlobalLock принимает HGLOBAL-ы, которые ссылаются на память GMEM_MOVEABLE или GMEM_FIXED. Правило для Win32 таково, что для фиксированной памяти сам HGLOBAL является указателем на память, в то время как для перемещаемой памяти HGLOBAL является описателем, который ещё нужно сконвертировать в указатель.

GlobalAlloc работает в тесной связи с GlobalLock, чтобы GlobalLock могла быть быстрой. Если память оказывается корректно выровненной и проходит несколько других проверок, то GlobalLock говорит: "Ухууу! Это описатель на блок памяти типа GMEM_FIXED, так что я просто верну его назад как указатель".

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

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

Что же за история скрывается с флагом GMEM_MOVEABLE в Win32?

Память GMEM_MOVEABLE выделяет "описатель" (handle). Этот описатель может быть сконвертирован в указатель через GlobalLock. Вы можете вызывать GlobalReAlloc на незаблокированном блоке GMEM_MOVEABLE (или заблокированном блоке GMEM_MOVEABLE, когда вы передаёте в GlobalReAlloc флаг GMEM_MOVEABLE, говоря: "перемести его, даже если он заблокирован"), и память будет перемещена, то описатель продолжит ссылаться на неё. Вы должны будете повторно заблокировать её, чтобы получить новый указатель, куда он был перемещён.

В основном флаг GMEM_MOVEABLE не нужен; он предоставляет дополнительную функциональность, которую большинство людей не станут использовать. Большинство людей не слишком заботят такие вещи, возвращает ли Realloc то же самое или другое значение. GMEM_MOVEABLE в основном для тех случаев, когда у вас на руках есть описатель памяти, и вы решаете изменить размер памяти за спиной описателя. Если вы используете GMEM_MOVEABLE, то описатель останется корректным, хотя память, на которую он указывает, была перемещена.

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

Соответственно, перемещаемая память бесполезна на практике.

Заметьте, однако, что GMEM_MOVEABLE всё ещё сохраняется в различных местах по соображениям совместимости. Например, данные для буфера обмена должны выделяться как перемещаемые. Если вы нарушите это правило, то некоторые программы будут вылетать, потому что они делали предположения о недокументированных особенностях того, как менеджер кучи внутренне управляет описателями перемещаемых блоков вместо вызова GlobalLock для конвертации их в указатели.

Очень частой ошибкой является забывание про блокировку глобальных описателей перед использованием. Если вы забудете это сделать и просто приведёте тип описателя к указателю, то вы получите странные результаты (и, вероятно, испортите кучу). Конкретно: глобальные описатели, передаваемые через поле hGlobal записи STGMEDIUM, возвращаемой через функцию GetClipboardData, а также как менее известные места типа полей hDevMode и hDevNames записи PRINTDLG, являются потенциально перемещаемыми. Что тут реально страшно, так это то, что вы можете сделать эту ошибку, и вам это может сходить с рук довольно долго (если память, на которую вы смотрите, чисто случайно была выделена как GMEM_FIXED), и неожиданно, в один прекрасный день, ваша программа начнёт вылетать, потому что кто-то дал вам блок памяти, который был выделен с флагом GMEM_MOVEABLE.

Okей, хватит уже о наследстве 16-ти битного менеджера памяти. А то у меня уже начинает болеть голова...

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

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

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

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

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

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

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