本章所介绍的各种编程主题面向需要详细了解 64 位 Solaris 操作环境的系统程序员。
尽管 64 位环境的几个新增特征是 64 位环境所特有的,但是大多数新增特征都是对普通 32 位接口的扩展。
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 ABI 允许充分利用所有 64 位 SPARC 指令和数据宽度为 64 位的寄存器。许多新的相关指令都是对现有 V8 指令集的扩展。请参见《The SPARC Architecture Manual, Version 9》。
基本调用约定相同。调用方的前六个参数放在外部寄存器 %o0-%o5 中。SPARC V9 ABI 对于较大的寄存器文件仍会使用寄存器窗口,以便使函数调用操作“降低成本”。结果将返回到 %o0 中。由于所有的寄存器现在都被视为 64 位,因此 64 位值现在可以通过单寄存器而不是寄存器对传送。
栈的布局不同。除了基本单元大小从 32 位增加到 64 位外,各种隐藏的参数术语均已删除。返回地址仍是 %o7 + 8。
%o6 仍然称作栈指针寄存器 %sp,%i6 称作帧指针寄存器 %fp。但是,%sp 和 %fp 寄存器距离栈的实际内存位置的偏移量(称为栈偏移量)是常量。栈偏移量的大小为 2047 个字节。
指令大小仍为 32 位。因此,生成地址常量需要更多指令。该调用指令无法再使用该调用指令在地址空间中的任何位置建立分支,因为它只能达到离 %pc ( 2 GB 的位置。
现在,整数乘除函数可以完全在硬件中实现。
结构的传递和返回以不同的方式实现。小型数据结构和某些浮点参数现在直接在寄存器中传递。
通过用户陷阱,用户陷阱处理程序可处理非特权代码中的某些陷阱(而不是传送信号)。
所有数据类型现在都与其长度对齐。
许多基本派生类型会更长,因此,许多系统调用接口数据结构现在具有不同的长度。
系统中存在两组不同的库:用于 32 位 SPARC 应用程序的库和用于 64 位 SPARC 应用程序的库。
SPARC V9。对于开发者来说,SPARC V9 ABI 的一个重要特征就是栈偏移量。 对于 64 位 SPARC 程序来说,必须向帧指针和栈指针中都添加大小为 2047 个字节的栈偏移量,才能达到栈帧的实际数据。 请参见下图。
有关栈偏移量的更多信息,请参见 SPARC V9 ABI。
SPARC V9。对于 64 位应用程序,尽管起始地址和寻址限制大不相同,但地址空间的布局与 32 位应用程序的布局密切相关。与 SPARC V8 一样,SPARC V9 栈从地址空间的顶部开始减小,而堆则从底部开始扩展数据段。
下图说明了为 64 位应用程序提供的缺省地址空间。地址空间中标记为保留空间的区域可能不是由应用程序进行映射。这些限制在将来的系统中可能会有所放松。
上图中的实际地址描述了特定计算机上的特定实现,仅供参考。
缺省情况下,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。编译器针对不同目的提供了不同的代码模型,旨在提高性能并减少 64 位 SPARC 程序中代码的大小。代码模型由以下因素确定:
可放置性(绝对独立代码与位置无关代码)
代码大小(< 2 GB)
位置(地址空间中的低、中或任意位置)
外部对象引用模型(小或大)
下表介绍了可用于 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 以下的地址空间。
64 位应用程序使用可执行和链接格式 (Executable and Linking Format, ELF64) 进行描述,使用该格式可以全面地描述大型应用程序和较大地址空间。
以下列出了 AMD ABI 的特征:
AMD ABI 允许充分利用所有的 64 位指令和 64 位寄存器。许多新指令都是对现有 i386 指令集的直接扩展。目前,共有十六个通用寄存器:
七个通用寄存器(%rdi、%rsi、%rdx、%rcx、%r8、%r9 和 %rax)在函数调用序列中具有明确定义的角色,该序列现在用于在寄存器中传递参数。 |
两个寄存器(%rsp 和 %rbp)用于管理栈。 |
两个寄存器(%r10 和 %r11)是临时寄存器。 |
五个寄存器(%r12、%r13、%r14、%r15 和 %rbx)由被调用方保存。 |
对于 AMD ABI,基本函数调用的约定不同。参数会放在寄存器中。对于简单的整数参数,前几个参数依次放在 %rdi、%rsi、%rdx、%rcx、%r8 和 %r9 寄存器中。
对于 AMD,栈的布局稍有不同。具体来说,栈在紧靠调用指令的前面始终以 16 个字节为边界对齐。
指令大小仍为 32 位。因此,生成地址常量需要更多指令。由于调用指令只能到达从 %rip 加/减 2 GB 的范围,因此无法再将其用于在地址空间中的任何位置建立分支。
现在,整数乘除函数可以完全在硬件中实现。
结构的传递和返回以不同的方式实现。小型数据结构和某些浮点参数现在直接在寄存器中传递。
使用新的 PC 相关寻址模式,可以生成更高效的位置无关代码。
所有数据类型现在都与其长度对齐。
许多基本派生类型会更长,因此,许多系统调用接口数据结构现在具有不同的长度。
系统中存在两组不同的库:一种用于 32 位 i386 应用程序的库,另一种用于 64 位 amd64 应用程序的库。
AMD ABI 极大地增强了浮点功能。
通过 64 位 ABI,可以使用所有使用 x87 浮点寄存器(%fpr0 至 %fpr7,%mm0 至 %mm7)的 x87 和 MMX 指令。 |
此外,还可以使用那些使用 128 位 XMM 寄存器(%xmm0 至 %xmm15)完整 SSE 和 SSE2 指令集。 |
请参见 amd64 psABI 草案文档《System V Application Binary Interface, AMD64 Architecture Processor Supplement》(草案版本 0.92)。
对于 64 位应用程序,尽管起始地址和寻址限制大不相同,但地址空间的布局与 32 位应用程序的布局密切相关。与 SPARC V9 一样,amd64 栈从地址空间的顶部开始减小,而堆则从底部开始扩展数据段。
下图说明了为 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 |
4 |
4 |
4 |
amd64 |
8 |
8 |
16 |
sparcv8 |
8 |
8 |
8 |
sparcv9 |
8 |
8 |
16 |
针对 SPARC 系统,尽管看起来已经是干净的 64 位代码,但是如果在 32 位和 64 位编程环境之间复制数据结构,则这两个环境中不同的对齐情况可能会产生问题。这些编程环境包括设备驱动程序的 ioctl 例程、doors 例程或其他 IPC 机制。通过对这些接口仔细进行编码,并且谨慎地使用 #pragma pack 或 _Pack 指令,可避免对齐问题。
以下进程间通信 (interprocess communication, IPC) 元语仍适用于 64 位和 32 位进程之间的通信。
mmap(2)(针对共享文件调用)
pipe(2)(在进程之间使用)
door_call(3DOOR)(在进程之间使用)
尽管所有这些元语都允许在 32 位和 64 位进程之间进行通信,但是可能需要明确执行一些步骤来确保在进程间交换的数据可以由所有这些元语正确解释。例如,两个进程共享由 C 数据结构所描述的数据,并且该数据结构中包含类型为 long
的变量,如果不了解 32 位进程将该变量视为 4 字节值,而 64 位进程将该变量视为 8 字节值,则在这两个进程之间交换的数据将无法正确解释。
处理此差异的一种方法是确保数据在这两个进程中具有完全相同的长度和含义。构建使用定宽类型(如 int32_t
和 int64_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
值进行编码,则编码操作将失败。
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 接口可供 32 位和 64 位应用程序使用。32 位应用程序可以检查和控制其他 32 位应用程序的状态。因此,可以使用现有的 32 位调试器来调试 32 位应用程序。
64 位应用程序可以检查和控制 32 位或 64 位应用程序。但是,32 位应用程序无法控制 64 位应用程序,因为 32 位 API 不允许描述 64 位进程的完整状态。因此,必须使用 64 位调试器才能调试 64 位应用程序。
使用 Solaris S10 操作环境中新的 sysinfo(2) 子代码,应用程序可确定有关可用指令集体系结构的更多信息。
SI_ARCHITECTURE_32 |
SI_ARCHITECTURE_64 |
SI_ARCHITECTURE_K |
SI_ARCHITECTURE_NATIVE |
例如,对于系统中可用的 64 位 ABI 的名称(如果有),可以通过使用 SI_ARCHITECTURE_64 子代码来对其进行使用。有关详细信息,请参见 sysinfo(2)。
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 位内核之间的数据结构偏移量和长度几乎肯定是不相同的。
许多内核统计信息的大小与内核是 64 位还是 32 位程序完全无关。通过指定的 kstats(请参见 kstat(3KSTAT))导出的数据类型是自述性类型,可用来导出带符号或无符号并且具有相应标记的 32 位或 64 位计数器数据。因此,使用 libkstat 的应用程序无需转换为 64 位应用程序即可成功处理 64 位内核。
如果要修改的设备驱动程序用来创建和维护指定的 kstats,则应当尝试使用定宽统计信息类型,使所导出的统计信息的大小在 32 位和 64 位内核之间保持不变。
在 64 位环境中,已经对 stdio 功能进行了扩展,允许同时打开 256 个以上的流。32 位 stdio 功能仍具有最多只能同时打开 256 个流这一限制。
64 位应用程序不应当依赖具有对 FILE 数据结构成员的访问权限。如果尝试直接访问特定于实现的专用结构成员,则可能会导致编译错误。现有的 32 位应用程序不会受到此更改的影响,但是,在任何代码中都不应当再直接使用这些结构成员。
FILE 结构有很长的历史,只有少数应用程序曾经查看过数据结构内部以收集有关流状态的其他信息。 由于 64 位版本的 FILE 结构现在是不透明的,因此,32 位 libc 和 64 位 libc 中均已添加了一系列新例程,以允许在不依赖内部实现的情况下检查同一个状态。 例如,请参见 __fbufsize(3C)。
以下几节将讨论 64 位性能的优缺点。
针对 64 位值更高效地执行算术和逻辑运算。
运算过程使用全寄存器集宽度、全寄存器集和新指令。
64 位值的参数传递效率更高。
小型数据结构和浮点值的参数传递效率更高。
提供了额外的整数寄存器和浮点寄存器。
对于 amd64,提供了 PC 相关寻址模式,从而可提高位置无关代码的执行效率。
64 位应用程序需要更多的栈空间才能容纳更大的寄存器。
由于使用了更大的指针,因此应用程序需要更大的高速缓存。
64 位应用程序不能在 32 位平台上运行。
以下几节将讨论系统调用问题。
每次用来从内核向外传递信息的数据结构中的一个或多个字段太小而无法容纳该值时,系统调用中便会返回 EOVERFLOW 返回值。
现在,许多 32 位系统调用在遇到 64 位内核中的大对象时都会返回 EOVERFLOW。在处理大文件时会出现上述情况,由于 daddr_t
、dev_t
、time_t
及其派生类型 struct timeval
和 timespec_t
现在包含 64 位值,因此这可能意味着 32 位应用程序会遇到更多的 EOVERFLOW 返回值。
过去,指定的一些 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() 会从字中向外复制或向其中复制过多的数据,从而导致读取错误的值或破坏用户栈上的相邻变量。