方法调用: 第一部分 (普通调用)

译者:我们都知道.NET托管代码如C#、VB.NET写成的代码,都是先被编译成中间语言(IL,Intermediate Language,在运行时,再由即时编译器(JIT,Just-In-Time)编译成本机代码。那么这个神秘的过程是怎么进行的呢,JIT会在什么时机编译你的代码呢,下面这篇翻译文章将给大家介绍这个过程,大家不要被开始复杂的工具和命令吓到,只要你坚持读下去,一定会有所收获

在接下来的几篇".net 揭密"系列文章中,我将会介绍大多数人认为理所当然的东西——代码调用,到底代码调用是怎么工作的(注意在这篇文章中我们要讨论的是非常基础的"调用"过程,虽然看起来十分浅显,实际上这确实十分重要的,因为他可以极大的影响代码的效率,并且让你深刻的认识ClR的工作方式

首先让我们建立一个测试代码

class Foo { publicvoid Test() { for (int i =0; i <10; i++) { Console.WriteLine("Test"); } } } class Program { staticvoid Main(string[] args) { Foo f =new Foo(); f.Test(); f.Test(); f.Test(); } }

代码清单 1: 用于讨论的简单代码

为了运行这一小段代码JIT必须解决许多问题,让我们进入他的工作流程,更好的了解到底发生了什么.

在程序的控制权没有交给我们的代码之前,Main方法首先被编译,控制权就被交给了Main方法,代码的反编译源如下

  1. static void Main(string[] args) {
  2. Foo f = new Foo();
  3. 00000000 push esi
  4. 00000001 mov ecx,913080h
  5. 00000006 call FFB21FAC
  6. 0000000b mov esi,eax
  7. f.Test();
  8. 0000000d mov ecx,esi
  9. 0000000f cmp dword ptr [ecx],ecx
  10. 00000011 call dword ptr ds:[009130B8h]
  11. f.Test();
  12. 00000017 mov ecx,esi
  13. 00000019 cmp dword ptr [ecx],ecx
  14. 0000001b call dword ptr ds:[009130B8h]
  15. f.Test();
  16. 00000021 mov ecx,esi
  17. 00000023 cmp dword ptr [ecx],ecx
  18. 00000025 call dword ptr ds:[009130B8h]
  19. 0000002b pop esi
  20. }
  21. 0000002c ret

代码清单 2: main方法的反编译源

我们可以看到生成的代码通过间接寻址(译者注:指的是上面00000011 call dword ptr ds:[009130B8h],我在这里给不熟悉这个术语的人解释一下间接寻址:通过内存地址中的地址来找到实际地址的寻址方法叫做间接寻址.听起来很复杂,其实你可以这样理解,我叫你找一个人,直接给你他的地址,然后你通过这个地址找到这个人,叫直接寻址,那么我给你一个地址,告诉你这个地址住的人知道你要找的人在哪里,这就是间接寻址,在程序里,内存地址就相当于我给你的地址,内存地址里存储的值才是你最终需要的地址)来发起调用,这样做当然是有原因的,在我们解释这个问题之前,先打开值得我们信赖的挚友——调试器,不过你可能需要先阅读这篇文章,怎样用Visual Studio查看非托管代码,并初步了解SOS(Son Of Strike)(译者注:SOS是一个VS自带的调试非托管代码的辅助模块,如果不了解,并不妨碍你理解本文的主要原理)

我将用粗体标识所有的调试器命令,并以普通字体标识其输出

在代码的第一行打上一个断点并开始调试,所有的SOS命令都需要在VisualStudio的"立即窗口"(Immediate window)中输入(译者:通过在命令窗口(command window)中输入immed并回车,就可以进入立即窗口)

.load SOS(译者注:此命令加载SOS模块)

extension C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\SOS.dll loaded

This first command loads the SOS debugging tool making it available for our use.

!Name2EE ConsoleApplication29.exe ConsoleApplication29.Foo.Test

PDB symbol for mscorwks.dll not loaded

Module: 00912c14 (ConsoleApplication29.exe)

Token: 0x06000001

MethodDesc: 00913070

Name: ConsoleApplication29.Foo.Test()

Not JITTED yet. Use !bpmd -md 00913070 to break on run.

这个命令给出了有关我们的方法的丰富信息,上面有许多有用的信息,但是其中最重要的莫过于方法描述(MethodDesc)的地址,我们可以用这个地址找到更多信息

!DumpMD 00913070

Method Name: ConsoleApplication29.Foo.Test()

Class: 009113b8

MethodTable: 00913080

mdToken: 06000001

Module: 00912c14

IsJitted: no

m_CodeOrIL: ffffffffffffffff

在这里我们可以获得"方法列表"(method table)的地址,我们可以通过这个地址得到方法列表

!DumpMT -md 00913080

EEClass: 009113b8

Module: 00912c14

Name: ConsoleApplication29.Foo

mdToken: 02000002 (C:\Documents and Settings\Greg\My Documents\Visual Studio 2005\Projects\ConsoleApplication29\ConsoleApplication29\bin\Release\ConsoleApplication29.exe)

BaseSize: 0xc

ComponentSize: 0x0

Number of IFaces in IFaceMap: 0

Slots in VTable: 6

--------------------------------------

MethodDesc Table

Entry MethodDesc JIT Name

79354bec 7913bd48 PreJIT System.Object.ToString()

793539c0 7913bd50 PreJIT System.Object.Equals(System.Object)

793539b0 7913bd68 PreJIT System.Object.GetHashCode()

7934a4c0 7913bd70 PreJIT System.Object.Finalize()

009130c8 00913070 NONE ConsoleApplication29.Foo.Test()

009130d4 00913078 NONE ConsoleApplication29.Foo..ctor()

很多人已经注意到我们的方法还没有被JIT编译,这就是为什么我们通过间接引用来调用方法的原因之一 :在main方法编译他之前,程序并不知道需要到哪里去调用.这就引出了一个有趣的问题

JIT怎么知道何时编译一个方法?

本质上来说,JIT是延迟加载我们的模块,通过一种被叫做"thunk"(块)的技术,JIT能捕获到我们对方法的第一次调用,所谓thunk是一小段非托管代码,当我们第一次加载某个类型的时候,由CLR通过emit生成thunk.thrunk简单的包含对JIT的调用

图1 : JIT的编译过程

图一中的过程看起来过于简单,但是在实际运用中效率太低.实际系统和途中呈现的流程的差别主要体现在对决策判断上,由于图片的误导,我们似乎觉得trunk中有分支出现;实际上是没有分支的,取而代之的是JIT使用一种叫做back patching的技术

术语"back patching"可能挺眼熟的,因为在GC垃圾处理中也用到了他,这个术语主要的意思是通过更新一个指针来反映信息的变化,当一个方法第一次被调用的时候,调用方从MethodTable中读取指向一个代码块(thunk)的地址,然后调用这个thunk,thunk,接着调用JIT.关键的地方在于,当JIT完成了编译后,将改变MethodTable,使其直接指向已经被JIT编译过的代码,图2图3反映了这个过程完成前后的对比.注意在图中,方法被直接调用,而实际上是通过读取一个经过变化的内存地址来完成的.(译者注:也就是说无论代码是否被JIT编译,对方法的调用都是通过调用MethodTable中方法地址来实现的,若代码尚未编译,则这个地址指向一个代码块(thunk,),他会帮助你编译代码,然后修改MethodTable中的指针,指向实际代码)

图2:JIT编译前


图3:JIT编译后

现在我们对这一切已经有了一个大概的认识,让我们通过查看调试器来印证我们的知识.你可以使用memory window(debug->windows->memory)输入方法调用的内存地址(i.e. 列表中的009130B8h) ,或者使用registers window (debug->windows->registers (请确认有效地址选项已经打开))来查看所需的数据 .

给你的朋友展示这些玩意,无庸置疑的表现你是办公室里的顶级高手

用调试器单步进入line 0011(第一次调用),我们可以看到在内存地址009130b8中(间接寻址的地址)包含着009130c8,这个地址也许看起来会挺熟悉,这就是指向Thunk的指针,通过!u反编译这个地址,我们甚至可以查看这段非托管代码.

!u 009130C8

Unmanaged code

009130C8 B870309100 mov eax,913070h

009130CD 89ED mov ebp,ebp

009130CF E938EEA2FF jmp 00341F0C

009130D4 B878309100 mov eax,913078h

009130D9 89ED mov ebp,ebp

009130DB E92CEEA2FF jmp 00341F0C

009130E0 0000 add byte ptr [eax],al

009130E2 0000 add byte ptr [eax],al

009130E4 0000 add byte ptr [eax],al

009130E6 0000 add byte ptr [eax],al

这段代码可能看起来有些令人迷惑,因为这里实际上有两段连接在一起的thunk,接着什么都不做,913070是否也有些面熟呢?他是我们的Method desc(代码描述)的地址,它被放入EAX,作为JIT编译器的变量传送给JIT(这样JIT才知道需要编译什么代码).我们在方法内上断点,并在断点停下,看看什么发生了变化

!DumpMT -md 00913080

EEClass: 009113b8

Module: 00912c14

Name: ConsoleApplication29.Foo

mdToken: 02000002 (C:\Documents and Settings\Greg\My Documents\Visual Studio 2005\Projects\ConsoleApplication29\ConsoleApplication29\bin\Release\ConsoleApplication29.exe)

BaseSize: 0xc

ComponentSize: 0x0

Number of IFaces in IFaceMap: 0

Slots in VTable: 6

--------------------------------------

MethodDesc Table

Entry MethodDesc JIT Name

79354bec 7913bd48 PreJIT System.Object.ToString()

793539c0 7913bd50 PreJIT System.Object.Equals(System.Object)

793539b0 7913bd68 PreJIT System.Object.GetHashCode()

7934a4c0 7913bd70 PreJIT System.Object.Finalize()

00de00b0 00913070 JIT ConsoleApplication29.Foo.Test()

009130d4 00913078 NONE ConsoleApplication29.Foo..ctor()

可以看到方法现在已经被JIT编译了,并且Method table也被更新,以反映这个变化(00de00b0).这就是JIT编译后的非托管代码入口,通过查看当前执行堆栈能够证实我们的想法

!CLRStack

OS Thread Id: 0x8e8 (2280)

ESP EIP

0012f47c 00de00b0 ConsoleApplication29.Foo.Test()

0012f480 00de0087 ConsoleApplication29.Program.Main(System.String[])

0012f69c 79e88f63 [GCFrame: 0012f69c]

从函数里跳出后,测试代码会又一次执行同样的调用,不过这次,他不会在经过thunk,而是直接进入已经产生好的非托管代码

以上就是代码调用的一般机制,除非代码发生"颠簸"(pitched),被反复调入调出,欲知详情,请听下回分解

源文地址:
http://codebetter.com/blogs/gregyoung/archive/2006/07/20/147512.aspx

怎样用Visual Studio调试非托管代码
http://www.cnblogs.com/yizhu2000/archive/2007/08/08/848160.html

.Net 揭密--JIT怎样运行你的代码的更多相关文章

  1. 添加可运行的js代码

    如何在博客园的文章/随笔中添加可运行的js代码 在博客园浏览大牛们写的文章时,经常会看到在文章中混有一些可运行示例,例如司徒正美的博客中: 带有可运行示例 可以点击“运行代码” 经过一番小小的探索,掌 ...

  2. 单点登录SSO:可一键运行的完整代码

    单点登录方案不同于一个普通站点,它的部署比较繁琐:涉及到好几个站点,要改host.安装证书.配置HTTPS. 看到的不少这方面示例都是基于HTTP的,不认同这种简化: 1. 它体现不出混合HTTP/H ...

  3. 在IDEA中停止和关闭SonarLint自动检查,手动运行SonarLint检查代码

    关闭SonarLint自动检查代码 有时敲一行代码SonarLint插件就会自动检查,让人感觉很不舒服,还会使电脑卡顿: 依次点击:File -> Settings 或直接Ctrl+Alt+S ...

  4. 浅析 Node.js 的 vm 模块以及运行不信任代码

    在一些系统中,我们希望给用户提供插入自定义逻辑的能力,除了 RPC 和 REST 之外,运行客户提供的代码也是比较常用的方法,好处是可以极大地减少在网络上的耗时.JavaScript 是一种非常流行而 ...

  5. 使用Maven编译运行Storm入门代码(Storm starter)(转)

    Storm 官方提供了入门代码(Storm starter),即 Storm安装教程 中所运行的实例(storm-starter-topologies-0.9.6.jar),该入门代码位于 /usr/ ...

  6. 【转】在本地运行leetcode核心代码

    https://zhuanlan.zhihu.com/p/342993772 在调用solution之前,要加一句 Solution solution; solution.函数名(输入变量); 以下是 ...

  7. 如何在文章/随笔中添加可运行的js代码

    <script type="text/javascript"> alert("你知道我是怎么弹出的吗?"); </script> 看大神 ...

  8. 【原】iOS动态性(三) Method Swizzling以及AOP编程:在运行时进行代码注入

    概述 今天我们主要讨论iOS runtime中的一种黑色技术,称为Method Swizzling.字面上理解Method Swizzling可能比较晦涩难懂,毕竟不是中文,不过你可以理解为“移花接木 ...

  9. php 运行客户提交代码(攻击)和运行图片中的代码

    1.$a=@strrev(ecalper_gerp);$b=@strrev(edoced_46esab);@$a($b(L3h4L2Ug),$_POST[POST],bxxb); 2.<?php ...

随机推荐

  1. LA 3485 (积分 辛普森自适应法) Bridge

    桥的间隔数为n = ceil(B/D),每段绳子的长度为L / n,相邻两塔之间的距离为 B / n 主要问题还是在于已知抛物线的开口宽度w 和 抛物线的高度h 求抛物线的长度 弧长积分公式为: 设抛 ...

  2. JAVA安卓和C# 3DES加密解密的兼容性问题(2013年8月修改版)

    近 一个项目.net 要调用JAVA的WEB SERVICE,数据采用3DES加密,涉及到两种语言3DES一致性的问题, 下面分享一下, 这里的KEY采用Base64编码,便用分发,因为Java的By ...

  3. Android设置布局背景为白色的三种方法

    一.在xml文件里可以直接设置: android:background="#ffffff" 其他颜色可以看这里;http://blog.csdn.net/yanzi1225627/ ...

  4. The resource could not be loaded because the App Transport

    Xcode7 beta 网络请求报错:The resource could not be loaded because the App Transport Xcode7 beta 网络请求报错:The ...

  5. this class is not key value coding-compliant for the key ##

    setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key ## 出现以上错误时很恶心,并 ...

  6. eclipse集承jboss服务器

    eclipse Kepler + Jboss7.1 参考引用文档: http://www.tekdigest.com/how-to-install-jboss-tools-in-eclipse.htm ...

  7. 转载:fstream和ifstream详细用法

    文件 I/O 在C++中比烤蛋糕简单多了.在这篇文章里,我会详细解释ASCII和二进制文件的输入输出的每个细节,值得注意的是,所有这些都是用C++完成的. 一.ASCII 输出 为了使用下面的方法, ...

  8. 转载:看c++ primer 学习心得

    学习C++ Primer时遇到的问题及解释 chenm91 感觉: l          啰嗦有时会掩盖主题:这本书确实有些啰嗦,比如在讲函数重载的时候,讲了太长一大段(有两节是打了*号的,看还是不看 ...

  9. Delphi TRichEdit加载word内容

    procedure TForm1.btn6Click(Sender: TObject);var WordApp: Variant; //声明一个word对象beginWordApp := Create ...

  10. Delphi word编辑

    private void but_Table_Click(object sender, EventArgs e) { object Nothing = System.Reflection.Missin ...