分类目录归档:.NET

什么是 CLR ?

作者:By Vance Morrison – 2007
原文链接:https://github.com/dotnet/coreclr/blob/master/Documentation/botr/intro-to-clr.md
翻译:dontpanic

什么是公共语言运行时(Common Language Runtime, CLR)?简单来说就是:

公共语言运行时(CLR)是一套完整的、高级的虚拟机,它被设计为用来支持不同的编程语言,并支持它们之间的互操作。

啊,有点绕口,同时也不太直观。不过这样的表述还是 有用的 ,它把 CLR 的特性用一种易于理解的方式分了类。由于 CLR 实在太过庞大和复杂,这是我们理解它的第一步——犹如从万米高空俯视它,我们可以了解到 CLR 的整体目标;而在这之后,我们就可以带着这种全局观念,更好地详细了解各个子模块。

CLR:一个(很少见的)完备的编程平台

每一个程序都有大量的运行时依赖。当然,一个程序需要由某种特定的编程语言编写而成,不过这只是程序员把想法变成现实的第一步。所有有意义的程序,都免不了需要与一些 运行时库 打交道,以便能够操作机器的其他资源(比如用户输入、磁盘文件、网络通讯,等等)。程序代码还需要某种变换(翻译或编译)才能够被硬件直接执行。这些依赖实在是太多了,不仅种类繁多还互相纠缠,因此编程语言的实现者通常都把这些问题交由其他标准来指定。例如,C++ 语言并没有制定一种 “C++可执行程序” 格式;相反,每个 C++ 编译器都会与特定的硬件架构(例如 x86)以及特定的操作系统(例如 Windows、Linux 或 macOS)绑定,它们会对可执行文件的格式进行描述,并规定要如何加载这些程序。因此,程序员们并不会搞出一个 “C++可执行文件”,而是 “Windows X86 可执行程序” 或 “Power PC Mac OS 可执行程序”。

通常来说,直接使用现有的硬件和操作系统标准是件好事,但它同样也会把语言规范与现有标准的抽象层次紧密捆绑起来。例如,常见的操作系统并没有支持垃圾回收的堆内存,因此我们就无法用现有的标准来描述一种能够利用垃圾回收优势的接口(例如,把一堆字符串传来传去而不用担心谁来删除它们)。同样,典型的可执行文件格式只提供了运行一个程序所需要的信息,但并没有提供足够的信息能让编译器把其他的二进制文件与这个可执行文件绑定。举例来说,C++ 程序通常都会使用标准库(在 Windows 上叫做 msvcrt.dll),它包含了大多数常用的功能(例如 printf),但只有这一个库文件是不行的。程序员如果想使用这个库,必须还要有与它相匹配的头文件(例如 stdio.h)才可以。由此可见,现有的可执行文件格式标准无法同时做到:1、满足运行程序的需求;2、提供使程序完整所必须的其他信息或二进制文件。

CLR 能够解决这些问题,因为它制定了一套非常完整的规范(已被 ECMA 标准化)。这套规范描述了一个程序的完整生命周期中所需要的所有细节,从构建、绑定一直到部署和执行。例如,CLR 制订了:

  • 一个支持 GC 的虚拟机,它拥有自己的指令集(叫做公共中间语言,Common Intermediate Langauge),用来描述程序所能执行的基本操作。这意味着 CLR 并不依赖于某种特定类型的 CPU。
  • 一种丰富的元数据表示,用来描述一个程序的声明(例如类型、字段、方法等等)。因此编译器能够利用这些信息来生成其他程序,它们能够从“外面”调用这段程序提供的功能。
  • 一种文件格式,它指定了文件中各个字节所表达的意含义。因此你可以说,一个 “CLR EXE”并没有与某个特定的操作系统或计算机硬件相捆绑。
  • 已加载程序的生命周期语义,即一种 “CLR EXE 引用其他 CLR EXE” 的机制。同时还制订了一些规则,指定了运行时要如何在执行阶段查找并引用其他文件。
  • 一套类库,它们能够利用 CLR 所支持的功能(例如垃圾回收、异常以及泛型)来向程序提供一些基本功能(例如整型、字符串、数组、列表和字典),同时也提供了一些与操作系统有关的功能(例如文件、网络、用户交互)。
继续阅读

使用Reflexil修改Unity3D游戏逻辑

曾经有个游戏叫做《新剑侠传奇》,曾经停留在一个尴尬的版本1.0.7。作为一名抠脚大汉,自然要坐在在电脑前为广大玩家发福利!

故事背景

游戏发售之后出现了一些Bug,官方在更新至1.0.7版本后决定回炉。然而这一版本依然有一些玩家比较需要的Feature没有实现。在长达两周多的回炉阶段,我的补丁横空出现啦!

技术背景

MSIL可以较为轻易地反编译回C#/VB代码,如果不做代码混淆的话,可读性非常高。目前很多国产单机游戏都选择Unity 3D做为引擎:scream: ,并使用C#作为主要逻辑的实现语言。这为我们修改游戏逻辑创造了相当好的条件。

实现功能

  • 打击感增强
    通过在命中目标后停顿画面0.1秒实现。
  • 新增操作模式
    《新》是一个具备跟随视角的3D游戏,鼠标旋转视角,鼠标点击怪物为普攻,感觉略坑爹。新增操作模式采用类似龙之谷的方式:默认隐藏鼠标,鼠标移动旋转视角,鼠标点击为普攻,ctrl键呼出鼠标。
  • 摄像机位置记忆
    每次剧情之后摄像机都会回到初始位置,不会记忆上次用户调整的摄像机与人物的距离。
  • 暂停游戏
    进入战斗或者开始剧情之后,终于可以随意去上厕所了……
  • 跳过开始动画
    打开游戏时无需强制观看片头。
  • 画面改善:对比度增强、Bloom、HDR
    使用Unity内置Shader实现。

工具准备

  1. .NET Reflector
    我们用 .NET Reflector 来对 .NET Managed DLL 进行反编译工作。对于没有进行混淆或加密操作的程序,可以得到可读性很高的代码。
    0<em>1448775094110</em>1.png
  2. Reflexil
    Reflexil 可以在已经编译好的Managed DLL中插入MSIL代码。它提供了.NET Relfector的插件,安装好后即可直接插入IL代码。
  3. XamarinStudio (MonoStudio)
    其实这个有无皆可。但是如果不想所有代码全部使用IL手写的话,还是搞一个比较好。

代码修改

拿打击感增强作为一个例子,我们来看一下如何修改。Unity 3D游戏的主体逻辑都在/xxxx_Data/Managed/Assembly-CSharp.dll中。这个游戏的逻辑由几百个类/枚举构成,如果游戏大量使用了Unity插件(比如可视化编程:see<em>no</em>evil: ),可能还会更多。

增强打击感的方法是在玩家命中敌人后画面停顿0.1秒。所以我们需要在命中时记录当前时间、停顿画面,然后在0.1秒后取消停顿。

经过浏览代码,发现负责进行攻击的逻辑集中在Fighter类中。我们需要修改的有两个函数:一个是CalcHurt,发生在玩家已经命中怪物之 后,用于计算伤害;另一个是Update,每次迭代更新逻辑时都会调用这个函数。我们还需要一个Field用于记录停顿时间,可以直接在类上右击通过 Reflexil添加。

打开CalcHurt方法,在Reflexil中添加IL代码。IL代码非常简单易懂,而且我们并不需要掌握所有的IL指令。简介可以参考Introduction to IL Assembly Language,或者在Common Language Infrastructure (CLI) | ECMA-335中有所有指令的列表。

在修改完IL代码后,.NET Reflector会重新加载并反编译代码。如果指令有错误,.NET Reflector会无法反汇编成功。

停顿功能使用了Unity提供的Time.settimeScale函数,获取时间使用Time.getrealtimeSinceStartup。这些函数可以参考Unity 3D Manual

举例来说,

Time.set_timeScale(0.001f); InjectedField = Time.get_realtimeSinceStartup + 0.1;  

的IL代码如下:

ldc.r4 0.0001 call Time.set_timeScale call Time.get_realtimeSinceStartup ldc.rc 0.1 add stsfld InjectedField #InjectedFiled为静态成员  

就是这么简单啦。

使用XamarinStudio

如果不想所有代码全部手写,可以使用XamarinStudio新写一个Class,将所有新的逻辑放在这里。然后再修改原来的Assembly-CSharp.dll时,所有位置都变成函数调用就行了。

在编写新Class时,需要把游戏的Managed目录中的相关DLL作为引用添加进我们的工程里。对于这个游戏来说,我们主要用到了 UnityEngine、Assembly-CSharp、Assembly-CSharp-firstpass、Assembly- UnityScript-firstpass。

写好Class后,可以使用Reflexil将改Class添加进Assemby-CSharp的引用中。
0<em>1448778641887</em>3.png

请原谅时间久远电脑上已经没有XamarinStudio了:joy:

替换原始文件

没什么问题之后,就可以去论坛或者贴吧收获一番经验了~