本书讲解了 .NET Core公共语言运行时的底层实现,从介绍MSIL和x86汇编语言开始,到讲解异常、多线程、GC以及JIT编译器的实现原理与实现细节。本书包含了大量图表让读者可以更容易了解其中的内容,同时涉及到 .NET Core底层实现的部分还给出了对应的源代码链接,让读者可以参考源代码有更深入的理解。此外,本书还有相关提问用的仓库和QQ 群便于读者交流,详见序言。
本书主要面向有一年以上 .NET (C#)开发经验的开发者,其他程序语言的开发者也可以阅读本书来比较 .NET Core与其他语言的运行时之间有哪些共同点和不同点,本书的知识可以为读者在编写高性能应用或底层应用时提供有力的支持。
首先非常感谢您在百忙之中翻开本书。本书主要介绍 .NET Core公共语言运行时的底层实现,包括异常、多线程、GC(Garbage Collection)以及JIT编译器(Justintime Compiler)的实现原理与细节。阅读本书可以加深对 .NET框架的理解,这些知识会在编写框架以及高性能程序时发挥重要的作用。如果您有兴趣,本书中的知识还可以帮您向CoreCLR添加或修改功能并贡献代码,或者实现一个自己的语言框架。
我在2017年的年初开始阅读 .NET Core源代码,在此之前我只是一个拥有一些 C++ 与汇编知识的开发者,直到某一天一个 QQ 群的群友推荐我去阅读 .NET Core 的源代码,从此之后的一年时间里,我把大量的时间花在了阅读源代码上,收获很多,包括理解了各种各样曾经以为是黑箱子的机制,以及编写代码时会更多地去思考执行代码时底层发生的处理。再之后,我在博客上发表了一些介绍 .NET Core底层实现的文章,虽然理解这些文章需要的知识很多,但还是收到了一些反响,并且得到了北京航空航天大学出版社的邀请。在此我非常感谢推荐我阅读 .NET Core源代码的群友以及邀请我编写此书的北京航空航天大学出版社编辑。
本书内容很大程度上依赖于对 .NET Core的源代码分析,与《CLR via C#》不一样的是,本书并没有全面地去介绍公共语言运行时的各个组成部分,而是选择了几个重要的主题,包括异常、多线程、GC以及JIT,并详细介绍它们的实现原理与实现细节,且会给出相关的汇编代码以及数据结构,我相信深入的学习比浅显的带过更有意义。作为基础内容,本书还会介绍MSIL与x86汇编,其中MSIL部分由微软的MVP刘浩杨编写,刘浩杨是 .NET的AOP框架AspectCore的作者以及APM框架SkyWalking的 .NET探针的主要开发者,对 .NET框架以及MSIL有着非常深入的理解。
虽然本书与 .NET Core源代码的关联比较大,部分章节也会给出相关的源代码链接,但理解本书的内容不需要阅读源代码,本书包含了大量的图表用于解释数据结构与处理流程,如果您是一个拥有一年以上经验的 .NET开发者,并且对 .NET运行时的底层实现有兴趣,那么应该可以顺利地阅读本书并理解大部分内容。部分相对难以理解的内容以及源代码链接会放在每节的最后(即“CoreCLR中的相关代码”小节),您可以跳过这部分的内容,对阅读接下来的章节不会有影响。如果您在阅读过程中有疑问,可以浏览网站,在上面找到提问用的仓库并建立issue提问,也可以加入网站上列出的QQ群讨论。不管您是出于兴趣还是工作需要阅读本书,都希望您可以从中得到收获。
第1章公共语言运行时概述1
1.1.1.NET框架简介1
1.1.2公共语言运行时中的各个组成部分3
1.1.3名称规范5
第2章MSIL入门7
第1节逆向 .NET程序到IL7
2.1.1ildasm7
2.1.2使用ILSpy10
2.1.3dnSpy10
第2节基础语法11
2.2.1IL语法格式11
2.2.2IL指令格式17
2.2.3评价堆栈18
2.2.4常用指令19
2.2.5常见的C#代码与IL代码的对比21
第3节流程控制26
2.3.1IL流程控制26
2.3.2常见的流程控制C#代码与IL代码对比28
第3章x86汇编入门37
第1节汇编与机器码37
3.1.1理解汇编语言与机器码37
3.1.2RISC与CISC42
3.1.3流水线42
第2节内存44
3.2.1位与字节44
3.2.2负数的表现46
3.2.3小端与大端47
3.2.4内存地址47
3.2.5虚拟内存48
3.2.6了解虚拟内存的实现50
第3节寄存器50
3.3.1通用寄存器50
3.3.2程序计数器52
3.3.3标志寄存器52
第4节基础指令55
3.4.1汇编指令记法55
3.4.2汇编指令格式56
3.4.3汇编指令简写57
3.4.4基础汇编指令58
3.4.5更多指令68
3.4.6机器码的编码方式68
第5节流程控制69
3.5.1流程控制实现69
3.5.2比较指令70
3.5.3跳转指令73
3.5.4其他流程控制77
3.5.5分支预测79
第6节函数调用82
3.6.1栈结构82
3.6.2函数调用85
3.6.3enter与leave指令89
3.6.4调用规范89
第7节系统调用91
3.7.1系统调用简介91
3.7.2在x86上发起系统调用(软中断)92
3.7.3在x86上发起系统调用(sysenter)93
3.7.4在x8664上发起系统调用(syscall)94
第8节内存屏障95
3.8.1乱序执行95
3.8.2内存屏障简介96
3.8.3双检锁97
第4章编译与调试CoreCLR100
第1节在Windows上编译CoreCLR100
4.1.1准备编译环境100
4.1.2下载CoreCLR源代码101
4.1.3编译CoreCLR102
4.1.4使用编译出来的CoreCLR103
4.1.5最新的编译文档103
第2节在Windows上调试CoreCLR104
4.2.1使用Visual Studio调试CoreCLR104
4.2.2使用WinDbg调试CoreCLR105
4.2.3在WinDbg中使用SOS扩展109
4.2.4更方便地调试托管方法对应的汇编代码113
第3节在Linux上编译CoreCLR113
第4节在Linux上调试CoreCLR116
4.4.1使用LLDB调试CoreCLR116
4.4.2在LLDB中使用SOS扩展119
第5章异常处理实现126
第1节异常处理简介126
5.1.1通过返回值报告错误与通过异常报告错误的区别126
5.1.2.NET中的异常处理129
第2节用户异常的触发132
5.2.1用户异常132
5.2.2通过throw关键词抛出异常133
5.2.3调用 .NET运行时内部函数抛出异常135
5.2.4JIT编译时自动插入抛出异常的代码135
5.2.5CoreCLR中的相关代码137
第3节硬件异常的触发137
5.3.1硬件异常137
5.3.2访问null对象的字段时抛出异常138
5.3.3调用null对象的方法时抛出异常142
5.3.4对整数进行零除时的处理144
5.3.5CoreCLR中的相关代码146
第4节异常处理实现146
5.4.1异常处理的过程146
5.4.2捕捉异常并获取抛出异常的位置147
5.4.3通过调用链跟踪获取抛出异常的函数与所有调用来源148
5.4.4获取函数元数据中的异常处理表150
5.4.5枚举异常处理表调用对应的finally块与catch块151
5.4.6重新抛出异常的处理151
5.4.7CoreCLR中的相关代码153
第5节异常处理对性能的影响154
第6章多线程实现158
第1节原生线程158
6.1.1原生线程简介158
6.1.2上下文切换159
6.1.3线程调度161
6.1.4栈空间161
第2节托管线程162
6.2.1托管线程简介162
6.2.2托管线程对象163
6.2.3创建托管线程的例子163
6.2.4前台线程与后台线程164
6.2.5CoreCLR中的相关代码166
第3节抢占模式与合作模式166
6.3.1切换模式的实现167
6.3.2CoreCLR中的相关代码169
第4节线程本地储存169
6.4.1ThreadStatic Attribute属性的实现171
6.4.2ThreadLocal类的实现172
6.4.3CoreCLR中的相关代码175
第5节原子操作175
6.5.1原子操作简介175
6.5.2.NET中的原子操作179
6.5.3无锁算法182
6.5.4CoreCLR中的相关代码183
第6节自旋锁184
6.6.1线程锁184
6.6.2使用Thread.SpinWait实现自旋锁185
6.6.3使用System.Threading.SpinWait代替187
6.6.4使用System.Threading.SpinLock实现自旋锁188
6.6.5Thread.Sleep(0)与Thread.Yield的区别189
6.6.6使用pause指令的另一个原因190
6.6.7CoreCLR中的相关代码190
第7节互斥锁191
第8节混合锁与lock语句197
6.8.1线程中止安全200
6.8.2CoreCLR中的相关代码201
第9节信号量204
6.9.1轻量信号量206
6.9.2通过信号量实现生产者—消费者模式206
6.9.3通过Monitor类实现生产者—消费者模式208
6.9.4CoreCLR中的相关代码210
第10节读写锁213
第11节异步操作216
6.11.1阻塞操作216
6.11.2事件循环机制217
6.11.3异步编程模型219
6.11.4异步编程模型的实现原理221
6.11.5任务并行库224
6.11.6任务并行库的实现原理226
6.11.7ValueTask229
6.11.8async与await关键字的例子230
6.11.9async与await关键字的实现原理231
6.11.10堆积的协程与无堆的协程239
6.11.11CoreCLR中的相关代码239
第12节执行上下文242
6.12.1异步本地变量与执行上下文242
6.12.2CoreCLR中的相关代码247
第13节同步上下文248
6.13.1同步上下文的使用例子(基于WinForm)249
6.13.2自定义同步上下文实现252
6.13.3CoreCLR中的相关代码258
第7章GC垃圾回收实现260
第1节GC简介260
7.1.1栈空间与堆空间260
7.1.2值类型与引用类型261
7.1.3.NET中的GC263
7.1.4垃圾回收VS引用计数271
第2节对象内存结构271
7.2.1值类型对象的内存结构271
7.2.2引用类型对象的内存结构273
7.2.3存活标记与固定标记276
7.2.4装箱与拆箱277
7.2.5CoreCLR中的相关代码278
第3节托管堆结构280
7.3.1.NET程序的内存结构280
7.3.2托管堆与堆段282
7.3.3分配上下文284
7.3.4分代的实现286
7.3.5自由对象列表287
7.3.6跨代引用记录289
7.3.7析构对象列表与析构队列291
7.3.8CoreCLR中的相关代码291
第4节分配对象流程293
7.4.1new关键字生成的代码293
7.4.2从托管堆分配空间的内部函数297
7.4.3分配小对象的流程299
7.4.4分配大对象的流程299
7.4.5记录包含析构函数的对象到析构对象列表302
7.4.6CoreCLR中的相关代码302
第5节垃圾回收流程303
7.5.1GC的触发303
7.5.2执行GC的线程306
7.5.3GC的总体流程307
7.5.4重新决定目标代309
7.5.5判断是否应该执行后台GC311
7.5.6CoreCLR中的相关代码312
第6节标记阶段314
7.6.1获取根对象314
7.6.2递归扫描根对象并设置存活标记315
7.6.3通过卡片表扫描跨代引用并设置存活标记318
7.6.4枚举强引用GC句柄并设置存活标记318
7.6.5枚举固定GC句柄并设置固定标记319
7.6.6枚举弱引用GC句柄并清空不再存活对象引用319
7.6.7扫描析构对象列表并添加不再存活对象到析构队列319
7.6.8枚举跟踪复活弱引用GC句柄并清空不再存活对象引用320
7.6.9决定是否启用升代320
7.6.10CoreCLR中的相关代码321
第7节计划阶段323
7.7.1构建Plug树323
7.7.2构建Brick表324
7.7.3模拟压缩325
7.7.4判断是否执行压缩与新建短暂堆段327
7.7.5CoreCLR中的相关代码328
第8节重定位阶段328
7.8.1修改对象引用地址328
7.8.2CoreCLR中的相关代码330
第9节压缩阶段330
7.9.1复制对象值330
7.9.2结束GC332
7.9.3CoreCLR中的相关代码333
第10节清扫阶段333
7.10.1创建自由对象并加到自由列表333
7.10.2结束GC334
7.10.3CoreCLR中的相关代码334
第11节后台GC335
7.11.1后台标记阶段335
7.11.2后台清扫阶段336
7.11.3CoreCLR中的相关代码337
第12节调整GC行为338
7.12.1设置GC模式338
7.12.2设置延迟模式339
7.12.3设置延迟等级340
7.12.4开启无GC区域341
7.12.5开启大对象堆压缩342
7.12.6保留堆段空间地址342
7.12.7更多选项(针对 .NET Core)343
第13节获取GC信息344
7.13.1获取GC执行次数344
7.13.2注册完整GC触发前的通知345
7.13.3在Windows系统上使用ETW捕捉GC事件347
7.13.4在Linux系统上使用Lttng捕捉GC事件350
7.13.5使用EventListener捕捉GC事件351
第8章JIT编译器实现354
第1节JIT简介354
8.1.1JIT编译器354
8.1.2.NET中的RyuJIT编译器356
8.1.3在Visual Studio中查看生成的汇编代码356
8.1.4使用JITDump日志查看JIT编译流程与生成的汇编代码357
第2节JIT编译流程358
8.2.1JIT的触发358
8.2.2分层编译360
8.2.3JIT编译流程362
8.2.4CoreCLR中的相关代码363
第3节IR结构366
8.3.1HIR与LIR366
8.3.2HIR的结构367
8.3.3HIR的例子367
8.3.4LIR的结构372
8.3.5LIR的例子372
8.3.6常见的HIR结构376
8.3.7CoreCLR中的相关代码382
第4节IL解析383
8.4.1创建本地变量表383
8.4.2创建基础块列表383
8.4.3创建异常处理表384
8.4.4构造语法树385
8.4.5CoreCLR中的相关代码386
第5节函数内联387
8.5.1内联的条件388
8.5.2内联的处理389
8.5.3CoreCLR中的相关代码390
第6节IR变形390
8.6.1添加内部代码390
8.6.2提升构造体391
8.6.3标记暴露地址的本地变量393
8.6.4对基础块中的各个节点进行变形操作393
8.6.5消除三元条件运算节点396
8.6.6CoreCLR中的相关代码398
第7节流程分析399
8.7.1计算前任基础块与后任基础块399
8.7.2计算边缘权重(Edge Weight)400
8.7.3调整基础块顺序400
8.7.4计算可到达的基础块400
8.7.5计算支配与支配边界401
8.7.6插入GC检测点402
8.7.7添加小函数402
8.7.8CoreCLR中的相关代码403
第8节本地变量排序404
8.8.1根据引用计数排序本地变量404
8.8.2CoreCLR中的相关代码404
第9节评价顺序定义405
8.9.1决定语法树节点的评价顺序405
8.9.2CoreCLR中的相关代码405
第10节变量版本标记406
8.10.1SSA406
8.10.2构建SSA407
8.10.3构建VN410
8.10.4CSSA与TSSA411
8.10.5CoreCLR中的相关代码411
第11节循环优化413
8.11.1循环的结构413
8.11.2循环反转415
8.11.3循环克隆416
8.11.4循环展开417
8.11.5循环不变代码外提418
8.11.6CoreCLR中的相关代码419
第12节赋值传播420
8.12.1替换拥有相同值的变量420
8.12.2CoreCLR中的相关代码421
第13节公共子表达式消除421
8.13.1合并拥有相同值的表达式421
8.13.2CoreCLR中的相关代码422
第14节断言传播424
8.14.1生成并传播断言424
8.14.2CoreCLR中的相关代码425
第15节边界检查消除426
8.15.1根据断言消除边界检查426
8.15.2CoreCLR中的相关代码427
第16节合理化427
8.16.1转换HIR结构到LIR结构427
8.16.2转换LCL_VAR节点428
8.16.3转换ADDR与IND节点428
8.16.4删除COMMA节点430
8.16.5CoreCLR中的相关代码430
第17节低级化431
8.17.1分割针对long类型的操作431
8.17.2转换算术运算到地址模式431
8.17.3转换除法运算和求余运算431
8.17.4转换SWITCH节点433
8.17.5针对函数调用添加PUTARG_REG与PUTARG_STK节点435
8.17.6转换CALL节点436
8.17.7标记节点是否为被包含节点440
8.17.8标记节点被使用时是否需要先加载到CPU寄存器440
8.17.9CoreCLR中的相关代码441
第18节线性扫描寄存器分配442
8.18.1寄存器分配442
8.18.2线性扫描寄存器分配简介442
8.18.3CoreCLR中的相关代码450
第19节汇编指令生成451
8.19.1计算帧布局451
8.19.2生成汇编指令453
8.19.3包含异常处理小函数的汇编代码456
8.19.4CoreCLR中的相关代码459
第20节机器代码生成460
8.20.1生成机器码与元数据460
8.20.2CoreCLR中的相关代码463
第21节函数头信息464
8.21.1除错信息的结构465
8.21.2异常处理表的结构466
8.21.3GC信息的结构466
8.21.4函数对象的结构467
8.21.5栈回滚信息的结构467
第22节AOT编译468
8.22.1使用.NET Framework的NGen工具执行AOT编译469
8.22.2使用.NET Core的CrossGen工具执行AOT编译469
附录A中英文专业名词对照表472
附录B常用IL指令一览480
附录C常用汇编指令一览485
附录DSOS扩展命令一览489
附录EIR语法树节点类型一览517
参考文献523