
Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004
.pdf
ГЛАВА 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.