Windows用户态程序高效排错 -- 汇编,CPU执行指令的最小单元

读懂机器的语言:汇编,CPU执行指令的最小单元

2.2.1  需要用汇编来排错的常见情况

汇编是CPU执行指令的最小单元。下面一些情况下,汇编级别的分析通常是必要的:

1.         阅读代码看不出问题,但是跑出来的结果就是不对,怀疑编译器甚至CPU有毛病。

2.         没有源代码可以阅读。比如,调用某一个API的时候出问题,没有Windows的源代码,那就看汇编。

3.         当程序崩溃,访问违例的时候,调试器里看到的直接信息就是汇编。

调试中涉及的汇编知识分为两部分:

1.         寄存器的运算,对内存地址的寻址和读写。这部分是跟CPU本身相关的。

2.         函数调用时候堆栈的变化,局部变量全局变量的定位,虚函数的调用。这部分是跟编译器相关的。

汇编的知识可以在大学计算机教程里面找到。建议先熟悉简单的8086/80286的汇编,再结合IA32芯片结构和32位Windows汇编知识深入。建议的资源:

AoGo汇编小站

http://www.aogosoft.com/

Intel Architecture Manual volume 1,2,3

http://www.intel.com/design/pentium4/manuals/index_new.htm

案例分析:用汇编读懂VC编译器的优化

问题描述

客户在开发一个性能敏感的程序,想知道VC编译器对下面这段代码的优化做得怎么样:

    int hgt=4;

    int wid=7;

    for (i=0; i<hgt; i++)

      for (j=0; j<wid; j++)

        A[i*wid+j] = exp(-(i*i+j*j));

最直接的方法就是查看编译器生成的汇编代码分析。有兴趣的话先自己调试一下,看看跟我的分析是否一样。

我的分析

我分析的平台是,VC6,release mode下编译:(因为当时做这个case的时候,客户用的VC6。现在VC6已经退出历史舞台,微软不再提供支持)。

int hgt=4;

int wid=7;

24:       for (i=0; i<hgt; i++)

0040107A   xor         ebp,ebp

0040107C   lea         edi,[esp+10h]

25:        for (j=0; j<wid; j++)

26:            A[i*wid+j] = exp(-(i*i+j*j));

00401080   mov         ebx,ebp

00401082   xor         esi,esi

// The result of i*i is saved in ebx

00401084   imul        ebx,ebp

00401087   mov         eax,esi

// Only one imul occurs in every inner loop (j*j)

00401089   imul        eax,esi    

// Use the saved i*i in ebx directly. !!Optimized!!

0040108C   add         eax,ebx 

0040108E   neg         eax         

00401090   push        eax

00401091   call        @ILT+0(exp) (00401005)

00401096   add         esp,4           

// Save the result back to A[]. The addr of current offset in A[] is saved in edi

00401099   mov         dword ptr [edi],eax 

0040109B   inc         esi

// Simply add edi by 4. Does not calculate with i*wid. Imul is never used. !!Optimized!!

0040109C   add         edi,4

0040109F   cmp         esi,7

004010A2   jl          main+17h (00401087)

004010A4   inc         ebp

004010A5   cmp         ebp,4

004010A8   jl          main+10h (00401080)

这段代码涉及到的优化有:

1.         i*i在每次内循环中是不变化的,所以只需要在外循环里面重新计算。编译器把外循环计算好的i*i放到ebx寄存器中,内循环直接使用。

2.         对A[i*wid+j]寻址的时候,在内循环里面,变化的只有j,而且每次j都是增加1,由于A是整型数组,所以每次寻址的变化就是增加1*sizeof(int),就是4。编译器把i*wid+j的结果放到了EDI中,在内循环中每次add edi,4来实现了这个优化。

3.         对于中间变量,编译器都是保存在寄存器中,并没有读写内存。

如果这段汇编让你手动来写,你能做得比编译器更好一点吗?

案例分析:VC2003 编译器的bug、debug模式正常,release模式会崩溃

不要迷信编译器没有bug。如果你在VS2003中测试下面的代码,会发现在release mode下面,程序会崩溃或者异常,但是在debug mode下工作正常。

例子程序

// The following code crashes/abnormal in release build when "whole program optimizations /GL"

// is set. The bug is fixed in VS2005

#include <string>

#pragma warning( push )

#pragma warning( disable : 4702 )     // unreachable code in <vector>

#include <vector>

#pragma warning( pop )

#include <algorithm>

#include <iostream>

//vcsig

// T = float, U = std::cstring

template <typename T, typename U>  T func_template( const U & u )

{

  std::cout<<u<<std::endl;

  const char* str=u.c_str();

  printf(str);

  return static_cast<T>(0);

}

void crash_in_release()

{

  std::vector<std::string>  vStr;

  vStr.push_back("1.0");

  vStr.push_back("0.0");

  vStr.push_back("4.4");

  std::vector<float>  vDest( vStr.size(), 0.0 );

  std::vector<std::string>::iterator _First=vStr.begin();

  std::vector<std::string>::iterator _Last=vStr.end();

  std::vector<float>::iterator _Dest=vDest.begin();

  std::transform( _First,_Last,_Dest, func_template<float,std::string> ); 

   _First=vStr.begin();

   _Last=vStr.end();

   _Dest=vDest.begin();

  for (; _First != _Last; ++_First, ++_Dest)

    *_Dest =  func_template<float,std::string>(*_First); 

}

int main(int, char*)

{

  getchar();

  crash_in_release();

  return 0;

}

编译设定如下:

1.         取消precompiled header。

2.         编译选项是: /O2 /GL /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /FD /EHsc /ML /GS /Fo"Release/" /Fd"Release/vc70.pdb" /W4 /nologo /c /Wp64 /Zi /TP。

跟踪汇编指令来分析

拿到这个问题后,首先在本地重现。根据下面一些测试和分析,认为很有可能是编译器的bug:

1.         程序中除了cout和printf外,没有牵涉到系统相关的API,所有的操作都是寄存器和内存上的操作。所以不会是环境或者系统因素导致的,可能性是代码错误(比如边界问题)或者编译器有问题。

2.         检查代码后没有发现异常。同时,如果调整一下std::transform的位置,在for loop后面调用的话,问题就不会发生。

3.         问题发生的情况跟编译模式相关。

代码中的std::transform和for loop的作用都是对整个vector调用func_template作转换。可以比较transform和for loop的执行情况进行比较分析,看看func_template的执行过程有什么区别。在VS2003里面利用main函数设定断点,停下来后用ctrl+alt+D进入汇编模式单步跟踪。下面的分析证明了这是编译器的bug:

在VisualStudio附带的STL源代码中,发现 std::transform的实现中用这样的代码来调用传入的转换函数:

*_Dest = _Func(*_First);

编译器对于该代码的处理是:

EAX = 0012FEA8 EBX = 0037138C ECX = 003712BC EDX = 00371338 ESI = 00371338 EDI = 003712B0 EIP = 00402228 ESP = 0012FE70 EBP = 0012FEA8 EFL = 00000297

 388: *_Dest = _Func(*_First);

00402228 push esi

00402229 call dword ptr [esp+28h]

0040222D fstp dword ptr [edi]

ESI寄存器中保存的是需要传入_Func的参数*_First。可以看到,std::transform把这个参数通过push指令传入stack给_Func调用。

对于for loop中的*_Dest =  func_templatefloatstd::string>(*_First);编译器是这样处理的:

EAX = 003712B0 EBX = 00371338 ECX = 003712BC EDX = 00000000 ESI = 00371338 EDI = 0037138C EIP = 00401242 ESP = 0012FE98 EBP = 003712B0 EFL = 00000297

37: *_Dest = func_template<float,std::string>(*_First);

00401240 mov ebx,esi

00401242 call func_template <float,std::basic_string<char,std::char_traits<char>,std::allocator<char> > > (4021A0h)

00401247 fstp dword ptr [ebp]

可以看到,使用for loop的时候,参数通过mov指令保存到ebx寄存器中传入func_template调用。

最后,看一下func_template函数是如何来获取传入的参数的。

004021A0 push esi

004021A1 push edi

16:  std::cout<<u<<std::endl;

004021A2 push ebx

004021A3 push offset std::cout (414170h)

004021A8 call std::operator<<<char,std::char_traits<char>,std::allocator<char> > (402280h)

这里直接把ebx推入stack,然后调用std::cout,并没有读取stack中的资料,说明func_template(callee)认为参数应该是从寄存器中传入的。然而transform函数(caller)却把参数通过stack传递。于是使用transform调用func_template的时候,func_template无法拿到正确的参数,因而导致崩溃。通过for loop调用的时候,由于参数通过寄存器传递,所以func_template就可以正常工作。

结论是编译器对参数的传入、读取、处理不统一,导致了这个问题。

为何问题在debug模式下不发生,或者调换函数次序后也不发生,留作你的练习吧 :-P

案例分析:臭名昭著的DLL Hell如何导致ASP.NET出现Server Unavailable

客户的ASP.NET程序,访问任何页面都报告Server Unavailable。观察发现,ASP.NET的宿主w3wp.exe进程每次刚启动就崩溃。通过调试器观察,崩溃的原因是访问了一个空指针。但是从call stack看,这里所有的代码都是w3wp.exe和.net framework的代码,还没有开始执行客户的页面,所以跟客户的代码无关。通过代码检查,发现该空指针是作为函数参数从调用者(caller)传到被调用者(callee)的,当callee使用这个指针的时候问题发生。接下来应该检查caller为什么没有把正确的指针传入callee。

奇怪的时候,caller中这个指针已经正常初始化了,是一个合法的指针,调用call语句执行callee的以前,这个指针已经被正确地push到stack上了。为什么caller从stack上拿的时候,却拿到一个空指针呢?再次单步跟踪,发现问题在于caller把参数放到了callee的[ebp+8],但是callee在使用这个参数的时候,却访问[ebp+c]。是不是跟前一个案例很像?但是这次的凶手不是编译器,而是文件版本。Caller和callee的代码位于两个不同的DLL,其中caller是.NET Framework 1.1带的,而callee是.NET Framework 1.1 SP1带的。在.NET Framework 1.1中,callee函数接受4个参数,但是新版本SP1对callee这个函数作了修改,增加了1个参数。由于caller还使用SP1以前的版本,所以caller还是按照4个参数在传递,而callee按照5个参数在访问,所以拿到了错误的参数,典型的DLL Hell问题。在重新安装.NET Framework 1.1 SP1让两个DLL保持版本一致,重新启动后,问题解决。

导致DLL Hell的原因有很多。根据经验猜测版本不一致的原因可能是:

1.         安装了.NET Framework 1.1 SP1后没有重新启动,导致某些正在使用的DLL必须要等到重新启动后才能够完成更新。

2.         由于使用了Application Center做Load Balance,集群中的服务器没有做好正确的设置,导致系统自动把老版本的文件还原回去了:
PRB: Application Center Cluster Members Are Automatically Synchronized After Rebooting
http://support.microsoft.com/kb/282278/en-us

2.2.2  题外话和相关讨论

Release比 Debug快吗

分别在debug/release模式下运行下面的代码比较效率,会发现debug比release更快。你能找到原因吗?

    long nSize = 200;

    char* pSource = (char *)malloc(nSize+1);

    char* pDest = (char *)malloc(nSize+1);

    memset(pSource, 'a', nSize);

    pSource[nSize] = '/0';

    DWORD dwStart = GetTickCount();

    for(int i=0; i<5000000; i++)

    {

      strcpy(pDest, pSource);

    }

    DWORD dwEnd = GetTickCount();

    printf("%d", dwEnd-dwStart);

如果让你自己实现一个strcpy函数,应该考虑什么?你能做到比系统的strcpy函数快吗?

一些讨论可以参考:

http://eparg.spaces.live.com/blog/cns!59BFC22C0E7E1A76!1498.entry

从效率上说,起决定性作用的至少有下面两点:

1.         在32位芯片上,应该尽量每次mov一个DWORD,而不是4个byte来提高效率。注意到mov DWORD的时候要4字节对齐。

2.         这里对strcpy的调用高达5000000次。由于call指令的开销,使用内联 (inline) 版本的strcpy函数可以极大提高效率。

所以,汇编、CPU习性、操作系统和编译器,是分析细节的最直接武器。

上面例子中的strcpy是否内联,取决于编译设定。由于strcpy是CRT(CRuntime C运行库)函数,函数的实现位于MSVCRT.DLL或者MSVCRTD.DLL。如果编译设定使得函数调用要跨越DLL,这个函数是无法内联的。

关于性能的另外一些讨论:

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值