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

译者:我们都知道.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方法,代码的反编译源如下

static void Main(string[] args) {
Foo f = new Foo();
00000000 push esi
00000001 mov ecx,913080h
00000006 call FFB21FAC
0000000b mov esi,eax
f.Test();
0000000d mov ecx,esi
0000000f cmp dword ptr [ecx],ecx
00000011 call dword ptr ds:[009130B8h]
f.Test();
00000017 mov ecx,esi
00000019 cmp dword ptr [ecx],ecx
0000001b call dword ptr ds:[009130B8h]
f.Test();
00000021 mov ecx,esi
00000023 cmp dword ptr [ecx],ecx
00000025 call dword ptr ds:[009130B8h]
0000002b pop esi
}
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. ASP.NET MVC 学习6、学习使用Code First Migrations功能,把Model的更新同步到DB中

     参考:http://www.asp.net/mvc/tutorials/mvc-4/getting-started-with-aspnet-mvc4/adding-a-new-field-to-th ...

  2. LA 3641 (置换 循环的分解) Leonardo's Notebook

    给出一个26个大写字母的置换B,是否存在A2 = B 每个置换可以看做若干个循环的乘积.我们可以把这些循环看成中UVa 10294的项链, 循环中的数就相当于项链中的珠子. A2就相当于将项链旋转了两 ...

  3. apache开源项目 -- tez

    为了更高效地运行存在依赖关系的作业(比如Pig和Hive产生的MapReduce作业),减少磁盘和网络IO,Hortonworks开发了DAG计 算框架Tez.Tez是从MapReduce计算框架演化 ...

  4. 【C#学习笔记】获得系统时间

    using System; namespace ConsoleApplication { class Program { static void Main(string[] args) { Conso ...

  5. ORACLE 全局索引和本地索引

    Oracle数据库中,有两种类型的分区索引,全局索引和本地索引,其中本地索引又可以分为本地前缀索引和本地非前缀索引.下面就分别看看每种类型的索引各自的特点. 全局索引以整个表的数据为对象建立索引,索引 ...

  6. 移动对meta的定义

    以下是meta每个属性详解 尤其要注意的是content里多个属性的设置一定要用分号+空格来隔开,如果不规范将不会起作用. 一.<meta http-equiv="Content-Ty ...

  7. 【转】android布局属性详解

    LinearLayout布局: 线性版面配置,在这个标签中,所有元件都是按由上到下的排队排成的.在这个界面中,我们应用了一个 LinearLayout的布局,它是垂直向下扩展的 ,所以创建的布局XML ...

  8. 认识solr结构,了解核心的文件目录

    下载solr并解压后,发现solr的目录里有很多的东西,此时我们可能会感到很恐慌,不知如何下手,下面让我带你认识它. 1.解压后的solr目录结构如下: 虽然里面有很多的文件,但是我们需要的其实就两个 ...

  9. 保护眼睛,开启浏览器的夜间模式 顺便学下!important的作用

    打开笔记本程序,复制以下代码 *{background-image: none !important; background: none !important; background:#333333 ...

  10. linux命令——rmdir

    rmdir是常用的命令,该命令的功能是删除空目录,一个目录被删除之前必须是空的.rm - r dir命令可代替rmdir rmdir [选项]... 目录... - p 递归删除目录dirname,当 ...