Visual C++ 2005的现代语言特性
本文讨论:
.NET C++/CLI语法
配置向导优化(PGO)
MSIL优化
OpenMP支持
增强的缓冲区安全检查
当年Visual Studio® .NET 2003 C++编译器的面世,就令无数C++语言爱好者对此垂涎不已;它与ISO C++标准保持98%的一致——这比以前任何一个版本都更加接近,和一体化的语言支持特性,如部分模板专用化;同时也包括了增强的缓冲区安全检查和改进的编译器诊断功能。以往使用C#和Visual Basic® .NET的开发者,通过简单的拖放操作,就能生成稳健的Windows窗体应用程序,如今,C++的开发者也加入了进来,而且,这个编译器还对Intel Pentium 4和AMD Athlon处理器作了优化处理。
如果Visual C++ .NET 2003都令你兴奋不已,那么它的最新版本Visual C++ 2005,足够令你高兴得发狂了。对于 .NET开发,Visual C++ 2005如今有了一套新的语法,不但显得更“优雅”,而且更强大;它拥有新的优化技术,可使程序的速度最高提高30%;同时新的编译模式,可保证在Microsoft® .NET Framework之上生成完全遵从CLI(Common Language Infrastructure)的可验证代码;而且新的interop模式,可提供本地与托管代码的无缝融合。相对于前两个版本,新编译器还包括了一个增强版本的缓冲区安全检查选项,而且对C++程序普遍使用的库,也有新的注重安全的版本。Visual C++ 2005对基于Intel Itanium和AMD64的64位平台,提供了对OpenMP标准的支持,并且修正了一些混合DLL加载的问题,对Double P/Invoke(双转换)的性能问题,提供了自动运行时消除。关于类似的增强和改进还有很多很多,正如一位C++架构师说的那样,“如今,C++终于站在了它应该站的位置上”。
C++/CLI 新的语法
可能我们中有不少人都发现,要在前两个版本的C++中使用托管扩展语法,会非常麻烦并且错误重重,也许还会觉得,Visual C++并非是 .NET开发的首选语言。微软Visual C++开发小组在广泛听取意见的基础上,重新改进了Visual C++ 2005,在Visual Studio .NET 2002中那种“笨拙”的C++托管扩展语法已经一去不复返了,而修订后的语言定义带来的是一个完全充满吸引力的新语法。
在语言设计上,微软Visual C++开发小组制定了一些主要的目标。首先(对那些认为编程是艺术的人来说,也许是最重要的),他们要保证在开发者在编写C++代码时感觉自然,为达到这个目的,他们对ISO C++标准作了一个在语法上优雅的纯粹扩展,目的是为了在像单击部署、窗体设计支持和SQL Server™ 2005的托管代码支持这些地方,可更简单地用C++编写可验证的代码。他们想要设计出一个超过C++的语言,要带给C++全部的 .NET能力,同时也要带给 .NET全部的C++能力。现在看来,他们非常的成功。
新的扩展规范被称为“C++/CLI”,并且正在被标准化。在阅读代码时,最引人注意之处就是,以往那种在托管扩展中定义垃圾回收类、属性等等常用到的双下划线关键字,现在已成为了历史。虽然也有一些类似的关键字被保留,但由于不会被经常用到,所以不会对代码的可读性造成影响。这些双下划线关键字如今被两种新的“关键字”取代:“上下文敏感”和“空格”。“上下文敏感”关键字是只在特定的上下文中,才是关键字;而“空格”关键字只在联合其他关键字时,才会是关键字。例如:在托管扩展中的关键字__property已被关键字property取代(不只是这样,定义和访问属性的整个语法都被精炼了,它的声明与C#看起来很相似,参见插1),而且在你的代码中,还可以使用property作为变量名,只有在一个类型中声明属性时,“property”才会被当作一个关键字。
插1:语法比较
托管扩展语法
public __gc __sealed class Student
{
private:
double m_grade;
String* m_name;
public:
__property double get_Grade() { return m_grade; }
__property void set_Grade(double newGrade) { m_grade = newGrade; }
__property String* get_Name() { return m_name; }
__property void set_Name(String* newName) { m_name = newName; }
}
C++/CLI语法
public ref class Student sealed
{
private:
double m_grade;
public:
// 标准属性语法
property double Grade
{
double get() { return m_grade; }
void set(double newGrade) { m_grade = newGrade; }
}
//其他属性
property String^ Name;
}
在新语法中的类型被声明为“什么的类”,前置的形容词描述了你将创建什么样的类,如下所示:
class
N { /*…*/ }; //本地类型
ref class
R { /*…*/ }; // CLR引用类型
value class
V { /*…*/ }; // CLR值类型
interface class I { /*…*/ }; // CLR界面接口类型
enum class
E { /*…*/ }; // CLR枚举类型
在前一个语言版本中,声明类型的方法,表明了它生存和工作的方式。只有本地类、结构和托管值类型才可以创建在堆栈中,托管引用类始终在托管堆中。在Visual C++ 2005中,所有的类型,不管是本地还是托管,通过使用基于堆栈的确定性清理语义,都能被创建在堆栈中。
为在本地堆中创建类型T的一个对象实例,可使用“new T”,它返回一个指向本地堆中对象位置的指针(在Visual Studio .NET 2002和Visual Studio .NET 2003中为__nogc)。而在托管堆中生成类型T的一个对象实例,Visual C++ 2005引入了关键字gcnew,“gcnew T”将返回整个对象在托管堆中的句柄。句柄在Visual C++ 2005中被当作一个新的结构,有点类似于托管扩展的__gc指针;如今要在堆栈上生成一个类型T的实例,标准的“T t”声明就足够了。
在此说明一下是如何定义一个实例,托管引用类建立于托管堆中,本地类型建立于堆栈或本地堆中,当一个托管引用类被声明建立在堆栈中时,编译器实际上还是在托管堆中把它实例化,如插2:
插2:
插2中所示的托管引用类型也带来了一些问题:当一个堆栈中的实例对象超出了范围,将会有什么问题发生呢?它是怎样被清理的呢?许多C#的开发者抱怨C#语言缺乏确定性清理,C#语言可通过使用关键字,非常容易地清除IDisposable的对象,但这需要额外的代码,并且与C++开发者熟悉的析构模式相比,显得有点晦涩。在C#中,安全清理在默认状态下是关闭的,要打开它,还需要显式编码。举例来说,看一下插3中的C#代码片断,对象StreamReader声明在托管堆中,当类中的方法执行完毕后,对StreamReader的实例就不再有任何引用了,但是这个对象仍然不会结束,直到垃圾回收器回收它;而且,文件不会被关闭,程序依然拥有打开文件的句柄。要加入确定性清理,必须通过那些使用非托管资源的类来实现IDisposable接口。
插3中的第二个示例,演示了C#中新的代码,此处的代码仍具有不错的可读性,但一旦引入更多需要清理的对象时,代码就会变得越来越难读懂了,同样,当垃圾回收器最终运行时,任何忘记清除的对象,都会加重最终结束器线程(finalizer)的负担,同时,还有可能锁住高价值资源。而且,当用Visual Basic .NET来实现同样的功能时,这种编码形式会变得更加丑陋(虽然Visual Basic 2005也有一个类似于C#的Using声明)。
插3:确定性清理代码
实现代码
C#(没有确定性清理)
string ReadFirstLineFromFile(string path)
{
StreamReader reader = new StreamReader(path);
return reader.ReadLine();
})
C#(有确定性清理)
string ReadFirstLineFromFile(string path)
{
using (StreamReader reader = new StreamReader(path))
{
return reader.ReadLine();
}
}
Visual Basic .NET(有确定性清理)
Function ReadFirstLineFromFile( _ByVal path As String) As String
Dim reader As StreamReader
Try
reader = New StreamReader(path)
ReadFirstLineFromFile = reader.ReadLine()
Finally
If Not reader Is Nothing Then _
CType(reader, IDisposable).Dispose()
End Try
End Function
C++(有确定性清理)
String^ ReadFirstLineFromFile(String^ path)
{
StreamReader reader(path);
return reader.ReadLine();
}
现在,Visual C++ 2005对任何类型的托管和本地对象,都提供了一个析构函数或结束器(finalizer)。当类型是托管时,编译器映射一个析构函数到IDisposable::Dispose方法中,这意味着,你可用C++来编写同样的方法——如插3中的第四段代码,而且reader的析构或清除方法会被自动调用,就好像在C#中使用“using”一样。这样,当创建在堆栈上的类型超出作用范围之后,它的析构函数就会被调用。
托管扩展带来的最大问题就是指针问题了,虽然指针很难理解,但却是应付多种任务和多种情况的“多面手”。在Visual C++ 2005中,指针仍旧是老式的C++指针,它指向一个对象,并能执行一些算法。引用一个对象的指针,它的生存期必须由开发者显示地管理,当与指针打交道时,运行时库可不负责清理它。
现在,来看一下Visual C++ 2005的设计者是怎样实现的吧,Visual Studio .NET 2003和Visual Studio .NET 2005中的new操作符通常返回一个指针,而gcnew操作符返回一个“句柄”,一个用脱字符 ^ 语法表示的一个结构,此句柄指向托管堆的对象。因此,它们不能指向interior类型,而且在用法上,编译器也作了不少限制,以便开发者正确、安全地使用它们。句柄不能执行指针算法,也不能转换成一个空指针或任何其他整数类型,话说回来,依然可使用星号(*)和箭号(->)操作符。
这不是说,你再不能取得一个指向垃圾回收堆中的指针了,在Visual C++ 2005中的pin_ptr,可用于取得托管堆中对象的一个固定指针,只要这个指针存在,对象就被固定在托管堆中,以防止垃圾回收器清除它;Visual C++ 2005同时也引入了“引用跟踪”操作符,以百分号 % 表示。当年在C++中引入 & 引用操作符时,大多数开发者把它理解成一个指向对象的指针,而且由编译器自动解引用。在很多方面来说,% 之于 ^,就像 & 之于 *。
在托管世界中,对托管对象的本地引用,与指向托管对象的本地指针一样危险,指针和引用的基本原理在于,被引用的对象不能四处移动。引用跟踪与本地引用非常类似,除了它引用的对象是在托管堆中,而且被垃圾回收器移动之后,还能继续跟踪它们。百分号 % 操作符用来取托管对象的地址,就像 & 操作符对于本地对象的功能一样,百分号 % 操作符可返回一个托管引用类型对象的句柄。
一般来说,C++开发者知道ISO标准控制着语言的发展方向,正是因为这个原因,为了提高被第三方采用的机率,同时保证语言稳步向前发展,这种新语法被提议为一种称作“C++/CLI”的标准。在2003年10月,ECMA(欧洲计算机制造商协会)投票决定建立一个特别工作组——TG5,专门负责分析和采用这个标准,其工作性质就像WG21是ISO C++的管理单位一样,实际上,WG21中的关键组员也同时服务于TG5中。计划在2004年底,把C++/CLI标准化。
Interop
在Visual Studio .NET 2003的所有基于 .NET Framework的语言当中,Visual C++ 7.1提供了最好的interop功能。它可不像是在纸上谈兵,如今具有了足够的能力来实现真实世界中的任意场景,一个最好的例子就是把Quake II移植到 .NET Framework平台上,而Visual C++ 2005则更加扩展了这项功能。
在本地和托管世界中,.NET有四种主要的方法可进行interop。对COM的interop可使用RCW(Runtime Callable Wrappers)和CCW(COM Callable Wrappers),而CLR主要负责类型调度(除了在极少数情况下,使用了定制的调度器),但这些调用是开销巨大的。因为接口极其复杂,所以必须加倍小心,否则将会导致严重的性能损失,而且还要保证这些包装器的底层组件是最新版本的。这就是说,当你试图引入大量本地COM代码时,只有对一些简单的情况而言,COM interop才非常有用。
第二种使用interop的方法是通过P/Invok,一般通过使用DLLImport属性,对需要引入的函数,在方法声明上指定此属性,根据声明是怎样指定的,从而决定调度怎样处理。然而,只有在一个DLL通过export属性有输出函数时,DLLImport才能起到作用。
如果想从本地代码中调用托管代码,可选择CLR宿主。在这种情况下,本地程序必须做好一切相关工作:设置宿主、绑定运行时、启动宿主、取回相应的AppDomain、设置调用上下文、定位所需的程序集和类、在相应的类上调用操作。这就是一个稳健的解决方案需要做的一切事情,很单调乏味,并且需要很多人工代码。
第四种,也可能是最容易和高效的一种方法,就是使用C++中的interop,通过设置/clr,编译器将生成MSIL代码——而不是本地机器码,其中包括了那些使用内联汇编的函数和CPU特定指令集,如SSE;/clr正是Quake II移植到 .NET平台的关键之处。在此不需要加入其他任何二进制代码,只是简单地包含一些相应的头文件,托管C++就能和本地C++互相调用,对开发者来说,不需做任何工作就能实现这一点,何乐而不为呢。实际上,编译器处理了相应转换器的创建,并在托管和本地两个世界中来回奔波。
对C++开发者来说,以上的结果还涉及到更多的方面,其中一个,就是在Visual Studio .NET 2002和Visual Studio .NET 2003中声名狼藉的混合DLL加载问题。如果你在加载器锁(loader lock)中运行本地代码,但又引用了程序集中一个未被加载的托管类型,此时,CLR会调用LoadLibrary来加载这个程序集。LoadLibrary会试图取得加载器锁,在这一点上,造成了程序死锁。而这个问题,在新版本的Visual Studio中,已经解决了。
虽然/clr给C++开发者带来了巨大的好处,但也有一些不利的方面。如之前所提到的,/clr会生成包含本地和托管代码的映像文件,这在有些时候会带来问题。首先,这些混合的映像文件不遵从CLI,它们有本地进入点,当超出托管边界时,会带来严重的转换性能损失。但最重要的是,本地进入点会对使用程序集和反射(reflection)的工具造成严重的影响。反射之前要先检查一个映像文件,此时必须先加载并运行程序集,只有在所有的初始化完成之后,反射才可能检查元数据,而此时却因为包含了本地进入点,造成反射不能正确地加载一个托管程序集。
此外,Visual Studio .NET 2003只在极少数情况下,能生成可验证代码,即使生成了,也需花一大番气力。然而,MSIL对不可验证指令提供了第一类的支持(指针算法、间接加载、访问本地堆),可验证代码也使你可参与到可信赖计算中,反过来,也极大地丰富了Visual Studio 2005的功能。单击部署基于局部可信赖计算,就像托管代码宿于SQL Server 2005中一样。Visual C++ 2005开发小组一个主要目标,就是使编译器可生成非混合及可验证映像,为此引入了两个新的编译器选项:/clr:pure和/clr:safe,但在研究这两个新选项之前,还需要弄清楚C++ interop是怎样工作的。
It Just Works
在Visual Studio .NET 2003,C++的interop技术叫做IJW或者“It Just Works”,在新版本中,已换成了更贴切的“Interop技术”。那它的工作原理是怎样的呢?对程序中的每一个本地方法,编译器同时生成一个托管和非托管进入点,它们中只有一个是真正方法的实现,另一个则是转发器,可进行相应的转换和必要的调度。托管进入点通常是真正方法的实现,除非代码不能解释为MSIL或开发者使用“#pragma unmanaged”强制指定进入点的实现为本地机器码。
当一个IJW转发器起作用时——例如转发到本地代码中,编译器提供转换的实现,并且通过偏移或IAT(Import Address Table)调用实际的实现代码;虽然针对特别开发的示例程序,转发器的一次调用所耗费的时钟周期可降到10,但通常来说,会在50至300个时钟周期之间。当转发器是MSIL时,将使用托管的P/Invoke,因为P/Invoke是由一系列声明和非真正代码实现组成的,所以在运行时,由CLR对转换提供支持;相对于本地代码,同样的转发器实现只是稍稍慢一点。
正如前面所提到的,对于每个函数,IJW都有两个进入点,托管和非托管,但它们的结构决定了,对进入点的调用,在编译时就要确定好(例如指针和vtable)。编译器在编译时,可不知道运行时的调用点托管状态,那它会选择哪一个进入点呢?在Visual Studio .NET 2003中,编译器始终会选择非托管的进入点,这样在调用者是托管代码时,这会产生一个问题,我们称其为“Double P/Invoke”,如插4所示。在这种情况下,非托管转换器对托管调用的转换,结果又被转换回到了托管代码中。
插4:Double P/Invoke问题
对此,Visual C++ 2005提供了一些解决方案,首先,是关键字__clrcall,它可以指定是否发布非托管进入点,在函数声明时,加上此关键字可防止产生非托管进入点(不利之处在于此函数以后都不能从本地代码中直接调用了);同时,关键字__clrcall也能加在函数指针前。其次,是由Visual C++ 2005提供的Double P/Invoke自动消除,通过运行时检查和cookie,并由运行时决定是否可以跳过非托管转换器,而直接把调用转发到托管进入点中。
最后一个解决方案就是纯MSIL了,新的编译器选项/clr:pure,可使编译器生成一个不含本地结构的纯托管映像。实际上,它不但可生成遵从CLI标准的程序集,而且通过防止产生非托管转换很好地解决了Double P/Invoke问题。这样,对每一个函数,只有一个进入点——托管进入点,因为没有了非托管进入点,vtable和函数指针也就不存在了。
虽然代码遵从CLI标准,但不一定意味着它是可验证的,而在例如文件共享等那些低度可信赖环境中,可验证性是非常重要的。为此,微软引入了一个更严格的编译器选项/clr:safe,这对那些需要可验证性代码的C++开发者来说,无疑是雪中送炭。此选项可使编译器生成绝对可验证的程序集,而任何不可验证的程序代码结构,都会导致一个编译时错误。例如,试图编译一个整型指针的变量将会导致错误“int* = this type is not verifiable”,并会指出是在代码中哪一行。而SQL Server 2005存储例程里的所有托管C++代码,都必须用此选项编译。
插5:编译模式
插5图示了托管和非托管的数据及代码结构,它们由不同的编译器选项生成。如果不包含任何/clr选项,将生成本地映像;使用/clr将生成包含托管和非托管代码及数据的混合映像。而纯MSIL,是通过/clr:pure选项生成的,它不会包含任何非托管代码,虽然在可验证及包含本地数据类型方面并不提供保证。在基于 .NET Framework的平台上,安全MSIL是可验证性代码的理想选择;简单地说,这两个新的编译模式可应用于不同的情况,甚至可以完成在以前看来是不可能或很难完成的任务。
优化
一个好的程序员者总会想方设法提高软件的执行效率,编译器的编写者是一种特殊类型的程序员,不仅自身代码要执行效率高,而且由它们生成的代码也必须极有效率。因此,任何一个成功的编译器产品,优秀的后台优化是必不可少的。而在这方面,Visual C++ 2005脱颖而出。
Visual Studio .NET 2002和Visual Studio .NET 2003在C++编译器中引入了一些非常好的优化方式,也花了很大气力改善本地代码的执行效率,加入了对Intel Pentium 4 CPU的SSE和SSE2指令支持。特别值得一提的是,还加入了全局程序优化WPO(Whole Program Optimization),可允许链接器在链接.obj文件时,对整个程序进行优化。这些.obj文件与一般.obj文件有所不同,因为它们不但包含了本地机器码,而且还包含了一些中间语言数据,以便编译器的前端和后台之间进行沟通。链接器可把这些文件当成一个大的整体单元来优化,生成更多的内联函数,进行更好的堆栈对齐,还可在多种情况下,使用定制的函数调用约定。Visual C++ 2005在基于自上而下、从底至上的程序结构分析基础上,在WPO上进行了改进,使之更进一步,而最大的改进之处就是配置向导优化PGO(Profile Guided Optimization)了。
对源代码的静态分析,仍留给了编译器许多未解决的问题。就拿对两个变量的比较语句来说,第一个通常比第二个大吗?在switch语句中,哪一个case子句是经常被执行的呢?哪个函数是经常被调用的,而又哪些代码是“冷代码”——即不经常执行的呢?如果编译器在编译时就能知道代码在运行时的状态,就能进行更好的优化,这就是Visual C++ 2005编译器改进的着力之处。
插6:配置向导优化
插6图示了PGO的编译流程,第一步是编译代码,并把它们链接成由一系列配置计数探测数据组成的配置文件。在WPO下,编译器生成的.obj文件不再包含本地机器码,而由中间语言数据组成。这些计数数据由两部分组成:数值计数与命中计数;数值计数常用来表示变量数值的柱状图,而命中计数用来跟踪程序中的特定代码区域被执行了多少次。先运行一个应用程序,再进行一些通常的操作,就能从这些计数中收集相应的数据,并写入一个配置数据库中。当原始的.obj文件被送往链接器时,配置数据也同时被送回链接器,此时链接器就可以进行分析,以决定采取怎样的优化,并最终生成一个不含配置信息的程序,而此最终版本就可发布给用户使用。
配置向导优化可进行多种多样的优化。基于命中计数,能在每个调用点都决定是否采用内联函数;而数值计数,可使switch和if-else结构重新排列,以便找出最常用的数值,从而避免不必要的检查。代码段也能被重新排列,使最常用的代码能一直执行,而不是强制一些不必要的跳转,从而避免TLB(Translation Lookaside Buffer)发生颠簸和页面调度。
“冷代码”被编译器放置在模块的特定区,以避免上述情况的发生;在某一特定类型的虚拟调用点上,虚拟调用推测能避免vtable查找;局部内联可对“热代码”进行内联化处理。另外,代码的特定区域也能有针对性地进行某种优化,而其他区域进行另外某种优化,例如,“热代码”或小型函数能被指定编译为最快速度(/O2),而“冷代码”或大型函数能被指定编译为占用最小空间(/O1)。
如果十分清楚程序运行的真实情况,可在配置文件生成时,不断地在此模拟情况下运行程序,而最终的程序执行效率将会得到极大的提升。最近,SQL Server使用PGO重新编译,结果在多数应用环境下,可得到最高30%的效率提升;由此看来,微软会使用此技术来编译它的全部产品。要注意的是,不要在配置文件生成时,试图进行完全代码路径覆盖,PGO的中心点是针对普通使用情况,来决定是否优化,如果试图进行完全代码路径覆盖,只会自食其果。
Visual C++ 2005也加入了对OpenMP的支持,OpenMP是一个用于创建多线程程序的开放规范,它由一组pragma组成,指示编译器可把某段代码进行并行处理。不依赖前一个迭代结果的大循环代码就非常适合OpenMP,请看下面简单的拷贝函数,它把数组a和b中的数值相加,存放于数组c中:
void copy(int a[], int b[], int c[], int length)
{
#pragma omp parallel
for(int i=0; i<length; i++)
{
c[i] = a[i] + b[i];
}
}
在多处理器电脑上,编译器将生成多线程来执行此循环的迭代,每个线程都会执行拷贝操作的一个子集。需注意,编译器不会去检查循环是否存在依赖性,因而甚至不会阻止你在一此不适合的情况下使用pragma。如果存在依赖,即使程序对规范而言是正确的,也会得到与预期相反的结果。
虽然OpenMP的最大好处是并行执行如上所示的循环,但顺序代码也能从中得到性能上的提高,“#pragma omp section”可被直接用于区分代码中的非依赖区,允许开发者指定可并行执行的区域,接下来,编译器可生成多线程代码,以在不同的处理器上执行这些代码段。
对使用 .NET的开发者来说,最重要的一个变化就是,当目标平台为MSIL时,编译器会像对待本地代码平台一样,进行绝大多数都相同的优化。虽然现今的JIT即时编译器是在运行时为优化进行分析,但允许C++编译器在初始编译期间进行优化,仍能产生可观的优化效果(相对JIT即时编译器,C++编译器有更多的时间进行它的分析)。Visual C++ 2005是首次对托管类型进行优化,包括循环优化、表达式优化、内联优化,而通常这些是编译器不能进行 .NET代码优化的地方。例如,因为指针算法的不可验证性,将导致强度消减问题;又因为CLR的严格类型和成员访问需要,某些代码可能不会被内联化。另外,优化MSIL也要根据即时编译器所面对的代码,作出一个平衡,举例来说,你可能不想打开一个循环,并把过多的变量暴露给即时编译器,因此,它就必须进行寄存器分配(一个NP-complete问题)。
安全
在2002年,Bill Gates宣布进行的可信赖计算,已对微软公司开发的所有产品带来了深刻的影响。Windows操作系统的开发者花了数月的时间来进行安全方面的训练及代码重审,带来的结果就是,Windows Server™ 2003成了该公司有史以来发布的最安全的操作系统。Microsoft Office 2003也包含了许多的安全功能,像IRM(Information Rights Management)、加强的宏安全、Outlook®中阻止下载HTML等等。而Visual C++ 2005编译器的开发者,在此方面也取得了长足的进步,使编译器及它们生成的代码都更加安全。
Visual Studio .NET 2002在编译器中引入了一个缓冲区安全检查开关/GS,如果编译器认为某些函数可能导致缓冲区溢出攻击,那么此开关可在函数的返回地址之前,在堆栈上分配一定的空间,并在函数入口处,把一个带有已知数值的安全cookie放在此缓冲区中;而在函数退出时,会检查此cookie是否已被损坏,cookie值的改变表示对返回地址,有潜在的覆写(overwrite),此时会产生一个错误,并终止程序。当然,这不能防止所有类型的溢出攻击。
Visual Studio .NET 2003扩展了/GS选项,通过把本地变量在堆栈上排序,以便数组可在比本地变量更高地址的内存中分配空间,从而防止本地变量超出限度。此方法能防止vtable劫持和其他指针攻击。
插7:老式 /GS
而Visual C++ 2005在此基础上又更进了一步,通常当有函数调用发生时,函数激活运行时的结构如插7所示,如果其中的一个本地缓冲区超出限度了,攻击者可能改写堆栈中在此之上的任何东西,包括异常处理、安全cookie、帧指针、返回地址和函数参数。而这些值的大多数被不同的机制所保护(如安全异常处理),但对一个有函数指针作参数的函数来说,仍有机会被溢出。如果一个函数接受一个函数指针(或结构、类中包含有函数指针)作为参数,攻击者就有可能改写指针中的值,使代码执行任何他想要的函数。鉴于此,Visual C++ 2005编译器分析所有可能存在此漏洞的函数参数,并把函数激活时的结构如插8所示排列,此时复制了一份函数参数——并不使用原有的函数参数,并把它们放在堆栈中本地变量之下。如果原有函数参数被溢出改写了,只要副本中的值仍保持不变,整个函数就不会被攻破。
插8:新式 /GS
为遵循“默认安全”的可信赖计算,Visual C++ 2005编译器是默认打开缓冲区安全检查选项,这可保证通过Visual C++编译的软件产品更加安全。事实上,微软已经用此选项重新构建了包括Windows、Office、SQL Server在内的所有产品。
有关Visual C++ 2005另一方面大的改进,就是使代码在编写时,安全思想就始终贯彻其中。绝大多数的应用程序依赖C运行时库(CRT)和标准模板库(STL),而这些库最初在设计时,代码安全并不是处在一个重要的位置,现今常用的很多攻击方法当时并不为人所知,结果,这些库中的多数函数,经常处于一种不安全的使用方法中,为潜在的攻击打开了方便之门。
在Visual C++ 2005中,微软引入了这些库的新版本。在新版本里,这些库已被基于所有的函数都会导致常见的安全问题这一认识而重写,并提供了一些可供替换的更安全的函数版本,对此的长期目标是以这些更健壮的副本来逐步取代它们所有“不安全”的版本。仅在新版本的CRT中,就引入了超过400个新的“安全”函数,并确保所有的指针参数都检查是否带有空值(NULL),所有进行内存复制操作的函数,包括源和目的地址,都明确带有需复制的字节数。
结论
Visual C++ 2005带来了许多激动人心的功能,如:对混合映像的CLR延迟加载、本地AppDomain API、新的declspecs对AppDomains和进程的全局变量可提供更好的支持、模块构造函数、对obj文件和 .NET模块的链接器支持、隐式装箱(boxing)、(C#的开发者可能会喜欢的相同的)XML注释语法、针对 .NET Framework的STL的新版本、param数组、别名提示、新的浮点模式、操作符重载等等;在此难以一一叙述,但相信每一种新的功能,都会给大家带来不同的编程体验。
任何基于 .NET Framework的语言新版本经常会引起这样的疑问:“如果编写 .NET平台程序,那我的开发小组该用哪种语言?”今天,如果你经常有一些本地interop操作,答案就简单了,C++是可用作本地interop操作最容易的语言,而且还最有效率。另外,如果你想移植一个现有的C++程序到 .NET平台,实际上,除了Visual C++,没有更好的路可选,同时,在把现有应用程序移植到 .NET平台上时,这也是微软最推荐的方法。
对开发新的应用程序来说,你可能会问,在选择基于 .NET的语言时,为什么开发者会显得茫然和不知所措,对此,没有一个直截了当的答案,因为每种语言都有它的长处,但对纯 .NET程序来说,C#、Visual Basic、C++的体验是相同的。作为一个开发者,如果你已经习惯了一种特定的语言,没有必要换到另一种去。
也许你会选择C++,不管怎样,在进行任何interop操作时,因为直接内置了对interop的大量支持,所以C++的体验比其他任何一种语言都要好。另外,除了避免资源泄漏和保证程序的正确性,析构函数提供的确定性清理没有任何价值。同时,C++也有一些强大的功能,可与上述CLR提供的功能组合使用,例如,C++不只是支持模板和泛型,而且支持两者结合起来使用,这样,可比单独使用其中一者的功能更强大,也更具表现力;特别地,一个实用的编写库文件的技巧是,编写一个模板,并实现一个泛型接口,这样得到的模板不但功能强大,而且具有高度可伸缩性,如模板特定化,这样其他语言也可直接使用通过模板泛型接口实例化的对象。总而言之,C++绝非浪得虚名,终于站在了它应该站的位置上。
Visual C++
Visual C++ 2005 将为面向 Windows 和 .NET Framework 的系统和应用程序开发人员提供比其较低版本更多的功能和更细粒度的控制。人们计划在该产品的所有领域(包括语言、编译器和核心库)进行关键改进,这些增强功能将使C++开发人员能够更有效地针对CLR、代码性能优化、64 位Windows以及提高应用程序的安全性。此外,Visual C++ 2005将使开发人员能够直接在Visual Studio 2005 IDE中为移动设备生成本地C++应用程序。
通过Visual C++ .NET 2002版和2003版,托管扩展使开发人员能够显式利用CLR和 .NET Framework。在Visual C++ 2005中,现有的托管扩展语法将继续得到完全支持。此外,Visual C++ 2005将通过强大的新功能以及用于编写托管代码的更为优雅的语法为CLR 和 .NET Framework提供扩展支持,该语法现正在作为公共语言基础结构(CLI)而进行 ECMA 标准化,这一新语法将称为C++/CLI。
C++语言的C++/CLI子集主要将在Visual C++ 2005中实现,并将为Visual C++开发人员提供很多新功能。C++开发人员将第一次能够使用真正的C++编程范型编写 .NET应用程序。例如,将为由CLR和垃圾回收器管理的代码和数据启用C++模板和C++样式的确定性终止。此外,C++/CLI还为C++提供很多CLR编程范型,包括泛型的CLR实现。
C++/CLI 还简化了定义和使用托管数据类型的方式。新标准引入的关键字对于C++开发人员而言,比当前托管扩展的下划线、下划线、关键字更为直观。在C++/CLI中,ref关键字用来以与托管扩展 __gc 关键字类似的方式定义垃圾回收类:
ref class MyRefClass { /* ... */ };
此外,C++/CLI 还引入一种新的类型——句柄,该类型用来表示自动垃圾回收的引用。句柄借用指针的语法,但使用脱字符号(^)而不是星号(*)。关键字 gcnew 用于创建上述垃圾回收对象,并且返回一个句柄:
MyRefClass ^ c = gcnew MyRefClass();
通过C++/CLI,开发人员还将能为句柄类型合并入传统的C++样式确定性终止。这意味着可使用正常的C++堆栈分配语法选择性地将引用类型作为对象进行实例化。这样,该对象将在脱离作用范围时自动调用它的析构函数。该功能启用了在其他面向CLR的语言中不可用的编程风格。
除了C++/CLI支持以外,Visual C++编译器还将为面向CLR的代码提供多个代码生成增强功能。这些增强功能包括:能够指定不包含本机 x86 指令的“纯粹”映像,以及通过CLR验证过程的“安全”映像。而且,Visual C++ 2005编译器能够比其他托管语言更好地优化 MSIL。
为Visual C++ 2005中的C++编译器规划了很多增强功能,其中之一便是 Profile Guided Optimizations (POGO)。POGO 技术使编译器能够测量应用程序的各种数据,并收集有关应用程序在运行时使用方式的信息。这些信息使Visual C++能够基于真实的应用程序使用模式进一步优化生成的代码。目前,在免费下载的 Platform SDK (PSDK) 中附带有 64 位 POGO 技术的预发布版本。在 Visual C++ 2005 中,该技术将扩展到核心 32 位编译器,且有关的支持将集成到 IDE 中。
该版本还将包含目前在 PSDK 中仅以预发布形式提供的64位C++编译器。这些编译器使Visual C++开发人员能够编写面向在Intel和AMD硬件上运行的64位版本Windows的非托管代码。对面向 64 位 Windows 的支持将完全集成到 IDE 中。
除语言和编译器增强功能以外,Visual C++ 2005还为核心C++库(如Microsoft 基础类库(MFC)、标准模板库(STL)和C运行时库(CRT))提供了改进和增强。这些增强将使C++开发人员能更好地支持 .NET Framework,并提供内置的应用程序安全性。
首先,将引入新版本的标准模板库(STL),该版本的STL将被调整以与托管代码和数据进行交互。那些习惯于使用STL编写传统C++应用程序的程序员将发现,他们能应用相同的编码技术来编写基于CLR的应用程序。其次,新版本带有增强安全功能的C运行库 (CRT)将随Visual C++ 2005一起发布,作为Microsoft对“可信赖计算”倡议的承诺的一部分,该版本的CRT将有助于增强集成了CRT应用程序的集体安全性。
对于Visual C++ 2005,开发人员将继续选择Visual C++来生成最为苛求的应用程序和组件。Visual C++ 2005通过提供令人兴奋的新增和改进功能,为具有控制和性能意识的C++开发人员进一步扩展了在Visual C++ .NET 2003版中提供的核心功能集。