Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004

.pdf
Скачиваний:
353
Добавлен:
13.08.2013
Размер:
3.3 Mб
Скачать

ГЛАВА 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.

Соседние файлы в предмете Программирование на C++