请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化平台 帖子:3364679 新帖:0

怎样使用崩溃记录来调试您的动态链接库(DLL)

耶伦发表于:4 月 17 日 20:09回复(1)

MetaTrader 4 客户终端有一个集成的方法来侦测终端运行时的错误状况, 并且会在出现错误报告的时候生成崩溃记录. 生成的报告保存在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 的意思是进程外内存地址为零.
现在让我们看一下调用堆栈(call stack).

地址 77C36FA3 与堆栈顶部是一样的. 这说明错误发生在执行memcpy函数时, 正在把内容从一块内存区域复制到另一区域. 在那里, 我们可以很确信地判定是否尝试从地址为0的内存区域复制数据.

调用堆栈的第二行通知我们是哪一个函数使用了错误的参数调用了memcpy函数. 这是来自名为ExpertSample.dll的库函数GetStringValue.

让我们看一下这个函数的源代码:

__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占用的变量, 我们可以确定是第二个参数出的错. 确实, 在提供的例子中没有对变量是否为0做检查. 加一行代码 if(spar==NULL) 将会保护我们不会崩溃.

还有, 如果在函数中多次调用了memcpy函数, 我们应该怎么做呢?在我们的项目设置中, 让我们设置输出最详细的编译信息.



在重新构建项目后, 我们会有一系列以.cod为扩展名的文件, 它们分别对应每一个.cpp源文件. 我们现在感兴趣的是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函数调用替换成我们自己的按字节复制的循环. 但是我们没有使用对零的检查, 这是为了创造一个错误条件和错误报告. 在新的报告中, 调用堆栈看起来有所不同:
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]
错误发生于 GetStringValue 函数的 003A 地址. 让我们看一下生成的列表.
?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 寄存器内容是0. 我们访问了进程外的内存并导致程序崩溃.

在最后, 我们有两行如何对输入参数传入0参数值的代码.

   string null_string;
   string sret=GetStringValue(null_string);
我们传一个未初始化的字符串作为参数. 对没有初始化的字符串要小心, 要永远检查参数是否为NULL, 这会使你的程序尽可能少崩溃.

全部回复

0/140

量化课程

    移动端课程