Ошибка в книге Windows Internals
Aug 16, 2012 · CommentsПрограммированиеProcess ExplorerWindows Internals bookWindows KernelWow64
В новой редакции Windows Internals обнаружился старый-знакомый баг. Там приводится пример стека 32-х битной нити, выполняющейся под Wow64:
Иллюстрация сопровождается комментарием:
An example of a Wow64 thread inside Microsoft Office Word 2007 is shown in Figure 5-13. The highlighted stack frame and all stack frames below it are the 32-bit stack frames from the 32-bit stack. The stack frames above the highlighted frame are on the 64-bit stack.
Что не совсем верно. На самом деле кадры с 0 по 6 включительно принадлежат 64-х стеку, а кадры с 7 по 16 - из 32-х битного стека. Более того, если отсортировать кадры по времени создания, то порядок кадров будет другим: 0, 1, 7-16, 2-6 (начиная с самого недавнего). Причина путаницы в том, что функции трассировки стека в Windows не возвращают информацию о переключаниях между разными стеками нити. Это довольно сложно и мало кому нужно. В результате Process Explorer трассирует каждый стек индивидуально (сначала ядерный, потом 64-х битный, а затем - 32-х битный), перекладывая задачу “кто кого позвал” на плечи разработчика.
Распознать начало 64-х битного стека легко - он, в отличие от 32-х битного всегда начитается с ntdll!LdrInitializeThunk. Эта функция - первое что выполняет нить после первого переключения из режима ядра. ntdll!LdrInitializeThunk завершает инициализацию нити и, поскольку нить выполняется в Wow64, переключается с 32-х режим и рекусривно вызывает ntdll!LdrInitializeThunk - на этот раз из 32-х битный ntdll.dll. Кадры стека, соответсвующие этому переходу - wow64!Wow64LdrpInitialize и wow64cpu!RunCpuSimulation. В отличие от Wow64, “нормальные” нити после завершения инициализации прыгают (с помощью nt!NtContinue) на ntdll!__RtlUserThreadStart. Указатель стека при этом сбрасывается в начало, так что ntdll!__RtlUserThreadStart становится первым кадром 32-х битного стека.
В других случаях, например когда нить прервана в момент обработки WM_CREATE, все может быть гораздо запутаннее. В этом случае последовательность переключений между стеками выглядит как x64-> x86 > x64 -> kernel -> x64 -> x86, причем переход из x86 в x86 через ядро может повторяться много раз, так как обратные вызовы из ядра могут быть вложенными. А если ещё вспомнить про обработку исключений…