Система Orphus

вторник, 9 декабря 2008 г.

Формат строковых ресурсов

Это перевод The format of string resources. Автор: Реймонд Чен. Примечание: в отличие от других постов, этот пост сильно отличается от оригинала. Произведено множество замен от C к Delphi.

В отличие от других типов ресурсов, где идентификатор ресурса совпадает с указаным в *.rc файле, строковые ресурсы упаковываются в "пачки" ("bundles"). В статье Knowledge Base Q196774 это описанно довольно лаконично. Сегодня мы расширим это сжатое описание в работающий код.

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

Прозрачная кисть

Это перевод The hollow brush. Автор: Реймонд Чен.
Для чего нужна прозрачная кисть?
Прозрачная кисть - это кисть, которая ничего не делает. Вы можете использовать её в ситуациях, когда от вас требуют кисть, а вы не хотите её использовать.

В качестве примера: вы можете использовать её как классовую кисть. Тогда, когда ваша программа перестаёт реагировать на сообщения, и Windows решает применить "вспышку белого", то она берёт кисть класса и в результате ничего не рисует (по крайней мере так было в Windows 2000. На XP поведение может отличаться).
Ещё одним местом, где вы можете применить прозрачную кисть, является обработка сообщений WM_CTLCOLOR*. Эти сообщения требуют от вас вернуть кисть, которая будет использована для закраски фона. Если вы не хотите стирать фон, то можете использовать прозрачную кисть.

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

Вспышка белого

Это перевод The white flash. Автор: Реймонд Чен.
Если у вас есть программа, которая долго не обрабатывает сообщения, но при этом по какой-либо причине нуждается в прорисовке (например, кто-то перетащил окно, закрывающее программу), Windows потеряет терпение с вами и закрасит ваше окно белым.
Или, по-крайней мере, так утверждают некоторые люди. В действительности Windows закрашивает ваше окно фоновой кистью класса окна. Поскольку чаще всего туда вписывают COLOR_WINDOW, а COLOR_WINDOW - это белый цвет на большинстве цветовых схем, то в результат получаются белые области.

Почему вообще нужно закрашивать окно? Почему бы не оставить его как есть?

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

Поведение Windows XP для зависшей программы немного другое. Теперь система захватывает пиксели зависшего окна и просто рисует эти пиксели снова, если окно не может само нарисовать себя. Заметьте, что если система не может захватить все пиксели (например, окно было частично закрыто), то тогда те части, что не были захвачены, всё равно будут рисоваться классовой кистью...
Которая обычно белая.

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

Что случилось с DirectX 4?

Это перевод What happened to DirectX 4? Автор: Реймонд Чен.
Если вы в курсе истории DirectX, то вы знаете, что никогда не выходило DirectX 4. Был DirectX 3, а сразу за ним - DirectX 5. Что тут за история?
После выпуска DirectX 3 одновременно стартовала разработка сразу двух последователей: на короткий срок разработки - DirectX 4 и продукт с длинным сроком разработки - DirectX 5.

Но судя по реакции общества разработчиков игр, они не сильно ждали небольшие фишки DirectX 4; зато они были действительно заинтересованы в продвинутом DirectX 5. Так что было принято решение отменить DirectX 4 и добавить все его возможности в DirectX 5.

Так почему бы не переименовать DirectX 5 в DirectX 4?

Потому что уже были написаны сотни и тысячи документов, которые ссылались на два проекта как на DirectX 4 и DirectX 5. Документы, в которых были слова вроде "Возможность XYZ будет доступна только в DirectX 5". Изменение имени в середине цикла разработки создало бы кучу путанницы. А в итоге вы получили бы заголовки типа "Microsoft сняло DirectX 5 с производства - скажите прощай возможности XYZ" и обсуждения, напоминающие Who's on First:
- У меня есть e-mail от вас, где говориться, что возможность ABC не будет готова до DirectX 5. Когда вы планируете выпустить DirectX 5?
- Мы ещё даже не начали планировать DirectX 5; мы полностью сфокусировались на работе над DirectX 4, который, как мы надеемся, будет выпущен к концу весны.
- Но мне нужна возможность ABC и вы сказали, что она не будет готова до DirectX 5.
- А, ну это письмо было написано две недели назад. Потом DirectX 5 был переименован в DirectX 4, а DirectX 4 отменили.
- Так, значит, если я получаю письмо от вас, в котором говориться о "DirectX 5", то мне надо считать, что на самом деле оно говорит о DirectX 4, а если там сказано "DirectX 4", то я должен читать это как "проект, который был отменён"?
- Точно. Только проверьте дату отправки: если она позже последней недели, тогда слова "DirectX 4" в действительности значат новый DirectX 4.
- А что если там будет написано DirectX 5?
- Ну значит кто-то облажался и перепутал.
- Окей, спасибо. Всё ясно как в тумане.

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

Исправление дыр безопасности в чужих программах

Это перевод Fixing security holes in other programs. Автор: Реймонд Чен.
Любой отчёт об ошибке, который включает в себя ошибку переполнения буфера, быстро поднимается в приоритете. Несколько последних таких отчётов, которые мне пришлось разбирать, на самом деле были отчётами об ошибках в чужих программах, которые были обнаружены Windows.
Например, было несколько программ, которые отвечали на уведомление LVN_GETDISPINFO переполнением буфера LVITEM.pszText, записывая туда больше, чем LVITEM.cchTextMax символов.

Другая программа, отвечая на IContextMenu.GetCommandString, переполняла буфер pszName, записывая туда больше, чем cchMax символов.

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

Другая программа переполняла свой собственный стековый буфер, если вы щёлкали правой кнопкой по файлу, чьё имя содержит больше чем MAX_PATH символов в пути (эти файлы легальны, но их непросто создать и манипулировать ими). Для этого случая мы смогли сделать не много.
Так что, ребята, не забываем: следим за размерами буферов и не переполняем их. Безопасность - это работа каждого. Мы с вами в этом все вместе.

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

ia64 - неверное объявление данных near и far

Это перевод ia64 - misdeclaring near and far data. Автор: Реймонд Чен.

Как я сказал вчера, ia64 - это очень требовательная архитектура. Сегодня я буду обсуждать ещё один способ наврать компилятору так, что ему потом придётся вас за это стукнуть.

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

Неинициализированный мусор на ia64 может быть смертелен

Это перевод Uninitialized garbage on ia64 can be deadly. Автор: Реймонд Чен.

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

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

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

Это перевод How can a program survive a corrupted stack? Автор: Реймонд Чен.

Продолжение вчерашнего разговора:
Архитектура x86 традиционно использует регистр EBP для установки стекового фрейма. Типичный пролог функции выглядит вот так:
  push ebp       ; сохранить старый ebp
mov ebp, esp ; установить новый ebp
sub esp, nn*4 ; выделить место под локальные переменные
push ebx ; надо сохранить для вызывающего
push esi ; надо сохранить для вызывающего
push edi ; надо сохранить для вызывающего
Этот код создаёт новый стековый фрейм, которой для stdcall-функции с двумя параметрами выглядит вот так:
.. другие данные ..
параметр_2
параметр_1
адрес_возврата
сохранённый EBP <- EBP
локальная_переменная_1
локальная_переменная_2
...
локальная_переменная_nn
сохранённый EBX
сохранённый ESI
сохранённый EDI <- ESP
К параметрам функции можно получить доступ, используя положительные смещения от EBP; например, параметр_1 лежит по адресу [ebp+8]. Локальные переменные имеют отрицательное смещение от EBP; например, локальная_переменная_2 - это [ebp-8].

Теперь предположим, что произошла путаница соглашений вызова, так что в стеке остаётся лишний мусор:
.. другие данные ..
параметр_2
параметр_1
адрес_возврата
сохранённый EBP <- EBP
локальная_переменная_1
локальная_переменная_2
...
локальная_переменная_nn
сохранённый EBX
сохранённый ESI
сохранённый EDI
мусор
мусор <- ESP
Сама функция не ощущает никаких изменений. Параметры всё так же доступны по тем же положительным смещениям от EBP, а локальные переменные всё ещё доступны по тем же отрицательным смещениям.

Настоящие проблемы начнут возникать, когда придёт время очистки и выхода. Посмотрим на эпилог функции:
  pop  edi       ; восстановим для вызывающего
pop esi ; восстановим для вызывающего
pop ebx ; восстановим для вызывающего
mov esp, ebp ; удаляем локальные переменные
pop ebp ; восстановим для вызывающего
retd 8 ; возврат из функции с очисткой стека
В нормальном стеке три инструкции "pop" соответствуют данным, размещённым в стеке тремя последними инструкциями "push" и никто при этом не пострадает. Но сейчас у нас в стеке лежит мусор, поэтому "pop edi" на самом деле загрузит в регистр EDI какой-то мусор, так же, как и "pop esi". А инструкция "pop ebx" (которая считает, что она восстанавливает значение EBX) на самом деле загрузит исходное значение, которое было в EDI в регистр EBX. Но потом инструкция "mov esp, ebp" исправит стек, поэтому команды "pop ebp" и "retd" выполнятся нормально - так же, как и при корректном стеке.

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

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

А потом выходит новый компилятор, например такой, который поддерживает оптимизацию FPO.

FPO расшифровывается как "frame pointer omission" (пропуск фреймового указателя) - функция перестаёт использовать регистр EBP как указатель фрейма и вместо этого использует его, как любой другой регистр. На x86, который имеет относительно мало регистров, дополнительный арифметический регистр будет большим бонусом.

С FPO пролог функции теперь выглядит вот так:
  sub  esp, nn*4 ; локальные переменные
push ebp ; надо сохранить для вызывающего
push ebx ; надо сохранить для вызывающего
push esi ; надо сохранить для вызывающего
push edi ; надо сохранить для вызывающего
Итоговый стек выглядит вот так:
.. другие данные ..
параметр_2
параметр_1
адрес_возврата
локальная_переменная_1
локальная_переменная_2
...
локальная_переменная_nn
сохранённый EBP
сохранённый EBX
сохранённый ESI
сохранённый EDI <- ESP
Теперь все данные (параметры и локальные переменные) доступны по смещениям относительно регистра ESP. Например, локальная_переменная_nn это [esp+$10].

В таких условиях мусор в стеке становится гораздо более фатальным. Эпилог функции будет таким:
  pop  edi       ; восстановим для вызывающего
pop esi ; восстановим для вызывающего
pop ebx ; восстановим для вызывающего
pop ebp ; восстановим для вызывающего
add esp, nn*4 ; удаляем локальные переменные
retd 8 ; возврат из функции с очисткой стека
Если в стеке есть мусор, то четыре инструкии "pop", как и ранее, восстановят неверные значения, но в этот раз очистка локальных переменных ничего не исправит. Команда "add esp, nn*4" подправит стек на значение, которое функция считает правильным, но поскольку в стеке лежит мусор, то в итоге указатель стека будет неверен:
.. другие данные ..
параметр_2
параметр_1
адрес_возврата
локальная_переменная_1
локальная_переменная_2 <- ESP (упс!)
Инструкция "retd 8" теперь попробует передать управление вызывающему, но вместо этого она перейдёт по адресу, который записан в локальная_переменная_2, который, вероятно, не будет являться указателем на корректный код.

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

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

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

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

Это перевод What can go wrong when you mismatch the calling convention? Автор: Реймонд Чен.

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

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

История соглашений вызова, часть 5: amd64

Это перевод The history of calling conventions, part 5: amd64. Автор: Реймонд Чен.
Четвёртая часть.
Последней архитектурой, которую мы рассмотрим будет архитектура AMD64 (также известная как x86-64).

AMD64 берёт традиционную архитектуру x86 и увеличивает регистры до 64-х бит, именуя их rax, rbx и т.д. И также добавляет восемь дополнительных регистров, называя их просто как R8-R15.

- Первые четыре параметры в функцию передаются в rcx, rdx, r8 и r9. Все другие параметры передаются в стеке. Более того, для параметров в регистре резервируется место в стеке, на случай если вызываемая функция захочет сбросить регистры в стек; это также важно для функций с переменным числом аргументов.
- Параметры меньше 64-бит не дополняются нулями; верхние биты содержат мусор, поэтому не забывайте обнулять их явно, если вы собираетесь их использовать. Параметры размером больше 64-х бит передаются по ссылке.
- Возвращаемое значение помещается в rax. Если возвращаемое значение больше 64-х бит, то в функцию будет передан неявный секретный параметр, который содержит адрес, по которому нужно записать результат.
- Все регистры обязаны сохраняться во всемя вызова, кроме регистров rax, rcx, rdx, r8, r9, r10 и r11, которые свободны.
- Вызываемый не чистит стек. Это работа вызывающего.
- Стек должен быть всё время выровненным на границу 16-ти байт. Поскольку инструкция "call" записывает в стек 8-ми байтовый адрес возврата, то это значит, что каждая не листовая функция должна подправлять стек на значение вида 16n + 8 для восстановления выравнивания на 16 байт.

Вот пример:
void SomeFunction(int a, int b, int c, int d, int e);

void CallThatFunction()
{
SomeFunction(1, 2, 3, 4, 5);
SomeFunction(6, 7, 8, 9, 10);
}
После входа в CallThatFunction стек выглядит примерно так:
xxxxxxx0  .. данные на стеке .. 
xxxxxxx8 адрес возврата <- RSP
Из-за наличия в нём адреса возврата стек оказывается не выровненным. Функция CallThatFunction настраивает свой фрейм примерно так:
    sub    rsp, 0x28
Заметим, что размер локального стекового фрейма равен 16n + 8, так что в результате у нас получается выравненный стек:
xxxxxxx0  .. данные на стеке ..
xxxxxxx8 адрес возврата
xxxxxxx0 (arg5)
xxxxxxx8 (место для arg4)
xxxxxxx0 (место для arg3)
xxxxxxx8 (место для arg2)
xxxxxxx0 (место для arg1) <- RSP
Теперь мы готовим первый вызов:
        mov     dword ptr [rsp+0x20], 5     ; параметр 5
mov r9d, 4 ; параметр 4
mov r8d, 3 ; параметр 3
mov edx, 2 ; параметр 2
mov ecx, 1 ; параметр 1
call SomeFunction ; Вперёд, Спиди-Гонщик!
Когда функция SomeFunction возвращает управление, стек ещё не очищен и поэтому выглядит так же, как и выше. Тогда для второго вызова нам просто нужно занести новые значения в уже подготовленное место:
        mov     dword ptr [rsp+0x20], 10    ; параметр 5
mov r9d, 9 ; параметр 4
mov r8d, 8 ; параметр 3
mov edx, 7 ; параметр 2
mov ecx, 6 ; параметр 1
call SomeFunction ; Вперёд, Спиди-Гонщик!
Теперь CallThatFunction завершена и она должна очистить стек и вернуть управление:
        add     rsp, 0x28
ret
Заметьте, что вы практичеси не встречаете инструкций "push" в коде amd64, поскольку парадигмой вызывающего является резервирование пространства и повторное использование его.

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

История соглашений вызова, часть 4: ia64

Это перевод The history of calling conventions, part 4: ia64. Автор: Реймонд Чен.

Третья часть.

Архитектура ia-64 (Itanium) и архитектура AMD64 (AMD64) относительно новые, так что маловероятно, чтобы многие из вас имели дело с соглашениями вызова на этих платформах, но я включу их обзор в эту серию, потому что, кто знает, может однажды у вас появится такая машина.

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

Почему у меня не получается вызвать GetProcAddress для функции, которую я экспортировал dllexport-ом?

Это перевод Why can't I GetProcAddress a function I dllexport'ed? Автор: Реймонд Чен.
Атрибут dllexport просит линкёр сгенерировать запись в таблице экспорта для указанной функции. Название функции в этой записи декорируется. Это необходимо для поддержки эксорта (dllexporting) перегружаемых (overload) функций. Но это также означает, что имя, которое вы указываете в GetProcAddress, тоже должно быть декорировано.
Как мы узнали ранее, схема декорирования зависит от архитектуры и модели вызова. Так что, например, если функция импортируется из DLL на PPC, вы должны будете написать GetProcAddress(hinst, '..SomeFunction'), но если она импортируется из DLL на 80386 с моделью вызова stdcall, то вам нужно вызывать GetProcAddress(hinst, '_SomeFunction@8'), но если соглашение вызова будет fastcall, то вам придётся использовать GetProcAddress(hinst, '@SomeFunction@8').

Но это ещё не всё. Декорация имён C++ может меняться в зависимости от компилятора. Экспортируемая функция C++ может требовать GetProcAddress(hinst, '?SomeFunction@@YGXHH@Z'), если она была скомпилирована компилятором Microsoft C++, но какую-то другое декорированное имя, если она была скомпилирована Borland C++.

Так что если вы планируете, что вашу функцию будут импортировать через GetProcAddress, и вы хотите сделать свой код портируемым или разрешить использование вашей DLL из языка, отличного от C/C++ или использовать компилятор, отличный от Microsoft Visual Studio, то тогда вы должны экспортировать функцию по её недекорированному имени (да, для этого вам понадобится DEF-файл).

Когда DLL генерируется, компоновщик C/C++ создаёт соответствующий LIB-файл, который переводит декорированные имена в недекорируемые. Так, к примеру, LIB-файл может содержать запись, которая говорит: "если кто-то спросит функцию _GetTickCount@0, то перенаправьте его на kernel32!GetTickCount".

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

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

Почему методы класса должны быть помечены словом "static", чтобы их можно было использовать в качестве функции обратного вызова?

Это перевод Why do member functions need to be "static" to be used as a callback? Автор: Реймонд Чен.

Как мы узнали вчера, обычные методы класса принимают секретный параметр "this", что, конечно же, делает их несовместимыми с сигнатурами функций обратного вызова в Win32.

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

История соглашений вызова, часть 3

Это перевод The history of calling conventions, part 3. Автор: Реймонд Чен.
Вторая часть.
Окей, поехали: 32-х битные модели вызова x86.

(Кстати, на случай, если вы не поняли: я говорю только о тех моделях вызова, которые вы можете встретить в Windows-программировании или в компиляторах от Microsoft. Я не буду рассматривать соглашения вызова других операционных систем или модели вызова, специфичные для конкретного языка или производителя компилятора).

Запомните: если какая-либо модель вызова используется для члена класса C++, тогда будет существовать скрытый параметр "this", который будет неявно передан первым аргументом в функцию (*).

Все модели

Все 32-х разрядные соглашения вызова x86 сохраняют регистры EDI, ESI, EBP и EBX и используют пару EDX:EAX для возврата результата (**).

C (cdecl)

В 32-х битном мире работают те же правила, что и в 16-ти битном. Параметры передаются справа налево (поэтому первый параметр будет ближе к вершине стека), а чистит стек вызывающий. Имена функций декорируются ведущим знаком подчёркивания.

stdcall

Это стандартное соглашение, используемое во всём Win32, за исключением функций с переменным числом параметров (которые используют cdecl) и очень небольшого числа функций, использующих fastcall. Параметры передаются справа налево, а стек чистит вызываемый. Имена функций декорируются ведущим знаком подчёркивания и в конце ставится знак @ и размер принимаемых функцией параметров в байтах.

fastcall

Первые два параметра передаются в ECX и EDX, а остаток передаётся через стек так же, как и при stdcall. И снова стек чистит вызываемый. Имена функций декорируются ведущим знаком @, в конце также ставится @ и после него указывается размер принимаемых функцией параметров в байтах (включая параметры в регистрах).

thiscall

Первый параметр (который "this") передаётся в ECX, а все остальные параметры передаются как в stdcall - через стек. И здесь стек чистит вызываемый. Имена функций декорируются весьма сложным образом компилятором C++, включая и типы параметров (среди всех прочих вещей). Это необходимо, потому что C++ допускает перегрузку функций, поэтому необходимо использовать сложные правила декорирования для того, чтобы два варианта перегруженных функций имели бы различные имена.

На MSDN есть несколько неплохих диаграм, демонстрирующих эти соглашения вызова.

(***)

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

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

Примечания переводчика:
(*) Для Delphi справедливо аналогичное утверждение: у любого метода класса есть неявный параметр Self, который является первым параметром функции этого метода. Поэтому у метода, объявленного с двумя параметрами, на самом деле будет три параметра, причём первым будет Self, а вторым и третьим - первый и второй параметры в объявлении метода.
(**) Вообще, пара EDX:EAX используется скорее как исключение из правила. Т.е. все возвращаемые значения возвращаются в EAX, а если они там не помещаются, то в функцию передаётся указатель, по которому и записывается результат. Однако, (только) для 8-ми байтовых записей есть спец. правило: они возвращаются в паре EDX:EAX. В Delphi даже есть баг: компилятор генерирует неверный код для функции, возвращающей 8-ми байтовую запись.
(***) Моделью вызова по-умолчанию в Delphi является модель register. В ней для передачи параметров используются регистры EAX, EDX, ECX и стек. Обычно первые три параметра идут в регистры, а остальные - в стек как в stdcall, но это необязательно. В языке есть свои правила для распределения параметров между регистрами и стеком - эти правила довольно сложны (так, например, действительные числа передаются через стек сопроцессора). Параметры передаются слева-направо и стек чистит вызываемый.

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

История соглашений вызова, часть 2

Это перевод The history of calling conventions, part 2. Автор: Реймонд Чен.

Первая часть.

Предварительно: информация ниже будет использована в дальнейшей дискуссии. Ну, здесь приведены не самые подробные детали, но вы можете увидеть объяснение... эммм... это трудно описать. Просто читайте далее.

Любопытно, что только платформы 8086 и x86 имеют несколько соглашений вызова. На всех других платформах есть только одна единственная модель вызова!

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

Почему диалог копирования так плохо показывает оставшееся время?

Это перевод Why does the copy dialog give such horrible estimates?. Автор: Реймонд Чен.

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

Вот аналогия: представьте, что ваш друг говорит вам: "давай я считать до ста, а ты будешь говорить, когда я досчитаю до конца". Он начинает считать: "один, два, три...". Вы замечаете, что он говорит примерно одно число в секунду, поэтому вы даёте оценку в 100 секунд. Ой-ёй, а теперь он замедляется. "Четыре... ... ... пять... ... ...". Теперь вам нужно изменить свою оценку, скажем, на 200 секунд. Теперь он ускоряется: "шесть-семь-восемь-девять". И вам снова нужно менять свою оценку.

Представьте, что кто-то слушал только ваши оценки, но не человека, который считает. Ваши оценки варьировались от 100 секунд к 200, а затем к 50 секундам: "Эй, что у тебя за проблемы? Ты что, не можешь дать нормальную оценку?".

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

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

История соглашений вызова, часть 1

Это перевод The history of calling conventions, part 1. Автор: Реймонд Чен.

Хорошо, когда вокруг так много соглашений вызовов: есть из чего выбирать!

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

Почему у процессоров x86 так мало регистров?

Это перевод Why does the x86 have so few registers?. Автор: Реймонд Чен.

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

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

Не доверяйте адресу возврата

Это перевод Don't trust the return address. Автор: Реймонд Чен.

Иногда люди спрашивают: "так, я знаю как получить адрес возврата [в C++ можно использовать _ReturnAddress(); Прим. пер.: в Delphi прямого аналога нет, но можно использовать Caller из JCL]; как мне определить, какой DLL принадлежит этот адрес?"

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

Вы не можете исправить проблему совместимости с помощью диалога.

Это перевод You can’t fix application compatibility problems with dialog boxes. Автор: Крис Джексон.

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

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

Пример диалога
Прим. пер.: это диалог с заголовком "Похоже тебе скучно, вот, прочти это", кучей текста "Бла-бла-бла" и кнопками "Работай!" и "Не работай!".

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

Adobe Photoshop показывал диалог UAC (UAC Prompt), и она не знала, как избавиться от него.

Я спросил, какую версию она использует. Она сказала, что последнюю (в то время). Я помнил, что эта версия Photoshop не требует элевации, поэтому я попросил посмотреть на этот диалог.

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

Диалог помошника по совместимости программ для Acrobat Reader
Итак, вот диалог, который выводит из себя кучу людей. “Мы можем помочь с проблемой совместимости приложений, если мы просто скажем пользователю, что происходит. Ну т.е. мы же помогаем, это же хорошо, да?”.

Давайте посчитаем вещи, которые пользователь не прочитает:

Заголовок диалогового окна. Это вовсе не диалог UAC. Но она слышала так много об этом противном UAC, который постоянно надоедает своими диалогами, что она решила, что это что-то, спрашивающее её о чём-то, - это тоже UAC.

Название приложения. Она уловила название компании, но не разницу между Photoshop и Acrobat Reader. Я видел немало таких примеров – самый значительный из них: люди считают Office частью Windows.

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

Чек-бокс для скрытия сообщения. Вспомните, проблема была не в том, что приложение работало неправильно - она просто ненавидела это постоянно всплывающее окно. У нас есть чек-бокс, прямо в диалоге, чтобы скрыть его - но она никогда его не читала.

Фактически, единственной кнопкой, которую она увидела, была та, на которой было написано “Запуск программы” (“Run program”) - та самая кнопка “Работай!”.

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

понедельник, 8 декабря 2008 г.

В чём разница между CreateMenu и CreatePopupMenu?

Это перевод What's the difference between CreateMenu and CreatePopupMenu? Автор: Реймонд Чен.

Функция CreateMenu создаёт горизонтальную полосу меню, пригодную для прикрепления к окнам верхнего уровня. Это тот тип меню, где написано "Файл", "Правка" и т.д.

Функция CreatePopupMenu создаёт вертикальное всплывающее меню, подходящее для использования в качестве подменю другого меню (в горизонтальном меню или всплывающем меню) или для использования в качестве контекстного меню.

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

А что это за пустые кнопки на панели задач, которые исчезают, когда я щёлкаю по ним?

Это перевод What's with those blank taskbar buttons that go away when I click on them? Автор: Реймонд Чен.

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

Есть несколько основных правил, согласно которым окна появляются на панели задач. Вкратце:
- Если установлен расширенный стиль WS_EX_APPWINDOW, тогда окно показывается (когда оно видимо).
- Если окно является окном верхнего уровня без владельца (owner в терминах WinAPI - прим. пер.), тогда оно показывается (когда оно видимо).
- В противном случае окно не показывается.

(хотя интерфейс ITaskbarList немножко всё это запутывает).

Когда "пригодное для панели задач" окно становится видимым, панель задач создаёт кнопку для него. Когда "пригодное для панели задач" становится скрытым - панель задач скрывает кнопку.

Пустые кнопки появляются, когда окно переходит от "пригодное для панели задач" к "не пригодное для панели задач" в то время, когда оно видимо. Посмотрите:
- Окно - "пригодное для панели задач".
- Окно становится видимым -> создаётся кнопка на панели задач.
- Окно становится "не пригодное для панели задач".
- Окно становится невидимым -> поскольку окно не является "пригодное для панели задач", то панель задач игнорирует окно.

Результат: на панели задач появляется ничейная кнопка, без прикреплённого к ней окна.

Именно поэтому документация советует: "если вы хотите динамически изменить стиль окна на такой, который не поддерживает кнопку на панели задач, то сначала вы должны скрыть окно (вызовом ShowWindow с SW_HIDE), изменить стиль окна, а затем показать окно".

Бонус-вопрос: почему панель задач не проверяет все окна подряд?
Ответ: потому что это было бы расточительно. Фильтрация окон, которые являются "таскбаро-непригодными", происходит внутри USER32, которая уведомляет панель задач (или любого, кто установил хук WH_SHELL) с помощью уведомлений HSHELL_* только об окнах, которые изменили своё состояние и являются "пригодными для панели задач". Таким образом, код панели задач может быть выгружен в файл подкачки, если для него нет никакой работы.

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

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

понедельник, 1 декабря 2008 г.

Вы можете читать контракт и с другой стороны

Это перевод You can read a contract from the other side. Автор: Реймонд Чен. Примечание: в отличие от других постов, этот пост сильно отличается от оригинала. Произведено множество замен от C к Delphi.

Любой интерфейс - это контракт, но помните, что контракт применим к двум сторонам. Чаще всего, когда вы читаете соглашения какого-то интерфейса, вы смотрите на него с точки зрения клиентской стороны (вызывающего), но иногда бывает полезно взглянуть на него и со стороны сервера.

Например, давайте посмотрим на интерфейс для приложений Панели управления.

Чаще всего, когда вы читаете эту документацию, на вас надета кепка "Сейчас я пишу приложение для Панели управления". Поэтому, к примеру, когда в документации написано:
Когда Microsoft Windows в первый раз загружает элемент панели управления, она получает адрес функции CPlApplet и в дальнейшем использует его для вызова функции и передаче ей сообщений.
Когда на вас кепка "Сейчас я пишу приложение для Панели управления", это будет означать: "дьявол, мне лучше бы написать функцию CPlApplet и экспортировать её, чтобы я мог получать сообщения".

Но если вместо этого вы носите кепку "Я пишу замену Панели управления", то это означает: "дьявол, мне лучше бы вызвать GetProcAddress для получения адреса функции CPlApplet, чтобы я мог передавать сообщения".

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

И, наконец, когда документация говорит "управляющее приложение освобождает приложение Панели управления вызовом функции FreeLibrary", ваша кепка "Сейчас я пишу приложение для Панели управления" говорит: "мне лучше бы подготовиться к выгрузке", в то время как ваша "Я пишу замену Панели управления" говорит: "а вот здесь я выгружаю DLL".

Так давайте же попробуем. Начнём с пустого приложения и изменим его FormCreate:
procedure TForm1.FormCreate(Sender: TObject);
var
S: String;
Last: PChar;
C: Integer;
begin
SetLength(S, MAX_PATH);
C := SearchPath(nil, 'access.cpl', nil, Length(S), PChar(S), Last);
if (C > 0) and (C < MAX_PATH) then
begin
Application.ShowMainForm := False;
try
RunControlPanel(S);
finally
Application.Terminate;
end;
end;
end;
Вместо обычного показа окна и входа в цикл сообщений, мы начинаем вести себя, как Панель управления. Сегодняшняя наша жертва: access.cpl (приложение "Специальные возможности"). После нахождения программы на диске, мы просим метод RunControlPanel выполнить всю тяжёлую работу:
uses
JwaWinBase, JwaWinCPL;

...

procedure TForm1.RunControlPanel(const AFileName: String);
var
Act: TACTCTX;
Ctx: THandle;
Cookie: ULONG_PTR;
CPL: HMODULE;
CPlApplet: TCPLApplet;
AppletsCount: Integer;
CPLI: TCPLINFO;
begin
// Может быть, приложение Панели управления имеет свой манифест
FillChar(Act, SizeOf(Act), 0);
Act.cbSize := SizeOf(Act);
Act.dwFlags := 0;
Act.lpSource := PChar(AFileName);
Act.lpResourceName := MAKEINTRESOURCE(123);
Ctx := CreateActCtx(Act);
if (Ctx = INVALID_HANDLE_VALUE) or ActivateActCtx(Ctx, Cookie) then
begin

CPL := SafeLoadLibrary(PChar(AFileName));
if CPL <> 0 then
begin
CPlApplet := GetProcAddress(CPL, 'CPlApplet');
if Assigned(CPlApplet) then
begin
if CPlApplet(Handle, CPL_INIT, 0, 0) <> 0 then
begin
AppletsCount := CPlApplet(Handle, CPL_GETCOUNT, 0, 0);
// Мы собираемся запустить приложение номер ноль
// (В реальной программе мы бы сначала вывели их список пользователю,
// чтобы он выбрал какое-то)
if AppletsCount > 0 then
begin
FillChar(CPLI, SizeOf(CPLI), 0);
CPlApplet(Handle, CPL_INQUIRE, 0, Integer(@CPLI));
CPlApplet(Handle, CPL_DBLCLK, 0, CPLI.lData);
CPlApplet(Handle, CPL_STOP, 0, CPLI.lData);
end;
end;
CPlApplet(Handle, CPL_EXIT, 0, 0);
end;
FreeLibrary(CPL);
end;

if Ctx <> INVALID_HANDLE_VALUE then
begin
DeactivateActCtx(0, Cookie);
ReleaseActCtx(Ctx);
end;
end;
end;
Пока проигнорируйте два блока в начале и в конце, отделённые пустыми строками от центрального кода; мы обсудим их позже.

Всё, что мы делаем - это следуем спецификации, только читаем её со стороны сервера. Поэтому, мы загружаем библиотеку, ищем её точку входа, и вызываем эту точку входа с CPL_INIT, а затем - с CPL_GETCOUNT. Если в этом файле есть приложение Панели управления, то тогда мы получаем информацию о первом апплете, "дважды щёлкаем" на нём (это здесь происходят все интересные вещи), а затем останавливаем его. После всего этого, мы выполняем очистку, согласно правилам (а именно: отправляем сообщение CPL_EXIT).

Вот и всё. Ну, за исключением тех двух блоков кода. Что насчёт них?
Эти два блока кода - поддержка приложений Панели управления, которые имеют свой манифест. Это новая возможность в Windows XP и она документирована в MSDN здесь.

Если вы перейдёте к секции "Using ComCtl32 Version 6 in Control Panel or a DLL That Is Run by RunDll32.exe" ("Использование ComCtl32 версии 6 в Панели управления или в DLL, запускаемой RunDll32.exe"), то вы увидите, что приложение предоставляет Панели управления свой манифест подключением его в виде ресурса с номером 123. Так вот что делает этот код: он загружает и активирует манифест, затем приглашает приложение Панели управления выполнить свою работу (с активным манифестом), потом выполняет очистку. Если манифеста нет, то CreateActCtx возвратит INVALID_HANDLE_VALUE. Мы не считаем это ошибкой, потому что многие программы не имеют манифеста.
Упражнение: какие последствия для безопасности несёт передача nil первым параметром в SearchPath?

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

Когда программы начинают закапываться в недокументированные структуры...

Это перевод When programs grovel into undocumented structures... Автор: Реймонд Чен.

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

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

Почему бы просто не заблокировать приложения, которые используют недокументированное поведение?

Это перевод Why not just block the apps that rely on undocumented behavior? Автор: Реймон Чен.

Потому что каждое приложение, которое будет заблокировано, станет ещё одной причиной, почему люди не станут обновлять Windows до следующей версии. Посмотрите на все эти программы, которые могли бы перестать работать, когда вы обновлялись с Windows 3.0 да Windows 3.1:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Compatibility

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

(Аналогичный список для Windows 2000-в-Windows XP хранится в вашей папке C:\WINDOWS\AppPatch, в двоичном формате, допускающим быстрое сканирование. Извините, но вот так просто вы его уже не сможете посмотреть. Я думаю, что в комплекте Application Compatibility Toolkit есть просмотрщик, но я могу ошибаться).

Вы купили бы Windows XP, если бы знали, что все эти программы были несовместимыми?

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

Предположим, что вы IT-менеджер в какой-то компании. Ваша компания использует Программу X как свой текстовый процессор, и вы обнаруживаете что по какой-то причине Программа X несовместима с Windows XP. Станете ли вы обновляться?

Ну конечно же нет! Ваш бизнес остановится.

"Почему не позвонить в Компанию X и не взять у них обновление?"

Конечно, вы можете это сделать, но часто ответ будет: "о, вы используете Версию 1.0 Программы X. Вам нужно обновиться до Версии 2.0 за $150 за экземпляр". Поздравляю, ваша цена апгрейда на Windows XP только что утроилась.

И это, если вам ещё повезло и Компания X ещё ведёт дела.

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

И это касается не только корпоративных клиентов. Это влияет и на потребителей.

Для Windows 95 моя работа по обеспечению совместимость была сфокусированна на играх. Игры - самый важный фактор потребительских технологий. Видеокарта, которую вставляют в типичный компьютер, со временем становится всё лучше и лучше, потому что этого требуют игры (Outlook, конечно же, не волнует, что ваша видеокарта способна нарисовать 20 базилионнов треугольников в секунду). И если ваша игра не запустится на последней версии Windows, то вы не станете делать апгрейд.

В любом случае, производители игр довольно похожи на эти корпорации. Я сделал бесчисленное множество звонков производителям игр, в попытке помочь им запустить их игры на Windows 95. Всех их это не интересовало. Жизненный цикл игры составляет несколько месяцев. Зачем им вообще волноваться ради какого-то патча, заставляющего их программу работать под Windows 95? Они уже получили свои деньги. Они не сделают больше денег с этой игры; её три месяца уже закончились. Иногда у них даже не остаётся исходных кодов.

Короче, их совершенно не волнует, что их программы не работают в Windows 95 (мой любимый случай: программа, которая пыталась провести меня через создание загрузочного диска DOS).

Да, и тот Application Compatibility Toolkit, что я упоминал выше. Это отличная утилита и для разработчиков. Одним из его компонент является программа Verifier: если вы запустите свою программу под verifier, он будет проверять тысячи вызовов API и вывалит вам отладчик, когда вы сделаете что-то плохое (например, дважды закроете дескриптор или выделите память GlobalAlloc-ом, а освободите LocalAlloc).
Новая архитектура совместимости приложений в Windows XP имеет одно большое преимущество (с точки зрения разработки ОС): видите все эти DLL в вашей папке C:\WINDOWS\AppPatch? Это тут теперь живёт большинство патчей совместимости. Патчи совместимости теперь больше не загрязняют файлы ядра ОС (не все классы патчей могут быть оформлены в виде таких DLL, но это всё же огромная помощь).

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

Иногда приложение просто просит вас вылететь

Это перевод Sometimes, an app just wants to crash. Автор: Реймонд Чен.

Мне кажется, что это было с Internet Explorer 5.0, когда мы нашли стороннее расширение браузера, которое имело в себе серьёзный баг, детали которого сейчас не так важны. Дело в том, что эта ошибка была настолько плоха, что расширение крэшило IE практически каждый раз. Это было не хорошо. Чтобы защитить пользователей от такой ужасной судьбы, мы отметили это расширение как "плохое", так что IE не запускал его при старте.

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

Как мне определить, что я владею критической секцией, если я не могу смотреть во внутренние поля?

Это перевод How do I determine whether I own a critical section if I am not supposed to look at internal fields? Автор: Реймонд Чен. Примечание: в отличие от других постов, этот пост сильно отличается от оригинала. Произведено множество замен от C к Delphi.

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

Я использую SEH (исключения - прим. пер.) и у меня есть несколько блоков try/except, в которых код входит и покидает критические секции. Если возникает исключение, я не знаю, владею ли я сейчас критической секцией или нет. Даже обёртка кода в try/finally не решает мои проблемы.

Ответ: вы знаете, владеете ли вы критической секцией, потому что вы вами вошли в неё.

Метод 1: сделать вывод из строки кода.
"Если я сейчас в этой строке кода, то я должен быть внутри критической секции".
  try
...
EnterCriticalSection(X);
try
... // если исключение возникнет на этом участке, ...
finally // ...то убедимся, что мы вышли из критической секции
LeaveCriticalSection(X);
end;
...
except
...
end;
Заметим, что эта техника устойчива к вложенным вызовам EnterCriticalSection. Если вы собираетесь войти в критическую секцию ещё раз, тогда просто оберните вложенный вызов в собственный блок try/finally.

Метод 2: сделать вывод из локального состояния.
"Я запомню, входил ли я в критическую секцию".
var
Entered: Integer;
...
Entered := 0;
try
...
EnterCriticalSection(X);
Inc(Entered);
...
Dec(Entered);
LeaveCriticalSection(X);
...
except
while Entered > 0 do
begin
LeaveCriticalSection(X);
Dec(Entered);
end;
...
end;
Заметим, что эта техника также устойчива к вложенным вызовам EnterCriticalSection. Если вы хотите занять критическую секцию ещё раз, то просто увеличьте Entered ещё раз.

Метод 3: отслеживать объектом.
Оберните TCriticalSection в другой объект.

Это наиболее точно передаёт то, что Seth делает сейчас.
type
TMyCriticalSection = class(TSynchroObject)
private
FOwner: Cardinal;
FDepth: Integer;
function GetOwned: Boolean;
protected
FSection: TRTLCriticalSection;
public
constructor Create;
destructor Destroy; override;
procedure Acquire; override;
procedure Release; override;
function TryEnter: Boolean;
procedure Enter; inline;
procedure Leave; inline;
property Owned: Boolean read GetOwned;
end;

{ TMyCriticalSection }

constructor TMyCriticalSection.Create;
begin
inherited Create;
FSection.Initialize;
end;

destructor TMyCriticalSection.Destroy;
begin
FSection.Free;
inherited Destroy;
end;

procedure TMyCriticalSection.Acquire;
begin
FSection.Enter;
FOwner := GetCurrentThreadId;
Inc(FDepth);
end;

procedure TMyCriticalSection.Release;
begin
Dec(FDepth);
if FDepth = 0 then
FOwner := 0;
FSection.Leave;
end;

function TMyCriticalSection.TryEnter: Boolean;
begin
Result := FSection.TryEnter;
if Result then
begin
FOwner := GetCurrentThreadId;
Inc(FDepth);
end;
end;

procedure TMyCriticalSection.Enter;
begin
Acquire;
end;

procedure TMyCriticalSection.Leave;
begin
Release;
end;

function TMyCriticalSection.GetOwned: Boolean;
begin
Result := (FOwner = GetCurrentThreadId);
end;

...

try
Assert(not CS.Owned);
...
CS.Enter;
...
CS.Leave;
...
except
if CS.Owned then
CS.Leave;
end;
Заметим, что этот код не устойчив к повторной входимости (и, соответственно, код Seth-а тоже). Если вы войдёте в критическую секцию дважды, то обработчик исключения выйдет из неё только 1 раз.

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

Метод 4: отслеживать умным объектом.
Оберните TCriticalSection в умный объект.

Добавьте такой private-метод с public-свойством к предыдущему классу:
function TMyCriticalSection.GetDepth: Integer;
begin
if Owned then
Result := FDepth
else
Result := 0;
end;
Теперь вы можете корректно освобождать вложенные входы в критическую секцию:
var
Depth: Integer;
...
Depth := CS.Depth;
try
...
CS.Enter;
...
CS.Leave;
...
except
while CS.Depth > Depth do
CS.Leave;
end;

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

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

В одном из будущих постов я выскажу ещё претензии к исключениям.

Упражнение: почему нам не нужно использовать синхронизацию для защиты использования FDepth и FOwner?

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

Как выбросить на свалку свою гарантию

Это перевод How to void your warranty. Автор: Реймонд Чен.

MSDN только что опубликовала статью Break Free of Code Deadlocks in Critical Sections Under Windows, в которой подробно описываются внутренние поля системной записи. Любой, кто воспользовался этой информацией просто отменил свою гарантию. Пожалуйста, наклейте стикер "Весьма вероятно, что эта программа перестанет работать после того, как вы установите очередной сервис пак или обновите свою систему" на своё лицензионное соглашение.

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

Читать далее: Как мне определить, что я владею критической секцией, если я не могу смотреть во внутренние поля?.

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

Есть ли ограничение на максимальную вложенность окон?

Это перевод What is the window nesting limit? Автор: Реймонд Чен.

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

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

В ранних версиях Windows NT максимальное число вложенных окон было установлено в 100. В Windows XP это ограничение снизили до 50, потому что увеличенные требования к стеку в некоторых внутренних функциях приводили к переполнению при значении около 75. Усиление ограничения до 50-ти было сделано для запаса.

Оговорка: я не был лично причастен к этому вопросу. Я только сообщаю, что я смог выяснить из чтения checkin-логов.

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

Почему размеры записей проверяются строго?

Это перевод Why are structure sizes checked strictly? Автор: Реймонд Чен.

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

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

Как бы мне передать большой объём данных процессу при его запуске?

Это перевод How do I pass a lot of data to a process when it starts up? Автор: Реймонд Чен. Примечание: в отличие от других постов, этот пост сильно отличается от оригинала. Произведено множество замен от C к Delphi.

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

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

Какой максимальный размер у командной строки?

Это перевод What is the command line length limit? Автор: Реймонд Чен.

Зависит от того, о чём вы спрашиваете.

Максимальная длина строки для функции CreateProcess - это 32767 символов. Это ограничение идёт из записи UNICODE_STRING (TUnicodeString в JwaWinTypes.pas).

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

Если вы используете коммандный процессор CMD.EXE, тогда вы сталкиваетесь с пределом в 8192 символов - это ограничение командной строки, вносимое самим CMD.EXE.

Если вы используете функции ShellExecute/Ex, тогда вы будете ограничены INTERNET_MAX_URL_LENGTH (около 2048) символами на командную строку, вводимыми функциями ShellExecute/Ex (а если вы работаете в Windows 95, то тогда предел и вовсе MAX_PATH символов).

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

Окей, но что если вам нужно передать процессу больше, чем 32767 символов данных? Тогда вам придётся поискать другие пути, отличные от командной строки.

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

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

Почему вам никогда не следует приостанавливать поток

Это перевод Why you should never suspend a thread. Автор: Реймон Чен.

Приостановка (suspend) потока почти также плоха, как его уничтожение (terminate).

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

Рассмотрим следующую программу (*):
program Project1;

{$APPTYPE CONSOLE}

uses
SysUtils, Classes;

type
TTestThread = class(TThread)
protected
procedure Execute; override;
end;

{ TTestThread }

procedure TTestThread.Execute;
var
P: Pointer;
begin
while not Terminated do
begin
GetMem(P, 1024); FreeMem(P);
end;
end;

var
Thread: TTestThread;
begin
try
Thread := TTestThread.Create(False);
try
WriteLn(DateTimeToStr(Now) + ': press Enter to suspend');
ReadLn;
Thread.Suspend;
WriteLn(DateTimeToStr(Now) + ': press Enter to resume');
ReadLn;
Thread.Resume;
finally
FreeAndNil(Thread);
end;
except
on E: Exception do
WriteLn(E.Classname, ': ', E.Message);
end;
end.
Когда вы запускаете эту программу и нажимаете Enter для приостановки, программа виснет. Но если вы измените метод Execute на пустой бесконечный цикл (закомментарить строчку с GetMem/FreeMem), то программа работает отлично. Посмотрим, сумеете ли вы понять почему.

Рабочий поток проводит почти всё своё время внутри вызовов функций менеджера памяти - GetMem/FreeMem, поэтому когда вы вызываете Thread.Suspend, рабочий поток почти обязательно будет внутри вызова одной из функций менеджера памяти.

Q: Являются ли вызовы функций менеджера памяти потоко-безопасными?

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

Q: Как обычно объект делают потоко-безопасным?

Q: Каков будет результат приостановки потока в середине потоко-безопасной операции?

Q: Что случится, если впоследствии вы попытаетесь обратиться к тому же объекту (менеджеру памяти в нашем случае) из другого потока?

Эти результаты применимы не только к Delphi, но и к любой другой модели потоков, в том числе в C#. К примеру, в чистом Win32 куча процесса является потоко-безопасным объектом, а поскольку без кучи в Win32 весьма тяжело делать какую-либо полезную работу, то приостановка потока в Win32 имеет высокий шанс блокировки вашего процесса.

Тогда почему вообще есть такая функция как SuspendThread?

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

(*) Примечание: этот пример для старых Delphi (например, версии 7). В новых Delphi используется другой менеджер памяти FastMM, который по-другому производит блокировку, так, что поток проводит значительно меньше времени внутри блокировки. Поэтому с новым менеджером памяти такую ситуацию воспроизвести сложнее. Кстати, в оригинальном посте Реймонда здесь стоял пример как раз на C#, а основным защищаемым объектом являлась консоль.
(**) Примечание: на самом деле, в Delphi с целью экономии ресурсов менеджер памяти переходит в потоко-безопасный режим только при создании дополнительных потоков. Это позволяет экономить ресурсы в однопоточных приложениях, которых, пока, большинство. Создание потока классом TThread или с помощью BeginThread автоматически переводит менеджер памяти в потоко-безопасный режим. Для всех других способов создания потока (например, CreateThread), вам нужно руками включить потоко-безопасный режим, установив переменную IsMultiThread.

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

Если FlushInstructionCache ничего не делает, почему я должен её вызывать?

Это перевод If FlushInstructionCache doesn't do anything, why do you have to call it? Автор: Реймонд Чен.

Если вы посмотрите на реализацию FlushInstructionCache в Windows 95, то вы увидите, что она содержит только инструкцию возврата (иными словами, FlushInstructionCache - пустая функция). Она, по-сути, ничего не делает. Ну так почему же нам нужно вызывать функцию, которая ничего не делает?

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

Почему я должен возвращать это глупое значение из WM_DEVICECHANGE?

Это перевод Why do I have to return this goofy value for WM_DEVICECHANGE?

Чтобы запретить изъятие устройства из запроса, вы должны вернуть специальное значение BROADCAST_QUERY_DENY, любопытно, что значение этой константы равно $424D5144. Какая история скрывается за этим числом?

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

Кому принадлежат различные биты в масках прав доступа?

Это перевод Which access rights bits belong to whom? Автор: Реймонд Чен.

Каждый ACE в дескрипторе безопасности (security descriptor) содержит 32-х битную маску доступа. Что означают отдельные биты в этой маске?

Маска доступа представляет собой 32-х битное значение. Верхние (старшие) 16 бит определяются операционной системой, а нижние (младшие) 16 бит - защищаемым объектом.

Например, в качестве маски доступа рассмотрим значение $00060002. Это значение разбивается на определяемые системой правила WRITE_DAC ($00040000) и READ_CONTROL ($00020000) и одно правило, определяемое объектом: $0002.

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

СмыслЕсли объект - это...
FILE_WRITE_DATAфайл
FILE_ADD_FILEкаталог
PROCESS_CREATE_THREADпроцесс
THREAD_SUSPEND_RESUMEпоток
JOB_OBJECT_SET_ATTRIBUTESзадание
EVENT_MODIFY_STATEсобытие
SEMAPHORE_MODIFY_STATEсемафор
TIMER_MODIFY_STATEтаймер
IO_COMPLETION_MODIFY_STATEпорт завершения ввода-вывода
KEY_SET_VALUEключ реестра
TOKEN_DUPLICATEтокен
WINSTA_READATTRIBUTESоконная станция
DESKTOP_CREATEWINDOWдесктоп

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

Если вы попросите функцию ConvertSecurityDescriptorToStringSecurityDescriptor перевести дескриптор безопасности в строковое представление, она попытается угадать тип объекта. Но поскольку здесь очень мало информации, то чаще всего она ошибается. К примеру, наша маска доступа из примера будет переведена в SDDL как "DCRCWD". Права RC = READ_CONTROL и WD = WRITE_DAC являются стандартными правами для любых объектов, но для $0002 SDDL предположил, что это DC = ADS_RIGHTS_DS_DELETE_CHILD.

Заметьте, что существует ещё несколько системных кодов, содержащих "GENERIC" в своём имени - к примеру, GENERIC_READ или GENERIC_WRITE. Каждый из типов объектов по-разному определяют права доступа "на чтение", "на запись" и, возможно, "на выполнение" (например, у ключей реестра есть KEY_QUERY_VALUE и KEY_SET_VALUE). Но все эти типы должны задать соответствие: какие из их прав являются правами чтения, записи и выполнения, так что вы можете запросить только маску доступа типа GENERIC и получить соответствующий вашему выбору маски набор прав - в зависимости от типа объекта.

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

Кому принадлежат разные типы сообщений?

Это перевод Which message numbers belong to whom? Автор: Реймонд Чен.

Допустимые номера оконных сообщений разбиваются на четыре категории.

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

Что это за странные значения, возвращаемые GWLP_WNDPROC?

Это перевод What are these strange values returned from GWLP_WNDPROC? Автор: Реймонд Чен.

GetWindowLongPtr(hwnd, GWLP_WNDPROC) (или GetWindowLong(hwnd, GWL_WNDPROC), если вы ещё не сделали свой код совместимым с 64-х разрядной ОС) должна возвращать текущую оконную процедуру. Почему же я иногда получаю при этом какие-то совершенно левые значения?

Потому что иногда "вы не можете справиться с истиной".

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

Например, предположим, что вы работаете на Windows XP и что окно является окном UNICODE, но компонент, скомпилированный как ANSI, вызывает GetWindowLong(hwnd, GWL_WNDPROC) (в действительности это будет вызов GetWindowLongA, т.к. компонент скомпилирован как ANSI - прим. пер.). При этом нельзя вернуть прямой указатель на оконную процедуру, потому что она ожидает параметры сообщений в формате UNICODE, а ваш компонент оперирует с сообщениями в формате ANSI. Поэтому вместо этого возвращается волшебное значение. Когда вы передаёте это волшебное значение в CallWindowProc, она распознаёт это значение как: "ох, мне нужно перевести параметры для сообщения из ANSI в UNICODE и передать UNICODE-сообщение в вон ту оконную процедуру".

В качестве другого примера, предположим, что вы работаете в Windows 95 и окно было создано 32-х битным приложением, но 16-ти битный компонент вызывает GetWindowLong(hwnd, GWLP_WNDPROC). И снова, вы не можете вернуть указатель на 32-х битную оконную процедуру, потому что сообщения нужно конвертировать между 16-ю и 32-мя битами (и кроме того, 16-ти битная программа просто не может перейти по 32-х битному плоскому адресу). Поэтому снова, вместо настоящего указателя, возвращается полшебное значение, по которому CallWindowProc понимает, что: "ох, мне нужно перевести сообщение из 16-ти битного в 32-х битное и передать его вон той оконной процедуре".

(Эти превращения известны как "thunks".)

Поэтому помните: единственная вещь, которую можно сделать со значением, полученным от GetWindowLongPtr(hwnd, GWLP_WNDPROC), это: (1) передать это значение в CallWindowProc, или (2) передать его обратно через SetWindowLongPtr(hwnd, GWLP_WNDPROC).

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

Что означают буквы W и L в WPARAM и LPARAM?

Это перевод What do the letters W and L stand for in WPARAM and LPARAM? Автор: Реймонд Чен.

Давным-давно, Windows была 16-ти разрядной. Каждое сообщение могло нести с собой две части данных, называемые WPARAM и LPARAM.

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

Почему "Быстрое переключение пользователей" не доступно в доменах?

Это перевод Why isn't Fast User Switching enabled on domains? Автор: Реймонд Чен.

Windows XP добавила новую возможность под названием "Быстрое переключение пользователей" ("Fast User Switching"), которое позволяет вам переключаться между пользователями, без необходимости выхода (log off). Но эта возможность отключается, как только ваш компьютер входит в домен. Почему?

Есть несколько причин, ни одна из которых не является препятствием, но все вместе они создают кучу работы для системных администраторов, которую они не желали бы получить (см. предыдущее сообщение о расходах на переподготовку).
- Как вы покажете всех пользователей в домене на экране приветствия (Welcome screen)? Уж наверно вы не захотите показывать список в 10'000 имён (скроллимся-скроллимся-скроллимся...).
- Как вы проверите, имеет ли пользователь пароль? Экран приветствия в Windows XP просто пытается впустить вас с пустым паролем. Если это срабатывает, то - пуфф! и вы вошли. Если же нет, то экран приветствия показывает диалог ввода пароля. Это работает, но при этом создаёт попытку входа с неверным паролем. Во-первых она заносится в системный лог. А во-вторых, многие системные администраторы устанавливают политику блокировки, когда ваш аккаунт блокируется, если вы ввели свой пароль неверно более N раз. Проба входа с пустым паролем приводила бы к отключению аккаунтов по всей компании.

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

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

Просто следуйте правилам и никто не пострадает

Это перевод Just follow the rules and nobody gets hurt. Автор: Реймонд Чен.

Может быть вы были ленивы и никогда не вызывали VirtualProtect(PAGE_EXECUTE), когда вы создавали код на лету. Вам это сходило с рук, потому что у системы защиты страниц процессора i386 нет режима "читать, но не выполнять", поэтому всё, что вы могли читать, вы могли и выполнять.

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

Длинная и печальная история ключа Shell Folders

Это перевод The long and sad story of the Shell Folders key. Автор: Реймонд Чен.

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

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