Использование крешлогов для отладки собственных dll
Клиентский терминал MetaTrader4 имеет встроенное средство определения исключительных ситуаций, возникающих во время работы терминала, и формирования отчетов о таких ситуациях. Сформированный отчет сохраняется в файле logs\crashlog.log, который отсылается на торговый сервер при следующем старте клиентского терминала. Отметим, что в отчете об исключительной ситуации не содержится никаких личных данных пользователя, а только системная информация, позволяющая локализовать место ошибки в клиентском терминале. Эта информация очень важна для разработчиков, так как позволяет исправлять критические ошибки, при этом разрабатываемое программное обеспечение становится еще более надежным.
25-30% всех крешлогов, поступающих от пользователей, возникают
в результате ошибок выполнения функций, импортируемых из пользовательских
dll. Эта информация никак не поможет разработчикам клиентского
терминала, зато может помочь разработчикам соответствующих
dll в поиске ошибок. Мы покажем, как можно использовать информацию
из отчета об ошибке. За основу взяты примеры ExpertSample.dll и ExportFunctions.
mq4, которые можно найти в директории experts\samples.

Ниже - полный текст представленного отчета об ошибке:
Time : 2006.07.12 14:43
Program : Client Terminal
Version : 4.00 (build: 195, 30 Jun 2006)
Owner : MetaQuotes Software Corp. (MetaTrader)
OS : Windows XP Professional 5.1 Service Pack 2 (Build 2600)
Processors : 2, type 586, level 15
Memory : 2095848/1727500 kb
Exception : C0000005
Address : 77C36FA3
Access Type : read
Access Addr : 00000000
Registers : EAX=000000FF CS=001b EIP=77C36FA3 EFLGS=00010202
: EBX=FFFFFFFF SS=0023 ESP=024DFABC EBP=024DFAC4
: ECX=0000003F DS=0023 ESI=00000000 FS=003b
: EDX=00000003 ES=0023 EDI=10003250 GS=0000
Stack Trace : 10001079 0045342E 0045D627 004506EC
: 7C80B50B 00000000 00000000 00000000
: 00000000 00000000 00000000 00000000
: 00000000 00000000 00000000 00000000
Modules :
1 : 00400000 00292000 C:\Program Files\MetaTrader 4\terminal.exe
2 : 10000000 00005000 C:\Program Files\MetaTrader 4\experts\libraries\ExpertSample.dll
... ..........................................................
35 : 7C9C0000 00819000 C:\WINDOWS\system32\SHELL32.dll
Call stack :
77C36F70:0033 [77C36FA3] memcpy [C:\WINDOWS\system32\msvcrt.dll]
10001051:0028 [10001079] GetStringValue [C:\Program Files\MetaTrader 4\experts\libraries\ExpertSample.dll]
00452DD0:065E [0045342E] ?CallDllFunction@CExpertInterior
00459AC0:3B67 [0045D627] ?ExecuteStaticAsm@CExpertInterior
004505E0:010C [004506EC] ?RunExpertInt@CExpertInterior
7C80B357:01B4 [7C80B50B] GetModuleFileNameA [C:\WINDOWS\system32\kernel32.dll]
Что же произошло?
- Exception : C0000005 означает исключительное состояние, возникшее вследствие доступа к "чужой" или нераспределенной области памяти (Access Violation).
- Access Type : read означает, что была произведена попытка чтения.
- Acess Addr : 00000000 означает, что "чужая" область памяти имеет нулевой
адрес.
Адрес 77C36FA3 совпадает с адресом вершины стека вызовов. То есть, ошибка возникла во время выполнения функции memcpy, которая копирует содержимое одной области памяти в другую область. Причём мы можем судить с достаточной уверенностью о том, что была произведена попытка скопировать данные из области данных, имеющей нулевой адрес.
Вторая строка стека вызовов информирует нас о том, какая функция вызвала функцию memcpy с неправильными параметрами. Это - функция GetStringValue из библиотеки ExpertSample.dll.
Посмотрим исходный код этой функции:
__declspec(dllexport) char* __stdcall GetStringValue(char *spar) { static char temp_string[256]; //---- printf("GetStringValue takes \"%s\"\n",spar); memcpy(temp_string,spar,sizeof(temp_string)-1); temp_string[sizeof(temp_string)-1]=0; //---- return(temp_string); }
Мы видим, что в данной функции всего один раз вызывается функция memcpy. Так как первый параметр указывает на существующую область памяти, занятую переменной temp_string, то можно сделать вывод о том, что ошибочным является второй параметр. Действительно, в представленном примере нет проверки переменной spar на 0. Строка if(spar==NULL) защитила бы нас от креша.
Что же делать в том случае, если бы в анализируемой функции было несколько вызовов функции memcpy? В настройках проекта нашей dll установим вывод полного листинга компиляции.

После сборки проекта на каждый исходный cpp-файл мы получим по файлу листинга с расширением .cod. Нас интересует ExpertSample. cod, но не весь, а только код, полученный для функции GetStringValue. Вот он:
?GetStringValue@@YGPADPAD@Z PROC NEAR ; GetStringValue
; 70 : {
00051 55 push ebp
00052 8b ec mov ebp, esp
; 71 : static char temp_string[256];
; 72 : //----
; 73 : printf("GetStringValue takes \"%s\"\n",spar);
00054 8b 45 08 mov eax, DWORD PTR _spar$[ebp]
00057 50 push eax
00058 68 00 00 00 00 push OFFSET FLAT:$SG19680
0005d ff 15 00 00 00
00 call DWORD PTR __imp__printf
00063 83 c4 08 add esp, 8
; 74 : memcpy(temp_string,spar,sizeof(temp_string)-1);
00066 68 ff 00 00 00 push 255 ; 000000ffH
0006b 8b 4d 08 mov ecx, DWORD PTR _spar$[ebp]
0006e 51 push ecx
0006f 68 00 00 00 00 push OFFSET FLAT:_?temp_string@?1??GetStringValue@@YGPADPAD@Z@4PADA
00074 e8 00 00 00 00 call _memcpy
00079 83 c4 0c add esp, 12 ; 0000000cH
; 75 : temp_string[sizeof(temp_string)-1]=0;
0007c c6 05 ff 00 00
00 00 mov BYTE PTR _?temp_string@?1??GetStringValue@@YGPADPAD@Z@4PADA+255, 0
; 76 : //----
; 77 : return(temp_string);
00083 b8 00 00 00 00 mov eax, OFFSET FLAT:_?temp_string@?1??GetStringValue@@YGPADPAD@Z@4PADA
; 78 : }
00088 5d pop ebp
00089 c2 04 00 ret 4
?GetStringValue@@YGPADPAD@Z ENDP ; GetStringValue
Цифры 10001051:0028 во второй строке стека вызовов указывают адрес
внутри функции GetStringValue, на который будет передано управление
после выполнения функции, указанной строкой выше в стеке вызовов.
В объектном коде функция GetStringValue начинается с адреса 00051 (заметим,
что адреса представлены в 16-ричной нотации). Прибавим к этому
значению 0028 и получим адрес 00079. По этому адресу находится инструкция
add esp,12, следующая сразу после инструкции вызова функции memcpy. Мы нашли
это место.Теперь попробуем расследовать ситуацию, когда ошибка возникает непосредственно внутри импортируемой функции. Модифицируем код:
__declspec(dllexport) char* __stdcall GetStringValue(char *spar) { static char temp_string[256]; //---- printf("GetStringValue takes \"%s\"\n",spar); for(int i=0; i<sizeof(temp_string)-1; i++) { temp_string[i]=spar[i]; if(spar[i]==0) break; } temp_string[sizeof(temp_string)-1]=0; //---- return(temp_string); }
Мы заменили вызов функции memcpy на собственный цикл побайтного копирования данных. Но проверку на 0 не вставили, чтобы получить исключительное состояние и отчет об ошибке. В новом отчете стек вызовов выглядит несколько иначе:
Call stack : 10001051:003A [1000108B] GetStringValue [C:\Program Files\MetaTrader 4\experts\libraries\ExpertSample.dll] 00452DD0:065E [0045342E] ?CallDllFunction@CExpertInterior 00459AC0:3B67 [0045D627] ?ExecuteStaticAsm@CExpertInterior 004505E0:010C [004506EC] ?RunExpertInt@CExpertInterior 7C80B357:01B4 [7C80B50B] GetModuleFileNameA [C:\WINDOWS\system32\kernel32.dll]Ошибка возникла по адресу 003A в функции GetStringValue. Посмотрим сгенерированный листинг.
?GetStringValue@@YGPADPAD@Z PROC NEAR ; GetStringValue
; 70 : {
00051 55 push ebp
00052 8b ec mov ebp, esp
00054 51 push ecx
; 71 : static char temp_string[256];
; 72 : //----
; 73 : printf("GetStringValue takes \"%s\"\n",spar);
00055 8b 45 08 mov eax, DWORD PTR _spar$[ebp]
00058 50 push eax
00059 68 00 00 00 00 push OFFSET FLAT:$SG19680
0005e ff 15 00 00 00
00 call DWORD PTR __imp__printf
00064 83 c4 08 add esp, 8
; 74 : for(int i=0; i<sizeof(temp_string)-1; i++)
00067 c7 45 fc 00 00
00 00 mov DWORD PTR _i$[ebp], 0
0006e eb 09 jmp SHORT $L19682
$L19683:
00070 8b 4d fc mov ecx, DWORD PTR _i$[ebp]
00073 83 c1 01 add ecx, 1
00076 89 4d fc mov DWORD PTR _i$[ebp], ecx
$L19682:
00079 81 7d fc ff 00
00 00 cmp DWORD PTR _i$[ebp], 255 ; 000000ffH
00080 73 22 jae SHORT $L19684
; 76 : temp_string[i]=spar[i];
00082 8b 55 08 mov edx, DWORD PTR _spar$[ebp]
00085 03 55 fc add edx, DWORD PTR _i$[ebp]
00088 8b 45 fc mov eax, DWORD PTR _i$[ebp]
0008b 8a 0a mov cl, BYTE PTR [edx]
0008d 88 88 00 00 00
00 mov BYTE PTR _?temp_string@?1??GetStringValue@@YGPADPAD@Z@4PADA[eax], cl
; 77 : if(spar[i]==0) break;
00093 8b 55 08 mov edx, DWORD PTR _spar$[ebp]
00096 03 55 fc add edx, DWORD PTR _i$[ebp]
00099 0f be 02 movsx eax, BYTE PTR [edx]
0009c 85 c0 test eax, eax
0009e 75 02 jne SHORT $L19685
000a0 eb 02 jmp SHORT $L19684
$L19685:
; 78 : }
000a2 eb cc jmp SHORT $L19683
$L19684:
; 79 : temp_string[sizeof(temp_string)-1]=0;
000a4 c6 05 ff 00 00
00 00 mov BYTE PTR _?temp_string@?1??GetStringValue@@YGPADPAD@Z@4PADA+255, 0
; 80 : //----
; 81 : return(temp_string);
000ab b8 00 00 00 00 mov eax, OFFSET FLAT:_?temp_string@?1??GetStringValue@@YGPADPAD@Z@4PADA
; 82 : }
000b0 8b e5 mov esp, ebp
000b2 5d pop ebp
000b3 c2 04 00 ret 4
?GetStringValue@@YGPADPAD@Z ENDP ; GetStringValue
Начальный адрес - тот же, 00051. Прибавим 003A и получим адрес 0008B.
По этому адресу находится инструкция mov cl, BYTE PTR [edx]. Посмотрим в отчете содержимое регистров:Registers : EAX=00000000 CS=001b EIP=1000108B EFLGS=00010246
: EBX=FFFFFFFF SS=0023 ESP=0259FAD4 EBP=0259FAD8
: ECX=77C318BF DS=0023 ESI=018ECD80 FS=003b
: EDX=00000000 ES=0023 EDI=000000E8 GS=0000
Ну конечно, в регистре EDX нули. Мы обратились к чужой памяти и
получили креш.В заключение две строчки о том, как мы передали в импортируемую функцию нулевой указатель.
string null_string; string sret=GetStringValue(null_string);В качестве параметра мы передали неинициализированную строку. Будьте осторожны с неинициализированными строками, всегда проверяйте принимаемые указатели на 0 и пусть у вас будет как можно меньше крешей.
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Многократный пересчет нулевого бара в некоторых индикаторах
MQL4 как инструмент трейдера, или Advanced Technical Analysis
Мой первый "грааль"
Генетические алгоритмы в MetaTrader 4. Сравнение с прямым перебором оптимизатора
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования