C#在ios系统上的语法限制引出的一些话题
http://ty-m.com/blog/2014/07/14/csharp在ios系统上的语法限制引出的一些话题/
以目前的开发者生态来说,在ios平台下探讨C#相关的话题不是那么的主流,可能的应用场景主要集中在:
- 使用Unity3D + C# 进行游戏开发
- 直接基于 Mono – Xamarin 分支进行跨平台应用程序开发
在使用Unity三维引擎进行工作的时候,基于状态机模型的程序设计使得编程语言的使用相对简单,但笔者近期在为公司的多机体感互动项目构建网络通讯框架(局域网,基于Tcp,星拓扑)时,使用了大量的C#语法特性,这些特性的使用最终导致Unity构建出来ios平台程序无法正常工作,以下为 Xamarin.iOS 编译C#语言的一些语法特性限制:
- 任何动态代码生成的特性不可用
- 由上,泛型虚方法是不可行的
- 由上条,反射API中的 System.Reflection.Emit 不可用(不涉及运行期编译的反射调用是没有问题的)
- 无法从 NSObject 派生具有泛型参数的子类 (ios 7.2.1 后的版本允许某些情形下可用)
- 从泛型类进行 P/Invokes 调用是不允许的
- 关于 反射API ,Property.SetInfo 赋值为 Nullable<T> 是不允许的
- 在 Dictionary<TKey, TValue> 中使用值类型作为键,在对该字典做包含该值键的操作时,将会崩溃
- 反向调用,即从非托管代码激发托管代码的时候,函数指针到委托包装类的转换程序基于 JIT 编译器,JIT 编译器在ios下处于禁用模式,这就使得该调用存在一些限制:所要传递的C#方法必须为静态类方法;方法必须标注有 MonoPInvokeCallbackAttribute
完整的特性限制清单请戳官方文档
这里简单解释一下泛型虚方法为什么必须基于 RTTI(Run Time Type Identification):
《Thinking in C++》 volume 2 —— Member template functions cannot be declared virtual.Current compiler technology experts to be able to determine the size of a class’s virtual function table when the class is parsed.Allowing virtual member template functions woule require knowing all calls to such member functions everywhere in the program ahead of time.This is not feasible,especially for multi-file projects.
简单的来说,C++编译器在需要确定一个类的规格的时候,虚表的尺寸必须是在编译期确定的。由于虚函数的动态绑定正是将行为实现的源延迟到运行时方能确定,这也就造成了同时附带泛型声明的虚函数无法在编译时靠代码的静态分析精确得出所有可能的泛型特化,除非在所有相关联的代码文本中搜索出该虚函数的所有可能调用,并对类继承链条上的该虚函数所有覆写进行特化。而这么做在复杂的工程中是不现实的,并且有显然的造成代码膨胀的风险,因此自然也就无法满足虚表尺寸在编译结束时必须确定的要求了。
这里需要说明的是,这样的能力限制并不是体现了C++的无能,而是语言的设计者本身渴望在程序设计工业化的背景下寻找出语言运行时能力与简洁高效的汇编目标代码之间的平衡点。
现在话题回到C#,由上阐述可知编译泛型虚方法需要额外的运行时信息,也就是在运行时完成对特定泛型虚方法的调用绑定并尝试进行第一次调用之前,必须有一套编译机制能够从主调代码代入所需的泛型信息,特化出该泛型方法的执行体,最后再完成虚函数的绑定与调用。这一过程需要 JIT(Just in Time)。C++的编译结果为纯粹的目标机器码,不具备 JIT 能力与可以被 JIT 二次解析的中间代码,因此没有运行时代码生成能力,这也是C++ 有别于 Java、C#一类半静态半动态语言的重要特征。
不幸的是 Mono运行时所需的 JIT 在ios真机运行时是不可用的(模拟器下没有禁用 JIT所需的系统服务,可用) 。不得不说,ios系统对 运行期动态代码生成 的限制,对编程的灵活性还是产生了比较大的阻碍,它是反射与元编程等 现代程序设计技巧的重要基础,但这样的限制付出的代价所换来的回报则是相对更为安全的系统运行环境,系统所面临的攻击风险更小,苹果操作系统的安全性有目共睹(虽然这样的安全成绩更多的是因为其嵌入式系统的本性所得到的)。那么,为什么禁用了操作系统的运行时代码编译服务,能够抵御部分的代码攻击呢?这要从其所依赖的一种更基础技术开始说起:Self-modifying code
Self-modifying code
可自修改代码,顾名思义为支持在代码本身执行期间,以代码自身作为被操作数据进行修改的一种代码技术。汇编是最早的支持自我修改的代码语言,也是后世众多支持自修改特性的高级语言的基础,当汇编代码向上封装到C等较为高级的程序语言时,语言的自修改特性一度消失了,直到带有反射特性的编程语言出现。
汇编语言实现代码的自修改是简单而直接的:在内存中创建一段全新的指令序列(亦或是在非保护内存段内覆写先前的代码块),恰当的执行时机修改 CP 寄存器的值到所需执行的新指令码地址,继续执行。
在高级语言中,调换 CP寄存器,覆写内存等细节被隐藏了,代码的运行时创建和自修改变得直观:
1 2 3 4 |
var f = function (x) {return x + 1}; // assign a new definition to f: f = new Function('x', 'return x + 2'); |
由此可见,Self-modifying code 是 JIT 提供运行时动态代码生成的基础。
Self-modifying code 的用途主要有:
- 优化具有状态依赖判断并进行逻辑分支的循环
- 运行时代码生成
- 实现 闭包 等语言语法糖
- 子程序修补,动态链接库技术
- 进化计算,遗传算法,自参照的机器学习系统(Self-referential machine learning systems)
- 实现代码隐藏,配合加壳算法使得程序更难以逆向破解,程序的目的更难以察觉
攻击与保护机制
在现代操作系统中,许多攻击的风险与案例并不是由于程序修改自身引起的,而是由于系统漏洞使得程序被第三方纂改,如果应用程序在使用加载器加载后不做代码段与数据段的保护,代码运行时修改的能力将很有可能被用来进行缓冲区溢出攻击。
W^X 是 OpenBSD 引入的安全机制,致力于解决运行时代码段的被纂改问题,这一安全策略规定进程的地址空间中的每一个页仅仅允许处于“可写” 或 “可执行” 状态中的其一,这一简单粗暴的安全策略使得操作系统从非硬件层的最底层对执行程序做出了保护。OpenBSD 由 NetBSD 衍生而来,它门的平行分支 FreeBSD 后来衍生出了许多著名的项目,Apple 的 OS X 与 ios 系统在 FreeBSD 之上派生出了大量的代码。
可以说,ios 系统使动态代码生成的系统服务不可用,还是能有效的杜绝运行时代码被纂改引发的安全风险。
AOT(Ahead of Time Compilation)
如题,提前编译 技术是一种解决托管代码在无法发挥 JIT 能力的系统平台上进行运行的编译解决方案。当 JIT 不可用,托管代码向动态类型中间码转化是没有意义的,AOT 编译一次性将高级语言全部或部分向目标机器码(亦或是静态虚拟机的中间码,如 LLVM)转化。AOT 也常常被用于需要追求代码启动速度与执行效率的场合,因其能较 JIT 执行更为复杂的编译优化策略,也因为其绕过了 JIT 的装载、二次编译等流程。