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

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

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

ГЛАВА 7 Усложненные технологии неуправляемого кода в Visual Studio .NET

291

 

 

Примеры соглашений вызова

Чтобы связать вместе описанные мной команды и соглашения вызова, я включил в книгу листинг 7 3, содержащий примеры всех соглашений вызова в том виде, в каком они отображаются в окне Disassembly отладчика Visual Studio .NET. Если вам захочется взглянуть на исходный код этого фрагмента, вы найдете его в файле CALLING.CPP на CD, прилагаемом к книге.

Листинг 7 3 — это отладочная компоновка, при создании которой с целью упрощения кода все дополнительные ключи, такие как /RTCs и /GS, были заблоки рованы; кроме того, код фактически ничего не делает. Я просто по очереди вы зываю функции с использованием каждого соглашения вызова. Обратите особое внимание на порядок передачи параметров в каждую функцию и на то, как очи щается стек. Чтобы сделать листинг понятнее, я вставил между функциями коман ды NOP.

Листинг 7-3. Примеры соглашений вызова

1:/*——————————————————————————————————————————————————————————————————————

2:"Отладка приложений для Microsoft .NET и Microsoft Windows"

3:Copyright (c) 1997 2003 John Robbins — All rights reserved.

4:——————————————————————————————————————————————————————————————————————*/

5:#include "stdafx.h"

6:

 

 

7: // Строки, передаваемые в каждую функцию.

 

8: static char * g_szStdCall

= "__stdcall"

;

9: static char * g_szCdeclCall = "__cdecl"

;

10: static char * g_szFastCall

= "__fastcall"

;

11: static char * g_szNakedCall = "__naked"

;

12:

 

 

13:// Объявление extern "C" полностью отключает расширение имен C++.

14:extern "C"

15:{

16:

 

 

 

 

17: // Функция с соглашением вызова __cdecl:

 

 

18: void CDeclFunction ( char *

szString

,

 

19:

unsigned long

ulLong

,

 

20:

char

chChar

) ;

 

21:

 

 

 

 

22: // Функция с соглашением вызова __stdcall:

 

 

23: void __stdcall StdCallFunction ( char *

szString

,

24:

unsigned long ulLong

,

25:

char

chChar

) ;

26: // Функция с соглашением вызова __fastcall:

 

 

27: void __fastcall FastCallFunction (

char *

szString ,

28:

 

unsigned long ulLong

,

29:

 

char

chChar

) ;

30:

 

 

 

 

31:// Функция, использующая соглашение вызова naked. В данном случае

32:// соглашение указывается в определении, но не в объявлении функции.

33: int NakedCallFunction ( char *

szString ,

см. след. стр.

292 ЧАСТЬ II Производительная отладка

34:

unsigned long ulLong

,

35:

char

chChar

) ;

36: }

 

 

 

37:

 

 

 

38:void main ( void )

39:{

00401000

push

ebp

00401001

mov

ebp,esp

40:// Вызовы функций, необходимые для генерирования кода компилятором.

41:// Чтобы сделать дизассемблированный код читабельнее,

42:// я вставил между функциями по две команды NOP.

43:__asm NOP __asm NOP

00401003 nop

00401004 nop

44:CDeclFunction ( g_szCdeclCall , 1 , 'a' ) ;

00401005

push

61h

00401007

push

1

00401009

mov

eax,dword ptr [g_szCdeclCall (403028h)]

0040100E

push

eax

0040100F

call

CDeclFunction (401056h)

00401014

add

esp,0Ch

45:__asm NOP __asm NOP 00401017 nop

00401018 nop

46:StdCallFunction ( g_szStdCall , 2 , 'b' ) ;

00401019

push

62h

0040101B

push

2

0040101D

mov

ecx,dword ptr [g_szStdCall (40301Ch)]

00401023

push

ecx

00401024

call

StdCallFunction (40105Dh)

47:__asm NOP __asm NOP 00401029 nop

0040102A nop

48:FastCallFunction ( g_szFastCall , 3 , 'c' ) ;

0040102B

push

63h

0040102D

mov

edx,3

00401032

mov

ecx,dword ptr [g_szFastCall (403038h)]

00401038

call

FastCallFunction (401066h)

49:__asm NOP __asm NOP 0040103D nop

0040103E nop

50:NakedCallFunction ( g_szNakedCall , 4 , 'd' ) ;

0040103F

push

64h

00401041

push

4

00401043

mov

edx,dword ptr [g_szNakedCall (403044h)]

00401049

push

edx

0040104A

call

NakedCallFunction (40107Ah)

0040104F

add

esp,0Ch

51:

__asm NOP __asm NOP

00401052

nop

 

ГЛАВА 7

Усложненные технологии неуправляемого кода в Visual Studio .NET

293

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

00401053

nop

 

 

 

 

52:

 

 

 

 

 

53: }

 

 

 

 

 

00401054

pop

ebp

 

 

 

00401055

ret

 

 

 

 

54:

 

 

 

 

 

55: void CDeclFunction ( char *

szString

,

 

56:

 

unsigned long ulLong

,

 

57:

 

char

chChar

)

 

58: {

 

 

 

 

 

00401056

push

ebp

 

 

 

00401057

mov

ebp,esp

 

 

 

59:__asm NOP __asm NOP 00401059 nop

0040105A nop

60:}

0040105B

pop

ebp

 

 

0040105C

ret

 

 

 

61:

 

 

 

 

62: void __stdcall StdCallFunction ( char *

szString

,

63:

 

unsigned long ulLong

,

64:

 

char

chChar

)

65: {

 

 

 

 

0040105D

push

ebp

 

 

0040105E

mov

ebp,esp

 

 

66:__asm NOP __asm NOP 00401060 nop

00401061 nop

67:}

00401062

pop

ebp

 

 

00401063

ret

0Ch

 

 

68:

 

 

 

 

69: void __fastcall FastCallFunction ( char *

szString

,

70:

 

unsigned long ulLong

,

71:

 

char

chChar

)

72: {

 

 

 

 

00401066

push

ebp

 

 

00401067

mov

ebp,esp

 

 

00401069

sub

esp,8

 

 

0040106C

mov

dword ptr [ebp 8],edx

 

 

0040106F

mov

dword ptr [ebp 4],ecx

 

 

73:__asm NOP __asm NOP 00401072 nop

00401073 nop

74:}

00401074

mov

esp,ebp

 

00401076

pop

ebp

 

00401077

ret

4

 

75:

 

 

 

76: __declspec(naked)

int NakedCallFunction ( char *

szString ,

см. след. стр.

294

ЧАСТЬ II Производительная отладка

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

77:

 

 

unsigned long ulLong

,

 

 

78:

 

 

char

chChar

)

 

 

79: {

 

 

 

 

 

 

 

80:

 

__asm

NOP __asm NOP

 

 

 

 

0040107A

nop

 

 

 

 

 

0040107B

nop

 

 

 

 

 

81:

 

// Функции с соглашением naked должны выполнять возврат ЯВНО.

 

 

 

82:

 

__asm

RET

 

 

 

 

0040107C

ret

 

 

 

 

 

 

 

 

 

 

 

 

Доступ к переменным: глобальные переменные, параметры и локальные переменные

Давайте перейдем к рассмотрению доступа к переменным. К глобальным перемен ным обращаться легче всего, потому что они находятся в памяти по фиксирован ному адресу. Если у вас есть символы для конкретного модуля, вы увидите имя глобальной переменной. В следующем примере показано, как обратиться к гло бальной переменной при помощи встроенного ассемблера, который позволяет использовать переменные и как операнды источника, и как операнды приемни ка, точно так же, как и при обычном программировании на языке C.

int g_iVal = 0 ;

void AccessGlobalMemory ( void )

{

__asm

{

//Присвоение глобальной переменной значения 48 059. MOV g_iVal , 0BBBBh

//Если символы загружены, в окне Disassembly вы увидите:

//MOV DWORD PTR [g_iVal (4030B4)],0BBBBh.

//Если символы не загружены, в окне Disassembly вы увидите:

//MOV DWORD PTR [4030B4],0BBBBh.

}

}

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

void AccessParameter ( int iParam )

{

__asm

{

// Помещаем в регистр EAX значение iParam.

MOV EAX , iParam

ГЛАВА 7 Усложненные технологии неуправляемого кода в Visual Studio .NET

295

 

 

//Если символы загружены, в окне Disassembly вы увидите:

//MOV EAX,DWORD PTR [iParam].

//Если символы не загружены, в окне Disassembly вы увидите:

//MOV EAX,DWORD PTR [EBP+8].

}

}

Ранее я уже упоминал одну очень важную фразу «от источника к приемнику», которую нужно проговаривать, перемещая палец от второго операнда к первому. Вот еще одна не менее важная фраза: «Параметры располагаются по положитель ным смещениям!» То, что в стандартных кадрах стека доступ к параметрам осу ществляется при помощи постоянных смещений от регистра EBP, позволяет легко узнать, к какому параметру обращается конкретная команда: первый параметр всегда располагается по адресу [EBP+8], второй — по адресу [EBP+0Ch], третий — по адресу [EBP+10h] и т. д. Если вам нравятся формулы, то адрес n ого параметра можно вычислить так: [EBP + (4 + (n 4))]. Чуть позже, обсудив локальные перемен ные, я проиллюстрирую стандартные кадры стека на примере и расскажу, почему значения смещений жестко закодированы.

Если при отладке оптимизированного кода вы видите ссылки по положитель ным смещениям от регистра стека ESP, знайте, что вы находитесь в функции с данными FPO. Регистр ESP в функции может изменяться, поэтому в данном слу чае для слежения за параметрами придется приложить чуть больше усилий. При работе с оптимизированным кодом надо внимательно следить за элементами, помещаемыми в стек, так как ссылка на адрес [ESP+20h] вполне может оказаться той же, что и более ранняя ссылка [ESP+8h]. Отлаживая оптимизированный код на уровне ассемблера в пошаговом режиме, я всегда слежу за расположением параметров в стеке.

В функциях со стандартными кадрами стека локальные переменные всегда располагаются по отрицательным смещениям от регистра EBP. Как было показа но в разделе «Частая последовательность команд: вход в функцию и выход из функции», пространство в стеке резервируется командыой SUB. Вот как присваи вается новое значение локальной переменной:

void AccessLocalVariable ( void )

{

int iLocal ;

__asm

{

//Присваивание локальной переменной значения 23. MOV iLocal , 017h

//Если символы загружены, в окне Disassembly вы увидите:

//MOV DWORD PTR [iLocal],017h.

//Если символы не загружены, в окне Disassembly вы увидите:

//MOV [EBP 4],017h.

}

}

296 ЧАСТЬ II Производительная отладка

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

При первом знакомстве кадры стека немного непонятны, поэтому я приведу еще один заключительный пример и несколько рисунков. Следующий фрагмент — очень простая функция C — покажет, почему в стандартных кадрах стека пара метры располагаются по положительным, а локальные переменные — по отри цательны смещениям от регистра EBP. В этом примере определяется функция C AccessLocalsAndParamsExample, после чего следует код ее вызова и значения параметров. В конце примера приводится дизассемблированный код функции в том виде, ка кой он имеет в программе ASMer.

// Сама функция C:

void AccessLocalsAndParamsExample ( int * pParam1 , int * pParam2 )

{

int iLocal1 = 3 ; int iLocal2 = 0x42 ;

iLocal1 = *pParam1 ; iLocal2 = *pParam2 ;

}

// Код, вызывающий функцию AccessLocalsAndParamsExample: void DoTheCall ( void )

{

int iVal1 = 0xDEADBEEF ; int iVal2 = 0xBADDF00D ; __asm

{

LEA

EAX , DWORD PTR [iVal2]

PUSH

EAX

LEA

EAX , DWORD PTR [iVal1]

PUSH

EAX

CALL

AccessLocalsAndParamsExample

ADD

ESP , 8

}

 

}

// Дизассемблированный код функции AccessLocalsAndParamsExample: void AccessLocalsAndParamsExample ( int * pParam1 , int * pParam2 )

{

 

 

0040107A

push

ebp

0040107B

mov

ebp,esp

0040107D

sub

esp,8

int iLocal1 = 3 ;

ГЛАВА 7

Усложненные технологии неуправляемого кода в Visual Studio .NET

297

 

 

 

 

00401080

mov

dword ptr [iLocal1],3

 

int iLocal2 = 0x42 ;

 

00401087

mov

dword ptr [iLocal2],42h

 

iLocal1 = *pParam1 ;

 

0040108E

mov

eax,dword ptr [pParam1]

 

00401091

mov

ecx,dword ptr [eax]

 

00401093

mov

dword ptr [iLocal1],ecx

 

iLocal2 = *pParam2 ;

 

00401096

mov

edx,dword ptr [pParam2]

 

00401099

mov

eax,dword ptr [edx]

 

0040109B

mov

dword ptr [iLocal2],eax

 

}

 

 

 

0040109E

mov

esp,ebp

 

004010A0

pop

ebp

 

004010A1

ret

 

 

Если вы установите точку прерывания в начале фукнции AccessLocalsAndParamsExample по адресу 0x0040107A, то увидите такие значения стека и регистров (рис. 7 11):

ESP

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

0x0012FE0C

 

0x0012FE0C

0x00401441

Адрес возврата

 

 

 

 

 

 

 

EBP

 

0x0012FE10

0x0012FE18

pParam1

 

 

 

 

 

 

0x0012FF68

 

0x0012FE14

 

0x0012FE1C

pParam2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Регистры

 

Адрес стека

Стек

Описание

Рис. 7 11. Стек перед прологом функции AccessLocalsAndParamsExample

Первые три ассемблерных команды в функции AccessLocalsAndParamsExample представляют собой пролог функции. В результате выполнения пролога указате ли стека и базы настраиваются так, что параметры будут располагаться по поло жительным, а локальные переменные — по отрицательным смещениям от регис тра EBP. На рис. 7 12 показаны значения стека и указателя базы после выполнения каждой команды пролога. Я советую вам изучить этот пример как в книге, так и при помощи программы ASMER.CPP, расположенной на CD.

298 ЧАСТЬ II Производительная отладка

Выполнение PUSH EBP

 

ESP

 

 

 

 

 

 

 

0x0012FE08

 

 

 

 

0x0012FE08

 

 

 

 

 

 

 

 

 

0x0012FE0C

 

 

 

 

 

EBP

0x0012FE10

 

 

 

 

 

0x0012FF68

 

 

 

 

0x0012FE14

 

 

 

 

 

 

Регистры

Адрес стека

Выполнение MOVE EBP, ESP

ESP

 

 

 

 

0x0012FE08

 

 

 

 

0x0012FE08

 

 

 

 

 

 

 

 

 

0x0012FE0C

 

 

 

 

 

EBP

 

0x0012FE10

 

 

 

0x0012FE08

 

 

 

 

0x0012FE14

 

 

 

 

 

 

 

 

Регистры

Адрес стека

ВыполнениеSUB ESP, 8 executes

 

 

 

 

 

0x0012FE00

 

 

 

 

 

ESP

 

 

 

0x0012FE04

 

 

 

 

 

0x0012FE00

 

 

 

 

0x0012FE08

 

 

 

 

 

 

 

 

 

0x0012FE0C

 

 

 

 

 

EBP

 

0x0012FE10

 

 

 

 

0x0012FE14

0x0012FE08

 

 

 

 

 

 

 

 

 

 

 

 

 

Адрес стека

Регистры

 

 

 

0x0012FF68

Сохраненный EBP

 

 

0x00401441

Адрес возврата

 

 

0x0012FE18

pParam1

 

 

0x0012FE1C

pParam2

 

 

 

 

 

Стек

Описание

 

 

 

 

 

0x0012FF68

Сохраненный EBP

 

 

0x00401441

Адрес возврата

 

 

0x0012FE18

pParam1

0x0012FE1C

pParam2

 

 

 

 

 

Стек

Описание

 

 

 

 

 

Uninitialized

[EBP - 8] iLocal2

 

[EBP - 4] iLocal1

Uninitialized

 

Сохраненный EBP

0x0012FF68

0x00401441

[EBP + 8] Адрес возврата

 

 

0x0012FE18

[EBP + 8] pParam1

 

 

0x0012FE1C

[EBP + C] pParam2

 

 

 

 

Описание

Стек

Рис. 7 12. Стек во время и после выполнения пролога функции

AccessLocalsAndParamsExample

ГЛАВА 7 Усложненные технологии неуправляемого кода в Visual Studio .NET

299

 

 

Дополнительные команды, которые нужно знать

В этом разделе описаны команды манипуляции с данными и указателями, срав нения и проверки, безусловных и условных переходов, организации циклов и работы со строками.

Манипуляции с данными

AND

Логическое И

OR

Логическое ИЛИ

Команды AND и OR выполняют логические побитовые операции, которые наверня ка известны всем программистам, потому что они лежат в основе манипуляций с отдельными битами.

NOT

Логическое НЕТ

NEG

Изменение знака числа

Команды NOT и NEG иногда вызывают некоторое замешательство, потому что, не смотря на все сходство, они выполняют совершенно разные операции. Команда NOT — это побитовая операция, изменяющая все нулевые биты на единичные и наоборот, NEG эквивалентна вычитанию операнда из 0 (она изменяет все биты и прибавляет к результату единицу). Различия между этими двумя командами иллю стрирует следующий код:

void NOTExample ( void )

{

__asm

{

MOV EAX , 0FFh

 

MOV EBX , 1

 

 

NOT

EAX

// В регистре EAX теперь содержится 0FFFFFF00h.

NOT

EBX

//

В регистре EBX теперь содержится 0FFFFFFFEh.

}

}

void NEGExample ( void )

{

 

__asm

 

 

 

{

 

 

 

MOV EAX , 0FFh

 

 

MOV EBX , 1

 

 

 

NEG EAX

// В EAX теперь содержится 0FFFFFF01h ( 0

0FFh ).

 

NEG EBX

// В EBX теперь содержится 0FFFFFFFFh ( 0

1 ).

 

}

 

 

}

 

 

 

XOR

Логическое исключающее ИЛИ

 

Вы будете сталкиваться с командой XOR очень часто, но не потому, что люди очень интересуются операцией «исключающее ИЛИ», а потому что это самый быстрый способ обнуления значения. Выполнение XOR на двух операндах устанавливает бит

300 ЧАСТЬ II Производительная отладка

операнда приемника в 1, если соответствующие биты операндов имеют разные значения. Если все соответствующие биты одинаковы, операнд приемника станет равен 0. Команда XOR EAX, EAX выполняется быстрее (за меньшее число тактов про цессора), чем MOV EAX, 0, поэтому именно ее компиляторы Microsoft генерируют для обнуления регистров.

INC

Инкремент

DEC

Декремент

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

SHL

Сдвиг влево/умножение на 2

SHR

Сдвиг вправо/деление на 2

Команды битового сдвига выполняются процессорами x86 быстрее, чем соответ ствующие команды умножения и деления. Эти команды аналогичны побитовым операторам << и >> языка C соответственно.

DIV

Деление без знака

MUL

Умножение без знака

Эти, казалось бы, простые команды на самом деле не так просты. Обе они выпол няют беззнаковые операции над регистром EAX. Однако для хранения результата неявно используется и регистр EDX. В регистр EDX помещаются старшие байты результата умножения двойных слов и более объемных операндов. Команда DIV сохраняет остаток в EDX, а частное в EAX. Обе команды требуют, чтобы исходное число находилось в регистре EAX, а делителем/множителем могут быть значения, содержащиеся в других регистрах или в ячейках памяти.

IDIV Деление со знаком

IMUL Умножение со знаком

Эти команды аналогичны командам DIV и MUL за исключением того, что они рас сматривают операнды как значения со знаком. Результат обрабатывается так же, как и в случае команд DIV и MUL. IMUL может иметь три операнда: первый — прием ник, а два оставшихся — источники. В наборе команд x86 IMUL — единственная команда, допускающая применение трех операндов.

LOCK Префикс блокировки шины (сигнал LOCK#)

Это не «настоящая» команда, а префикс для других команд, который говорит про цессору, что доступ к памяти, выполняемый следующей командой, должен быть атомарной операцией, в результате чего процессор блокирует шину памяти, за прещая доступ к памяти другим процессорам системы. Если вы хотите увидеть префикс LOCK в действии, дизассемблируйте функцию InterlockedIncrement Windows XP или более поздней версии ОС Microsoft.

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