
Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004
.pdf
ГЛАВА 7 Усложненные технологии неуправляемого кода в Visual Studio .NET |
301 |
|
|
MOVSX Перемещение значений с расширением знакового бита MOVZX Перемещение значений с обнулением старших битов
Эти две команды копируют значения из меньших операндов в большие, опреде ляя правило заполнения старших битов. При выполнении команды MOVSX старшие биты регистра приемника становятся равны знаковому биту операнда источни ка. Команда MOVZX заполняет старшие биты регистра приемника нулями. На эти команды следует обращать особое внимание, если программа неправильно рабо тает со знаками чисел.
Сравнение и проверка
CMP |
Сравнение двух операндов |
Сравнивает операнды: вычитает второй операнд из первого и отбрасывает резуль тат, устанавливая при этом соответствующие флаги в регистре EFLAGS. Можете считать CMP условной частью оператора if языка C. В табл. 7 9 показаны флаги и их значения, соответствующие выполнению команды CMP.
Табл. 7-9. Значения результата и устанавливаемые флаги
Результат |
Флаги в окне Registers |
Флаги из руководства Intel |
Операнды равны |
ZR = 1 |
ZF = 1 |
Первый операнд меньше |
PL != OV |
SF != OF |
Первый операнд больше |
ZR = 0 и PL = OV |
ZF = 0 и SF = OF |
Операнды не равны |
ZR = 0 |
ZF = 0 |
Первый операнд больше, |
PL = OV |
SF = OF |
или операнды равны |
|
|
Первый операнд меньше, |
ZR = 1 или PL != OV |
ZF = 1 или SF != OF |
или операнды равны |
|
|
|
|
|
TEST Логическое сравнение
Выполняет над операндами побитовое логическое И, устанавливая соответству ющим образом флаги PL, ZR и PE (флаги SF, ZF и PF в руководстве Intel). Эта коман да позволяет проверить, равен ли бит единице.
Команды безусловного и условного переходов
JMP |
Безусловный (абсолютный) переход |
Как говорит само название, JMP передает управление по абсолютному адресу.
JE |
Переход, если равно |
JL |
Переход, если меньше |
JG |
Переход, если больше |
JNE |
Переход, если не равно |
JGE |
Переход, если больше или равно |
JLE |
Переход, если меньше или равно |
Команды CMP и TEST не имели бы особого смысла, если бы у нас не было способа использования их результатов. Команды перехода позволяют перейти к нужному

302 ЧАСТЬ II Производительная отладка
месту программы согласно установленным флагам. С этими командами вы буде те чаще всего сталкиваться в окне Disassembly. В документации к процессору Pentium Xeon II указаны 62 команды условного перехода, однако многие из них образуют идентичные пары, отличающиеся только тем, что функция одной из команд характеризуется при помощи частицы «не». Так, команда JLE (переход, если меньше или равно) имеет тот же идентификатор операции, что и команда JNG (переход, если не больше). Применяя какой то другой дизассемблер, а не отлад чик Visual Studio .NET, вы можете встретить и другие команды условного перехо да. Чтобы узнать про все команды перехода, изучите в документации Intel коды «Jcc».
Я привел команды условного перехода в том же порядке, в каком указаны усло вия в табл. 7 9, чтобы вы могли их сопоставить. Обычно за командами CMP или TEST сразу же следует один из условных переходов. В оптимизированном коде между командами сравнения или проверки и перехода могут иметься и другие коман ды, но при этом гарантируется, что они не изменяют флаги.
Изучая дизассемблированный код, вы заметите, что команды условного пере хода обычно противоположны условию, указанному в коде высокого уровня. Это поясняется в первом разделе следующего кода:
void JumpExamples ( int i )
{
//Это оператор C. Заметьте: условие имеет вид "i > 0".
//Тем не менее компилятор генерирует противоположное условие. Указанный
//мной ассемблерный код очень похож на код, генерируемый компилятором.
//При различных методах оптимизации может генерироваться различный код.
//if ( i > 0 )
//{
//printf ( "%i > 0\n" ) ;
//}
char szGreaterThan[] = "%i > 0\n" ; __asm
{
CMP |
i , 0 |
// Сравнение i с нулем путем вычитания (i # 0). |
|||
JLE |
JE_LessThanOne // Если |
i |
меньше или равно 0, выполняется переход |
||
|
|
// к |
метке JE_LessThanOne. |
||
PUSH |
i |
// В |
стек |
помещается параметр i. |
|
LEA |
EAX , |
szGreaterThan |
// |
В |
стек помещается адрес строки формата. |
PUSH |
EAX |
|
|
|
|
CALL |
DWORD |
PTR [printf] |
// |
Вызов функции printf. Можно догадаться, |
//что printf скорее всего находится в DLL,
//так как она вызывается при помощи указателя.
ADD ESP , 8 |
// printf вызывается |
по соглашению |
__cdecl, поэтому |
||
|
// очистка стека выполняется |
в вызывающей функции. |
|||
JE_LessThanOne: |
// |
При помощи встроенного ассемблера можно |
|||
|
// |
выполнять переход |
к любой |
метке |
языка C. |
} |
|
|
|
|
|
////////////////////////////////////////////////////////////////////
ГЛАВА 7 Усложненные технологии неуправляемого кода в Visual Studio .NET |
303 |
|
|
//Вычисление абсолютного значения параметра и его проверка.
//Код на языке C:
//int y = abs ( i ) ;
//if ( y >=5 )
//{
//printf ( "abs(i) >= 5\n" ) ;
//}
//else
//{
//printf ( "abs(i) < 5\n" ) ;
//}
char szAbsGTEFive[] = "abs(i) >= 5\n" ; char szAbsLTFive[] = "abs(i) < 5\n" ; __asm
{
MOV |
EBX , i |
// Значение i помещается |
в регистр EBX. |
|
CMP |
EBX , 0 |
// Сравнение |
EBX с нулем |
(EBX # 0). |
JG |
JE_PosNum |
// Если результат больше |
0, то |
|
|
|
// EBX содержит положительное значение. |
||
NEG |
EBX |
// Преобразование отриц. числа в положит. |
||
JE_PosNum: |
|
|
|
|
CMP |
EBX , 5 |
// Сравнение |
EBX с 5. |
|
JL |
JE_LessThan5 |
// Если EBX < 5, переход |
к метке. |
|
LEA |
EAX , szAbsGTEFive // Загрузка указателя на |
верную строку |
||
|
|
// в регистр |
EAX. |
|
JMP |
JE_DoPrintf |
// Переход к |
вызову функции printf. |
JE_LessThan5:
LEA |
EAX , |
szAbsLTFive |
// Загрузка указателя на верную строку в EAX. |
JE_DoPrintf: |
|
|
|
PUSH |
EAX |
|
// Адрес строки помещается в стек. |
CALL |
DWORD |
PTR [printf] // Печать строки. |
|
ADD |
ESP , |
4 |
// Восстановление стека. |
} |
|
|
|
} |
|
|
|
Команды цикла
LOOP Цикл согласно значению регистра ECX
В коде, сгенерированном компиляторами Microsoft, команда LOOP встречается редко. И все же в некоторых участках кода ядра ОС (которые, похоже, написаны на ас семблере) она время от времени попадается. Использовать команду LOOP просто. Загрузите в регистр ECX число итераций цикла и выполните блок кода. Сразу же

304 ЧАСТЬ II Производительная отладка
после блока укажите команду LOOP, которая каждый раз будет уменьшать ECX на 1 и, если ECX не равен 0, передавать управление на начало блока. Когда ECX станет равным 0, начнет выполняться код, расположенный после команды LOOP.
Большинство циклов, с которыми вы столкнетесь, будут представлять собой комбинацию команд условного и абсолютного перехода. Они очень похожи на представленный чуть выше код оператора if за исключением того, что в конце блока if располагается команда JMP, передающая управление в начало блока. Вот типичный пример цикла:
void LoopingExample ( int q )
{
//Код на языке C:
//for ( ; q < 10 ; q++ )
//{
//printf ( "q = %d\n" , q ) ;
//}
char szFmt[] |
= "q = %d\n" ; |
|
|
__asm |
|
|
|
{ |
|
|
|
JMP |
LE_CompareStep |
// При первой итерации цикла сразу |
|
|
|
|
// переходим к проверке условия. |
LE_IncrementStep: |
|
|
|
INC |
q |
|
// Увеличение q на 1. |
LE_CompareStep: |
|
|
|
CMP |
q , |
0Ah |
// Сравнение q с 10. |
JGE |
LE_End |
// Если q >= 10, цикл завершен. |
|
MOV |
ECX |
, DWORD PTR [q] // Значение q помещается в ECX, |
|
PUSH |
ECX |
|
// а затем — в стек. |
LEA |
ECX |
, szFmt |
// Загрузка адреса строки формата в ECX. |
PUSH |
ECX |
|
// Адрес строки формата помещается в стек. |
CALL |
DWORD PTR [printf] |
// Печать номера итерации цикла. |
|
ADD |
ESP |
, 8 |
// Очистка стека. |
JMP |
LE_IncrementStep |
// Увеличение q и переход к началу цикла. |
|
LE_End: |
|
|
// Цикл завершен. |
} |
|
|
|
} |
|
|
|
Манипуляции со строками
Процессоры Intel предоставляют развитые средства работы со строками. Под этим понимается то, что одна команда процессора может выполнить некоторые дей ствия над большими блоками памяти. Все описываемые мной команды работы со строками имеют несколько мнемонических обозначений, которые можно узнать
ГЛАВА 7 Усложненные технологии неуправляемого кода в Visual Studio .NET |
305 |
|
|
в документации Intel, однако в окне Disassembly среды Visual Studio .NET они все гда будут выглядеть так, как я покажу. Все эти команды могут работать с блоками памяти объемом в байт, слово и двойное слово.
MOVS Перемещение данных из одной строки в другую
Перемещает значение, содержащееся в ячейке памяти по адресу ESI, в ячейку по адресу EDI. Она позволяет использовать только регистры ESI и EDI. Можете счи тать ее аналогом функции memcpy языка C. В окне Disassembly среды Visual Studio
.NET всегда указывается спецификатор размера операции, так что вы сразу смо жете определить, какой объем памяти перемещается. После перемещения одного значения регистры ESI и EDI увеличиваются или уменьшаются в зависимости от флага направления в регистре EFLAGS (в окне Registers среды Visual Studio .NET этот флаг обозначается как UP). Если поле UP равно 0, значения регистров увеличива ются, если 1 — регистры уменьшаются. Все компиляторы Microsoft всегда гене рируют код, перемещающий строки в порядке уменьшения адресов (UP = 1). Зна чение, на которое увеличиваются или уменьшаются регистры, зависит от разме ра операции: 1 для байтов, 2 для слов и 4 для двойных слов.
SCAS Сканирование строки
Сравнивает значение ячейки памяти, на которую указывает регистр EDI, со значе нием, содержащимся в AL, AX или EAX, что зависит от указанного размера. Сравне ние приводит к установке флагов регистра EFLAGS в те же значения, что указаны в табл. 7 9. Сканируя строку при помощи команды SCAS на предмет наличия терми нального символа NULL, можно воспроизвести функцию strlen языка C. Как и MOVS, команда SCAS автоматически увеличивает или уменьшает значения регистра EDI.
STOS Сохранение значения в строке
Сохраняет значение из AL, AX или EAX (в зависимости от заданного размера) по адресу памяти, на который указывает регистр EDI. Эта команда аналогична функции memset языка C. Как и MOVS и SCAS, команда STOS также автоматически увеличивает или уменьшает значение регистра EDI.
CMPS Сравнение строк
Поочередно сравнивает значения двух строк и устанавливает соответствующие значения флагов регистра EFLAGS. В то время как SCAS сравнивает все символы с одним значением, команда CMPS «перебирает» по очереди значения обеих строк. CMPS аналогична функции memcmp языка C. Как и все остальные команды работы со строками, CMPS сравнивает значения заданного размера и автоматически увели чивает или уменьшает указатели на обе строки.
REP |
Повтор согласно счетчику ECX |
REPE |
Повтор, пока равно или пока ECX не равен 0 |
REPNE |
Повтор, пока не равно или пока ECX не равен 0 |
Команды работы со строками удобны, но они не заслуживали бы внимания, если б работали только с одним элементом за раз. Префиксы повтора позволяют выпол нять строковые команды заданное (в ECX) число раз или пока не будет достигну то указанное условие. Выбрав функцию Step Into (шаг внутрь), когда в окне Disas

306 ЧАСТЬ II Производительная отладка
sembly выполняется команда с префиксом повтора, вы останетесь на той же строке, потому что команда просто выполнится еще раз. Чтобы пройти все итерации, используйте функцию Step Over (шаг через). Отлаживая программу, вы можете при помощи функции Step Into проверить, на какие строки указывают регистры ESI и EDI. Если ошибка вызывается строковой командой с префиксом повтора, нужно также следить за значением регистра ECX: так вы узнаете, какая итерация заканчи вается неудачей.
Описывая команды работы со строками, я говорил, на какие функции стандар тной библиотеки C они похожи. Вот как могут выглядеть их эквиваленты на язы ке ассемблера, не включающие проверку ошибок:
void MemCPY ( char * szSrc , char * szDest , int iLen )
{
__asm
{
MOV ESI , |
szSrc |
// Загрузка в ESI адреса строки источника. |
MOV EDI , |
szDest |
// Загрузка в EDI адреса строки назначения. |
MOV ECX , |
iLen |
// Указание числа копируемых элементов. |
|
|
// Скопировать! |
REP MOVS BYTE PTR [EDI] |
, BYTE PTR [ESI] |
|
} |
|
|
} |
|
|
int StrLEN ( char |
* szSrc ) |
|
{ |
|
|
int iReturn ; |
|
|
__asm |
|
|
{ |
|
|
XOR EAX , |
EAX |
// Обнуление EAX (Ищем символ NULL). |
MOV EDI , |
szSrc |
// Загрузка адреса проверяемой строки в EDI. |
MOV ECX , |
0FFFFFFFFh |
// Максимальное число проверяемых символов. |
REPNE SCAS BYTE PTR [EDI] // Сравнение, пока ECX не станет равным 0 // или пока не будет найден символ NULL.
CMP |
ECX , 0 |
// Если ECX=0, |
значит, символ NULL |
JE |
StrLEN_NoNull |
// в строке обнаружен не был. |
|
NOT |
ECX |
// ECX уменьшался, поэтому его нужно |
|
|
|
// преобразовать в положительное число. |
|
DEC |
ECX |
// Символ NULL |
в строку не входит. |
MOV |
EAX , ECX |
// Возвращение |
числа символов. |
JMP |
StrLen_Done |
// Переход на возвращение из функции. |
StrLEN_NoNull:
ГЛАВА 7 Усложненные технологии неуправляемого кода в Visual Studio .NET |
307 |
|
|
|
|
MOV EAX , 0FFFFFFFFh |
// NULL не был обнаружен — возвращаем #1. |
|
StrLEN_Done:
}
__asm MOV iReturn , EAX ; return ( iReturn ) ;
}
void MemSET ( char * szDest , int iVal , int iLen )
{
__asm |
|
|
|
|
|
{ |
|
|
|
|
|
MOV EAX , iVal |
// В EAX заносится символ заполнения. |
||||
MOV EDI , szDest |
// |
Загрузка |
в |
EDI |
адреса строки. |
MOV ECX , iLen |
// |
Загрузка |
в |
ECX |
числа итераций. |
REP STOS BYTE PTR [EDI] // Заполнение памяти указанным значением.
}
}
int MemCMP ( char * szMem1 , char * szMem2 , int iLen )
{
int iReturn ; __asm
{
MOV ESI , szMem1 MOV EDI , szMem2 MOV ECX , iLen
//В ESI — адрес первого блока памяти.
//В EDI — адрес второго блока памяти.
//Макс. число сравниваемых байтов.
// Сравнение блоков памяти. REPE CMPS BYTE PTR [ESI], BYTE PTR [EDI]
JL |
MemCMP_LessThan |
// Переход, если szSrc < szDest |
JG |
MemCMP_GreaterThan // Переход, если szSrc > szDest |
|
|
|
// Блоки памяти идентичны. |
XOR |
EAX , EAX |
// Возвращаем 0. |
JMP |
MemCMP_Done |
|
MemCMP_LessThan: |
|
|
MOV |
EAX , 0FFFFFFFFh |
// Возвращаем #1. |
JMP |
MemCMP_Done |
|
MemCMP_GreaterThan: |
|
|
MOV |
EAX , 1 |
// Возвращаем 1. |
JMP |
MemCMP_Done |
|
MemCMP_Done:
}

308 ЧАСТЬ II Производительная отладка
__asm MOV iReturn , EAX
return ( iReturn ) ;
}
Распространенные ассемблерные конструкции
До этого момента я просто описывал основные команды языка ассемблера. Теперь я рассмотрю ассемблерные конструкции и объясню, как их определить и преоб разовать в операции более высокого уровня.
Доступ к памяти при помощи регистра FS
В ОС Win32 регистр FS играет специальную роль: в нем хранится указатель на блок информации о потоке (TIB), который еще иногда называют блоком переменных окружения потока (Thread environment block, TEB). Этот блок содержит все спе цифичные для потока данные, нужные для того, чтобы ОС могла легко предоста вить доступ к потоку. Они включают все цепочки структурной обработки исклю чений (Structured exception handling, SEH), локальную память потока, стек потока и другую необходимую ОС информацию. Подробнее о SEH см. главу 13. Пример локальной памяти потока я приведу при обсуждении программы MemStress (см. главу 17).
Блок TIB хранится в специальном сегменте памяти, и, когда ОС нужно полу чить доступ к TIB, она преобразует сумму значения регистра FS и смещения в нормальный линейный адрес. Если вы видите команду, использующую регистр FS, знайте, что выполняется одна из операций: создание или уничтожение кадра SEH, доступ к блоку TIB или доступ к локальной памяти потока.
Создание или уничтожение кадра SEH
Первые команды после создания кадра стека часто похожи на указанный ниже фрагмент, стандартный для начала блока __try. Первый узел цепи обработчиков SEH располагается в TIB по смещению 0. В приведенном ниже дизассемблирован ном коде компилятор помещает в стек данные и указатель на функцию __except_han# dler3 ОС Windows 2000. В Windows XP эту роль играет функция _SEH_prolog. Пер вая команда MOV получает доступ к TIB; смещение 0 показывает, что узел добавля ется на вершину цепи исключений. Две последних команды определяют место цепи, в которое узел будет перемещен.
PUSH 004060d0
PUSH 004014a0
MOV EAX , FS:[00000000]
PUSH EAX
MOV DWORD PTR FS:[0] , ESP
Этот пример прост и ясен, но компилятор не всегда генерирует такой чистый код. Иногда он распределяет создание кадра SEH по большему участку кода. В за висимости от установленных флагов генерирования кода и оптимизации компи лятор упорядочивает команды так, чтобы лучше использовать конвейеры процес сора. В следующем дизассемблированном фрагменте, для которого загружены символы KERNEL32.DLL, показано начало функции IsBadReadPtr Microsoft Windows 2000:
|
ГЛАВА 7 Усложненные технологии неуправляемого кода в Visual Studio .NET |
309 |
|
|
|
PUSH |
EBP |
|
MOV |
EBP , ESP |
|
PUSH |
0FFFFFFFFh |
|
PUSH |
77E86F40h |
|
PUSH |
OFFSET __except_handler3 |
|
MOV |
EAX , DWORD PTR FS:[00000000h] |
|
PUSH |
EAX |
|
MOV |
DWORD PTR FS:[0] , ESP |
|
Windows XP имеет один интересный аспект: похоже, код этой ОС включает собственный механизм обработки исключений, генерирующий вызов функции _SEH_prolog, в которой выполняется большая часть предыдущего кода. Это приво дит к значительному уменьшению кода, и на уровне ассемблера кажется, что фун кции Windows XP, работающие с SEH, используют для колдовства собственные пролог и эпилог.
А вот уничтожение кадра SEH гораздо менее интересно, чем его создание; нужно только помнить, что любой доступ по адресу FS:[0] означает работу с SEH:
MOV ECX , DWORD PTR [EBP#10h]
MOV DWORD PTR FS:[0] , ECX
Доступ к TIB
Значение FS:[18] — линейный адрес структуры TIB. Вот как функция GetCurrent# ThreadId ОС Windows XP получает сначала линейный адрес TIB, а затем — иден тификатор потока, расположенный по смещению 0x24:
GetCurrentThreadId:
MOV EAX , FS:[00000018h]
MOV EAX , DWORD PTR [EAX+024h]
RET
Доступ к локальной памяти потока
Локальная память потока — это механизм Win32, позволяющий каждому потоку иметь собственные экземпляры глобальных переменных. По смещению 0x2C в структуре TIB располагается указатель на массив локальной памяти потока. Вот как получить доступ к указателю локального хранилища потока:
MOV ECX , DWORD PTR FS:[2Ch]
MOV EDX , DWORD PTR [ECX+EAX*4]
Ссылки на структуры и классы
При разработке приложений для Windows приходится иметь дело со структура ми и классами, поэтому я хочу вкратце рассмотреть доступ к занимаемой ими памяти. Со структурами и классами удобно работать на языках высокого уровня, но на уровне ассемблера они на самом деле не существуют. В языках высокого уровня структуры и классы — это просто удобные способы указания смещений в блоке памяти.

310 ЧАСТЬ II Производительная отладка
Обычно компиляторы организуют память для структур и классов именно так, как вы указываете. Иногда компилятор расширяет поля, чтобы они были выров нены по естественным границам блоков памяти, которые для процессоров x86 кратны 4 или 8 байтам.
Для ссылок на структуры и классы используется сочетание регистра и смеще ния. Ниже я привожу пример структуры MyStruct и указываю в комментариях к каждому ее члену его смещение от начала структуры. После определения струк туры MyStruct я демонстрирую способы доступа к ее полям.
typedef struct |
tag_MyStruct |
|
|
{ |
|
|
|
DWORD |
dwFirst ; |
// Смещение равно 0. |
|
char |
szBuff[ 256 ] ; |
// 4#байтовое смещение. |
|
int |
iVal |
; |
// 260#байтовое смещение. |
} MyStruct , * |
PMyStruct ; |
|
void FillStruct ( PMyStruct pSt )
{
char szName[] = "Pam\n" ;
__asm
{
MOV EAX , pSt // В EAX помещается pSt. Ниже я использую
//непосредственные смещения, чтобы показать,
//как они выглядят в дизассемблированном коде.
//Встроенный ассемблер позволяет использовать
//нормальные ссылки вида <структура>.<поле>.
//Код на языке C: pSt#>dwFirst = 23 ;
MOV DWORD PTR [EAX] , 17h
//Код на языке C: pSt#>iVal = 0x33 ; MOV DWORD PTR [EAX + 0104h] , 0x33
//Код на языке C: strcpy ( pSt#>szBuff , szName ) ;
LEA ECX |
, szName |
// Адрес szName помещается в стек. |
PUSH ECX |
|
|
LEA ECX , [EAX + 4] // Адрес szBuff помещается в стек.
PUSH ECX
CALL |
strcpy |
|
ADD |
ESP , 8 |
// Функция strcpy использует соглашение __cdecl. |
//Код на языке C: pSt#>szBuff[ 1 ] = 'A' ; MOV BYTE PTR [EAX + 5] , 41h
//Код на языке C: printf ( pSt#>szBuff ) ;
MOV EAX , pSt |
// |
В EAX снова |
помещается pSt, |
так как |
|
// |
регистр EAX |
был изменен при |
вызове strcpy. |