怎样用VisualStudio查看非托管代码

148 篇文章 1 订阅

(译者:这篇文章作者是一位美国的MVP,这是他的系列文章"Under the cover"的第一篇,文章的本意从最底层的角度来优化代码的性能,并作为阅读作者其他文章的技术基础,这种通过这样的做法虽然初看起来有些过分,但是对读者了解.Net许多底层运作是十分有益的)

我们从使用visual studio进行非托管代码调试的基础开始,以便大家可以更容易的学习今后的例子,并让这篇文章作为我以后文章的基础,虽然我也使用windbg,visual studio已经成为了一个功能强大的调试工具,对于简单的代码优化问题反而更容易使用

当我们需要校调对性能要求很高的代码时,查看IL通常不是最好的做法,因为JIT优化器会默默的优化我们的代码,使用reflector或者ildasm你能很快发现releasedebug模式下产生的IL代码几乎完全相同,那么是什么让release模式的代码运行起来如此迅速呢?这就是JIT优化的结果,通过查看managed代码(IL代码),我们没有办法看到这些优化,所以我们将通过native code(本地代码)来查找蛛丝马迹。

必须说明我不提倡大家经常这样做,我不赞成过早的进行优化,你必须使你的代码先工作起来,你必须清楚的知道哪些代码是不值得优化的,当你的代码完成后再来找那些需要提速的地方,当你发现有的地方10% 的代码却使用了70%的时间的时候,再回过头去优化那10%的代码.同时你总是应该把判断的依据建立在对速度的实际测量上,而非仅仅是阅读代码,最后,其实数据结构的选择比底层的优化重要的多

当然话又说回来,了解隐藏在.Net底层的秘密是非常有趣的,那就让我们开始设置visualstudio,并动手试验一个简单的例子

首先我们需要一些试验代码

        static void Main (string[] args) {

            for (int i = 0; i < 10; i++) {

                Console.WriteLine("Hello World!");

            }

        }

为了打开非托管代码调试,我们需要对visual studio进行设置.打开项目的属性并进入Debug Tab,选择该页上的“Enable unmanaged code debugging”复选框

(注意,这个选项只对当前使用的配置有效,因此我们应该为我们使用的所有配置设置这个选项.)在循环的开始处插入一个断点,并运行程序,你将会像往常一样击中一个断点。这时你的屏幕应该看起来如图二(译者:原文缺图)如果你没有stack窗口,可以通过menu -> windows -> call stack (或者  ctrl + d  c)将其呼出,打开call stack,我们就可以通过右击鼠标,选择go to disassembly进入下面的代码
    


  
  static void Main(string[] args) {
   
   
00000000  push        ebp
   
   
00000001  mov         ebp,esp
   
   
00000003  push        edi
   
   
00000004  push        esi
   
   
00000005  push        ebx
   
   
00000006  sub         esp,38h
   
   
00000009  xor         eax,eax
   
   
0000000b  mov         dword ptr [ebp-10h],eax
   
   
0000000e  xor         eax,eax
   
   
00000010  mov         dword ptr [ebp-1Ch],eax
   
   
00000013  mov         dword ptr [ebp-3Ch],ecx
   
   
00000016  cmp         dword ptr ds:[00912DC8h],0
   
   
0000001d  je          00000024
   
   

  
  
   
   0000001f
  
    call        792B228E
   
   
00000024  xor         esi,esi
   
   
00000026  xor         edi,edi
   
   
00000028  nop
   
   
for (int i = 0; i < 10; i++) {
   
   
00000029  xor         esi,esi
   
   
0000002b  nop
   
   

  
  
   
   0000002c
  
    jmp         0000003D
   
   
0000002e  nop
   
   
Console.WriteLine("Hello World!");
   
   

  
  
   
   0000002f
  
    mov         ecx,dword ptr ds:[022B303Ch]
   
   
00000035  call        785D9074
   
   

  
  
   
   0000003a
  
    nop
   
   
}
   
   
0000003b  nop
   
   
for (int i = 0; i < 10; i++) {
   
   

  
  
   
   0000003c
  
    inc         esi
   
   
0000003d  cmp         esi,0Ah
   
   
00000040  setl        al
   
   
00000043  movzx       eax,al
   
   
00000046  mov         edi,eax
   
   
00000048  test        edi,edi
   
   

  
  
   
   0000004a
  
    jne         0000002E
   
   
}
   
   

  
  
   
   0000004c
  
    nop
   
   
0000004d  lea         esp,[ebp-0Ch]
   
   
00000050  pop         ebx
   
   
00000051  pop         esi
   
   
00000052  pop         edi
   
   
00000053  pop
   
   

  
  
   
    
  
  

 我们正在查看的就是JIT为我们的代码产生的native code(本地代码),我们可以看到简单的循环在native code层次上怎么运行的,如果你从来没有研究过native code,这些本来很普通的代码可能看起来相当的奇怪,让我们来自己看看这里发生了什么

00000029  xor         esi,esi

0000002b  nop             

0000002c   jmp         0000003D

上面代码初始化我们在ESI中的计数器,ESI是一个索引寄存器,可以用来索引数组,你可以看到这里用了一个很古老的"把戏"来把计数器清0,代码没有使用把0值放入寄存器,而是让寄存器自己异或(xor)自己来达到清0的目的,接下来的一行Nop,意思是"没有操作",而他们的作用就和他们的名字一样,什么也不做,代码接下来立即跳转到3D.有时候像这样的跳转使得我们的代码不是自上而下的运行(就象许多高级语言比如c,vb,c#里面一样),如果跟着这个跳转进入这个循环的另外一个部分,就可以继续分析我们的代码

0000003c   inc         esi 

0000003c 后面第一个指令把ESI中的计数器加一(通过register窗口或者组合键ctrl+D R  你可以看到它的值),在第一次循环时代码会跳过这行,因为上面的跳转指令直接指向了0000003D


0000003d  cmp         esi,0Ah

00000040  setl        al  

00000043  movzx       eax,al

00000046  mov         edi,eax

00000048  test        edi,edi

0000004a   jne         0000002E

0000003D开始到 4a ,代表于循环停止值的实际比较和跳转如果我们没有达到这个值(i<10),最后一行会跳转到2e(译者注:原著这里为 4a ,是个笔误)继续这个循环,也就是循环体开始的地方
0000002f   mov         ecx,dword ptr ds:[022B303Ch]

00000035  call        785D9074

上面的第一条行将会把字符串从从内存装在到ECX 寄存器 (这是一个通用寄存器), 一般ECX总是用作把第一个参数传给方法,在实例的方法中,ECX将总是包含this,紧接着是包含第二个参数的EDX,然后是一系列的push,用于把其他参数入栈

下一条语句执行实际的调用。我们待会再来探讨怎么去查找所调用的方法,但是现在我们可以从VisualStudio给出的源代码看到,这毫无疑问就是 Console.WriteLine ,代码接着执行索引的自增,并返回来继续执行loop循环内部的代码

 

然而,我们的微不足道的例子中已经产生产生了明显的浪费。下面是一个例子
00000009  xor         eax,eax

0000000b  mov         dword ptr [ebp-10h],eax

0000000e  xor         eax,eax

00000010  mov         dword ptr [ebp-1Ch],eax

我们在一行里两次对EAX0,这时因为我们正运行在debug模式下,调试模式下是不进行优化的,换句话说,这段代码只是被JIT执行,但是却没有允许JIT作任何智能优化

下面让我们来看看经过优化的代码:

这里有一些关于查看优化代码的问题
1)
是调试器默认关闭了JIT的优化(我自己就曾经在大半夜花了很长时间才意识到自己一直在看没有被优化的代码)
 2)
是必须处理"Just My Code"选项对优化代码的影响

我最初在Vance Morrison的帖子上看到了解决这个问题的办法(谢谢 Vance,我已经被整个问题困扰了很长一段时间,并最终使用直接查看没有源码的原始assemble的方式).

要搞定这个问题,清跟着以下的步骤作

1) 打开 Tools -> Options -> Debugging -> General 

2)确保 ‘Suppress JIT optimization on module load’没有被选中

3)也确保‘Enable Just My Code’没有被选中

Vance 也建议我们进入advanced build设置release dllpdb only,这时我们可以用前面同样的方式运行这段代码

JIT 看我们的代码另外一种方式是使用release模式,使用Start the executable without the debugger,再附加visualstudio到进程进行调试.

使用任一方法我们都能让 JIT 将代码优化了。得到优化的代码如下


  
           for (int i = 0; i < 10; i++) {
   
   
00000000  push        esi
   
   
00000001  xor         esi,esi
   
   
Console.WriteLine("Hello World!");
   
   
00000003  cmp         dword ptr ds:[02271084h],0
   
   

  
  
   
   0000000a
  
    jne         00000016
   
   

  
  
   
   0000000c
  
    mov         ecx,1
   
   
00000011  call        786FC654
   
   
00000016  mov         ecx,dword ptr ds:[02271084h]
   
   

  
  
   
   0000001c
  
    mov         edx,dword ptr ds:[0227307Ch]
   
   
00000022  mov         eax,dword ptr [ecx]
   
   
00000024  call        dword ptr [eax+000000D8h]
   
   
for (int i = 0; i < 10; i++) {
   
   

  
  
   
   0000002a
  
    add         esi,1
   
   
0000002d  cmp         esi,0Ah
   
   
00000030  jl          00000003
   
   
00000032  pop         esi
   
   
}
   
   
}
   
   
00000033  ret
   
   

  
  
   
    
  
  

,这次的代码比第一次少多了,JIT 优化确实工作的很好,这就是为什么查看实际的反编译代码而非IL是这样重要,因为JIT经常会通过识别IL中的模式来进行优化,机敏的读者可能注意在我们的循环的内部事实上产生了更多的密码。初看起来这是非常可怕的,但其实这说明优化器已经帮助我们inlineConsole.WriteLine方法,实际是节省了很多代码在接下来的帖子中我会谈到inline,但是大家先明白这是一个很重要的优化

我们这时已经准备好了怎样在调试器中去欣赏优化的和没有优化的代码,我想这是一个好的开始,下面的几个帖子我会为更深入的了解JIT的一般优化过程而打好基础,我们也可以接触一些工具,看看他们会怎样帮助我们得到更好的代码

 希望能在那里见你。

原文地址

http://codebetter.com/blogs/gregyoung/archive/2006/06/09/146298.aspx

原文的一些资源:

http://en.wikipedia.org/wiki/X86 

http://www.codeguru.com/csharp/.net/net_general/il/article.php/c4635/ IL tutorial

http://burks.brighton.ac.uk/burks/language/asm/asmtut/asm1.htm ASM tutorial  

 

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值