原文网址:http://www.oracle.com/technetwork/cn/server-storage/solaris/ilp32tolp64issues-137107-zhs.html
将 32 位应用程序转换成 64 位应用程序时的主要问题是 int
类型相对 long
类型和指针类型的大小发生了变化。将 32 位程序转换成 64 位程序时,只有 long
类型和指针类型的大小从 32 位转换成 64 位;int
类型整数的大小仍然为 32 位。这导致将指针类型或 long
类型赋值给 int
类型时会发生数据截断问题。而且,使用较短 int
类型的表达式赋值给 unsigned long
或指针时,可能会发生符号位扩展问题。本文讨论如何避免或消除这些问题。
考虑 32 位和 64 位数据模型之间的差异
32 位和 64 位编译环境之间最大的不同之处在于数据类型模型的变化。32 位应用程序的 C 数据类型模型是 ILP32 模型,如此命名是因为 int
类型、long
类型和指针都是 32 位数据类型。64 位应用程序的数据类型模型是 LP64 数据模型,如此命名是因为 long
和指针类型变成了 64 位。其余的 C 整数类型和浮点类型在这两种数据类型模型中相同。
当前的 32 位应用程序通常假设 int
类型、long
类型和指针大小相同。因为在 LP64 数据模型中,long
和 pointer
的大小发生了变化,这种变化正是导致 ILP32 到 LP64 转换问题的主要原因。
使用 lint 实用工具检测 64 位 long
类型和指针类型是否存在问题
使用 lint 检查同时针对 32 位编译环境和 64 位编译环境编写的代码。指定 -errchk=longptr64 选项以生成 LP64 警告。还可使用 -errchk=longptr64
标志检查对以下环境的可移植性:其中长整型和指针的大小为 64 位,而普通整型的大小为 32 位。-errchk=longptr64 标志检查从指针表达式和长整型表达式到普通整型表达式的赋值(即使使用显式类型转换)。
使用 -errchk=longptr64,signext 选项可发现满足以下条件的代码:其中常规 ISO C 值保留规则允许在无符号整型表达式中使用有符号整型值的符号扩展。要检查打算在 Solaris 64 位 SPARC 或 x86 64 位环境中运行的代码,请使用 lint 的 -m64 选项。
lint 生成警告时,它将输出有问题代码的行号、描述该问题的消息以及是否涉及到指针。警告消息还会指出所涉及的数据类型大小。知道涉及到指针并且知道数据类型大 小之后,就可以发现具体的 64 位问题,从而可以避免在 32 位类型和更小类型之间转换的预先存在的问题。
通过在前一行添加一条 "NOTE(LINTED(<optional message>))"
形式的注释,可以取消指定代码行的警告。如果您希望 lint 忽略类型转换和赋值之类的特定代码行,那么这种方法很有用。使用 "NOTE(LINTED(<optional message>))"
注释时需要特别谨慎,因为它可能掩盖真正的问题。使用 NOTE
时,还要包括 #include<note.h>
。更多信息,请参考 lint man 页。
检查指针大小相对于普通整型大小的变化
由于普通整型和指针在 ILP32 编译环境中的大小相同,所以 32 位代码通常以这个假设为基准。指针经常被转换成 int
或 unsigned int
以进行地址运算。还可以将指针转换成 unsigned long
,因为在 ILP32 和 LP64 数据类型模型中,long
和指针类型的大小相同。然而,不是显式地使用 unsigned long
,而是使用 uintptr_t
,因为后者能更确切地表达您的意图,并使代码更容易移植,使其免受将来变化的影响。要使用 uintptr_t
和 intptr_t
,必须添加 #include <inttypes.h>
。
看看下面的示例:
char *p;
p = (char *) ((int)p & PAGEOFFSET);
% cc ..
warning: conversion of pointer loses bits
以下版本在编译为 32 位和 64 位目标文件时都能正常工作:
char *p;
p = (char *) ((uintptr_t)p & PAGEOFFSET);
检查长整型大小相对于普通整型大小的变化
因 为在 ILP32 数据类型模型中整型和 long 没有实质区别,所以现有代码可能无区别地使用了它们。请修改整型和 long 互换使用的代码,使其符合 ILP32 和 LP64 数据类型模型的要求。在 ILP32 数据类型模型中整型和 long 都是 32 位,而在 LP64 数据类型模型中 long 为 64 位。
看看下面的示例:
int waiting;
long w_io;
long w_swap;
...
waiting = w_io + w_swap;
% cc
warning: assignment of 64-bit integer to 32-bit integer
检查符号扩展
转换到 64 位编译环境时,符号扩展是常见问题,因为类型转换和提升规则比较含糊。为了防止符号扩展问题,请使用显式类型转换以获得期望的结果。
了解发生符号扩展的原因有助于了解 ISO C 的转换规则。在以下操作期间,会存在一些可能导致 32 位和 64 位编译环境之间的大多数符号扩展问题的转换规则:
- 整型提升
在任何要求使用整型的表达式中,都可使用有符号或无符号的 char、short、枚举类型或位域。如果整型可以容纳原始类型的所有可能的值,那么该值将被转换成整型;否则,该值将被转换成无符号的整型。
- 有符号和无符号整型之间的转换
将带有负号的整数提升为相同类型或更大类型的无符号整数时,首先将其提升为更大类型的有符号等量值,然后再转换成无符号值。
将以下示例编译成 64 位程序时,addr 变量将发生符号扩展,尽管 addr 和 a.base 都是无符号类型。
%cat test.c
struct foo {
unsigned int base:19, rehash:13;
};
main(int argc, char *argv[])
{
struct foo a;
unsigned long addr;
a.base = 0x40000;
addr = a.base << 13; /* Sign extension here! */
printf("addr 0x%lx ", addr);
addr = (unsigned int)(a.base << 13); /* No sign extension here! */
printf("addr 0x%lx ", addr);
}
发生符号扩展的原因是应用了以下转换规则:
- 结构成员
a.base
从unsigned int
位域转换成int
是因为整型提升规则。换句话说,因为 32 位整型可以容纳无符号的 19 位域,所以该位域提升为整型,而不是无符号整型。因此,表达式a.base << 13
的类型为int
。如果将结果赋值给unsigned int
,没有关系,因为没有发生符号扩展。 - 表达式
a.base << 13
是int
类型,但是在将其赋值给addr
之前,它被转换成long
然后又被转换成unsigned long
,这是因为有符号和无符号整型提升规则。执行int
到long
转换时,将发生符号扩展。
因此,编译成 64 位程序时,结果如下:
% cc -o test64 -m64 test.c
% ./test64
addr 0xffffffff80000000
addr 0x80000000
%
编译为 32 位程序时,unsigned long
的大小与 int
的大小相同,因此不发生符号扩展。
% cc -o test test.c
% ./test
addr 0x80000000
addr 0x80000000
%
检查结构封装
检查应用程序中的内部数据结构以查找漏洞;即,结构中的域之间出现额外填充以满足对齐要求。当 long
或指针域变成 LP64 数据类型模型的 64 位时,并且出现在大小仍然为 32 位的 int
之后,就会分配额外填充。由于 long
和指针类型在 LP64 数据类型模型中为 64 位对齐,所以填充出现在 int
和 long
或指针类型之间。在以下示例中,成员 p
为 64 位对齐,因此成员 k
和成员 p
之间出现了填充。
struct bar {
int i;
long j;
int k;
char *p;
}; /* sizeof (struct bar) = 32 bytes */
并且,结构与其中最大的成员大小对齐。因此,在以上结构中,成员 i
和成员 j
之间出现了填充。
重新封装结构时,请遵循将 long 和指针域移到结构开始部分的简单规则。考虑以下结构定义:
struct bar {
char *p;
long j;
int i;
int k;
}; /* sizeof (struct bar) = 24 bytes */
检查联合成员的大小是否平衡
一定要检查联合成员,因为其域大小在 ILP32 和 LP64 数据类型模型之间转换时可能会发生变化,从而使成员的大小变得不同。在以下联合中,成员 _d
和成员数组 _l
在 ILP32 模型中大小相同,但是在 LP64 模型中就不同了,因为 long
类型在 LP64 模型中变成 64 位,但是 double
类型没有变化。
typedef union {
double _d;
long _l[2];
} llx_
通过将 _l
数组成员从 long
类型变成 int
类型,可以重新使成员大小变得平衡。
确保在常量表达式中使用常量类型
精度损失可能导致一些常量表达式丢失数据。指定常量表达式中的数据类型时,请显式指定。通过添加一些 { u,U,l,L
} 组合指定每个整型常量。还可以使用类型转换来指定常量表达式的类型。看看下面的示例:
int i = 32;
long j = 1 << i; /* j will get 0 because RHS is integer expression */
通过如下所示向常量 1
附加类型,可使以上代码按照预期的方式工作:
int i = 32;
long j = 1L << i; /* now j will get 0x100000000, as intended */
检查格式字符串转换
确保 printf
(3S)、sprintf
(3S)、scanf
(3S) 和 sscanf
(3S) 的格式字符串可容纳 long 或指针类型自变量。对于指针自变量,格式字符串中给出的转换操作应该为 %p
,以便能同时在 32 位和 64 位编译环境中工作。对于 long
自变量,应该优先考虑在格式字符串中使用 long 大小规范 l
作为转换运算字符。
还要检查以确保传递给 sprintf
中第一个自变量的缓存包含足够的存储空间,以容纳扩展之后用来表示 long 和指针类型值的数字。例如,在 ILP32 数据模型中,指针是使用 8 个十六进制数字表示的,但是在 LP64 数据模型中扩展到了 16 个。
sizeof()
运算符返回的类型为 unsigned long
在 LP64 数据类型模型中,sizeof()
的有效类型为 unsigned long
。如果将 sizeof()
传递给期望 int
类型自变量的函数,或通过赋值或类型转换将其变为 int
,那么截断可能会导致数据丢失。这只有在包含非常长数组的大型数据库程序中才可能成为问题。
对于二进制接口数据使用可移植的数据类型或固定的整数类型
对于 32 位和 64 位应用程序版本共享的数据结构,请坚持使用 ILP32 和 LP64 程序中大小相同的数据类型。避免使用 long
数据类型和指针。并且,避免使用在 32 位和 64 位应用程序中大小发生变化的派生数据类型。例如,<sys/types.h>
中定义的以下类型在 ILP32 和 LP64 数据模型中大小发生了变化:
clock_t
,以时钟计时周期表示的系统时间dev_t
,用于表示设备编号off_t
,用于表示文件大小和偏移量ptrdiff_t
,用于表示两个指针相减所得结果的有符号整型size_t
,反映内存中对象的大小(字节)ssize_t
,用于返回字节计数或错误指示的函数time_t
,计时(秒)
对于内部数据来说,使用 <sys/types.h>
中的派生数据类型是个不错的主意,因为它有助于代码免受数据模型变化的影响。然而,正是因为这些类型的大小容易随数据模型发生变化,所以不推荐在 32
位和 64 位应用程序共享的数据中使用,也不推荐在其他数据大小必须固定的情况下使用。然而,对于前面讨论的 sizeof()
运算符,在更改代码之前,请考虑精度损失是否会对程序产生实质影响。
对于二进制接口数据,考虑使用 <inttypes.h> 中固定宽度的整数类型。这些类型适用于以下显式的二进制表示:
- 二进制接口规范
- 磁盘数据
- 数据线传输
- 硬件寄存器
- 二进制数据结构
检查副作用
注意,一个区域中的类型变化可能导致其他区域发生意想不到的 64 位转换。例如,检查所有调用以下函数的内容:该函数以前返回 int
,现在返回 ssize_t
。
考虑 long
数组对性能的影响
相对于 int
或 unsigned int
类型的数组,long
或 unsigned long
类型的大数组在 LP64 数据类型模型中可能严重降低性能。long
类型的大数组导致缓存命中率大幅下降,并且消耗更多内存。因此,如果 int
能够和 long
一样实现应用程序的目的,最好使用 int
而不是 long
。这个论点对 int
型数组和指针数组也同样适用,即,最好使用 int
型数组,而不使用指针数组。一些 C 应用程序在转换为 LP64 数据类型模型后会出现严重的性能退化,这是因为它们依赖于很多较大的指针数组。