将 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 标记检查对以下环境的可移植性:其中 long 整数和指针的大小为 64 位,而无格式整数的大小为 32 位。 -errchk=longptr64 标记检查从指针表达式和 long 整数表达式到无格式整数表达式的赋值(即使使用显式转换)。
使用 -errchk=longptr64,signext 选项可发现满足以下条件的代码:其中常规 ISO C 值保留规则允许在无符号整型表达式中使用带符号整数值的符号扩展。要检查仅打算在 64 位 SPARC 编译环境中运行的代码,请使用 lint 的 -xarch=v9 选项。要检查打算在 x86 64 位环境中运行的代码,请使用 -xarch=amd64。
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);
检查 Long 整数大小相对于无符号整数大小的变化
因为在 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、枚举类型或位字段(bit-field)。如果整数可以容纳原始类型的所有可能的值,那么该值将被转换成整数;否则,该值将被转换成无符号的整数。
- 有符号和无符号整型之间的转换
将带有负号的整数提升为相同大小或更大的无符号整数类型时,首先将其提升为更大类型的有符号当量,然后再转换成无符号值。
将以下示例编译成 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\n”, addr);
addr = (unsigned int)(a.base << 13); /* No sign extension here! */
printf(“addr 0x%lx\n”, 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 -xarch=v9 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 类型的数组,而不是指针类型的数组。一些 C 应用程序在转换到 LP64 数据类型模型之后发生严重的性能退化,这是因为它们依赖于很多较大的指针数组。
以上文章转自于 : http://developers.sun.com.cn/