Writing portable code regarding the processor architecture/zh CN
│
English (en) │
Bahasa Indonesia (id) │
русский (ru) │
中文(中国大陆) (zh_CN) │
编写处理器架构无关的可移植代码
在编写处理器架构无关的可移植代码时,主要问题有:字节序、32 位或 64 bit 位处理器。
字节序
字节序是指处理器如何存储超过单个字节的数据(比如 16/32/64 位整数)。
Free Pascal 支持两种字节序的处理器:
旁注:
中端序(也称混合序)处理器目前已很少见了。最有名的中端序处理器是 DEC 的 PDP-11,但已作古。
每种系列的处理器通常会固定采用一种字节序,但有些系列处理器能根据主板的不同而采用大端或小端序(如ARM、PPC)。
最有名的小端序处理器系列就是用于 PC 的 x86,以及其兄弟 x86-64。典型的大端序处理器有 PPC(通常情况下,参阅上注)、m68k、诸如 HP3000 之类的小型机、诸如 IBM 370(Z 系列)之类的大型机。
因为 TCP/IP 规定了所有网络协议头部结构都应该是大端序,所以有时大端序也被称做“网络序”。
在以下情况下,字节序十分的重要:
- 在不同架构的处理器之间交换数据
- 访问同一块数据,有时视作较大类型(如整数)的数组,有时又要视作字节数组
以下是后一种情况的示例
type
Q = record
case Boolean of
True: (i: Integer);
False: (p: array[1..4] of Byte);
end;
var
x:^Q;
begin
// 首先显示一下编译器识别出来的字节序
{$IFDEF ENDIAN_LITTLE}
Writeln('The compiler has compiled this program for Little Endian machines (such as Intel x86, ARMEL)');
{$ENDIF}
{$IFDEF ENDIAN_BIG}
Writeln('The compiler has compiled this program for Big Endian machines (such as PowerPC, ARMEB)');
{$ENDIF}
New(x);
x^.i := 5;
if x^.p[1] = 5 then
WriteLn(x^.p[1],' Your machine is Little Endian')
else
if x^.p[4] = 5 then
WriteLn(x^.p[1],' Your machine is Big Endian')
else
WriteLn(x^.p[1],' ',x^.p[2],' ',x^.p[3],' ',x^.p[4],' Your machine''s endianness is indeterminate; please report the results to the compiler development team');
WriteLn;
{ 暂停一下,便于用户查看结果 }
Write('Press Enter when you finish reading this');
ReadLn;
end.
在小端序机器(PC)中,上述代码将会输出 '5'(因为 longint(5) 在内存中存为 05 00 00 00),而在大端序机器上(如 Powermac)则会显示 '0'(因为 longint(5) 在内存中存为 00 00 00 05)。(如果显示 indeterminate,请在本 wiki 页报告,并注明处理器型号和显示的内容!)
若要检测处理器的字节序,请使用 ENDIAN_BIG 或 ENDIAN_LITTLE(自 1.9 版起还可用 FPC_LITTLE_ENDIAN 和 FPC_BIG_ENDIAN),Free Pascal 会自动根据处理器进行定义。
改变字节序
system 单元中包含了将指定字节序转换为本机(CPU)字节序的函数,大端序(BEtoN、 NtoBE),小端序为(LEtoN、NtoLE),还有 SwapEndian。
很多网络库(如 Synapse)都提供了自己的转换函数,以供在网络和主机之间转换字节序。
内存对齐
有些处理器允许处理未对齐的数据,只是会降低性能(IBM 370/Z 系列)。而有些处理器在数据未对齐时则会触发硬件异常(如 Alpha 和 ARM)。有时操作系统会捕获这类硬件异常并通过模拟器进行修复,但处理过程会非常缓慢,应该尽量避免。未对齐的数据还会导致记录大小不一致,因此一定要用 sizeof(记录类型); 作为记录的大小。如果定义的是 packed record,请尽量确保数据的自然对齐。有些处理器(如老款 PowerPC)只会对特定的数据类型有对齐的要求,比如浮点数。
若要知道 CPU 是否要求数据对齐,可以检测 FPC_REQUIRES_PROPER_ALIGNMENT(1.9 及以上版本)。在32位 CPU 中,通常意味着小于 4 字节的数据必须自然对齐。 如果要访问未对齐的数据,请在处理之前用 move 将其转移至对齐的内存区域。move 函数会检查未对齐数据并进行妥善处理。
对齐有以下多种策略:
- 每个字段都对齐至某值的倍数(通常是2的幂,1、2、4、8。1 等同于 packed)
- 在每个字段前填充值,确保按自身大小的倍数对齐(longint 按4字节对齐,int64 按8字节对齐,依此类推)。通常这会由 C 编译器完成,因此 FPC 称为 {$packrecords C}。
macOS 中的 {$packrecords C} 貌似在末尾填充了整条记录,使其达到一定大小。此问题还在调查中,以后可能会在编译器中修复。
32 位 和 64 位
为了最大程度保持与老旧代码的兼容性,在从 32 位转为 64 位时,FPC 不会更改预定义数据类型的大小(如 integer、longint 或 word)。但在 64 位架构中,指针的大小为 8 个字节,因此像 longint(pointer(p)) 这种代码必定会崩溃。但为了支持可移植代码的编写,FPC 的 system 单元引入了 PtrInt 和 PtrUInt 类型,表示有符号和无符号整数,大小与指针相同。
请记住,修改 "pointer" 类型的大小也会影响其指向的记录大小。如果记录大小是固定的,而不是通过 new 或 getmem (<x>,sizeof(<x>)) 分配内存的,那就必须对修改指针的代码进行修正。
上述规则与大多数开放 Unix 平台一致。不过在商用领域有一些例外,比如 Tru64 和 ILP64。
函数调用约定
一般而言:请勿依赖内部代码,例如 const 参数的传入是通过栈还是直接传值。
修饰符 | 顺序 | 栈的清理者 | 对齐方式 | 寄存器值保存吗? |
---|---|---|---|---|
none | 从左到右 | 函数 | 默认值 | 否 |
Register | 从左到右 | 函数 | 默认值 | 否 |
CDecl | 从右到左 | 调用方 | GCC 对齐 | GCC 寄存器 |
Interrupt | 从右到左 | 函数 | 默认值 | 全部寄存器 |
Pascal | 从左到右 | 函数 | 默认值 | 否 |
SafeCall | 从右到左 | 函数 | 默认值 | GCC 寄存器 |
StdCall | 从右到左 | 函数 | GCC 对齐 | GCC 寄存器 |
OldFPCCall | 从右到左 | 调用方 | 默认值 | 否 |
详细信息和其他约定,请参阅 $CALLING。
各种处理器架构下的大小限制
处理器架构 | 参数 | 本地变量 |
---|---|---|
i386 | 64 KiB | 没有限制 |
AMD64/x86-64 | 64 KiB | 没有限制 |
Motorola 68000 | 32 KiB | 32 kiB |
Motorola 68020 | 32 KiB | 没有限制 |
PPC | 没有限制 | 没有限制 |
ARM | 没有限制 | 没有限制 |
SPARC | 没有限制 | 没有限制 |
大型机
关于 IBM 370 Z 系列,更多内容请参阅 ZSeries。
x86
在 x86 处理器上,通常所有参数都通过堆栈传递。但在 Free Pascal 中,首选采用 FastCall 或 Register 约定(Darwin 和 macOS 除外)。与 Delphi 兼容的 Register 约定规定了前三个值或单地址参数通过寄存器 EAX、EDX 和 ECX 传递。
ARM
PPC
由于 PowerPC 架构拥有大量寄存器,因此大多数函数的单级调用可用寄存器传递所有参数。必要时会用栈传递其他参数。如果是多级调用且必须保存寄存器值,则一定会在栈上为基于寄存器的参数额外分配空间。
PPC 处理器采用标准的 AIX 或 SysV 调用约定。详情请参阅 PPC Calling conventions。
68K
68K 处理器支持 CDecl 和 Pascal 约定。68K 栈帧模型是具有帧指针(FP)和堆栈指针(SP)的传统向下增长栈。68K 硬件对栈中的参数强制执行 16 位对齐。详情请参阅68K 与 PowerPC 的对比。
大多数 Macintosh Toolbox 函数(经典 Mac OS 的高层函数)都推荐 Pascal 式的调用,尽管一些较新的管理器遵循 C 约定。Macintosh 操作系统函数(经典 Mac OS 的底层函数)会将调用结果放入寄存器中,也从寄存器中读取结果。
Pascal 函数刚被调用时,返回地址位于栈顶。随后函数会用 LINK 指令创建一个栈帧,并将临时寄存器中的值保存其中,因此在函数刚开始执行时的栈将如下所示:
返回值 | |
第一个实参 | |
... | |
最后一个实参 | |
指向 opt 的静态链接 | |
返回地址 | |
A6 | 前一个 A6 寄存器 |
本地变量 | |
SP | 保存的寄存器值 |
参考
- THINK Pascal 用户手册,Symantec Corporation, Cupertino, 1988
- Physinfo, Université libre de Bruxelles: PowerPC 函数调用约定, 68K vs. PowerPC (改编自 Apple Developer University)
simple data types |
|
---|---|
complex data types |
参阅
- [[Multiplatform Programming Guide]|跨平台编程指南]