基于虚拟化的安全性 - 第1篇:引导过程

作者在文章中使用了CC协议,InfoQ翻译本文。

本文是关于基于虚拟化的安全和设备保护功能的系列文章的第一篇。这些文章的目的是从技术角度分享对这些特征的更好理解。第一篇文章将介绍从Windows引导加载程序到VTL0启动的系统引导过程。

基于虚拟化的安全

基于虚拟化的安全(Virtualization Based Security,VBS)是Microsoft Windows的主要安全特色,随Windows 10和Windows Server 2016一起提供。例如,DeviceGuardCredentialGuard都依赖它。对于那些不知道Windows 10的这两个关键的安全创新,DeviceGuard允许系统阻止任何东西,包括受信任的应用。对于CredentialGuard,它允许系统隔离lsass.exe进程,以阻止密码收集器(如Mimikatz)的内存读取尝试。

这个新功能的主要思想是使用硬件虚拟化技术,如Intel VT-X,以便在两个虚拟机(VM)之间提供强大的隔离,并且,在将来可能会更多。这些技术允许虚拟机管理器(Virtual Machine Manager,VMM)使用扩展页表(Extended Page Tables,EPT)在物理页上设置不同的权限。换句话说,VM可以在其页表项(Page Table Entry,PTE)中设置物理页可写(+W),并且VMM可以通过在其EPT中设置适当的访问权限来静默地授权或阻止这一点。

基于虚拟化的安全性依赖于Hyper-V技术,这将产生不同虚拟信任级别(Virtual Trust Levels,VTL)的虚拟机。Hyper-V的构成,包括一个管理程序hypervisor以及运行着任何操作系统的VM,包括物理机的操作系统Windows本身都被视为一个组件。也就是说,Hyper-V的架构是CPU之上级别的,然后才是它的核心思想——VTL的分层,每一层的权限严格限制和区分。Hyper-V信任它并接受管理订单,例如控制其他VM。其他VM可以是“开明的”,如果是这样的话,那么就向Hyper-V发送受限消息以用于它们自己的管理。

VTL被编号,编号较高的是最可信的。现在,有两个VTL:

  • VTL0,这是正常的环境,基本上都在标准的Windows操作系统。
  • VTL1,它是安全的环境,包含一个最小化的内核和安全的应用程序,称为trustlet。

图1:基于虚拟化的安全性概述

CredentialGuard安全功能利用此技术隔离VTL1信任单(lsaiso.exe,上图中的“隔离LSA”)中的关键lsass.exe进程,甚至使VTL0内核不能访问其内存。只有消息可以从VTL0转发到隔离的进程,有效地阻止内存密码和散列收集器(如Mimikatz)。

DeviceGuard安全功能允许在VTL0内核地址空间实现W^X内存缓解(物理页不能同时处于可执行和可写的状态),并接受包含授权代码签名的策略。如果VTL0内核想要使物理页可执行,它必须要求VTL1进行改变(图中的“HVCI”),这将根据其策略检查签名。对于usermode代码,目前还没有完成,VTL0内核仅仅要求签名验证。策略在引导启动期间加载,并且不能在之后修改,这强制用户重新启动以加载新策略。

策略也可以写死:在这种情况下,在UEFI变量中设置授权签名者,并将针对这些签名者检查新策略。UEFI变量包括SetupBoot标志设置,这意味着它们不能在启动后访问或修改。为了清除这些变量,本地用户必须使用访客账号的Microsoft EFI引导加载程序重新启动,这将在用户交互(通过按键)后删除它们。为了清除这些变量,本地用户必须使用自定义的Microsoft EFI引导加载程序重新启动,这将在用户交互(通过按键)后删除它们。

因此,VBS主要依赖SecureBoot:必须检查引导加载程序的完整性,因为它负责加载策略、Hyper-V、VTL1等等。

如果您对详细的设备保护(Device Guard)概述感兴趣,可以阅读MSDN的这篇文章

您还可以看看2015年、2016年黑客大会上Alex Ionescu和Rafal Wojtczuk的演示,在这项工作中,给我们提供了很多帮助。

您还可以阅读Hyper-V Internals博客的两篇文章,了解Hyper-V更多相关技术信息: 
"Hyper-V debugging for beginners" (also covers Hyper-V startup);  
"Hyper-V internals"。 
“初学者的Hyper-V调试”(也包括Hyper-V启动); “Hyper-V内部”。

在本文中,我们将介绍从Windows引导加载程序到VTL0启动的系统引导过程。为了分析VBS在引导过程中如何初始化,我们对Windows 10 1607版本的以下文件进行了逆向工程:

  • bootmgr.efi:EFI引导加载程序(它的一小部分);
  • winload.efi: EFI Windows加载器;
  • hvix.exe: Hyper-V(真的很小);
  • ntoskrnl.exe: NTOS内核;
  • securekernel.exe: 安全内核;
  • ci.dll: 检测VTL0代码完整性;
  • skci.dll: 检测VTL1代码完整性。

因此,让我们进入VBS引导过程,从执行winload.efi到ntoskrnl.exe入口点执行。

引导过程

引导过程可以总结为以下五个基本步骤:

  • bootmgr.efi是要加载的第一个组件。它由SecureBoot验证,然后执行;
  • bootmgr.efi加载并检查winload.efi,主要的Windows加载器;
  • winload加载并检查VBS配置;
  • winload加载并检查Hyper-V和VTL0/VTL1内核组件;
  • winload退出EFI模式,启动Hyper-V。

Bootmgr.efi

当系统启动时,Bootmgr.efi是第一个执行的Microsoft组件。其完整性和签名已事先由Secure Boot UEFI代码验证。为了能够识别撤销的签名,检查包含已撤销的签名的DBX数据库(截止2016年底,该数据库包含71个黑名单和未知的SHA256哈希值)。在bootmgr.efi代码结束时,执行将传递到winload.efi入口点:OslpMain/OslMain

OslpMain首先调用OslpPrepareTarget,这是winload.efi的“核心”函数:它将启动管理程序、内核等。但是首先,它使用OslSetVsmPolicy启动VBS配置。

VBS策略负载

OslSetVsmPolicy首先检查VbsPolicyDisabledEFI变量值(Microsoft命名空间的值,请参见下文)。如果设置,则清除此变量(设置为0),这意味着不会加载Credential Guard配置。因此,此EFI变量允许仅禁用单引导的凭据保护(并且可以通过来自VTL0 ring3的特权调用设置)。如果不存在,则从SYSTEM注册表配置单元加载配置,并对BlVsmSetSystemPolicy执行调用,BlVsmSetSystemPolicy将根据需要读取和更新VbsPolicyEFI变量。相应的值然后存储在BlVsmpSystemPolicyglobal变量中。如果启用UEFI锁,则设置此EFI变量,并且不能由winload.efi禁用(仅仅只是没有删除它的代码,必须使用自定义EFI代码)。

函数OslpPrepareTarget也调用OslpProcessSIPolicy(它被调用两次,第一次直接调用,然后从函数OslInitializeCodeIntegrity调用)。OslpProcessSIPolicy使用三个EFI变量“池”检查SI策略签名。每个池包含三个EFI变量,第一个包含策略,第二个包含其版本,第三个包含授权的策略更新签名者。例如,对于C:\Windows\System32\CodeIntegrity\SIPolicy.p7b,变量是SiSiPolicyVersionSiPolicyUpdateSigners。如果设置了“版本”和“更新签名者”变量,系统将强制执行SI策略签名:它必须存在并且正确签名,否则引导过程将失败。验证本身由BlSiPolicyIsSignedPolicyRequired函数执行。

三个策略和相关联的变量总结如下:

Policy file EFI variables
C:\Windows\System32\CodeIntegrity\SIPolicy.p7b Si
\EFI\Microsoft\Boot\SIPolicy.p7b SiPolicyVersionSiPolicyUpdateSigners
C:\Windows\System32\CodeIntegrity\RevokeSiPolicy.p7b RevokeSiRevokeSiPolicyVersionRevokeSiPolicyUpdateSigners
\EFI\Microsoft\Boot\SkuSiPolicy.p7b SkuSiSkuSiPolicyVersionSkuSiPolicyUpdateSigners

表1:SI政策和相应的EFI变量

我们没有确定“revokeSiPolicy”和“skuSiPolicy”的目的,但它们似乎与常规的“SiPolicy”类似。

Hyper-V和内核组件负载

接下来系统将跳转并执行一个参数预先设置为0的OslArchHypervisorSetup函数。第一次,它将启动Hyper-V(加载hvloader.efi并通过HvlpLaunchHvLoader执行它)。然后通过OslInitializeCodeIntegrity检查安全引导设置。

OslpPrepareTarget然后加载NTOS内核(ntoskrnl.exe),并使用OslpLoadAllModules函数加载hal.dllmcupdate.dll模块。它们的签名验证在加载过程中执行(在ImgpLoadPEImageOslLoadImage中)。然后通过OslVsmProvisionLKeyOslVsmProvisionIdk函数从EFI变量加载“本地密钥”和“标识密钥”。

此时,NTOS内核开始初始化但还未启动。然后使用“0”参数调用OslVsmSetup(与OslArchHypervisorSetup相同:它需要一个“step”参数),它首先检查Hyper-V是否已经启动,然后初始化OslVsmLoaderBlock全局变量(在初始化期间赋值)。然后,OslVsmSetup通过OslpVsmLoadModules函数(OslLoadImage再次用于检查其签名)加载安全内核(securekernel.exe)及其依赖(skci.dll)。然后将EFI变量OsLoaderIndications的第一位设置为1。

最后,再次调用OslVsmSetupfunction,但此时该函数的参数为1。这将触发几个OslVsmLoaderBlock参数的初始化。

当函数OslpPrepareTarget返回时,VBS参数已验证,并且加载NTOS和安全内核。它们的入口点地址已存储在OslpVsmSystemStartupOslEntryPoint全局变量(分别为securekernel.exentoskrnl.exe入口点)中,以便进一步重用。

Microsoft EFI变量

VBS EFI变量(以及更常见的微软变量)属于命名空间:{0x77FA9ABD, 0x0359, 0x4D32, 0xBD, 0x60, 0x28, 0xF4, 0xE7, 0x8F, 0x78, 0x4B}。这些变量的“Boot”和“Setup”属性已设置,因此不允许在EFI引导阶段后访问或修改它们。

然而,可以转储它们以便在分析期间帮助逆向。与VBS相关的EFI变量及其相应的用法总结如下:

EFI variable name Usage
VbsPolicy VBS settings
VbsPolicyDisabled Disable “magic” variable
VsmLocalKeyProtector  
VsmLocalKey  
VsmLocalKey2  
WindowsBootChainSvn  
RevocationList  
Kernel_Lsa_Cfg_Flags_Cleared  
VsmIdkHash  
Si First CodeIntegrity policy
SiPolicyUpdateSigners Update signers
SiPolicyVersion Version
RevokeSi Second CodeIntegrity policy
RevokeSiPolicyVersion Update signers
RevokeSiPolicyUpdateSigners Version
SkuSi Third CodeIntegrity policy
SkuSiPolicyUpdateSigners Update signers
SkuSiPolicyVersion Version
表2:Microsoft命名空间EFI变量列表

为了转储这些变量的内容,可以关闭安全启动和使用一个简单的EFI自定义启动加载程序(gnu-efi和VisualStudio工作相当完美)。下面给出一些变量转储作为示例:

Name Value
CurrentActivePolicy 0
CurrentPolicy 2
BootDebugPolicyApplied 0x2a
WindowsBootChainSvn 0x00000001
   
VsmLocalKey2 4c 4b 45 89 50 4b 47 31 96 00 00 00 01 00 01 00 2c 00 00 00 01 00 01 00 01 00 00 00 b2 21 ae a7 12 86 07 a8 15 28 d5 49 33 ac 09 ac 93 c8 e0 12 61 8f 10 d6 4c 68 d1 5a 5f 00 90 0c 5a 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 50 6c 1a 00 00 00 00 00 00 00 00 00 00 00 00 00 c2 0f 94 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 03 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00

表3:EFI变量转储示例

Hyper-V和安全内核的启动

OslpPrepareTarget返回,执行流程现在已启动Hyper-V并单独切分VTL0和VTL1空间。这个过程可以总结为以下几点:

  • winload在“第一个/最底层”的Hyper-V虚拟机里启动;
  • winload唤醒securekernel;
  • securekernel初始化,并根据写死的策略,向Hyper-V申请内存保护;
  • securekernel激活VTL1层;
  • Hyper-V允许securekernel激活VTL1,并返回ShvlpVtl1Entry函数;
  • 通过ShvlpVtlReturn函数,Hyper-V把VTL1层的securekernel状态告诉VTL0层的winload(自从它唤醒securekernel之后等待了很久);
  • 在得到securekernel已经完成安全检查(启用内存保护等动作)的消息后,winload才开始唤醒ntoskrnl

这些是在securekernel初始化之前和之后的状态(VTL0 VM是蓝色块,VTL1是绿色块,而Hyper-V是橙色块):

图2:securekernel初始化之前和之后的状态

当遵循执行流程时,OslpMain通过调用OslFwpKernelSetupPhase1退出EFI模式,并通过步骤“1”的OslArchHypervisorSetup启动Hyper-V。Hvix64通过将RSP保存到HvlpSavedRsp全局中并通过将HvlpReturnFromHypervisor传递给hvix64来启动。当命中HvlpReturnFromHypervisor时,使用cpuid指令检查启动,并恢复RSP。我们实际上是在第一个虚拟机,这将很快成为VTL1。

OslVsmSetup最后一次被调用(步骤“2”),这将会发生:

  • 检查VBS参数;
  • 验证Hyper-V是否正确运行;
  • 修改OslVsmLoaderBlock设置;
  • 在同一块中复制OslVsmLKeyArray(Local Key)和OslVsmIdk(“idk”用于“Identification Key”);
  • 调用已存储到OslpVsmSystemStartup全局中的安全内核入口点,指定OslVsmLoaderBlock及其大小作为参数。

然后,安全内核将执行初始化,安全内核通过SkmiProtectSecureKernelPages这一特殊函数的调用来申请独占内存(以确保安全性),同时安全内核还注册了Hyper-V的事件监听例程(HyperGuard及其Skpg *前缀例程)。根据特殊模块寄存器的说明文献对以下MSR的操作,由函数SkpgxInterceptMsr拦截和处理:

  • 0x1B(APIC_BASE);
  • 0x1004(?);
  • 0x1005(?);
  • 0x1006(?);
  • 0x100C(?);
  • 0xC0000080(EFER);
  • 0xC0000081(STAR);
  • 0xC0000082(LSTAR);
  • 0xC0000083(CSTAR);
  • 0xC0000084(FMASK);
  • 0xC0000103(TSC_AUX);
  • 0x174(SEP_SEL);
  • 0x175(SEP_RSP);
  • 0x176(SEP_RIP);
  • 0x1a0(MISC_ENABLE)。

我们的假设是这些处理程序设置为捕获VTL0中的CPL转换和阻止关键的MSR修改。还有两个其他例程,SkpgxInterceptRegistersSkpgInterceptRepHypercall。一种可能性是,第一个可能能够拦截CRXXX寄存器操作(例如,CR4写入SMEP禁用),第二个可以拦截未授权的超级调用(这仅仅是一个假设)。

关于HyperGuard,似乎VTL0完整性检查由SkpgVerifyExtents执行。这个特定的函数由SkpgHyperguardRuntime调用,它可以被定期执行(使用SkpgSetTimer)。HyperGuard的执行和回调函数被复制到了SkpgContext的全局函数中(由SkpgAllocateContextSkpgInitializeContext初始化)。

请记住,前面的讨论只是假设,可能是错误的,因为我们现在没有在VTL1 HyperGuard/PatchGuard例程花时间研究。

在其初始化结束时,安全内核将最终执行两个超级调用:

  • 0x0F,进入ShvlEnableVpVtl,指定一个ShvlpVtl1Entry函数指针;
  • 0x12,进入ShvlpVtlCall,它不在代码的任何其他部分使用,并且使用它自己的超级调用trampoline(我们将在下一篇文章中给出关于这些超级调用trampolines的更多细节)。

ShvlpVtl1Entry结束了SkpPrepareForReturnToNormalMode,似乎这个过程实际上使Hyper-V启用VTL1和VTL0,返回到ShvlpVtl1Entry,然后返回到winload.efi到VTL0上下文。

最后,当回到winload.efi主程序时,它将通过OslArchTransferToKernel执行NTOS入口点,它使用OslEntryPoint全局调用其入口点。

然后执行下一个操作,就像Windows在正常环境中启动一样,只是NTOS内核现在知道与VBS相关的组件(如Device Guard)。

结论

基于虚拟化的安全性是Microsoft Windows 10安全功能的关键组成部分。通过覆盖VBS的安全内核初始化,我们希望本文将给予更多资源,以便更深入地了解这些功能。

在第二篇中,我们将介绍VTL0和VTL1之间的内核通信以及Hyper-V超级调用实际如何工作。

展开阅读全文

『gab第1篇』多态

03-30

一、多态—2004.03.28rn上大学那会儿,我曾经给一对双胞胎兄弟补过英文课。他俩的模样至今依然清晰。哥哥我给他取了个English Name叫David,他很好动,也很能讲;而弟弟Davis呢,有点腼腆,但很聪明,从接受能力上更好一点。性格上,David更像姜文,Davis更像姜武,呵呵,我喜欢的两个中国纯爷们儿!这是了解他们之后才发现的。最初见到他们时,我常常要问,你们谁是哥哥来着。rn嗯,说起这件小事是为了聊一下delva中的多态。rn先来聊聊重载问题(也叫超载,总之是那个Overload)吧。在同一个类中,出现多个同名的方法的现象就是Overload。联系起上面提到的兄弟俩,我想这更好理解。他们从外观上是相似的,不同的是某些“参数”,类型上可能各异或是个数上不同。rn在Delphi中,所有方法都默认是静态的,便于编译器和链接器解析调用。为实现超载,要在方法明的后面加上关键字overload,不然会产生报错。先看个小例子吧:rntype //这是Delphi对类Ta的定义部分rn Ta=class rn publicrn procedure XiongDi(age,height:integer);overload; //这是Ta的两个公共过程,名字一样rn procedure XiongDi(name:string);overload; //但他们的参数不同。rn end;rnimplementation //以下为实现部分rnprocedure Ta.XiongDi(age, height: integer); //这是两个过程的实现。rnbeginrnForm1.Edit1.Text:='age: '+inttostr(age)+' height: '+inttostr(height);rnend;rnprocedure Ta.XiongDi(name: string);rnbeginrnForm1.Edit2.Text:='name: '+name;rnend;rnDelphi用很强的图形化控件,我们用一个窗体的Button控件来激活上述过程,并用Edit控件显示结果。rnprocedure TForm1.Button1Click(Sender: TObject); rnvarrna:Ta; //这是Delphi对对象的定义,a是Ta的实例化,就是Ta这个类的对象。rnbeginrna.XiongDi(15,130);rna.XiongDi('David');rn这是效果图。rn rn我们看到第一个XiongDi反映的是年龄和身高信息,而第二个XiongDi则反映的是姓名信息。它们的参数个数和类型都不同,要表达的效果也就不同。所以,面对Overload我们不要被表面上的名称所迷惑,而应该了解函数、过程和方法实质的作用。多说一句,函数和过程是Delphi里的叫法,Java中统成为方法,无返回值的方法用void关键字表示,代表Delphi里的过程,而有返回值的方法就是Delphi中的函数。rn再来说说在Java中实现Overload。Java的方法和Delphi刚好相反,所有方法都默认成虚拟的,所以Java中可以随时Overload而不需要标明我要超载这个方法了。为了对比,我在Java中写个和上面一样的例子:rnA.java文件:rnclass A //这是Java中对类A的定义。rn public void XiongDi(int age,int high) //这里定义了两个同名方法rn System.out.println("age: "+age+" high: "+high);rn rnrn public void XiongDi(String name)rn System.out.println(name);rn rnrnDo.java文件:rnpublic class Dorn public static void main(String[] args)rn A a=new A();rn a.XiongDi(15,130);rn System.out.println(" **********");rn a.XiongDi("David");rn rnrn编译并执行的结果。rn rn好了,到此我们对delva的overload算是有了一个感性的认识了。我们可以看到,重载事发生在同一个类中,不同方法之间的现象。就像两兄弟之间一样。呵呵,如果是父子之间又会发生什么有趣的现象呢?rn投掷,这个从我们祖先继承下来的动作,因人而异。比如,我的爸爸投掷东西时是用右手,而我是个左撇子。这种动作相同而执行方式不同的现象就是下面我们要gab的覆盖(又叫改写,就是Override)。嘿嘿,我们中国人看到这两个相似的单词,一定很迷惑,就像老外看到横戌点戍空心戊一样,而他们其实大不相同。覆盖事发生在父类和子类的方法之间的。我们聊到这里就决不会再弄混这两个名词了。但我要说的是,他们还是有联系的。那就是他们同为面向对象编程的主要特点,是包装、继承和多态三大要素中,多态的具体表现。rn闲话少叙,就先在Delphi中来个覆盖的小例子吧:rntypern Tfather=class //我们定义一个父类rn publicrn function throwhand:string; virtual;rn end;rntypern Tson=class(Tfather) //我们定义一个继承父类的子类rn publicrn function throwhand:string;override;rn end;rnrn//这是两个函数的实现:rn Tfather rnfunction Tfather.throwhand: string;rnbegin Form1.edit1.Text:='right'; end;rn Tson rnfunction Tson.throwhand: string;rnbegin Form1.edit2.Text:='left'; end;rnrn//这是对函数的调用:rnprocedure TForm1.Button1Click(Sender: TObject);rnvar FatherUse:Tfather; SonUse:Tson;rnbeginrnFatherUse:=Tfather.Create;rnSonUse:=Tson.Create;rnrnFatherUse.throwhand;rnSonUse.throwhand;rnrnFatherUse.Free;rnSonUse.Free;rnend;rn这是效果图:rn rn我们看到,父类被一个叫virtual的关键字所修饰。这是何故?不加行吗?当然不行,原因就是前面所述,Delphi的方法都是静态的。我们知道,Java中,被修饰成final的方法是不能被覆盖的。由此可知,Delphi的默认的静态方法也不能被覆盖。因此,Delphi定义了两个关键字:virtual(虚拟)和dynamic(动态)。它们让函数和过程成为动态的,用于后关联。Virtual和dynamic作用大致相同。只是virtual的方法较dynamic的方法在VMT中占用空间较大,而执行时间较短。这是因为,子类在override父类中的某一个virtual方法时,VMT会为子类提供未被voerride的其它父类的virtual方法。而dynamic则只提供被override的方法。这两个方式个有好处,在Delphi中常被一起使用。rn嗯,再多想想。如果父类的一个virtual方法不但要被子类方法override,还要被同一个类中的同名方法overload,那要怎么做呢?看看下面的例子:rntypern Tfather=classrn public //多了关键字overloadrn function throwhand:string; overload;virtual;rn function throwhand(m:integer):integer; overload;virtual;rn end;rntypern Tson=class(Tfather)rn public //多了关键字reintroduce,我发现不用它也行。但建议使用。rn function throwhand:string;reintroduce;override;rn end;rn我们再来看看实现部分:rn Tfather rnfunction Tfather.throwhand: string;rnbeginrn result:='right';rnend;rnfunction Tfather.throwhand(m: integer): integer;rnbeginrnresult:=m;rnend;rn Tson rnfunction Tson.throwhand: string;rnbeginrn Form1.edit2.Text:='left';rnend;rn这是调用部分:rnprocedure TForm1.Button1Click(Sender: TObject);rnvarrn FatherUse:Tfather;rn SonUse:Tson;rnbeginrnFatherUse:=Tfather.Create;rnSonUse:=Tson.Create;rnrnForm1.edit1.Text:='usehand:'+FatherUse.throwhand+'throwfar:'+inttostr(FatherUse.throwhand(500))+'m';rnSonUse.throwhand;rnrnFatherUse.Free;rnSonUse.Free;rnend;rn好了,Delphi的覆盖就聊到这里,嘻嘻,该来杯咖啡享受一下了。下面是Java的覆盖,它可没有那么多关键字。rn这是Do.java文件的代码:rnclass A //这是父类rn void throwhand()rn System.out.println("right!");rn rnrnclass SA extends A //这是子类rn void throwhand()rn System.out.print("left!");rn rn rnpublic class Dorn public static void main(String[] args)rn A a=new A(); //父类实例化;rn SA s=new SA(); //子类实例化;rn System.out.println("-------------------------");rn System.out.print("Father throwhand is: ");rn a.throwhand(); //调用rn System.out.println(" ");rn System.out.print("Son throwhand is: "); rn s.throwhand(); //调用rn rnrn这是效果图:rn rnrn到此,我对多态的gab就该结束了。希望你至少没有被我的文字弄烦。如果你真的烦了,那后面的系列文章,哎,不看也罢。 论坛

【Gabing Delva 第1篇

03-31

一、多态—2004.03.28rn上大学那会儿,我曾经给一对双胞胎兄弟补过英文课。他俩的模样至今依然清晰。哥哥我给他取了个English Name叫David,他很好动,也很能讲;而弟弟Davis呢,有点腼腆,但很聪明,从接受能力上更好一点。性格上,David更像姜文,Davis更像姜武,呵呵,我喜欢的两个中国纯爷们儿!这是了解他们之后才发现的。最初见到他们时,我常常要问,你们谁是哥哥来着。rn嗯,说起这件小事是为了聊一下delva中的多态。rn先来聊聊重载问题(也叫超载,总之是那个Overload)吧。在同一个类中,出现多个同名的方法的现象就是Overload。联系起上面提到的兄弟俩,我想这更好理解。他们从外观上是相似的,不同的是某些“参数”,类型上可能各异或是个数上不同。rn在Delphi中,所有方法都默认是静态的,便于编译器和链接器解析调用。为实现超载,要在方法明的后面加上关键字overload,不然会产生报错。先看个小例子吧:rntype //这是Delphi对类Ta的定义部分rn Ta=class rn publicrn procedure XiongDi(age,height:integer);overload; //这是Ta的两个公共过程,名字一样rn procedure XiongDi(name:string);overload; //但他们的参数不同。rn end;rnimplementation //以下为实现部分rnprocedure Ta.XiongDi(age, height: integer); //这是两个过程的实现。rnbeginrnForm1.Edit1.Text:='age: '+inttostr(age)+' height: '+inttostr(height);rnend;rnprocedure Ta.XiongDi(name: string);rnbeginrnForm1.Edit2.Text:='name: '+name;rnend;rnDelphi用很强的图形化控件,我们用一个窗体的Button控件来激活上述过程,并用Edit控件显示结果。rnprocedure TForm1.Button1Click(Sender: TObject); rnvarrna:Ta; //这是Delphi对对象的定义,a是Ta的实例化,就是Ta这个类的对象。rnbeginrna.XiongDi(15,130);rna.XiongDi('David');rn这是效果图。rn rn我们看到第一个XiongDi反映的是年龄和身高信息,而第二个XiongDi则反映的是姓名信息。它们的参数个数和类型都不同,要表达的效果也就不同。所以,面对Overload我们不要被表面上的名称所迷惑,而应该了解函数、过程和方法实质的作用。多说一句,函数和过程是Delphi里的叫法,Java中统成为方法,无返回值的方法用void关键字表示,代表Delphi里的过程,而有返回值的方法就是Delphi中的函数。rn再来说说在Java中实现Overload。Java的方法和Delphi刚好相反,所有方法都默认成虚拟的,所以Java中可以随时Overload而不需要标明我要超载这个方法了。为了对比,我在Java中写个和上面一样的例子:rnA.java文件:rnclass A //这是Java中对类A的定义。rn public void XiongDi(int age,int high) //这里定义了两个同名方法rn System.out.println("age: "+age+" high: "+high);rn rnrn public void XiongDi(String name)rn System.out.println(name);rn rnrnDo.java文件:rnpublic class Dorn public static void main(String[] args)rn A a=new A();rn a.XiongDi(15,130);rn System.out.println(" **********");rn a.XiongDi("David");rn rnrn编译并执行的结果。rn rn好了,到此我们对delva的overload算是有了一个感性的认识了。我们可以看到,重载事发生在同一个类中,不同方法之间的现象。就像两兄弟之间一样。呵呵,如果是父子之间又会发生什么有趣的现象呢?rn投掷,这个从我们祖先继承下来的动作,因人而异。比如,我的爸爸投掷东西时是用右手,而我是个左撇子。这种动作相同而执行方式不同的现象就是下面我们要聊的覆盖(又叫改写,就是Override)。嘿嘿,Overload、Override,我们中国人看到这两个相似的单词,最初都有些迷惑,就像老外看到横戌点戍空心戊一样,而他们其实大不相同。覆盖事发生在父类和子类的方法之间的。我们聊到这里就决不会再弄混这两个名词了。但我要说的是,他们还是有联系的。那就是他们同为面向对象编程的主要特点,是包装、继承和多态三大要素中,多态的具体表现。rn闲话少叙,就先在Delphi中来个覆盖的小例子吧:rntypern Tfather=class //我们定义一个父类rn publicrn function throwhand:string; virtual;rn end;rntypern Tson=class(Tfather) //我们定义一个继承父类的子类rn publicrn function throwhand:string;override;rn end;rnrn//这是两个函数的实现:rn Tfather rnfunction Tfather.throwhand: string;rnbegin Form1.edit1.Text:='right'; end;rn Tson rnfunction Tson.throwhand: string;rnbegin Form1.edit2.Text:='left'; end;rnrn//这是对函数的调用:rnprocedure TForm1.Button1Click(Sender: TObject);rnvar FatherUse:Tfather; SonUse:Tson;rnbeginrnFatherUse:=Tfather.Create;rnSonUse:=Tson.Create;rnrnFatherUse.throwhand;rnSonUse.throwhand;rnrnFatherUse.Free;rnSonUse.Free;rnend;rn这是效果图:rn rn我们看到,父类被一个叫virtual的关键字所修饰。这是何故?不加行吗?当然不行,原因就是前面所述,Delphi的方法都是静态的。我们知道,Java中,被修饰成abstract 或final的方法是不能被覆盖的。由此可知,Delphi的默认的静态方法也不能被覆盖。因此,Delphi定义了两个关键字:virtual(虚拟)和dynamic(动态)。它们让函数和过程成为动态的,用于后关联。Virtual和dynamic作用大致相同。只是virtual的方法较dynamic的方法在VMT中占用空间较大,而执行时间较短。这是因为,子类在override父类中的某一个virtual方法时,VMT会为子类提供未被voerride的其它父类的virtual方法。而dynamic则只提供被override的方法。这两个方式个有好处,在Delphi中常被一起使用。rn嗯,再多想想。如果父类的一个virtual方法不但要被子类方法override,还要被同一个类中的同名方法overload,那要怎么做呢?看看下面的例子:rntypern Tfather=classrn public //多了关键字overloadrn function throwhand:string; overload;virtual;rn function throwhand(m:integer):integer; overload;virtual;rn end;rntypern Tson=class(Tfather)rn public //多了关键字reintroduce,我发现不用它也行。但建议使用。rn function throwhand:string;reintroduce;override;rn end;rn我们再来看看实现部分:rn Tfather rnfunction Tfather.throwhand: string;rnbeginrn result:='right';rnend;rnfunction Tfather.throwhand(m: integer): integer;rnbeginrnresult:=m;rnend;rn Tson rnfunction Tson.throwhand: string;rnbeginrn Form1.edit2.Text:='left';rnend;rn这是调用部分:rnprocedure TForm1.Button1Click(Sender: TObject);rnvarrn FatherUse:Tfather;rn SonUse:Tson;rnbeginrnFatherUse:=Tfather.Create;rnSonUse:=Tson.Create;rnrnForm1.edit1.Text:='usehand:'+FatherUse.throwhand+'throwfar:'+inttostr(FatherUse.throwhand(500))+'m';rnSonUse.throwhand;rnrnFatherUse.Free;rnSonUse.Free;rnend;rn好了,Delphi的覆盖就聊到这里,嘻嘻,该来杯咖啡享受一下了。下面是Java的覆盖,它可没有那么多关键字。rn这是Do.java文件的代码:rnclass A //这是父类rn void throwhand()rn System.out.println("right!");rn rnrnclass SA extends A //这是子类rn void throwhand()rn System.out.print("left!");rn rn rnpublic class Dorn public static void main(String[] args)rn A a=new A(); //父类实例化;rn SA s=new SA(); //子类实例化;rn System.out.println("-------------------------");rn System.out.print("Father throwhand is: ");rn a.throwhand(); //调用rn System.out.println(" ");rn System.out.print("Son throwhand is: "); rn s.throwhand(); //调用rn rnrn这是效果图:rn rnrn到此,我对多态的gab就该结束了。希望你至少没有被我的文字弄烦。如果你真的烦了,那后面的系列文章,哎,不看也罢。rn 论坛

没有更多推荐了,返回首页