登录/注册
梧桐雨
2256
占位
1
占位
0
浏览量
占位
粉丝
占位
关注
代码是怎么运行的?
梧桐雨
2022-01-05 21:45:38 2022-01-05
151
0

本文内容来自我写的开源电子书《WoW C#》,现在正在编写中,可以去WOW-Csharp/学习路径总结.md at master · sogeisetsu/WOW-Csharp (github.com)来查看编写进度。预计2021年年底会完成编写,2022年2月之前会完成所有的校对和转制电子书工作,争取能够在2022年将此书上架亚马逊。编写此书的目的是因为目前.NET市场相对低迷,很多优秀的书都是基于.NET framework框架编写的,与现在的.NET 6相差太大,正规的.NET 5学习教程现在几乎只有MSDN,可是MSDN虽然准确优美但是太过琐碎,没有过阅读开发文档的同学容易一头雾水,于是,我就编写了基于.NET 5的《WoW C#》。本人水平有限,欢迎大家去本书的开源仓库sogeisetsu/WOW-Csharp关注、批评、建议和指导。

前言

“我花了几个星期…试着弄清楚“强类型”、“静态类型”、“安全”等术语,但我发现这异常的困难…这些术语的用法不尽相同,所以也就近乎无用。 来源:编程语言专家 Benjamin C. Pierce

今天,我突然发现,我虽然自认为会几门编程语言和会使用几种框架,却对编程语言的原理的理解停留在十分浅薄的层面,我原先对编程语言的理解不过是编译器将语言编译成机器代码,然后机器再执行机器代码,解释型语言是解释器边解释边运行,我还知道像java这种语言是先编译成bytecode(class文件),然后在jvm里面用JIT即时编译……过去我对编程语言的理解仅仅限于上面这些。如果问我诸如解释器和编译器的区别,即时编译、动态编译、AOT的异同,什么叫解释什么叫编译,静态语言和动态语言的区别,python算是强类型语言吗等问题,我八成会答错,更让人沮丧的是在很多博客网站上像csdn和简书里关于编程语言的基本认知错误有很多,笔者通过广泛的阅读维基百科相关内容和大学教材《编译原理》,同时阅读了msdn的官方文档和知乎上一些高质量回答,写下本文,希望抛砖引玉的同时能够对互联网上一些关于编程语言的错误言论起到正本清源的作用。

什么是编程语言

编程语言(英语:programming language),是用来定义计算机程序的形式语言。它是一种被标准化的交流技巧,用来向计算机发出指令,一种能够让程序员准确地定义计算机所需要使用数据的计算机语言,并精确地定义在不同情况下所应当采取的行动。 来源:编程语言 - 维基百科,自由的百科全书 (wikipedia.org)

编程语言,其实就是一个能够让计算机明白使用者意图,同时能让使用者对计算机发出指令的有统一规则的交流技巧。这里可以打个拟人化的比喻,cpu所能听懂的语言和人说的语言是不一样的,所以需要有一个中间语言——编程语言来搭建交流的桥梁,事实上从人类的语言到cpu能听懂的语言往往并不是一个编程语言就能办到的,通常需要多个编程语言参与其中。

本篇文章的价值观为一切定义以维基百科为准,如果维基百科和《编译原理》的定义有冲突,则以《编译原理》为准。

强弱与动静态,从C语言说起

先说结论,c语言是弱类型、静态类型、有编译器的编译语言

强弱类型和动静态类型不是一个东西,也不是一个标准下的产物。

强类型:如果一门语言倾向于不对变量的类型做隐式转换,那我们将其称之为强类型语言 弱类型:相反,如果一门语言倾向于对变量的类型做隐式转换,那我们则称之为弱类型语言 动态语言:是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化 静态语言:相反,如果一门语言不可以在运行时改变结构,则是静态语言。 静态类型:与动态类型语言刚好相反,它的数据类型检查发生在在编译阶段,也就是说在写程序时要声明变量的数据类型。C/C++、C#、Java都是静态类型语言的典型代表。 动态类型:绝大多数动态语言都使用动态类型,但不绝对。是指在运行期间才去做数据类型检查的语言。在用动态语言编程时,不用给变量指定数据类型,第一次赋值给变量时,在内部将数据类型记录下来。JavaScript、Ruby、Python是典型的动态类型语言。 作者:网仙 部分内容来自知乎,稍作修改,链接:https://www.zhihu.com/question/43498005/answer/266431585

强弱类型指的是类型检查的严格程度,动静态类型区分的是类型检查在程序运行的哪个阶段发生的。(当然了,你如果说类型检查是早就在IDE编辑器里面发生的,那就没意思了。😶)

动态和静态往往是有比较明显的区分,可是强弱类型确实没有一条明显的分界线。就拿python来说,一个整数和浮点数相加会变成浮点数,这就是对变量的类型做了隐式变换。可是即使如此,python在大多数情况下是要比JavaScript这种语言更加的倾向于不做隐式变换,至少python不会去认为整数1和字符串1相等(当然,这得是在没有重写equals函数和没有自定义一个函数去主动将字符串转变为浮点数的情况下)。即使和大多数高级语言一样,在特定的时候会进行隐式转换,但是那是为了编程的方便,而不是因为自身的特性。

比如这段C#代码里面,试图去进行做隐式数据转换,可以看到即使在某些情况下这是被允许的,但是,c#的倾向为不做隐式转换。故而c#为强类型语言。

/// <summary>
/// 验证隐式变换
/// </summary>
internal void YinShiChange()
{
Console.WriteLine("-=-=-=-=-=-=-=-=-=-=-=-=");
var a = 1;
//a += "121"; 这是错误的
Console.WriteLine(a.GetType());
//在打印的时候是可以的,这时候是将所有的都转换成字符串
Console.WriteLine(a + "123");
var v = Convert.ToString(a);
Console.WriteLine(v.GetType());
Type intType = typeof(int);
Console.WriteLine(intType == a.GetType());
// 值类型的隐式转换
var aa = 1;
Console.WriteLine(intType == aa.GetType());
double cc = aa;
Console.WriteLine(cc.GetType() + "\t" + (cc.GetType() == typeof(double)));
#region
/*
-=-=-=-=-=-=-=-=-=-=-=-=
System.Int32
1123
System.String
True
True
System.Double True
*/
#endregion
}

下面这张图片,清晰的说明了目前维基百科·对几种知名的语言的强弱类型和动静态类型的区分。事实上,在维基百科里面清楚的说明了强弱类型(Strong and weak typing)这两个术语并没有非常明确的定义。甚至python的作者吉多一度认为python属于弱类型语言。下面这个表格仅仅代表了目前比较主流的看法。

b0aeb7ffd1667b9162e5329154d43777_720w.jpg (500×337) (zhimg.com)

C语言是怎么运行的?

总的来说,从C语言代码翻译为二进制的过程,主要经历以下四个阶段

  • 阶段一:预编译
  • 阶段二:编译
  • 阶段三:汇编
  • 阶段四:链接

python 是怎么运行的?

python语言在运行过程中,是先把python编译成一种叫做pyc的类似字节码的语言,然后cpython编译器将pyc文件逐步解释成机器代码来运行,pyc到机器代码这一步是逐句翻译(边运行边编译)的。

我们需要注意到的是python文件从.py到.pyc的过程并不属于AOT(预先编译),.py到.pyc的过程默认是在每一次的python运行过程中发生的。但是从.py到.pyc的过程可以用python -m py_compile file.py进行提前编译。

python提前编译成字节码

这是一个one.py的普通文件:

def forEachList(list):
for i in list:
if(i % 2 == 0):
print(i)
else:
print("{}无法被整除".format(i))
if __name__ == '__main__':
ll = []
for i in range(0, 10):
ll.append(i)
forEachList(ll)
print("hello wrold")

运行python -m py_compile .\one.py,就会目录变成以下样子:

.
├── __pycache__
│ └── one.cpython-38.pyc
└── one.py

one.cpython-38.pyc就是编译出的pyc文件,可以直接用python .\one.cpython-38.pyc来正常运行这个文件。

提前编译的好处

字节码在某种意义上会保护代码,让普通人无法直接看到代码内部的内容,但是这种保护措施在多如牛毛的反编译工具面前是没有什么意义的。另一方面,提前编译成字节码会省去编译代码的一道步骤,加快运行速度,事实上,像cpython这种没有JIT(即时编译)的解释器,最好是能直接编译成native code,因为cpython在代码运行过程中起到的优化作用有限,甚至所起到的优化作用比不上直接编译成native code的好处。

请注意,cython≠cpython,cpython是python官方的解释器,Cython 是包含 C 数据类型的 Python,Cython 是 Python,几乎所有 Python 代码都是合法的 Cython 代码。 (存在一些限制,但是差不多也可以。) Cython 的编译器会转化 Python 代码为 C 代码,这些 C 代码均可以调用 Python/C 的 API。

使用cython来达到AOT的效果

Cython入门教程 - 簡書 (jianshu.com)

README | Cython 官方文档中文版 (gitbooks.io)

使用cython可以将pyx文件直接编译成native code,这大大提高运行效率,但是这个所谓的native code是离不开cpython解释器的,原因是因为现在的高级语言它需要解释器提供类库来达到最终的效果,如果python有像dotnet这种self-contained的模式并且可以自动裁剪未使用的程序集的话,就会方便很多,可是我并没有发现python有提供这样的功能(当然,可能会有一些开源的第三方工具来实现类似功能)。python官方也没有提供明确的python运行时下载地址(或者说整个python安装文件夹就是运行时),人们通常认为python安装文件夹内的lib文件夹为python标准库的地址,python.exe为python解释器。

在很多linux发行版的软件源中提供了名为python-dev的文件夹,里面包含了python运行所需要的调用python api的c/c++文件。

示例

文件结构是这样的:

.
├── one.py
├── run.pyx
└── setup.py

run.pyx:

def sum(x:int,y:int)->int:
if(x!=0 and y!=0):
return x+y
return 0

setup.py:

from distutils.core import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize("run.pyx")
)

运行python setup.py build_ext --inplace,文件结构如下:

.
├── build
│ └── temp.win-amd64-3.8
├── one.py
├── run.c
├── run.cp38-win_amd64.pyd
├── run.pyx
└── setup.py

pyd格式是D语言(C/C++综合进化版本)生成的二进制文件,实际也会是dll文件。可以在正常的代码中引用run.cp38-win_amd64.pyd。以下是one.py引用它的方式:

import run
if __name__ == '__main__':
c:int=run.sum(12,4)
print(c)

可以正常运行。

pyd文件的本质是一个动态链接库,这个没法直接运行,只能被调用。当然了,写一个命令,自动将pyd包裹进一个python文件,然后该文件调用pyd并运行指定的方法也是可行的。

运行时

运行时被叫做runtime,这是一个很形而上学的概念,可以将它看作是程序运行所必须的东西,这就叫运行时。

不同语言,不同解释器所定义的运行时内容是有不同的。比如说java的运行时叫做jre,它包含jvm虚拟机和运行时库,jvm虚拟机的作用就是对字节码进行JIT编译,内存管理,GC垃圾清理等功能,最终生成机器代码,将机器代码交给cpu执行。.NET的运行时被称作是CLR(公共语言运行时),和java不同,尽管CLR 也是一种虚拟机,但是.NET不像java一样对虚拟机单独命名(jvm),CLR 处理内存分配和管理。 CLR 也是一种虚拟机,不仅可执行应用,还可使用 JIT 编译器快速生成和编译代码。BCL是.net的基类库,在过去的一些书中,BCL并不被认为是CLR的一部分,但是,现在BCL已经被放在了.NET的runtime开源仓库中,.NET 5(和 .NET Core)及更高版本的 BCL 的源代码包含在 .NET 运行时存储库中。所以BCL应该被看作是CLR的一部分。尽管MSDN仍然会在很多时候将CLR和BCL放在同样的位置却称呼BCL为运行时库

AOT

In computer science, ahead-of-time compilation (AOT compilation) is the act of compiling an (often) higher-level programming language into an (often) lower-level language before execution of a program, usually at build-time, to reduce the amount of work needed to be performed at run time. 来源:Ahead-of-time compilation - Wikipedia

AOT编译(Ahead-of-time compilation)指的是预先编译,即提前将更高级的代码转换成低级的代码,这个低级的代码并非专指native machine code。在有的论文中,人们将让bytecode转变成c语言代码的过程称之为AOT,也有一个学术项目中将JavaScript代码编译成不依赖JavaScriptCore的字节码的过程称之为AOT,但是人们很少将java源代码编译成bytecode的过程称之为AOT。

从某种意义上来讲,AOT和静态编译(static compilation)一样都是提前编译。在一般使用的过程中,通常将提前编译且预编译的行为能带来某种优势的行为称之为AOT,之前说java源代码到bytecode不被认为是AOT是因为这是java运行的要求,却不是为java做优化。(In fact, since all static compilation are technically performed ahead of time, this particular wording are often used to emphasize some kind of performance advantages from the act of such pre-compiling. The act of compiling Java to Java bytecode is hence rarely referred to as AOT since it's usually a requirement, not an optimization.)

JIT

即时编译(英语:just-in-time compilation,缩写为JIT;又译及时编译、实时编译),也称为动态翻译或运行时编译,是一种执行计算机代码的方法,这种方法涉及在程序执行过程中(在执行期)而不是在执行之前进行编译。 来源:即时编译 - 维基百科,自由的百科全书 (wikipedia.org)

JIT也是一个形而上学的概念,现在人们通常将JIT等同于动态翻译。事实上JIT仅仅是动态翻译的一种实现方式动态编译(dynamic compilation)指的是“在运行时进行编译”;与之相对的是事前编译(ahead-of-time compilation,简称AOT),也叫静态编译(static compilation)。JIT编译(just-in-time compilation)狭义来说是当某段代码即将第一次被执行时进行编译,因而叫“即时编译”。

在这里,用.NET的JIT编译作为讲解的对象,因为MSDN上有比较官方的说法。但要注意的是不同的编译器在JIT的实现上是有不同的,就像之前说的,JIT是一个形而上学的概念。

Smalltalk(1983年)开创了JIT编译的新领域。例如,按需翻译为机器代码,缓存结果以供以后使用。当内存不足时,系统会删除部分代码,并在需要时重新生成。[4][21]Sun的Self语言广泛地改进了这些技术,一度是世界上速度最快的Smalltalk系统;运用完全面向对象的语言实现了高达优化C语言一半的速度。[22] Self被Sun抛弃了,但是研究转向了Java语言。“即时编译”这个术语是从制造术语“及时”中借来的,并由Java普及,James Gosling从1993年开始使用这个术语。[23]目前,大多数Java虚拟机的实现都使用JIT技术,因为HotSpot创建在这个研究基础之上,而且使用广泛。 来源:即时编译 - 维基百科,自由的百科全书 (wikipedia.org)

.NET的JIT,从字面上来理解,及时编译,按需编译。

这是.NET的托管代码的执行过程:

选择编译器。 若要获取公共语言运行时提供的好处,必须使用一个或多个面向运行时的语言编译器。 将代码编译为 MSIL。 编译将你的源代码转换为 Microsoft 中间语言 (MSIL) 并生成必需的元数据。 将 MSIL 编译为本机代码。 在执行时,实时 (JIT) 编译器将 MSIL 转换为本机代码。 在此编译期间,代码必须通过检查 MSIL 和元数据的验证过程以查明是否可以将代码确定为类型安全。 运行代码。 公共语言运行时提供启用要发生的执行的基础结构以及执行期间可使用的服务。 来源:托管执行过程 | Microsoft Docs

代码编译的第一步是编译器将源代码转换为 Microsoft 中间语言 (MSIL),MSIL也可以被称作CIL或者IL,这个类似于java的bytecode。其实也可以管他叫字节码。

JIT编译为针对每个文件、每个函数甚至任何任意代码片段进行编译; 代码可以在即将执行时进行编译(因此称为“即时”),然后缓存并在以后重用,无需重新编译。JIT在运行时只编译一次,之后都是复用。

简单点说,JIT即时编译器就是把一些经常跑的代码(字节码)提前编成本地机器码,后面就直接跑机器码,可以跑的更快些。在人们通常将JIT等同于动态翻译的今天,JIT是一种更聪明、更有效果的动态编译。

关于JIT编译的具体辨析请查看本文JIT正本清源章节

What is the difference between Just-in-time compilation and dynamic compilation?

这是一个来自stack overflow的问题,也是我关于动态编译最大的困惑,我知道JIT是动态编译的一部分。既然有了JIT,那么常规的动态编译(dynamic compilation)或者解释器(Interpreter)和JIT的区别在哪里呢?就拿python来举例,为什么说cpython的从pyc到native machine code 的过程不是JIT呢?

What is the Difference Between Interpreter and JIT Compiler - Pediaa.Com

compiler construction - What is the difference between Just-in-time compilation and dynamic compilation? - Stack Overflow

java - JIT vs Interpreters - Stack Overflow

我试着查阅了一些资料,不止是Wikipedia is confusing,很多人的回答也是令人困惑。所以我得出了一个武断的结论(因为很多网友的说法难以自圆其说,维基百科又是模棱两可),下面是我对我的结论的陈述:

首先,Till now computers don't execute anything other than machine code。现在事实上电脑只能看懂native machine code,不管是编译器(compiler)还是解释器(Interpreter)的最终目的只有一个就是让是将便于人编写、阅读、维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序,也就是可执行文件。最终让程序运行在电脑上。

编译器追求的是一次性将所有源代码编译成二进制文件。

一个现代编译器的主要工作流程如下: 源代码(source code)→ 预处理器(preprocessor)→ 编译器(compiler)→ 汇编程序(assembler)→ 目标代码(object code)→ 链接器(linker)→ 可执行文件(executables),最后打包好的文件就可以给电脑去判读执行了。 来源:编译器的工作流程 - 维基百科,自由的百科全书 (wikipedia.org)

解释器的目的是边解释边执行(by line),因此依赖于解释器的程序启动速度比较缓慢。解释器的好处是它不需要重新编译整个程序,从而减轻了每次程序更新后编译的负担,并且因为解释器往往会有运行时对代码进行分析和处理,运行速度一般会更快(python除外,据我的经验,python运行的真心不快,网上有大佬认为是cpython的设计不科学的问题)。

解释器执行程序的方法一般被认为是下面3种:

直接执行高级编程语言(如Shell内建的编译器) 转换高级编程语言到更有效率的字节码(Bytecode),并执行字节码 用解释器包含的编译器对高级语言进行编译,并指示中央处理器执行编译后的程序(例如:JIT) 来源:解释器 - 维基百科,自由的百科全书 (wikipedia.org)

python是属于第二种方法,即从源代码全部翻译为到pyc,然后在边解释边执行pyc,执行pyc的过程就是pyc到native machine code的过程,当然这个过程里面或许会需要虚拟机(不是真的硬件,而是一种字节码解释器,pyx就相当于虚拟机的native machine code)。

JIT编译器的执行过程从大的方面(源代码->字节码->虚拟机运行字节码,将字节码解释为机器代码)来看是和cpython是一致的。

JIT概念的争论

我并不认同“源代码–>字节码”这一过程是JIT运行的一部分,我认为“源代码–>字节码”是AOT的一部分(对虚拟机来说),JIT仅是“字节码->虚拟机运行字节码,将字节码解释为机器代码”这一部分。在MSDN和维基百科的某些页面上也是这么认为的,MSDN的托管代码执行过程认为实时 (JIT) 编译器将 MSIL 转换为本机代码。 维基百科关于解释器的说明中认为即时编译(Just-in-time compilation)是指一种在执行时期把字节码编译成原生机器代码的技术;这项技术是被用来改善虚拟机的性能的。但是在维基百科的关于JIT的页面中认为JIT编译是两种传统的机器代码翻译方法——提前编译(AOT)和解释——的结合,它结合了两者的优点和缺点。维基百科的本意可能不是将AOT归入JIT,但是确实有可能让人产生困惑,Wikipedia is confusing。在CSDN的讨论中认为JIT是编译和解释的结合。

就像前面说的一样,人们有时会默认将JIT代表动态编译,人们也因为JIT和源代码到字节码的过程紧密相关而将提前编译为字节码认为是JIT的一部分。这不能说是错的,这应该是不准确的。

JIT正本清源

JIT的基本运行过程,基本上没有什么争论:

JIT编译的一个常见实现是首先进行AOT编译,把源代码编译成字节码(虚拟机代码),称为字节码编译,然后将JIT编译为机器码(动态编译),而不是解释字节码。与解释相比,这提高了运行时性能,但代价是编译造成的延迟。与解释器一样,JIT编译器不断地进行翻译,但是对编译后的代码进行缓存可以最大限度地减少在给定运行期间将来执行相同代码的延迟。 来源:即时编译 - 维基百科,自由的百科全书 (wikipedia.org)

尽管JIT称之为编译,但和解释器一样JIT也是不断地进行翻译,而非直接全部翻译出来。但是JIT的编译后的代码会被缓存在内存中,来增加复用率。更高级的JIT甚至后对不同代码所发挥价值的大小来进行不同优化程度的编译。早在1983年,当时self语言的动态编译(当时还没有JIT这个词汇)就已经会根据内存大小来动态进行编译和在内存不足时删除部分编译结果并在需要的时候重新生成了。

JIT和一般意义上地解释器的区别为是否对到机器代码的过程进行动态的优化,这也是动态编译和一般解释器的区别,有些优化是只有到了运行过程中在有可能根据当前的机器状态等信息进行优化。维基百科的动态编译词条认为使用动态编译的执行环境一开始执行速度较慢,之后,完成大部分的编译和再编译后,会执行得比非动态编译程序快很多。维基百科应该是说的不全面,它遵循了现在的习惯,将动态编译指代了JIT。动态编译应该像AOT一样,之所以单独命名成动态编译而非延用解释器这个名称是因为和单纯的解释器相比能带来某种优势。

JIT和一般意义上的动态编译(这个一般意义指的是1993年以前,当时java还没有提出JIT,在1995年之前的论文中,动态编译是唯一术语,The term dynamic compilation used the be the standard and only term to refer to the family of techniques of compiling code at run-time before 1995. )的区别是优化的程度以及优化的成本,JIT编译器不断地进行翻译,但是对编译后的代码进行缓存可以最大限度地减少在给定运行期间将来执行相同代码的延迟。其实看到这里会清楚的发现,试图去理清JIT和动态编译的区别是很难的,JIT脱胎于动态编译,1983年self语言虽然被称作是动态编译,但是已经和现在的JIT的逻辑很相似了。

历史原因

我们现在都知道JIT是动态编译的一种形式,动态编译还有一种形式叫做自适应动态编译(adaptive dynamic compilation,也是一种动态编译,但它通常执行的时机比JIT编译迟,先让程序“以某种形式”先运行起来,收集一些信息之后再做动态编译。现在只能这么说,常规意义上的动态编译和JIT编译的定义几乎一致。现在的高级语言的JIT已经有了和我们常规意义上的动态编译有了很大的区别,比如.NET的分层编译能够根据代码的重要程度和复杂程度来确定优化的程度来达到运行速度和性能的平衡。

之所以难以厘清JIT和动态编译之间的区别有很大一部分是因为历史原因。动态编译的历史并不是像我们想象的那样是由一个技术概念而引发的技术应用,事实情况是,在动态编译的发展历史之中,往往是一个技术在被使用了多年之后,人们才将其抽象成一个概念。这样就会导致往往会有一个时间段里面某个技术的运用包含了太多后来的技术概念,可是因为历史的原因,依然很难将当时的技术命名为之后才抽象出来的概念。就像隋炀帝开始设进士科,进行科举值之前的近百年里已经有一种名为分科举人的通过考试来选拔人才的制度了,尽管这样,历史书上还是只能将开创科举制归功于隋炀帝。

从1960年,计算机学家约翰·麦卡锡就已经提出了动态编译的雏形(其实准确的来说这应该是解释器的优化雏形,当时所谓的机器代码还是以打孔卡作为保存介质),后来在1986年的sun公司的self语言中运用了一种能够按需翻译为机器代码,缓存结果以供以后使用。当内存不足时,系统会删除部分代码,并在需要时重新生成的技术。从能查阅到最早的关于这一使用这一技术而非处于科学研究阶段的论文是1984年的 Efficient implementation of the Smalltalk-80 system中可以看到当时并没有将这一技术命名为dynamic compilation或Just-in-time compilation,但是从原理上来看已经和今天的JIT几乎一致了,在这之前,dynamic compilation作为一个概念曾广泛存在于论文中,比如1983年的Performance enhancements to a relational database system就提到了dynamic compilation这一名词,总之我们可以确定的是在早期人们并不明确的将现在的动态编译技术和动态编译这一名词结合起来,直到后来sun公司将原用于self的这一技术运用于java语言,并命名为Just-in-time compilation,也就是JIT编译。其实准确的将这一技术命名为JIT是在1993年,当时java语言还没有发布,sun公司正在研发java语言的前身oak(Oak was renamed Java in 1994 after a trademark search revealed that Oak was used by Oak Technology)。

结论

因为之前一直没有明确的结论,现在我武断的下一个结论,dynamic compilation这一技术的本质是能提供某种优化的Interpreter,Just-in-time compilation、dynamic recompilation和adaptive dynamic compilation都是dynamic compilation的一种形式,其中JIT编译(Just-in-time compilation)的技术标准是为针对每个文件、每个函数甚至任何任意代码片段进行编译; 代码可以在即将执行时进行编译(因此称为“即时”),然后缓存并在以后重用,无需重新编译。JIT在运行时只编译一次,之后都是复用。

在过去某一个时间里面,JIT虽然被运用,但是并没有被命名成Just-in-time compilation而是依然用dynamic compilation来命名。由于JIT的广泛使用,一般情况下,人们会将dynamic compilation等同于Just-in-time compilation。

现在很多优秀的语言编译器已经不在满足于仅仅使用JIT,而是更加智能和提供了更多选择,比如java的HotSpot技术会将经常被执行的代码采用JIT编译成机器代码并缓存,对只执行几次的字节码采用普通的解释执行。微软的.NET的JIT采用了分层编译的技术,对不重要和不频繁调用的字节码采用快速JIT编译成优化程度较低的机器代码,对调用频繁,复杂的的字节码采用完全优化的JIT进行编译(.NET Core 3.0 的新增功能 | Microsoft Docs)。.NET也有类似于AOT的readytorun和NativeAOT预编译解决方案。

  • ReadyToRun

可以通过将应用程序集编译为 ReadyToRun (R2R) 格式来改进.NET Core 应用程序的启动时间。 R2R 是一种预先 (AOT) 编译形式。应用的大多数代码都是 AOT 编译的,但有些代码是 JIT 编译的。 某些代码模式不适用于 AOT(如泛型)R2R 二进制文件更大,因为它们包含中间语言 (IL) 代码(某些情况下仍需要此代码)和相同代码的本机版本。

目前还是预览版,工作就是通过.NET将代码完全编译成机器代码。不包含IL代码。微软计划在.NET 7时间范围中对NativeAOT做这些优化。现在最新的版本是preview 7.0.0-*版本。

代码混淆

代码混淆可以有效的保证代码的安全性,从理论上来讲所有的代码最后都要变成指令去让cpu执行,而cpu是由人类设计的,也就是说,反编译这个事情是一个困难程度的事情而不是有没有可能的事情,想让源码不被人看到是不太可能。但是让自己的源码别人看不懂是由可能的,如果自己的代码打印个“hello wrold”都需要100行代码,那么别人能看懂的几率就不是很大。

我的源码保护经验是,能用私有属性就尽量用私有属性,能不public就不public。代码一定要经过混淆。这是保护代码的最后一道防线。在这里我用.NET Reactor来做代码混淆,用de4dot来进行去壳操作。用dotpeek作为反编译工具。看一下最终和源代码的区别。

这是使用dotpeek反编译未经代码混淆的控制台程序:

using System;
using System.Collections.Generic;
namespace OOB
{
/// <summary>
/// 定义了方法
/// </summary>
internal class Program
{
private static void Hi(ref int a, List<string> list)
{
if (a != 0)
{
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine(list[i]);
if (i % 2 == 0)
{
list.Add(list[i] + "a");
}
else
{
list.RemoveAt(i);
}
}
}
else
{
Console.WriteLine(a);
}
}
/// <summary>
/// 主方法
/// </summary>
/// <param name="args"></param>
private static void Main(string[] args)
{
Console.WriteLine("Hello World!");
List<string> listA = new() { "123", "你好", "<name>", "hahah" };
int a = 12;
Hi(ref a, listA);
}
}
}

可以看到.NET的dll文件毫无还手之力,被及其容易的反编译。

现在用.NET Reactor进行混淆,混淆之后效果如下:

using HS1iDvP6cPSr6EVC1K;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Y04pXxuti1Wwbm9GOm;
namespace SnxhMamAqk3EZqxPU3
{
internal class zvuf870dMXwr3ZTmjl
{
[MethodImpl(MethodImplOptions.NoInlining)]
private static void FG3ePcOJ0(ref int _param0, List<string> _param1)
{
if (_param0 != 0)
{
for (int index = 0; index < _param1.Count; ++index)
{
Console.WriteLine(_param1[index]);
if (index % 2 == 0)
_param1.Add(_param1[index] + ynPCefDwj0sfr3loBQ.MQb0Gqmik(0));
else
_param1.RemoveAt(index);
}
}
else
Console.WriteLine(_param0);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void yjhx0jGwM(string[] _param0)
{
if (26902 - 382226 == -355324)
;
do
{
int num;
List<string> stringList;
do
{
do
{
Console.WriteLine(ynPCefDwj0sfr3loBQ.MQb0Gqmik(6));
}
while (110020 - 555598 == -445577);
stringList = new List<string>()
{
ynPCefDwj0sfr3loBQ.MQb0Gqmik(34),
ynPCefDwj0sfr3loBQ.MQb0Gqmik(44),
ynPCefDwj0sfr3loBQ.MQb0Gqmik(52),
ynPCefDwj0sfr3loBQ.MQb0Gqmik(68)
};
if (288185 - 437976 == -149791)
num = 12;
}
while (227838 - 260470 == -32631);
zvuf870dMXwr3ZTmjl.FG3ePcOJ0(ref num, stringList);
}
while (18107 - 405292 == -387184);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public zvuf870dMXwr3ZTmjl()
{
VO4Fe7fvUeFLREiK87.MfgIrOivFT5UF();
// ISSUE: explicit constructor call
base.\u002Ector();
}
[MethodImpl(MethodImplOptions.NoInlining)]
internal static bool ilDR2IAPbOxK9JUgww() => true;
[MethodImpl(MethodImplOptions.NoInlining)]
internal static bool joInk2HxpG3g4TIRnb() => false;
}
}

可以说面目全非。

用著名去壳软件de4dot来反混淆,效果如下:

using HS1iDvP6cPSr6EVC1K;
using System;
using System.Collections.Generic;
using Y04pXxuti1Wwbm9GOm;
namespace SnxhMamAqk3EZqxPU3
{
internal class zvuf870dMXwr3ZTmjl
{
private static void FG3ePcOJ0(ref int int_0, List<string> list_0)
{
if (int_0 != 0)
{
for (int index = 0; index < list_0.Count; ++index)
{
Console.WriteLine(list_0[index]);
if (index % 2 == 0)
list_0.Add(list_0[index] + ynPCefDwj0sfr3loBQ.MQb0Gqmik(0));
else
list_0.RemoveAt(index);
}
}
else
Console.WriteLine(int_0);
}
private static void yjhx0jGwM(string[] args)
{
Console.WriteLine(ynPCefDwj0sfr3loBQ.MQb0Gqmik(6));
List<string> list_0 = new List<string>()
{
ynPCefDwj0sfr3loBQ.MQb0Gqmik(34),
ynPCefDwj0sfr3loBQ.MQb0Gqmik(44),
ynPCefDwj0sfr3loBQ.MQb0Gqmik(52),
ynPCefDwj0sfr3loBQ.MQb0Gqmik(68)
};
int int_0 = 12;
zvuf870dMXwr3ZTmjl.FG3ePcOJ0(ref int_0, list_0);
}
public zvuf870dMXwr3ZTmjl()
{
VO4Fe7fvUeFLREiK87.MfgIrOivFT5UF();
// ISSUE: explicit constructor call
base.\u002Ector();
}
internal static bool ilDR2IAPbOxK9JUgww() => true;
internal static bool joInk2HxpG3g4TIRnb() => false;
}
}

可以看到已经很接近源代码了,可是依然含有很多无用代码,再加上注释已经被全部删除了。阅读起来依然有很大的困难。况且这个代码非常简单,如果是一个稍微复杂一点的项目,混淆之后的阅读难度将会呈几何增加。在这次代码混淆demo里面,我只使用了.NET Reactor的如下功能,只占了.NET Reactor功能的一小部分:

LICENSE

本文已将所有引用其他文章之内容清楚明白地标注,其他部分皆为作者劳动成果。对作者劳动成果做以下声明:

copyright © 2021 苏月晟,版权所有。

知识共享许可协议

作品

苏月晟

采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。不可以商业目的转载和引用此文章,在非商业转载时请注明来源和作者信息,并采用相同的许可协议。如需商业转载需联系作者并取得作者本人同意。

原文: https://www.cnblogs.com/sogeisetsu/p/15555797.html

暂无评论