股票量化软件:怎样使用崩溃记录来调试您的动态链接库(DLL)
现在让我们看一下调用堆栈(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);