天马行空(笨笨)
希望下辈子不要那么笨了

6. 64 位平台上的托管代码

发布日期: 2006-7-10 | 更新日期: 2006-7-10

6.1 开发环境

在 64 位平台上创建托管代码的开发环境与 32 位平台相同。使用 Visual Studio .NET 2003,开发人员可以创建在两个平台上无缝运行的托管应用程序。此外,还通过 Visual Studio 2005(在 2005 年上半年,进行了可用性规划)进行了许多改进,从而使开发、部署、调试和优化过程更加简单和强大。

本节将说明为 64 位平台创建可验证托管代码的意义,简要概述 .NET Framework,并重点强调 64 位 CLR。然后,讨论 .NET 在不同的 64 位处理器上的含义,并进一步详细考察可用的 .NET 编译器。

6.1.1自适应应用程序

自适应应用程序是可验证的托管 .NET 应用程序。自适应应用程序的主要优势在于,同一个可执行文件能够用于所有平台(x86、IPF 和 x64)。开发人员只需从一个源代码基开发,并通过编译器选项 /clr(位于下拉菜单中)选择针对“任何 CPU”即可。

6.1.2.NET概述

在说明 .NET Framework 64 位之前,这里先回顾一下 .NET Framework 的基本概念,重点强调如何在 64 位平台上实现。

使用 .NET Framework 开发有几个主要优势,包括:

可以使用几种语言混合开发。

能够使用诸如 ASP.NET 和 Windows 窗体这样的 Microsoft 技术。

基类库包含成千上万个有用的实用工具类,可以加快开发速度。

CLR 允许单个可执行文件在多个平台上运行。

托管代码程序可以与 CLR 一起操作,以提供多种服务,例如,内存管理、跨语言集成、代码访问安全性,以及自动的对象生存期控制。


请注意,对于 64 位开发而言,只有 Visual Basic .NET、C++ 和 C# 是有效的语言;目前不支持 J#。其他语言(如 Fortran 和 COBAL)正处于针对 64 位开发的认证过程中。

CLR 提供的基础结构使得托管执行能够发生,并且能够在执行期间使用多种服务。如果创建的是可验证的托管应用程序,则可执行映像 (PE32) 仅包含 Microsoft 中间语言 (MSIL) 代码。方法必须编译为特定于处理器的代码才能运行。当首次调用为其生成 MSIL 的每个方法时,都会先对其进行实时 (JIT) 编译,然后再运行。下次运行方法时,会运行现有的 JIT 编译的本机代码。先进行 JIT 编译,再执行代码的过程会一直重复,直到完成执行。

在执行过程中,托管代码会接收各种服务,例如,垃圾回收、安全性、与非托管代码的互操作、跨语言调试支持,以及增强的部署和版本控制支持。正是 CLR 允许开发人员通过 JIT 使其应用程序面向多个平台。因此,仅包含 MSIL 的应用程序将在 64 位系统(x64 和 IPF)上运行本机代码。

6.1.3 64 位处理器上的.NET

.NET Framework 2.0 将引入跨 .NET Framework 类库的增强,并可用于 64 位平台;.NET Framework 2.0 计划在 2005 的上半年推出。Visual Studio .NET 2005 也将在 2005 的上半年推出,它将支持运行 Windows Server 2003 SP1 和未来的 Windows 64 位客户端版本的 IPF 和 x64 处理器。在 64 位平台上安装 .NET 2005 时,开发人员不仅要安装在 64 位中执行托管代码所必需的所有基础结构,还要安装使托管代码在 WoW64 子系统上运行所必需的基础结构。

目前,.NET 1.0 和 1.1 还不能在 64 位上运行本机代码。为这些版本的 FRAMework 开发的所有代码都被视为“旧式”应用程序。旧式应用程序不区分 32 位和 64 位,因此它们将在 32 位上运行,除非它们是纯 MSIL。

Visual Studio .NET 2003 不在 64 位中运行本机代码。这意味着,开发人员必须在 32 位系统或 WoW64 上开发,并通过 Visual Studio.NET 远程调试 64 位托管应用程序。通过 Visual Studio .NET 2005,开发人员能够在任何体系结构上开发、部署和调试,并面向他们所选的体系结构。

6.1.4.NET编译器

本节讨论在 Visual Studio .Net 2003 中编译托管代码。托管代码在 .NET CLR 的上下文中运行。使用托管代码不是强制性的,但这样做有许多好处。托管代码程序可以与 CLR 一起操作,以提供各种服务,例如,内存管理、跨语言集成、代码访问安全性,以及自动的对象生存期控制。

Visual Basic .NET、C# 和 J# 的编译器将创建 PE32 或 ILONLY 可执行文件。这意味着,可执行文件仅包含 MSIL(Microsoft 中间语言)。如果可执行文件只调用安全的代码,则它是可验证的。可验证的托管代码是托管代码的子集,它可以验证为正确,从而可以信任其运行,而不会危及用户的安全性。

PE32 代表“可移植的 32 位可执行文件”,它作为原始 Win32 规范的一部分引入。要确定某个可执行文件是否经过验证,开发人员可以使用 .NET Framework 附带的一个名为 PEVerify 的工具。PE32 和对 PE32 的 64 位扩展 (PE32+) 将在 6.2.3 小节中详细说明。

但是,C# 可执行文件有可能会调用不安全的代码,并且是不可验证的。不安全代码的一个示例就是非托管 C++ DLL。因此,还需要检查特定于平台的调用。例如,在使用 DLL 导入时,开发人员必须验证用于调用 API 的数据类型。如果他们使用的是与 64 位中不同的 32 位类型,则调用将失败,因为数据类型大小是硬编码的,而不是多态的。

非托管 C/C++ 编译器将创建混合模式映像,并在 WoW64 下运行。通过使用 C++ 托管扩展,可以使 C++ 可执行文件成为托管文件。Visual C++ 的托管扩展是对标准 C++ 语言的语法扩展。要创建可执行文件,开发人员需要使用 /clr 编译器选项。/clr 编译器选项指导 Visual C++ 生成将在 CLR 的上下文中运行的托管代码。

但是,如果开发人员使用的是指针运算,则无法创建可验证的托管可执行文件。并非所有的源代码都会干净地编译,因此可能需要程序员注意修正警告和错误。

序列化和封送处理问题可能导致错误,代码可能无法正确运行。这将在 6.3 小节中详细说明。

在 Visual Studio .NET 2005 中,重新面向现有源代码的过程将更加简单和强大,程序员需要花费的精力将更少。C# 有一个新的开关,它允许开发人员通过单个编译器开关来面向许多不同的平台。Visual Studio .Net 2005 中的 C++ CLI 编译器选项是 Visual C++ .NET 2003 托管扩展的一个演变。CLR 功能更加紧密地与现有 C++ 语法和语义集成在一起,并提供紧凑的系统语言以充分利用整个体系结构。

下图显示当前可用的编译器与 Visual Studio .NET 2005 在所有平台上运行以及面向所有平台时的功能对比。


返回页首返回页首

6.2 托管代码执行

本节讨论托管代码执行如何发生,并说明在运行时 .NET 如何在 32 位和 64 位执行模式之间进行选择。

6.2.1可验证代码

“托管”和“可验证”是两个常用的术语。在 CLR 控制下执行的代码称为托管代码。反之,在 CLR 之外运行的代码称为非托管代码。创建可验证托管代码的一个极大优势是,它可以在 64 位系统上原样运行。它不需要重新编译,并且开发人员可以为 32 位和 64 位平台提供单个应用程序。他们可以标记程序集,使其以 32 位或 64 位方式运行,或者指示该程序集是中立的 — 即,它既可以在 32 位也可以在 64 位中运行本机代码。不可验证的代码可能需要更改才能在 64 位上顺利运行。包含本机代码的任何程序集将被视为本机程序集。

有两种形式的验证需要在运行库中完成:验证 MSIL 和程序集元数据。运行库中的所有类型指定了它们将实现的协议,并且该信息作为元数据与 MSIL 一起存储在托管 PE/COEFF 文件中。例如,如果一个类型指定它从另一个类或接口继承,则表明它将实现多个方法,这就是协议。协议也可以与可视性有关。例如,可以在程序集中将类型声明为公开(导出的),也可以不声明。类型安全是一个代码的属性,它只访问符合协议的类型。您可以验证 MSIL 是类型安全的。验证是 .NET Framework 安全系统中的基本构造块;目前,仅在托管代码上执行验证。运行库执行的非托管代码必须是完全信任的,因为它无法由运行库验证。

要了解 MSIL 验证,需要先了解 MSIL 的分类。MSIL 可以分为无效、有效、类型安全和可验证的。

无效的 MSIL 是 JIT 编译器无法为其生成本机表示的 MSIL。例如,包含无效操作码的 MSIL 就无法转换为本机代码。另一个示例是目标为操作数(而非操作码)地址的跳转指令。

有效的 MSIL 可以被视为符合 MSIL 语法的所有 MSIL,因此可以在本机代码中表示。该类 MSIL 中包含使用非类型安全形式的指针运算来访问类型成员的 MSIL。

类型安全的 MSIL 只通过公开的协议与类型交互。尝试从另一个类型访问类型私有成员的 MSIL 不是类型安全的。

可验证 MSIL 是类型安全的 MSIL,它可以通过验证算法来证明是类型安全的。验证算法是保守型的,因此某些类型安全的 MSIL 可能无法通过验证。当然,可验证 MSIL 也是类型安全和有效的,而不会是无效的。

6.2.2不可验证的代码

不可验证的代码可能需要更改后才能在 64 位上顺利运行。包含本机代码的任何程序集将被视为本机程序集,并且是不可验证的。使代码不可验证很容易。只需调用非托管代码即可使之不可验证。在使用 COM 互操作或 P/Invoke 时,需要特别小心,以确保代码在 64 位上干净地运行。

6.2.3程序集格式

选择术语“可移植可执行文件”(PE) 的原因是,在所有支持的 CPU 上为所有 Windows 行为提供一个通用的文件格式。为了扩大范围,已经在 Windows NT 及其子代、Windows 95 及其子代以及 Windows CE 上使用相同的格式来实现了这个目标。

64 位 Windows 的增加只需要对 PE 格式进行少量修改。这个新格式称为 PE32+。没有添加新的字段,只删除了 PE 格式中的一个字段。余下的更改只是将特定字段从 32 位扩展到 64 位。在大多数情况下,开发人员可以编写能够轻松使用 32 位和 64 位 PE 文件的代码。Windows 标头文件对大多数基于 C++ 的代码隐藏了这些差异。在 64 位 Windows 上,操作系统加载程序通过将映像从 PE32 转换到 PE32+ 格式来修改内存中的映像。

以下列表列出 PE 中能够帮助您制定决策的信息:

Win64 — 表示开发人员已经生成专门针对 64 位进程的程序集。

Win32 — 表示开发人员已经生成专门针对 32 位进程的程序集。在本例中,程序集将在 WoW64 下运行。

Agnostic — 表示开发人员使用 Visual Studio .NET 2005 或更新的工具生成了程序集,并且该程序集既可以在 64 位模式也可以在 32 位模式下运行。在本例中,64 位 Windows 加载程序将在 64 位中运行程序集。

Legacy — 表示生成程序集的工具早于 Visual Studio .NET 2005。在这种特殊情况下,程序集将在 WoW64 下运行。

Visual Studio .NET 2005 附带的 C#、Visual Basic.NET 和 C++ 编译器都具有命令行开关,这些开关可用于在 PE 标头中设置适当的标志。例如,C# 具有 /platform: {anycpu, x86, Itanium, AMD, x64} 开关。

6.2.4应用程序加载

在 Windows 中,操作系统加载程序通过在公共对象文件格式标头中检查一个位来检查托管模块。设置该位就表示是托管模块。如果加载程序检测到托管模块,会执行以下操作:

确定代码是有效的托管代码。

将映像的入口点更改为运行库的入口点。

通过目标平台管理程序集加载并促进正确程序集链接的系统资源有两个:Fusion 和 GAC。Fusion 是程序集管理器,GAC 是全局程序集缓存。Fusion 决定应该将哪些版本的引用程序集绑定到应用程序。GAC 存储在计算机上专门由多个应用程序共享的程序集。

运行库了解程序集的目标体系结构,并使用这些实用工具确保将正确的程序集链接到应用程序。开发人员不需要特别考虑如何确保兼容性。GAC 对 32 位应用程序隐藏 64 位程序集,以确保 32 位不会尝试运行本机 64 位代码。托管代码可以在 32 位或 64 位平台上无缝运行,因为程序集加载程序不识别体系结构,并且程序集不包含处理器体系结构。

下图展示操作系统加载程序在加载程序集时的逻辑流程。如果程序集为 PE32+ 格式,则它必须是 64 位并在本地运行。如果它是 PE32 格式,但它仅包含 IL 并且没有 32 位所需的标志设置,则仍然可以在本地运行 64 位。否则,该程序集将在 WoW64 中启动。


返回页首返回页首

6.3 编码问题

本节讨论 .NET 应用程序编程人员面临的一些常见问题。介绍编程人员应该注意的问题,以确保应用程序既可以在 32 位也可以在 64 位环境中工作。

6.3.1浮点

对浮点操作和 IL 还应该有一些特殊考虑。相同的 IL 在 32 位和 64 位上不会产生相同的结果。在某些情况下,这可能是个问题。但在任何情况下,差异都很小,并且对于绝大多数应用程序而言,该差异可以忽略。开发人员应该了解这些问题,以防浮点运算中的小更改严重影响算法结果(例如,远程天气预报)。

第一种情况,方法主体更改但方法的逻辑函数保持不变;就像代码已经过语法重组,但保留了在算法上完全相同的逻辑。在这种情况下,即使在同一平台上运行,也不能保证结果相同。请记住,IL 将传递到 JIT 以针对该平台进行编译。这种假设对于非托管 C++ 开发人员而言没有问题,但是在 .NET 托管代码中不再有效。

此外,精度边缘的极小差异可能会显示为超越函数,如 sin()、cos() 等。在比较结果值时(特别是,当它们是跨平台生成的),开发人员应该小心对待超越函数。

一个较好的编码做法是,永远不比较两个浮点数。如果开发人员遵循这个正确的编码做法,就不会遇到这些问题了。但是,开发人员不应该假设在 64 位上进行浮点运算遵循与 32 位相同的算法规则。IEEE-754 概述浮点运算如何在 64 位平台上执行。

虽然可以将这些视为标准的编码做法,但在针对 MSIL 开发时,它们甚至更为重要。

6.3.2序列化

对于 XML(因为它独立于平台),序列化应该能够顺利执行。二进制序列化难以保证兼容性。例如,系统整数指针在 64 位 CLR 中更改了大小,并且将更改对象的包装结构。但是,开发人员不需要关心对齐问题,因为这由 CLR 处理。下一节讨论一些互操作性的问题与难题,并概述如何正确地序列化一个跨平台传输的对象。

6.3.3互操作性

Microsoft .NET Framework 促进了与 COM 组件、COM+ 服务、外部类型库和许多操作系统服务的交互。托管与非托管对象模型之间的数据类型、方法签名和错误处理机制不同。要简化 .NET Framework 组件与非托管代码之间的互操作,并简化迁移路径,CLR 应该为客户端和服务器隐藏这些对象模型中的差异。

开发人员可以向托管代码公开现有的 COM 组件,反之,也可以向 COM 公开 .NET Framework 组件。还必须考虑与对齐相关的问题。在 .NET Framework 中使用非托管组件时,数据类型大小在某些情况下不同,并且会更改应用程序结构的布局。

在通过 DLL Import 或其他方法导入组件时,开发人员应该小心处理,以确保组件 API 存在于所有平台上。

在调用期间,Interop 封送处理控制如何在托管和非托管内存之间的方法参数和返回值中传递数据。这是 CLR 的封送处理服务执行的运行时操作。大部分数据类型在托管和非托管内存中具有公共的表示形式。interop 封送拆收器负责处理这些类型。其他类型在托管内存中不明确,或者根本不表示。不明确类型可以有多个映射到单个托管类型的非托管表示形式,或者缺少类型信息(如数组大小)。对于不明确类型,封送拆收器提供了默认表示形式和可选表示形式,其中可以存在多个表示形式。开发人员还可以为封送拆收器提供显式指令,以指示如何使用 .NET Framework 的 StructLayoutAttribute 和 Marshal 类来封送处理不明确类型。.NET Framework 的 Marshal 类提供了一个方法集合,用于分配非托管内存、复制非托管内存块以及将托管类型转换为非托管类型,它还提供其他一些在与非托管代码交互时使用的方法。方法 sizeof() 可返回非托管类的大小(以字节为单位)。

6.3.4结构布局问题

.NET Framework 类 StructLayoutAttribute 允许用户控制类或结构的数据字段的物理布局。通常,CLR 控制托管内存中类或结构的数据字段的物理布局。如果类或结构需要以特定的方式排列,则开发人员可以使用 StructLayoutAttribute。如果要将类传递到期望特定布局的非托管代码,那么显式控制类的布局很重要。

LayoutKind 值 Sequential 用于强制成员按照显示的顺序进行布局。Explicit 控制每个数据成员的精确位置。通过 Explicit,每个成员必须使用 FieldOffsetAttribute 来指示该字段在类型中的位置。

默认情况下,C#、Visual Basic .NET 和 C++ 编译器对类和结构应用 Sequential 布局值。Type Library Importer 也应用该属性;在导入类型库时,它始终应用 Sequential 值。

下面是托管和非托管结构中的 Sequential 布局示例。它演示开发人员如何强制在托管和非托管代码中具有相同的结构对齐。在本质上,使用 Marshal 类与在非托管结构中使用 #pragma pack() 完全相同。


下一个示例显示使用 StructLayout 的显式布局的用法。请注意,FieldOffset 的使用指示该字段在类型中的位置。System.IntPointer 可能需要 4 个或 8 个字节,这取决于平台。


6.3.5COM 公开

在编写 .NET 类型并在非托管代码中使用该类型时,还需要考虑几个问题。32 位和 64 位体系结构之间不能互操作。这意味着,开发人员必须为这两个体系结构创建单独的 COM 对象,并相应地注册它们。如果程序集是平台中立的,则开发人员可以将其公开给 32 位及 64 位体系结构。由于数据类型转换,类型库可能不是中立的。

返回页首返回页首

6.4 调试托管代码

本节讨论开发人员可以使用哪些工具在 64 位平台上调试托管代码。并考察了几个有用的工具,它们可以帮助您编写独立于平台的托管代码。

6.4.1针对 64 位的.NET调试器

Microsoft CLR 调试器通过图形界面提供了调试服务,帮助应用程序开发人员在面向 CLR 的程序中查找和修正错误。该调试器的命令行版本位于 .NET Framework SDK 中,名为 CorDbg.exe。DbgClr 和 CorDbg 均不编译应用程序,它们只负责调试托管代码。

要使用这些调试器,首先需要生成应用程序,以便它包含适当的 /debug 开关。在 Visual Studio .NET 2003 中,调试器可以读取包含托管代码、非托管代码以及这两者信息的转储文件。对于非托管代码,开发人员可以使用所有常见的调试器窗口来查看转储信息。但是,对于托管代码,大多数调试器窗口不显示转储信息。相反,开发人员可以使用名为 SOS 的工具在 Command 窗口显示调试信息。

SOS 接受来自 Command 窗口的命令,并使用 Command 窗口显示信息。它不使用其他调试器窗口,如 Call Stack 或 Locals 窗口。如果转储同时包含托管代码和非托管代码的信息,则开发人员可以在普通调试器窗口和 SOS 调试之间来回切换。他们还可以通过从 Command 窗口调用 SOS,将 SOS 合并到 VS.NET 2003 中。Command 窗口可以用于发出命令,也可以用于在集成开发环境 (IDE) 中调试和计算表达式。要查看 Command 窗口,请按 CTRL+ALT+A,或者在 View 菜单的 Other Windows 子菜单上单击 Command Window。在 Command 窗口中键入“.load sos”。

6.4.2FXCop

FxCop 是一个工具,它可以扫描 .NET 程序集以确定程序集中的类是否符合命名约定、库设计、本地化、安全性和性能的最佳做法。FxCop 有许多可以检查是否符合各种类别的最佳做法的规则。要检查的规则类别包括设计规则、命名规则、全球化规则、性能规则、使用规则等。FxCop 不仅包括一个有用的 GUI — 用于列出和描述在指定程序集中违反的规则,还提供了一个 SDK 以供开发人员用添加自己的自定义规则。对于在托管代码中查找移植问题而言,这是一个非常有用的工具。

6.4.3WinDBG

WinDBG 是一个功能强大的调试器,与 Visual Studio 调试器相比,它能够调试更低级别的代码;它已在 5.2.2 节中详细说明。其 GUI 界面非常类似于 Visual Studio,其功能对于 Visual Studio 开发人员而言很熟悉。它能够调试托管代码、非托管代码以及两者的混合体。它可以通过 CLR 调试,其代码在 WoW64 上执行。它可以与一个进程连接和分离,并可以远程执行这些操作。

返回页首返回页首

6.5 小结和建议

本节回顾了 64 位开发人员可用的环境,并预习了 Visual Studio .Net 2005。Visual Studio .Net 2005 将针对 32 位和 64 位平台进行编译,面向 IPF 和 X64 体系结构。本节说明了托管代码执行的内部工作方式,以及开发人员可能遇到的一些困难。此外,演示了可验证代码的运行与平台无关;这是针对 .NET Framework 进行开发的一个主要动机。本节还讨论了开发人员应该注意的编码问题,说明了序列化和对齐问题并提供了处理潜在缺陷的最佳做法。最后,本节讨论了 Windows 64 位开发人员可用于编写托管代码的功能强大的调试工具,以及目前正在开发的新的和现有的调试功能。

开发人员在 WoW64 上测试代码至关重要(如果可行)。在许多情况下,迁移到 .NET 还意味着迁移到 64 位,因此开发人员应该考虑至少将一部分代码基迁移到 .NET。

posted on 2008-02-01 16:07  天马行空(笨笨)  阅读(617)  评论(0编辑  收藏  举报