首页
社区
课程
招聘
疑问
发表于: 2005-6-2 18:14 9048

疑问

2005-6-2 18:14
9048
收藏
免费 0
支持
分享
最新回复 (14)
雪    币: 1223
活跃值: (469)
能力值: (RANK:460 )
在线值:
发帖
回帖
粉丝
2
你能感受到的最大的区别是字长
2005-6-2 18:42
0
雪    币: 100
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
zmh
3
对于win64感觉不大啊  哈哈 让我想到了ipv4和ipv6啊
2005-6-2 18:51
0
雪    币: 209
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
有谁有64位CPU啊,拿出来看看。
2005-6-2 19:37
0
雪    币: 209
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
贴一段64位的反汇编代码:
;       B0:       488F45F0         POP QWORD PTR [RBP-16]
;       B4:       488D65C0         LEA RSP, [RBP-64]
;       B8:       488BD9           MOV RBX, RCX
;       BB:       488BF2           MOV RSI, RDX
;       BE:       4883FB08         CMP RBX, 8
;       C2:       752C             JNE L0
;       C4:       488BC6           MOV RAX, RSI
;       C7:       40240F           AND AL, 15
;       CA:       403C07           CMP AL, 7
;       CD:       7526             JNE L1
;       CF:       488BC6           MOV RAX, RSI
;       D2:       488B50F9         MOV RDX, [RAX-7]
;       D6:       488B4DF0         MOV RCX, [RBP-16]
;       DA:       488B45F8         MOV RAX, [RBP-8]
;       DE:       4883C103         ADD RCX, 3
;       E2:       488BE5           MOV RSP, RBP
;       E5:       488BE8           MOV RBP, RAX
;       E8:       FFE1             JMP ECX
2005-6-2 19:42
0
雪    币: 615
活跃值: (1267)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
6
明白!
2005-6-2 19:55
0
雪    币: 236
活跃值: (70)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
7
如果用dos和win为例的话,win下可以开n个dos。那么到了64时代,我们可以在win64下开n个win98。
2005-6-2 19:57
0
雪    币: 1223
活跃值: (469)
能力值: (RANK:460 )
在线值:
发帖
回帖
粉丝
8
楼上的话好眼熟阿 那本书看来很流行
2005-6-2 20:19
0
雪    币: 519
活跃值: (1223)
能力值: ( LV12,RANK:650 )
在线值:
发帖
回帖
粉丝
9
貌似老梁同志说的
2005-6-2 20:48
0
雪    币: 199
活跃值: (45)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
最初由 xeno 发布
如果用dos和win为例的话,win下可以开n个dos。那么到了64时代,我们可以在win64下开n个win98。


除非你装虚拟机
2005-6-2 21:03
0
雪    币: 339
活跃值: (1510)
能力值: ( LV13,RANK:970 )
在线值:
发帖
回帖
粉丝
11
作者:wowocock

首先让我们来看看 WINXP-64下的GDT概貌吧。

0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 9b 20 0
ff ff 0 0 0 93 cf 0
ff ff 0 0 0 fb cf 0
ff ff 0 0 0 f3 cf 0
0 0 0 0 0 fb 20 0
0 0 0 0 0 0 0 0
68 0 70 10 4e 8b 0 0
0 f8 ff ff 0 0 0 0
ff f 0 0 fe f3 40 ff
0 0 0 0 0 0 0 0
ff ff 0 0 0 9a cf 0
0 0 0 0 0 0 0 0
GDTR
Limit: 6f 0
BASE: 0 0 4e 0 0 f8 ff ff

IDTR
Limit: ff f
BASE: 70 0 4e 0 0 f8 ff ff

基本概念我也就不说了,大家可以参考AMD64和INTEL EM64T的技术手册。
先简单说下2者的区别,其实说实在的基本没什么区别,但就目前INTEL 所出来的那
批所谓的INTEL EM64T的CPU,必须要说明的是都不支持NX执行保护,在对应 EM64T的技术手册
中原本应该是NX位的地方,都是RESERVED的,所以建议大家还是先用AMD64的CPU吧,不过估计最近出来的CPU
,INTEL好象开始加入NX保护技术了,至于那个对于SSE3的支持,大家就自己看着办吧,嘿嘿。。。。
言归正传。让我们来看下WINXP -64 1218版本中的GDT吧。
可以看到原来的NULL SELECTOR还是一样,不过好象扩展到16位的。
后面开始就是所谓的正式的东西了,先简单说下,INTEL在PII以后利用的FASTCALL技术,从RING3切换进RING0
利用指令SYSENTER,SYSEXIT来进行切换,而64位下则采用了SYSCALL,SYSRET来切换,提高了系统切换的性能,但
也有了一定的限制,就是不能象9X那样随意了,在GDT中RING0代码段,数据段,RING3代码段,数据段必须连续排列才行。

而SELECTOR 10H估计就是64位下的RING0代码段的SELECTOR了。
0 0 0 0 0 9b 20 0
由于在LONG MODE下代码段只考虑D,L,P,DPL,C位由此可见他属于64位模式的,存在的RING0,非一致代码段

而SELECTOR 18H估计就是64位下的RING0数据段的ELECTOR了。奇怪的是64位下的只考虑P位,其他一概忽略,确实令人费解。
需要说明的是在64位下DS,ES,SS,所引用的数据段根本就不考虑基地址和界限,完全是FLAT模式的,而FS,GS则有些特殊
系统专门培配置了FS。BASE,GS。BASE,的专门的MSR寄存器来存放其基地址,估计在64位WINDOWS下他们有特殊的用途。并专门
配备SWAPGS等指令快速切换RING0,3的GS地址等。。。。。。

后面是相应的RING3代码段和数据段
ff ff 0 0 0 fb cf 0
ff ff 0 0 0 f3 cf 0
0 0 0 0 0 fb 20 0
23H,2BH,33H这里有3个段。
也就是说RING3代码段有2个,这是为什么呢???
其实也很简单,就是为了兼容性,大家都知道64位WNDOWS下核心是64位的,所有的驱动程序作为OS的一个部分所以也必须是64位的
而应用层则不同,可以同时使用32位和64位,这也就是为什么要有2个RING3代码段的原因了。
首先我们看23H的代码段
ff ff 0 0 0 fb cf 0
其CS。L=0,CS。D=0也就是当指令在这个段的时候系统处于COMPATIABLE MODE下,我们所有的32位程序执行的时候,系统会根据其
是32位还是64位的PE来进行加载。而WOW64估计也是根据他来判断是否需要进行相应的参数转换(进行系统调用的时候)。
而33H的代码段
0 0 0 0 0 fb 20 0
其CS。L=1,CS。D=0也就是当指令在这个段的时候系统处于64 BIT MODE下,也就是64位应用程序执行时候的SELECTOR。
至于 CS。L=1,CS。D=1的这个模式AMD说是为未来处理器准备的,所以我们也就不得而知了。

大家注意40H所对应的描述符是16个字节的,也就是我们所说的TSS
68 0 70 10 4e 8b 0 0
0 f8 ff ff 0 0 0 0
可见他是个基地址为0XFFFFF800004E1070界限为68H的TSS
另外要说的是64位下的GDT是混合8,16字节的,数据代码段的描述符是8字节,而系统描述符是16个字节的,
所以程序员在自己定位的时候需要小心。

53H对应是RING3数据段,界限是0FFFH,基地址0FFFE0000,运行时FS指向他,估计就是运行在ring3下的程序,当前线程的 TEB 所在地址空间。
(由此可见每个RING3程序都有4GB空间,另外的好处就是在驱动通过影射内存来实现共享内存的方式可以直接在应用的32位指针中使用,而不用进行其他转换。)
ff f 0 0 fe f3 40 ff

而后面哪个 ff ff 0 0 0 9a cf 0有什么用目前还不清楚请大家指点下。

不知道大家注意了没有GDT的空间在64位下很小了,后面直接就是IDT,也就是说你根本没地方能插入我们最爱的CALL GATE,
CALL GATE在64位下需要16个字节。估计MS也考虑到这个问题了,所以嘿嘿。。。。。
不过还是有办法解决的,只要我们能进RING0,又有什么是不能做的呢????。。。。。。
后面附上显示XP64下GDT的程序,由于应用和驱动都是64位的,所以建议你在WINXP-64下用,嘿嘿。。。。。。
2005-6-2 21:11
0
雪    币: 100
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
zmh
12
呵呵啊 在好象是vxd中有谈到过gdt和ldt 描述符的时候开几个dos的情况,
2005-6-2 22:55
0
雪    币: 2319
活跃值: (565)
能力值: (RANK:300 )
在线值:
发帖
回帖
粉丝
13
我有一些资料,是我从微软的讲座里听到的

在新的 64 bit 系统上,是可以运行 32 bit 程序

系统会使用 WOW64 layer 来作为 32 bit 程序与系统之间的桥梁,把 API 呼叫进行转换,调整参数,等等的工作。WOW64 作为处理不同 Virtual Space 之间的中介者。它使程序可以在 64 bit 的 AMD64,或 64 bit 的 Itanium 上运行

如果在编程的角度,如果 32 bit 的程序使用了固定长度 ( 4 bytes) 的 pointer,那里在 64 bit 系统里便会运行出错。否则,便没有大问题。

使用 SDK 方式编程来说,为了使 C 语言源码能够达到在不同的系统中运作,所以Windows 的 header 档会增加类似  INT_PTR 这种 type,来代表变数 pointer ,并建议程序员不要使用固定大小的内存,来处理变数 pointer

编程的时候只要依照 Microsoft 在 64 bit 架构的一些守则,便可以顺利地运行在两种平台上,对程序员不会增加额外的忧虑。 另一点,是即使是一个编译了的 32 bit 程序,只要符合规格,是可以在 64 bit 的 windows 上运行

在 C 语言上为例, integer 将会保留 32 bit 长度,为了使程序员不产生混淆。最大的分别,是 DWORD 不可以再用作储存 pointer,因为 pointer 将会是 8 byte 长度。对于一些系统软件,例如 PE 工具,档案处理程序等等,影响比较大。

这些是很初步的资料,希望大家继续补充
2005-6-2 23:09
0
雪    币: 1223
活跃值: (469)
能力值: (RANK:460 )
在线值:
发帖
回帖
粉丝
14
MSDN上的一份很详细的资料。
源地址是:http://www.microsoft.com/china/MSDN/library/windev/Windows2003/NFdnnetservws0364bitdev.mspx

开发 Windows 64 位版本应用程序的简介
发布日期: 09/02/2004 | 更新日期: 09/02/2004
Microsoft Corporation

适用于:
Microsoftt_ Windows Server™ 2003

摘要: 通过引入 64 位的操作系统,Microsoft 已经将 Windows 环境提升到了新的水平。Windows 64 位版本是企业级的操作系统,可以运行在高端系统上,例如内存为 16 TB、处理器多达 64 个的 Intel_ Itanium_ platform。将现有的应用程序从 Windows 32 位版本迁移到 Windows 64 位版本相对来说并不费力气,即使允许两个版本同时从相同的代码库中构建也是如此。Windows API 所提供的现有功能与高性能的 Intel Itanium 平台结合使用可以使开发人员利用他们现有的 Windows API 知识来构建可伸缩的、企业级应用程序。

下载 SimpleWebServerSample.exe。

本页内容
Itanium 概述
Windows 64 位版本简介
Windows 64 位版本的 C/C++ 编程
常规 Windows 编程
小结
资源

Itanium 概述
EFI:Itanium 的 BIOS
Itanium 的 BIOS 并不是您通常熟悉的个人计算机的 BIOS。可扩展固件接口 (EFI) 是将操作系统从 BIOS 和硬件中分离出来的抽象层。EFI Shell 与 Windows 中的命令提示符很相似。在某些方面,EFI Shell 就像是一个内置的小型操作系统。从 EFI Shell,您可以访问驱动器(包括 CD-ROM)、运行可执行文件(例如 Windows 的安装程序),甚至可以执行简单的文本编辑。系统配置数据存储在非挥发性内存中,而不是存储在硬盘上,并且可以通过 EFI Shell 进行配置。

EPIC:Intel 的顺序处理器
Itanium 是一种顺序处理器,意味着它会以指令提供的顺序来执行这些指令。这与普通的 x86 处理器不同,x86 处理器在可能的情况下会在管线中重新排序指令,然后尝试并行执行指令。对于 Itanium 而言,编译器必须明确地排序指令,然后负责检查指令之间的相互依存关系。编译器还必须负责通知处理器可以并行执行的指令。Intel 有一个为此定义的新术语:EPIC。EPIC 表示明确并行指令集计算。它负责编译器执行所有优化。处理器将不会进行任何重新排序。这使得编译器责任更加重大,稍后将在本文中进行讨论。

执行单元
Itanium 由九个执行单元组成,如下所示:

• 两个整数单元

• 两个整数/负载存储单元

• 两个浮点单元

• 三个分支单元

Itanium 具有一个十阶管线,负责提取、解码和执行指令。Itanium 最多可以同时处理六条指令。

寄存器
Itanium 具有多达 328 个寄存器:128 个 64 位整数通用寄存器、128 个 82 位浮点寄存器、64 个 1 位谓词寄存器、8 个分支寄存器以及用于各种目的的其他寄存器的集合,例如 x86 后向兼容性(当运行在 x86 兼容模式中时,Itanium 会将一些 x86 寄存器映射到 64 位寄存器上,同时提供专门用于处理器的 x86 模式的其他寄存器)。

要协助管理如此大量的寄存器,Itanium 有能力同时设计和轮换寄存器。我们将通用寄存器分成两组:前 32 个寄存器是固定的、全局寄存器。后 96 个寄存器可以用于设计和轮换。

寄存器设计
ALLOC 指令用于建立寄存器框架。寄存器框架将物理寄存器(硬件)映射到逻辑寄存器(软件)上,这样当调用某个函数时,不再需要推出或弹出所有参数,编译器就可以为子例程分配一定范围的寄存器,其中的一些寄存器可能会映射到父例程的寄存器上。可以在两者之间重叠的寄存器用于传递参数。这样比将参数推出和弹出到堆栈上更为有效。当然,推出和弹出参数的传统方法仍然可以使用。

由于前 32 个寄存器是固定的,您无法设计它们。因此,可以设计的寄存器的最大数量为(其余的)96 个寄存器。另外,只有整数寄存器可以设计,浮点寄存器和谓词寄存器不能进行设计。

寄存器轮换
寄存器也可以轮换或转移到一个或多个位置。在解开循环时,这可能很有帮助,因此使用不同的物理寄存器,在不相互干预的情况下,那些周而复始地在相同的寄存器集中运行的循环可以同时运行。利用这个选项,编译器可以更进一步改进指令的并行处理。

指令集
IA-64 指令是 41 位长的指令。指定 128 个通用寄存器中之一需要使用七位,并且指定两个源寄存器和一个目标寄存器,一共是 21 位。每条指令可以指定 64 个谓词寄存器中的一个,再加上 6 位。这占用了 27 位,而我们尚未指定实际的操作代码。

指令封装到 128 位的“绑定”中。其中三条指令(123 位),再加上 5 位模板字段。然后,这些绑定会被汇编到“组”中。组是理论上可以同时执行的指令集合。组中的指令没有相互依存关系。在编译时,编译器必须对此进行计算并对绑定一起分组。处理器将不会再次检查编译器的工作,所以编译器必须保证其正确。组可以是任意的长度。模板字段中的一位表示组的结束。

绑定和组是不同的。绑定是指令分派到处理器的方式。Itanium 的总线和解码电路为 128 位宽,刚好用于 3 条指令(Itanium 实际上同时分派两个绑定)。组是指令进行交互的逻辑方式。

有关 Itanium 平台和 IA-64 体系结构的详细信息,请访问 Intel Itanium Web site。有关开发针对 Itanium 平台软件的详细信息,请参阅 Intel Itanium Developer Center。

返回页首
Windows 64 位版本简介
它只是 Windows API
Microsoft_ Windows Server™ 2003 64 位平台并不要求您了解新的 API 就可以利用 64 位环境的好处。并没有出现 Win64 API,它仍然是熟悉的 Win32 API(现在更合适的名称为 Windows API)。出现了一些新的兼容 64 位的数据类型,因此您可能需要对代码进行少量的更改。关键的一点是所有现有的 Win32 知识都可以直接应用到 Windows 64 位版本中,并且您的大部分代码都可以针对 64 位平台进行编译,而无需更改。这也意味着您可以从单个代码库构建代码的 32 位和 64 位版本,减少了由于维护两个代码库所带来的维护开销。

但是,在两个操作系统版本之间存在着非常重要的一些差异,这一点需要注意。Microsoft 已经去除了一些旧的组件,例如 Win16 子系统―Windows 64 位代码不支持16 位的 Windows 版本。也不支持 POSIX 和 OS/2 子系统。不过,出现了一个新的子系统,称为 WOW64。

WOW64
WOW64 是 Windows-32-on-Windows-64 的缩写。它为现有的 32 位应用程序提供了 32 位的模拟,可以使大多数 32 位应用程序在无需修改的情况下运行在 Windows 64 位版本上。它类似于旧的 WOW32 子系统,负责在 Windows 32 位版本下运行 16 位的代码。

硬件本身具有 32 位兼容性模式,可以处理 IA-32 指令的实际执行,而 WOW 层处理诸如在 32 位和 64 位模式之间切换处理器以及模拟 32 位系统的事务。例如,32 位和 64 位程序具有不同的注册表配置单元。还有一个用于 32 位二进制文件的不同的系统目录。64 位二进制文件仍然使用 System32 目录,因此,当 32 位应用程序安装到系统中时,WOW 层会确保将 32 位二进制文件置于一个新的目录 SysWOW64 中。这是通过如下方式实现的:根据应用程序是否运行在 WOW 下,截获对 API 的调用(如 GetSystemDirectory)并返回适当的目录。相同的问题可能会存在于注册表中。因为 32 位和 64 位的 COM 服务器都可以安装在系统上,并位于相同的类标识符 (CLSID) 下,因此 WOW 层需要将对注册表的调用重定向到适当的 32 位或 64 位配置单元中。WOW 层也会处理注册表中某些区域之间的镜像更改,以便使其更简单地支持 32 位和 64 位代码之间的交互操作。

WOW64 非常重要,因为当不关注性能和可伸缩性的问题时,它使您可以利用大多数现有的 32 位代码。它是两种方法的最佳结合。您可以将您的服务迁移到 64 位,同时将 Microsoft 管理控制台 (MMC) 配置管理单元保留为 32 位。Windows 64 位版本包括 MMC 的 32 位和 64 位的版本。当选择保留管理工具为 32 位时,进程间的通讯可能会遇到某些问题,但是只要接口设计正确,诸如远程过程调用 (RPC) 的协议应该可以在 32 位和 64 位进程之间运行。有关 WOW64 的另外一点需要牢记:它并不是为要求高性能的应用程序而设计的。至少,WOW64 子系统需要将 32 位参数扩展到 64 位,并且需要将 64 位的返回值截断为 32 位。在最糟糕的情况下,WOW64 子系统将需要进行内核调用,涉及到的不仅仅是到内核的转换,还有从处理器的 32 位兼容性模式到其本机 64 位模式的转换。在 WOW64 下运行时,应用程序将无法妥当地进行调整。对于那些您要将其保留为 32 位的应用程序而言,请在 WOW64 下测试它们。如果性能不能满足您的期望,您需要考虑将应用程序迁移到 64 位。

WOW64 是在用户模式下实现的,作为 ntdll.dll 和内核之间的层。WOW64 及其支持的一些 DLL 仅仅是可以加载到 32 位进程中的 64 位的 DLL。对于所有其他情况,进程保持为纯进程。32 位的进程无法加载 64 位的 DLL,反之亦然。

有关 WOW64 的详细信息,请参阅 Microsoft_ Platform SDK 中的“64-bit Windows Programming - Running 32-bit Applications”。

虚拟内存和地址空间
默认情况下,Windows 32 位版本的地址空间限制在 4 GB,其中一半是为内核保留的。这限制了普通的应用程序只能使用 2 GB 的有效虚拟内存。2 GB看起来好像很多,但是由于错误的分配算法、大型文件映射甚至过多的使用 DLL,地址空间很容易在应用程序中变得零碎。看一下任务管理器中的“VM Size”列,就会发现普通应用程序消耗的虚拟内存量。当然,就想过去的 DOS 时期(利用 XMS/EMS)一样,有很多种方法可以使 32 位的应用程序访问多于 4 GB 的物理内存。进入物理地址扩展 (PAE) 和地址窗口扩展(Address Windowing Extensions,AWE)。PAE 通过将地址位的数量从 32 扩展到 36 来工作,这样使应用程序可以寻址的空间达到 64 GB。AWE 使应用程序可以将大于 4 GB 的物理内存范围映射到虚拟地址空间中。这两种方法都引入了开销并增加了代码的复杂性。

Windows 64 位版本提供 16 TB 的有效寻址空间,其中一半可用于用户模式的应用程序。这意味着整个数据库可以移动到内存中,显著地提高了性能,或者整个网站可以缓存到内存中。它还可以使代码保留并委托到巨型的邻近虚拟内存块中,无需实际地担心虚拟内存碎片问题。这也考虑到了巨型文件映射对象或共享的内存部分。

返回页首
Windows 64 位版本的 C/C++ 编程
/WP64:使编译器警告您潜在的问题
Microsoft_ Visual C 和 Microsoft_ Visual C++_ .NET 2002 编译器添加了 /WP64 开关,这使您可以测试 32 位代码的 64 位兼容性问题。编译器将发出有关指针截断和不正确转换的警告。将 32 位应用程序迁移到 Windows 64 位版本中前面的一个步骤就是打开这个标记,然后就像通常编译代码那样来编译您的代码。第一次会有几个错误。例如,请看下面这个代码片段:

DWORD i = 0;
size_t x = 100;

i = x; // C4267: warning C4267: '=' : conversion from
    // 'size_t' to 'DWORD', possible loss of data.

在 32 位的平台上,这段代码能够很好的进行编译,因为 size_t 是 32 位的,但是在 64 位的平台上,size_t 就是 64 位的整数。启用 /WP64 后,编译器将会警告您类似的情况。

其他示例:

void func(DWORD context)
{
  char* sz = (char*)context; // C4312: warning C4312:
                // 'type cast' : conversion
                // from 'DWORD' to 'char *' of
                // greater size
  // Do something with sz..
}

char* string = "the quick brown fox jumped over the lazy dog.";

func((DWORD)string); // C4311: warning C4311: 'type cast' :
           // pointer truncation from 'char *'
           // to 'DWORD'

在修复这些错误后,请测试您的 32 位代码。您希望确保 32 位的代码继续按预期那样工作。32 位和 64 位二进制文件应该从相同的代码库中构建。这就是编写不断前进的 Windows 应用程序的关键概念。开始时,您需要考虑 32 位和 64 位的问题,并且为应用程序编写可以运行在这两个平台上的代码。

新的数据类型
Windows 64 位版本使用 LLP64 数据模型。这意味着标准 C 类型 int 和 long 保持为 32 位整数。数据类型 size_t 将映射到处理器词大小(IA32 为 32 位,IA64 为 64 位),并且 __int64 是 64 位整数。在协助迁移 32 位代码时就会完成上述操作。意义在于您可以对应用程序的 32 位版本和 64 版本使用相同的代码库。

还有一个称为 LP64 的数据模型,它将标准的 C 类型 long 映射到 64 位整数,并使 int 保持为 32 位的整数。这种数据模型常见于 Unix 平台,但从单个代码库同时创建应用程序的 32 位和 64 位版本时可能有一些困难。您可能注意到了此处的常见主题。32 位平台与 64 位平台的思想就是应该能够从单个代码库中构建两个版本的应用程序。如果无法做到,那么您可能要重新审视您的设计。具有单个代码库就是巨大的胜利,尤其是如果您计划发行两个版本。

多态类型
由于 Win32 API 是针对 C 的,在很多情况下,您都需要将整数转换成指针或者相反。在 32 位的硬件上不会有问题,其中指针的大小和整数的大小是相同的,但在 64 位的硬件上却完全不一样。这就是多态类型出现的原因。

对于特定的精度,您可以使用固定精度的数据类型。不管处理器的词大小如何,它们的大小都是一致的。大多数这些类型都在它们的名称中包含精度,可以从下面的表中看出:

表 1. 固定精度的数据类型
类型 定义
DWORD32
32 位无符号整数

DWORD64
64 位无符号整数

INT32
32 位有符号整数

INT64
64 位有符号整数

LONG32
32 位有符号整数

LONG64
64 位有符号整数

UINT32
无符号 INT32

UINT64
无符号 INT64

ULONG32
无符号 LONG32

ULONG64
无符号 LONG64

此外,当您需要数据类型的精度随着处理器词大小变化时,请使用指针精度数据类型。这些类型又称为“多态”数据类型。这些类型通常以 _PTR 后缀结尾,如下面的表格所示:

表 2. 指针精度的数据类型
类型 定义
DWORD_PTR
指针精度的无符号长类型

HALF_PTR
指针大小的一半。用于包含一个指针和两个小型字段的结构中

INT_PTR
指针精度的有符号整型

LONG_PTR
指针精度的有符号长类型

SIZE_T
指针可以引用的最大字节数。用于必须跨指针的整个范围的计数

SSIZE_T
有符号 SIZE_T

UHALF_PTR
无符号 HALF_PTR

UINT_PTR
无符号 INT_PTR

ULONG_PTR
无符号 LONG_PTR

LPARAM
与 LONG_PTR 为同义词,(在WTypes.h 中定义)

WPARAM
与 UINT_PTR 为同义词,(在 WTypes.h 中定义)

通过整数参数传递参数或上下文信息的所有 Win32 API 都更改为使用这些新的类型。SetWindowLong 和 SetWindowLongPtr 函数都是很好的示例:

旧方法:

LONG SetWindowLong(
    HWND hWnd,
    int nIndex,
    LONG dwNewLong);

新的多态方法:

LONG_PTR SetWindowLongPtr(
      HWND hWnd,
      int nIndex,
      LONG_PTR dwNewLong);

请注意,该函数的 xxxPtr 版本使用新的多态类型。对于开发人员而言,通过在窗口的额外数据区域中存储指针来存储窗口的上下文信息是相当常见的。使用 SetWindowLong 函数在 Windows 32 位版本上存储指针的任何代码必须更改为调用 SetWindowLongPtr。该更改非常简单并且很快就可以完成,因为大多数更改要求使用多态类型。

另外,WindowProc 和 GetQueuedCompletionStatus 也是很好的示例:

LRESULT CALLBACK WindowProc(
          HWND hWnd,
          UINT uiMsg,
          WPARAM wParam,
          LPARAM lParam);

BOOL GetQueuedCompletionStatus(
          HANDLE hCompletionPort,
          LPDWORD lpNumberOfBytes,
          PULONG_PTR lpCompletionKey,
          LPOVERLAPPED* lpOverlapped,
          DWORD dwMilliseconds);

WindowProc 使用 LPARAM,后者是多态类型。GetQueuedCompletionStatus 使用 ULONG_PTR,后者也是多态类型。这使那些假设整数的大小与指针大小相同的现有代码可以在进行很少修改的情况下继续工作。

编译器的新优化模式:PoGO 和 LTCG
包括 Microsoft_ Visual Studio_ .NET 2002 的编译器包含两个新的优化模式:Link Time Code Generation(LTCG,又称 Whole Program Optimization)和 Profile Guided Optimization (PoGO)。代码优化在 Itanium 处理器上比在 x86 平台上更为重要,因为编译器对生产高效代码负有全部责任。这两种优化模式将增加构建次数,并且要求良好的测试方案,尤其是 PoGO(因为它需要捕获分析数据)。LTCG 允许链接器跨模块边界执行优化,并且通过生成更好的内嵌代码甚至使用自定义调用约定,在链接阶段实际地生成代码以产生更有效的二进制文件。PoGO 使编译器可以根据使用模式来进行优化。它要求两个阶段的构建过程。在第一个阶段中,二进制文件用于使它收集分析数据。在第二个阶段中,对分析数据进行分析后,数据用于指导优化。

杂项性能
当前的编译器擅长于产生高优化的代码。在 Itanium 处理器上,编译器负责非常多的工作。因为 Itanium 是顺序处理器,编译器必须执行优化(如重新排列指令),以便它们可以并行执行。同样,借助于增加的谓词寄存器,编译器在优化分支处理上有了更多的自由。使用谓词寄存器,编译器可以完全去除一个分支,并可以使用指令的谓词字段来控制是否实际地执行某个指令。这对于性能是有好处的,因为不在小块的代码上跳转以及不再使指令预取无效,编译器可以通知处理器有条件地忽略这些指令。

校准
考虑 Itanium 处理器上的校准非常重要。指针必须在 64 位边界上校准,否则您将收到一个校准异常。您可以使用 UNALIGNED 关键字来允许未校准的指针差异,但您将为此付出巨大的性能代价。通常情况下,可以使用 #pragma pack 指令,让编译器处理结构的校准。这使您可以指定用于 32 位和 64 位的不同的校准(在编译时),而不是手动校准代码中的结构。

返回页首
常规 Windows 编程
以下信息提供了在设计可伸缩性时需要牢记的实践指导原则;您可以利用的构建到 Windows Server 2003 中的某些令人兴奋的功能的简介;以及对要避免的一些模式的讨论。

性能和可伸缩性
为了可以伸缩,您必须了解这对于您特定的方案意味着什么。例如,对于 Web 服务器,可伸缩性意味着可以为与所连接的用户数量相关的页面提供服务。请将其考虑为线图表。

随着用户数量的增加,每秒的页面数量也应该增加。上图显示了一个线性比例。当用户数量达到 3 倍时,每秒提供的页面也相应增加。

另一个比例的定义与硬件相关。如果我将系统上的处理器数量增加一倍,那么我的 Web 服务器是否也会将输出增加一倍呢?RAM 或磁盘等情况如何呢?应用程序也需要根据这个想法设计。您所创建的线程数量应该基于系统中的处理器数量以及每个线程进行的工作类型。用于缓存网页内容的内存量应该与可用于应用程序的内存量成一定比例,等等。这个概念通常被称为“向上扩展”。如果我将框构建得越来越大,那么可以相应的生成越来越多吗?

伸缩的其他形式是当您谈论分布式计算或服务器场时。这通常被称为“向外伸缩”。如果我将服务器场中的计算机数量增加一倍,那么我的输出也会增加一倍吗?

当设计可伸缩的系统时,需要考虑这些方案。当前,硬件变得越来越大(Itanium 最多支持 64 个处理器计算机),因此向上扩展需要在开发人员头脑中处于最重要的位置。如果您的图表转为水平、甚至随着您增加资源开始下降,这尤为正确。如果整个系统的某个部分无法伸缩,它可能会对整个系统产生负面的影响。

线程:如何有效地使用它们
在线程间分割您的工作可以简化代码,在多处理器系统上可以使您的代码更有效,但是如果您不知道自己在做什么的话,还会降低性能和可伸缩性。例如,如果应用程序中的所有线程都需要获得相同的全局关键部分,那么对该关键部分的争用可能会使您的线程花费其大部分休眠时间。它还可能导致发生过多的上下文转换,进而可能会引起应用程序占用系统内核中相当比例的处理时间,甚至根本没有运行您的代码。如果在多处理器的系统上,这些问题会尤其糟糕,您额外的处理器可能会结束当前闲置,等待访问共享数据。

要使用的线程的理想数量等于系统中处理器的数量。如果您的线程相互独立并受到处理器的限制,那么它们应该能够每次都消耗掉其整个时间片。如果您具有可能执行阻止操作的线程,那么您可能希望增加线程的数量,以便当一个线程休眠时,另一个线程可以取代其位置。您将要确定线程阻塞的位置及频率。意识到这一点后,您就可以知道应该运行的线程数量。您始终要为每个处理器准备好一个线程。否则,您就浪费了处理能力。当然,这些仅仅是指导原则,并且确定应用程序是否以尽可能高的效率运行的唯一方法就是对其进行分析和测试。

异步 I/O:不会阻塞等待数据
基于 NT 内核的 Windows 系统支持异步 I/O,又称重叠的 I/O。大多数形式的 I/O 都可以异步完成。这包括文件 I/O 和网络 I/O。对于文件 I/O,您可以使用 ReadFile/WriteFile API。当读/写时,通过借助 FILE_FLAG_OVERLAPPED 标记打开文件并指定 OVERLAPPED 结构,您将使系统在 I/O 完成时通知您。这使您可以在等待的过程中完成其他工作。对于使用 Windows Socket (WinSock) 的网络 I/O,您可以使用 WSASocket 创建套接字,并指定 WSA_FLAG_OVERLAPPED 标记,然后当调用 WSARecv/WSASend API 时,您可以指定一个 OVERLAPPED 结构或一个回调函数。当您编写网络服务器时,异步 I/O 尤为有效。您可以将多个接收请求“排入队列”,然后去休息,等待其中一个完成。当一个完成时,就会处理传入的数据,然后将另一个接收“排入队列”。这比使用 select API 来轮询数据好得多,并且它可以更有效地使用系统资源。

等待异步 I/O 请求完成有几种选项:

调用 GetOverlappedResult API

在发出异步 I/O 请求后,您可以使用 GetOverlappedResult API 来轮询请求的状态,或者仅仅等待请求的完成。当请求完成时,GetOverlappedResult 将返回请求过程所传输的字节数。

使用 HasOverlappedIoCompleted 宏

您可以使用 HasOverlappedIoCompleted 宏来有效地进行轮询与 OVERLAPPED 结构相关联的请求是否已经完成。请求完成后,您就可以使用 GetOverlappedResult API 来获得有关请求的更多信息(例如传输的字节数)。

指定 OVERLAPPED 结构中的事件

通过在 OVERLAPPED 结构的 hEvent 字段中指定一个事件,您可以执行自己的轮询或等待请求的完成,方法是在对 WaitForSingleObject 或 WaitForMultipleObjects 的调用中指定一个事件。当重叠操作完成时,内核将发信号通知该事件。

将内核对象绑定到 I/O完成端口

I/O 完成端口是系统提供的非常有用的工具。有关信息,请参阅下面的部分。对于事件驱动的系统(例如网络服务器等待输入),I/O 完成端口提供了用于等待和处理传入事件的完美机制。

I/O 完成端口:事件驱动 I/O
大多数 Windows 开发人员都熟悉窗口消息和消息队列。将 I/O 完成端口理解为高性能、高可伸缩性的超级消息队列。如果您具有一个事件驱动的系统,则您需要使用完成端口。完成端口从根本上设计用于提供性能。如果您从头开始编写代码,您绝对应该使用 I/O 完成端口。它们要求进行一些尝试才能正确完成,但是只要您熟悉了它们的工作方式,使用起来就会非常简单。如果从另一个系统或使用异步 I/O 的代码库迁移应用程序,那么您必须提前完成一些工作,但由此带来的好处证明所做的努力是完全值得的。

您可以使用 CreateIoCompletionPort API 来创建完成端口。这也是您用于关联内核对象与完成端口的 API。在文件句柄或套接字句柄与完成端口相关联后,在该句柄上完成的所有 I/O 请求都将排列到完成端口队列中。

通知可以被排列到完成端口队列中,或者按照先进先出 (FIFO) 顺序进行处理。您还可以使用 PostQueuedCompletionStatus API 将自定义的通知排列到完成端口队列中。使用这个自定义通知方法是一个很好的方法,用于向线程发出信号通知其关机或插入任何其他自定义外部事件。在下面的示例代码中,PostQueuedCompletionStatus 用于通知工作线程退出:

HRESULT StopCompletionThreads()
{
  // Tell the threads that were started, to shut down
  for (size_t i = 0; i < COMPLETION_THREAD_COUNT; i++)
  {
    assert(g_completionThreads[i]);
    PostQueuedCompletionStatus(g_completionPort, 0, 0, NULL);
  }

  // Wait for the threads to shutdown
  WaitForMultipleObjects(
    COMPLETION_THREAD_COUNT,
    g_completionThreads,
    TRUE,
    INFINITE);

  // Close the handle for each thread
  for (size_t i = 0; i < COMPLETION_THREAD_COUNT; i++)
  {
    CloseHandle(g_completionThreads[i]);
    g_completionThreads[i] = NULL;
  }

  return S_OK;
}

请注意,为 dwNumberOfBytesTransferred 和 dwCompletionKey 参数传递零,而为 OVERLAPPED 参数传递 NULL。这些组合的值是工作线程检查用于关机的值:

UINT __stdcall CompletionThread(PVOID param)
{
  BOOL      result      = FALSE;
  OverlappedBase* overlapped    = NULL;
  ULONG_PTR    key        = 0;
  DWORD      numberOfBytes   = 0;

  for (;;)
  {
    result = GetQueuedCompletionStatus(
          g_completionPort,
          &numberOfBytes,
          &key,
          (OVERLAPPED**)&overlapped,
          INFINITE);
    if (result)
    {
      if (numberOfBytes == 0 && key == 0 && !overlapped)
        break;

      OverlappedCallback callback =
          overlapped->callback;

      callback(
        NO_ERROR,
        numberOfBytes,
        key,
        overlapped);
    }
    else
    {
      if (overlapped)
      {
        OverlappedCallback callback =
          overlapped->callback;

        if (callback)
        {
          callback(
            GetLastError(),
            numberOfBytes,
            key,
            overlapped);
        }
      }
    }
  }

  return 0;
}

I/O 完成方法的核心是 OVERLAPPED 结构。OVERLAPPED 结构包含特定于每个 I/O 请求的上下文信息。通常情况下,将结构进行扩展以添加自己的上下文信息。当处理完成通知时,可以获得对该结构(以及您的上下文数据)的访问。

通过从 OVERLAPPED 结构继承或将其包括为自己结构的第一个字段来扩展 OVERLAPPED 结构,如下所示:

//C++
struct OverlappedBase : public OVERLAPPED
{
   OverlappedCallback   callback;
};
或者
//C
struct OverlappedBase
{
   OVERLAPPED         overlapped;
   OverlappedCallback   callback;
};

OVERLAPPED 结构包含下列字段:

typedef struct _OVERLAPPED {
   ULONG_PTR   Internal;
   ULONG_PTR   InternalHigh;
   DWORD      Offset;
   DWORD      OffsetHigh;
   HANDLE   hEvent;
} OVERLAPPED;

当读取文件或写入文件时,Offset 和 OffsetHigh 字段用于指定偏移量。Internal 字段包含操作的状态(或错误)。InternalHigh 字段包含在 I/O 请求过程中传输的字节数。在 GetOverlappedResult 返回 TRUE(或者完成通知排列到完成端口队列中)之前,Internal 和 InternalHigh 字段都是无效的。

可以扩展该结构以包括您可能需要的任何其他字段。但是,请牢记,结构必须在 I/O 请求的生存期中保持可用。

下面的代码片段显示了 OVERLAPPED 和 OverlappedBase 结构是如何为网络 I/O 操作进行扩展的:

#define SOCKET_BUFFER_SIZE     128

#define SOCKOP_ACCEPT        1
#define SOCKOP_RECV         2
#define SOCKOP_SEND         3

struct SocketOverlapped
{
   OverlappedBase  base;
   int        op;
   SOCKET      sock;
   DWORD       numberOfBytes;
   DWORD       flags;
   BYTE       buffer[SOCKET_BUFFER_SIZE];
};

这允许每个请求信息可以与已启动的每个 I/O 请求一起存储。op 字段存储正在启动、发送、接收或接受的操作。numberOfBytes 字段包含有效的(用于发送或接收)buffer 字段中的字节数。

划分和征服:让线程独立工作
可伸缩性的弊端在于争用。例如,当一个线程必须等待另一个线程以获取锁定时,该线程就在浪费时间,并且潜在地可以完成的工作必须等待。这会引起线程关系和非一致的内存访问 (NUMA)。如果您的处理可以在线程之间进行分割(在线程之间没有实际的依存关系),那么可以将每个线程锁定到其自己的处理器上。在 NUMA 系统上,您还可以分割每个线程使用的内存,这样对于 NUMA 节点,内存是本地的。

线程关系
Windows Server 2003 使您可以指定允许某个线程在哪个处理器上运行。这称为设置线程的处理器关系。您可以使用 CODE>SetThreadAffinityMask/GetThreadAffinityMask 函数来进行设置并检查特定线程的关系。设置线程的关系在降低处理器间总线通讯方面很有用。当线程从一个处理器移动到另一个处理器时,当前处理器的缓存必须与新的处理器进行同步。处理器之间的线程跳转可能会引起性能问题。另外,某些系统使您可以将特定的设备中断绑定到特定的处理器。在您的软件中,您可以将特定的线程“绑定”到该处理器,并且从该线程发出/处理该设备的所有 I/O,因此通过增加潜在的系统并发(即,在多处理器之间快速传播如网卡这样的活动设备)。

NUMA
NUMA 表示非一致的内存访问。在传统的对称多处理 (SMP) 系统上,系统中的所有处理器对整个范围的物理内存具有相同的访问权限。传统 SMP 系统的问题在于添加越多的处理器和内存,总线通讯量就会越高。也就会抑止性能。在 NUMA 系统上,处理器分组成较小的系统,每个小系统都有其自己的“本地”内存。访问“本地”内存成本很低,然而访问另一个节点上的内存代价可能会非常昂贵。Windows 将尝试在正在使用内存的节点上计划线程,但是可以使用 NUMA 函数来改进线程计划和内存使用情况来帮助 Windows。使用下列功能来确定哪个处理器属于哪个节点,以及为特定的处理器/节点设置线程的关系:

GetNumaHighestNodeNumber

GetNumaProcessorNode

GetNumaNodeProcessorMask

SetThreadAffinityMask

另外,大量利用内存的应用程序可以使用以下函数来改进它们在 NUMA 系统上的内存使用情况:

GetNumaAvailableMemoryNode

开始考虑 NUMA 和大型多处理器系统以及从头开始为它们进行设计是非常重要的。大多数最初的 64 位部署都将用于大型多处理器系统,该系统的处理器多于 8 个,运行诸如 Secure Audio Path (SAP) 这样的巨型企业应用程序。NUMA 对于整体可伸缩性和性能非常关键。

WinSock Direct
在大型的数据中心中,服务器之间的通信量可能会超出传统基于 TCP/IP 网络的带宽。通过卸载一些来自 CPU 的网络协议处理,系统区域网 (SAN) 设计用于解决这个问题。在服务器之间提供更快速的通讯对服务器应用程序很有好处,这样就改进了向外扩展解决方案的性能。大多数 SAN 要求直接针对供应商的 API 编写应用程序,这就导致很少有应用程序可以用于在 SAN 环境中进行部署。Microsoft 开发的 Windows Sockets (WinSock) Direct 针对低级 SAN 实现提供了一个通用编程接口。WinSock Direct 位于标准 WinSock API 下,但是绕过了内核网络层直接与 SAN 硬件进行对话。因为 WinSock Direct 位于现有的 WinSock API 下,所以 IT 部门可以在 SAN 环境中部署应用程序,而无需对应用程序进行修改。

SAN 通过两种标准的传输模式,提供了可靠的、顺序的数据提交,这两种模式是:消息和远程直接内存访问 (RDMA)。消息很像传统的网络协议,其中数据包发送到某个对等方,而该对等方会从网络中请求数据包。RDMA 允许指定数据包的目标缓冲区。

通常情况下,SAN 硬件将直接在硬件中实现大部分其数据传输功能。这使得 SAN 实现可以完成诸如绕过内核这样的操作。通常由内核提供的处理直接卸载到硬件中。

WinSock Direct 避免了应用程序直接编程到 SAN 特定的 API 的要求。只通过安装 SAN 硬件以及为硬件安装 WinSock Direct 驱动程序,现有的应用程序就可以利用由 SAN 提供的更高的性能。

下列资源提供了有关线程关系、NUMA 和 WinSock Direct 的其他信息。

DLLs, Processes, and Threads:Multiple Processors

NUMA Support

WinSock Direct:The Value of System Area Networks

WinSock Direct and Protocol Offload on SANs

Windows Sockets 2.0:Write Scalable WinSock Apps Using Completion Ports

安全性
在编写软件时,安全性应该在每个开发人员头脑中处于最重要的位置。编写安全代码的一个最重要工具就是深入地了解正在使用的 API。处理空终止字符串的 API是一个很好的示例来进行研究。大多数 API 将为您使用空终止字符串,但并不是所有。例如,从 Visual Studio .NET 产品文档中有关 _snprintf, _snwprintf 的备注部分摘录如下:

The _snprintf function formats and stores count or fewer characters and values (including a terminating null character that is always appended unless count is zero or the formatted string length is greater than or equal to count characters) in buffer.

基本上,这说明如果目标缓冲区并不足够大,或者大小完全正好,它将不会以空终止字符串结束。这是非常非常重要的一点。如果在堆栈上有一个 1024 字符的缓冲区,并且用户的输入恰好是 1024 个字符,那么缓冲区将不会以空终止字符串结尾,并且假设缓冲区总是以空终止字符串结尾的任何代码(且小于 1024 个字符)都会出现缓冲区溢出。底线是非常熟悉您的 API。有关 Windows Server 2003 API 的文档,请参阅 MSDN 上的 Platform SDK。

所有用户输入应该经过正确性验证,意味着对期望的格式进行验证或者对最大长度进行验证。应该尽早地执行验证。另外,应该仔细检查产生自验证的任何错误消息以避免漏掉太多的信息。尽管您应该始终假设黑客具有访问源代码的权限(并在设计安全性时牢记这一点),但是您不希望让黑客很容易就得手。错误消息如:“The buffer cannot exceed 1024 characters”,这样的错误消息立即告诉黑客 1024 个字符非常可能是保存数据的所有堆栈缓冲区的大小。而且,这样的错误消息对于普通用户而言没有什么意义,只会使其混淆。

您的应用程序应该设计为“默认情况下安全”。这意味着您需要评估您的功能及其潜在的安全风险。不要低估意志坚定的黑客的能力。即使您认为功能是安全的,如果它不是最基本的,也请将其关闭。为用户提供信息,使其能够决定是否启用该功能。开发人员可能会犯错误;您并不希望拿客户的系统和数据来冒风险。

同样,在考虑安全性时,不需要再重新做一次。Windows API 提供了很多有用的 API 来执行如访问检查、加密和存储敏感数据的操作。

如果您创建了命名的内核对象(如命名的 mutex 或共享的内存部分),请使用访问控制列表 (ACL) 来保护到对象的访问。在所有创建的文件上使用 ACL(或者在已经具有正确的 ACL 的目录中创建文件)。也要考虑在哪里创建注册表键。用户是否应该可以更改它们?它们是否应该是计算机范围的?管理员是否应该能够锁定对它们的访问?请记住,用户特定的设置应该转到 HKEY_CURRENT_USER 中,而计算机范围的设置需要转到 HKEY_LOCAL_MACHINE。

如果您需要存储敏感的用户数据,例如用户名/密码组合或个人信息,请使用操作系统提供的加密提供程序来加密这些数据。使您入门的简单函数是 CryptProtectData 和 CryptUnprotectData。

小结
正如您所看到的那样,将应用程序迁移到代码库(该代码库同时为 Windows 32 位和 64 位版本进行编译)中的过程是相对非常简单的。有一些方案要进行考虑,例如指针、截断和数据校准。但是,这些问题并不是无法逾越的,仅仅是挑战而已。请牢记,为了构建可以很好伸缩的应用程序,仔细考虑线程的使用和线程模型是非常重要的。差的线程执行是伸缩的杀手。
2005-6-3 09:33
0
雪    币: 229
活跃值: (168)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
15
好东西要顶!
2005-6-3 18:10
0
游客
登录 | 注册 方可回帖
返回
//