Solaris(64 位)开发者指南

第 6 章 高级主题

本章所介绍的各种编程主题面向需要详细了解 64 位 Solaris 操作环境的系统程序员。

尽管 64 位环境的几个新增特征是 64 位环境所特有的,但是大多数新增特征都是对普通 32 位接口的扩展。

SPARC V9 ABI 特征

64 位应用程序使用可执行和链接格式 (Executable and Linking Format, ELF64) 进行描述,使用该格式可以全面地描述大型应用程序和较大地址空间。

SPARC V9。《SPARC Compliance Definition Version 2.4》中包含 SPARC V9 ABI 的详细信息。此文档描述了 32 位 SPARC V8 ABI 和 64 位 SPARC V9 ABI,可以从 SPARC International 公司的网站 www.sparc.com 上获取。

以下列出了 SPARC V9 ABI 的特征:

栈偏移量

SPARC V9。对于开发者来说,SPARC V9 ABI 的一个重要特征就是栈偏移量。 对于 64 位 SPARC 程序来说,必须向帧指针和栈指针中都添加大小为 2047 个字节的栈偏移量,才能达到栈帧的实际数据。 请参见下图。

该图说明如何为 64 位 SPARC 程序添加大小为 2047 个字节的栈偏移量

有关栈偏移量的更多信息,请参见 SPARC V9 ABI。

SPARC V9 ABI 的地址空间布局

SPARC V9。对于 64 位应用程序,尽管起始地址和寻址限制大不相同,但地址空间的布局与 32 位应用程序的布局密切相关。与 SPARC V8 一样,SPARC V9 栈从地址空间的顶部开始减小,而堆则从底部开始扩展数据段。

下图说明了为 64 位应用程序提供的缺省地址空间。地址空间中标记为保留空间的区域可能不是由应用程序进行映射。这些限制在将来的系统中可能会有所放松。

该图说明为典型的 SPARCV9 64 位应用程序分配的地址空间

上图中的实际地址描述了特定计算机上的特定实现,仅供参考。

SPARC V9 ABI 文本和数据的位置

缺省情况下,64 位程序与起始地址 0x100000000 链接,整个程序(包括其文本、数据、堆、栈和共享库)将超过 4 GB。这有助于确保 64 位程序正确无误,方法是使其在截断其任何指针时在较低的 4 GB 地址空间中出错。

尽管 64 位程序会链接到 4 GB 以上的地址空间,但是仍可以通过使用链接程序映射文件以及编译器或链接程序的 -M 选项,将其链接到 4 GB 以下的地址空间。/usr/lib/ld/sparcv9/map.below4G 中提供了一个用来将 64 位 SPARC 程序链接到 4 GB 以下地址空间的链接程序映射文件。

有关更多信息,请参见 ld(1) 链接程序手册页。

SPARC V9 ABI 的代码模型

SPARC V9。编译器针对不同目的提供了不同的代码模型,旨在提高性能并减少 64 位 SPARC 程序中代码的大小。代码模型由以下因素确定:

下表介绍了可用于 64 位 SPARC 程序中的不同代码模型。

表 6–1 代码模型说明:SPARC V9

代码模型 

可放置性 

代码大小 

位置 

外部对象引用模型 

abs32

绝对 

< 2 GB 

低(低 32 位地址空间) 

无 

abs44

绝对 

< 2 GB 

中(低 44 位地址空间) 

无 

abs64

绝对 

< 2 GB 

任意位置 

无 

pic

PIC(位置无关代码) 

< 2 GB 

任意位置 

小(<= 1024 个外部对象) 

PIC

PIC 

< 2 GB 

任意位置 

大(<= 2**29 个外部对象) 

在某些情况下,可以使用较小的代码模型实现较短的指令序列。 在绝对代码中执行静态数据引用所需的指令数在不同的代码模型中各不相同:在 abs32 代码模型中最少,在 abs64 代码模型中最多,在 abs44 代码模型中居于两者之间。 同样,在执行静态数据引用时,pic 代码模型使用的指令比 PIC 代码模型使用的要少。 因此,代码模型越小,代码块越小,对于无需利用较大代码模型的更完整功能的程序,还可能会提高其性能。

要指定要使用的代码模型,应使用 -xcode=<model> 编译器选项。目前,对于 64 位对象,编译器在缺省情况下使用 abs64 模型。 通过使用 abs44 代码模型可以优化代码,此时将使用较少的指令,并且仍能涵盖当前的 UltraSPARC 平台所支持的 44 位地址空间。

有关代码模型的更多信息,请参见 SPARC V9 ABI 和编译器文档。


注 –

对于使用 abs32 代码模型编译的程序,必须使用 -M /usr/lib/ld/sparcv9/map.below4G 选项将其链接到 4 GB 以下的地址空间。


AMD64 ABI 特征

64 位应用程序使用可执行和链接格式 (Executable and Linking Format, ELF64) 进行描述,使用该格式可以全面地描述大型应用程序和较大地址空间。

以下列出了 AMD ABI 的特征:

请参见 amd64 psABI 草案文档《System V Application Binary Interface, AMD64 Architecture Processor Supplement》(草案版本 0.92)。

amd64 应用程序的地址空间布局

对于 64 位应用程序,尽管起始地址和寻址限制大不相同,但地址空间的布局与 32 位应用程序的布局密切相关。与 SPARC V9 一样,amd64 栈从地址空间的顶部开始减小,而堆则从底部开始扩展数据段。

下图说明了为 64 位应用程序提供的缺省地址空间。地址空间中标记为保留空间的区域可能不是由应用程序进行映射。这些限制在将来的系统中可能会有所放松。

该图说明为典型的 amd 64 位应用程序分配的地址空间

上图中的实际地址描述了特定计算机上的特定实现,仅供参考。

对齐问题

还有一个问题也与数据结构中 32 位 long long 元素的对齐相关;i386 应用程序仅以 32 位为边界将 long long 元素对齐,而 amd64 ABI 则限制 long long 元素以 64 位为边界,这有可能会在数据结构中产生更宽的空白区域。 这与 SPARC 中有所不同,在 SPARC 中 32 位或 64 位 long long 项均以 64 位为边界对齐。

下表说明了指定体系结构中的数据类型对齐情况。

表 6–2 数据类型对齐

体系结构 

long long 

double 

long double 

i386 

amd64 

16 

sparcv8 

sparcv9 

16 

针对 SPARC 系统,尽管看起来已经是干净的 64 位代码,但是如果在 32 位和 64 位编程环境之间复制数据结构,则这两个环境中不同的对齐情况可能会产生问题。这些编程环境包括设备驱动程序的 ioctl 例程、doors 例程或其他 IPC 机制。通过对这些接口仔细进行编码,并且谨慎地使用 #pragma pack_Pack 指令,可避免对齐问题。

进程间通信

以下进程间通信 (interprocess communication, IPC) 元语仍适用于 64 位和 32 位进程之间的通信。

尽管所有这些元语都允许在 32 位和 64 位进程之间进行通信,但是可能需要明确执行一些步骤来确保在进程间交换的数据可以由所有这些元语正确解释。例如,两个进程共享由 C 数据结构所描述的数据,并且该数据结构中包含类型为 long 的变量,如果不了解 32 位进程将该变量视为 4 字节值,而 64 位进程将该变量视为 8 字节值,则在这两个进程之间交换的数据将无法正确解释。

处理此差异的一种方法是确保数据在这两个进程中具有完全相同的长度和含义。构建使用定宽类型(如 int32_tint64_t)的数据结构。处理对齐问题时仍需要小心。对于共享数据结构,可能需要对其进行填充,或者使用编译器指令(如 #pragma pack_Pack)对其重新打包。请参见对齐问题

<sys/types32.h> 中提供了用于镜像系统派生类型的派生类型系列。这些类型的符号和长度与 32 位系统的基本类型相同,但是按照特定方式进行定义,以便长度在 ILP32 和 LP64 编译环境中保持不变。

在 32 位和 64 位进程之间共享指针极为困难。显然,指针长度不同,但更重要的是,尽管在现有的 C 用法中有一个 64 位整数值 (long long),但是 64 位指针在 32 位环境中没有等效指针。为了使 64 位进程可以与 32 位进程共享数据,必须使 32 位进程一次最多只能查看 4 GB 共享数据。

XDR 例程 xdr_long(3NSL) 可能是一个问题,但是,在线上仍然会将其作为 32 位值进行处理,以便与现有协议兼容。如果要求该例程的 64 位版本对不适合 32 位值的 long 值进行编码,则编码操作将失败。

ELF 和系统生成工具

64 位二进制对象存储在 ELF64 格式的文件中,此格式与 ELF32 格式非常相似,不同的是大多数字段都已增大,可以适应所有的 64 位应用程序。ELF64 文件可以使用 elf(3ELF) API(例如 elf_getarhdr(3ELF))进行读取。

32 位和 64 位版本的 ELF 库 elf(3ELF) 可同时支持 ELF32 格式和 ELF64 格式及其对应的 API。这允许应用程序从 32 位或 64 位系统(尽管 64 位系统仍需执行 64 位)生成、读取或修改这两种文件格式。

此外,Solaris 还提供了一组 GELF(常规 ELF)接口,以便允许程序员使用一个单独的公用 API 来处理这两种格式。请参见elf(3ELF)

所有的系统 ELF 实用程序(包括 ar(1)nm(1)ld(1)dump(1))均已进行更新,可以接受这两种 ELF 格式。

/proc 接口

/proc 接口可供 32 位和 64 位应用程序使用。32 位应用程序可以检查和控制其他 32 位应用程序的状态。因此,可以使用现有的 32 位调试器来调试 32 位应用程序。

64 位应用程序可以检查和控制 32 位或 64 位应用程序。但是,32 位应用程序无法控制 64 位应用程序,因为 32 位 API 不允许描述 64 位进程的完整状态。因此,必须使用 64 位调试器才能调试 64 位应用程序。

sysinfo(2) 的扩展

使用 Solaris S10 操作环境中新的 sysinfo(2) 子代码,应用程序可确定有关可用指令集体系结构的更多信息。

SI_ARCHITECTURE_32

SI_ARCHITECTURE_64

SI_ARCHITECTURE_K

SI_ARCHITECTURE_NATIVE

例如,对于系统中可用的 64 位 ABI 的名称(如果有),可以通过使用 SI_ARCHITECTURE_64 子代码来对其进行使用。有关详细信息,请参见 sysinfo(2)

libkvm/dev/ksyms

64 位版本的 Solaris 系统是使用 64 位内核实现的。直接检查或修改内核中内容的应用程序必须转换为 64 位应用程序,并且必须与 64 位版本的库链接。

执行此转换和清理工作之前,应当考虑为什么应用程序需要首先直接查看内核数据结构。可能是由于在此过程中首先会导入或创建程序,因此 Solaris 平台上将启用用来提取系统调用所需数据的其他接口。请参见 sysinfo(2)kstat(3KSTAT)sysconf(3C)proc(4),这些接口是最常见的替换 API。如果可以使用这些接口来替代 kvm_open(3KVM),请使用它们并使应用程序保持 32 位以实现最大可移植性。另一个益处是其中的大多数 API 可能更快,并且可能不要求访问内核内存所需的相同安全权限。

如果尝试针对 64 位内核或崩溃转储使用 kvm_open(3KVM),则 32 位版本的 libkvm 会返回失败信息。同样,如果尝试针对 32 位内核崩溃转储使用 kvm_open(3KVM),则 64 位版本的 libkvm 也会返回失败信息。

因为内核是一个 64 位程序,所以用来打开 /dev/ksyms 以直接检查内核符号表的应用程序需要进行增强,以便可以识别 ELF64 格式。

无法明确判断 kvm_read()kvm_write() 的地址参数应该是内核地址还是用户地址这一问题对于 64 位应用程序和内核更加严重。所有使用 libkvm 并且还使用 kvm_read()kvm_write() 的应用程序应当转换为使用相应的 kvm_read()kvm_write()kvm_uread()kvm_uwrite() 例程。(这些例程最初是在 Solaris 2.5 中提供的。)

尽管直接读取 /dev/kmem/dev/mem 的应用程序尝试解释从这些设备中读取的数据可能会出错,但这些应用程序仍可以运行;32 位和 64 位内核之间的数据结构偏移量和长度几乎肯定是不相同的。

libkstat 内核统计信息

许多内核统计信息的大小与内核是 64 位还是 32 位程序完全无关。通过指定的 kstats(请参见 kstat(3KSTAT))导出的数据类型是自述性类型,可用来导出带符号或无符号并且具有相应标记的 32 位或 64 位计数器数据。因此,使用 libkstat 的应用程序无需转换为 64 位应用程序即可成功处理 64 位内核。


注 –

如果要修改的设备驱动程序用来创建和维护指定的 kstats,则应当尝试使用定宽统计信息类型,使所导出的统计信息的大小在 32 位和 64 位内核之间保持不变。


stdio 的更改

在 64 位环境中,已经对 stdio 功能进行了扩展,允许同时打开 256 个以上的流。32 位 stdio 功能仍具有最多只能同时打开 256 个流这一限制。

64 位应用程序不应当依赖具有对 FILE 数据结构成员的访问权限。如果尝试直接访问特定于实现的专用结构成员,则可能会导致编译错误。现有的 32 位应用程序不会受到此更改的影响,但是,在任何代码中都不应当再直接使用这些结构成员。

FILE 结构有很长的历史,只有少数应用程序曾经查看过数据结构内部以收集有关流状态的其他信息。 由于 64 位版本的 FILE 结构现在是不透明的,因此,32 位 libc 和 64 位 libc 中均已添加了一系列新例程,以允许在不依赖内部实现的情况下检查同一个状态。 例如,请参见 __fbufsize(3C)

性能问题

以下几节将讨论 64 位性能的优缺点。

64 位应用程序的优点

64 位应用程序的缺点

系统调用问题

以下几节将讨论系统调用问题。

EOVERFLOW 的含义

每次用来从内核向外传递信息的数据结构中的一个或多个字段太小而无法容纳该值时,系统调用中便会返回 EOVERFLOW 返回值。

现在,许多 32 位系统调用在遇到 64 位内核中的大对象时都会返回 EOVERFLOW。在处理大文件时会出现上述情况,由于 daddr_tdev_ttime_t 及其派生类型 struct timevaltimespec_t 现在包含 64 位值,因此这可能意味着 32 位应用程序会遇到更多的 EOVERFLOW 返回值。

谨慎使用 ioctl()

过去,指定的一些 ioctl(2) 调用非常不当。遗憾的是,ioctl() 完全不执行编译时类型检查,因此,可能难以找到错误的来源。

请考虑使用两个 ioctl() 调用:一个用来处理 32 位值 (IOP32) 的指针,另一个用来处理长值 (IOPLONG) 的指针。

以下代码样例作为 32 位应用程序的一部分来运行:

	int a, d;

	long b;

	...

	if (ioctl(d, IOP32, &b) == -1)

			return (errno);

	if (ioctl(d, IOPLONG, &a) == -1)

			return (errno);

编译此代码段并将其作为 32 位应用程序的一部分运行时,这两个 ioctl(2) 调用均可正常工作。

编译此代码段并将其作为 64 位应用程序的一部分运行时,这两个 ioctl() 调用也将成功返回。但是,这两个 ioctl() 均无法正常工作。第一个 ioctl() 传递的容器太大,并且在大端字节序实现中,内核将从 64 位字的错误部分向外复制数据或向其中复制数据。即使在小端字节序实现中,该容器也可能在高 32 位中包含栈垃圾。第二个 ioctl() 会从字中向外复制或向其中复制过多的数据,从而导致读取错误的值或破坏用户栈上的相邻变量。