27、遗留代码迁移:将32位代码迁移到64位、处理LP64与LLP64模型差异

说实话,做C语言跨平台开发这么多年,我遇到最头疼的事情之一,就是迁移遗留代码。尤其是把32位代码搬到64位平台,看着简单,踩坑无数。今天咱们就聊聊这个。

为什么64位迁移会出问题?

32位和64位,最核心的区别就是指针和整数类型的宽度。32位下,指针是4字节,int也是4字节。到了64位,指针变成了8字节,但int还是4字节。这就出问题了。

我当年接手过一个老项目,代码里到处都是 int 来存指针。32位下跑得飞起,一编译64位版本,直接崩。为什么?因为 int 只有4字节,装不下8字节的指针地址。

核心问题: 指针宽度变了,但整数类型没变。你想想看,一个4字节的容器,硬要塞8字节的数据,不溢出才怪。

LP64 与 LLP64:两个主流模型

64位系统下,主要有两种数据模型。我列个表,你一看就明白。

类型 LP64(Unix/Linux/macOS) LLP64(Windows)
char 1 字节 1 字节
short 2 字节 2 字节
int 4 字节 4 字节
long 8 字节 4 字节
long long 8 字节 8 字节
指针 8 字节 8 字节

看到了吗?long 在 LP64 下是8字节,在 LLP64 下还是4字节。这就是个大坑。你在Linux上写 long 存指针,没问题。代码移植到Windows,long 只有4字节,又溢出了。

注意: 不要假设 long 在任何平台下都是8字节。跨平台代码里,long 是最容易出问题的类型之一。

迁移中的常见陷阱

1. 用int存指针

这个我见过太多次了。老代码里经常有这种写法:

// 错误示例
int ptr = (int)malloc(100);
// 32位下没问题,64位下ptr被截断

// 正确做法
intptr_t ptr = (intptr_t)malloc(100);
// intptr_t 保证在任何平台都能完整存放指针

我个人习惯,只要涉及指针和整数之间的转换,一律用 intptr_tuintptr_t。这两个类型是C99标准定义的,专门干这个事。

2. 结构体对齐和填充

32位和64位下,结构体的内存布局可能不一样。因为指针宽度变了,对齐规则也跟着变。

struct example {
    char a;      // 1字节
    void *ptr;   // 32位下4字节,64位下8字节
    int b;       // 4字节
};
// 32位下 sizeof 可能是12
// 64位下 sizeof 可能是24

我曾经在序列化数据时踩过这个坑。32位下序列化的结构体,到64位下反序列化,直接读出一堆乱码。解决方案?要么用 #pragma pack 强制对齐,要么用明确宽度的类型,比如 int32_tint64_t

3. 格式化字符串

这个坑比较隐蔽。你写 printf("%d", ptr) 在32位下没问题,因为 int 和指针都是4字节。到了64位,%d 期望4字节,你传了8字节的指针,行为未定义。

// 错误
printf("ptr = %d\n", ptr);

// 正确
printf("ptr = %" PRIxPTR "\n", (uintptr_t)ptr);
// PRIxPTR 是 inttypes.h 里定义的宏,自动适配平台
小技巧: 编译时加上 -Wformat-Wformat-signedness 警告,编译器会帮你检查格式化字符串和参数类型是否匹配。

迁移步骤:我总结的实战流程

嗯,这里我分享一下自己的经验。迁移32位代码到64位,别上来就改代码。先做这几步:

  1. 扫描所有指针相关操作:搜索代码里所有 (int)(long) 强制转换指针的地方。
  2. 检查所有序列化/反序列化代码:特别是读写二进制文件、网络数据包的地方。
  3. 启用编译器警告-Wall -Wextra -Wconversion -Wpointer-to-int-cast,一个都不要少。
  4. 用静态分析工具:比如 cppcheckclang-tidy,能自动发现很多类型不匹配的问题。
  5. 编译64位版本:先编译,看有多少警告。我一般要求团队把警告数降到0。
  6. 跑测试:特别是边界测试,比如分配大内存、处理大文件。

SVG:64位迁移核心知识图

32位 → 64位 迁移核心知识 LP64(Unix/Linux/macOS) LLP64(Windows) 共同陷阱(无论哪种模型) 陷阱1:int存指针 intptr_t 替代 int 陷阱2:结构体对齐 使用明确宽度类型 陷阱3:格式化字符串 使用 PRIxPTR 宏 解决方案:启用编译器警告 + 静态分析 + 使用标准类型(intptr_t、int64_t等) 核心原则:不假设类型宽度,用标准类型显式声明

实战案例:一个真实的迁移故事

我记得有一次,帮一个客户迁移他们的图像处理库。代码量大概10万行,32位下跑了七八年。客户说编译64位版本,一跑就崩。

我排查了一整天,最后发现问题出在一个结构体里:

typedef struct {
    int width;
    int height;
    unsigned char *data;  // 图像数据指针
    int data_size;        // 用int存大小
} Image;

32位下,int 最大能表示2GB,图像数据一般不会超过这个值。但64位下,int 还是4字节,而图像数据可以轻松超过2GB。客户处理的卫星图像,单张就4GB。data_size 直接溢出,变成负数。

解决方案很简单:把 data_size 改成 size_t。但因为这个结构体被序列化到磁盘文件里,还得处理文件格式的兼容性。嗯,这里要注意,改类型的同时,文件读写代码也得同步更新。

教训: 不要用 int 存大小、长度、索引。用 size_tssize_t。它们在任何平台下都能正确表示内存大小。

总结一下

32位到64位的迁移,说白了就是一件事:不要假设类型的宽度。用标准类型,开编译器警告,多测试。我见过太多项目因为一个 int 存指针的问题,上线后崩溃。这种问题,编译阶段就能发现,别等到运行时才去排查。

最后说一句,迁移不是一次性的工作。代码库会持续演进,新加入的代码也可能引入32位假设。所以,把编译警告当作错误来处理,是个好习惯。

我的建议: 在CI/CD流程里加上64位编译和静态分析。每次提交代码,自动检查。这样能避免遗留问题越积越多。

公众号:蓝海资料掘金营,微信deep3321