如何编写可转换的32位/64位代码?

5
一道关于C++的问题。我看到了一个有关程序如何变成32位/64位的问题,得到的答案大致是这样的(抱歉我找不到那个问题了,几天前我看过它,但现在找不到了:():只要你没有做出“指针假设”,你只需要重新编译它。那么我的问题是,什么是指针假设?据我理解,有32位指针和64位指针,所以我想这与此有关。请展示它们之间的代码区别。写代码时需要注意哪些好习惯,以便更容易地在两者之间进行转换,也欢迎分享相关示例。
注:我知道有这篇文章: 如何编写既适用于32位又适用于64位的代码? 但我认为它太笼统了,没有好的示例,对像我这样的新手来说不是很友好。像32位存储单元是什么之类的概念,我希望能更详细地解释一下(没有恶意^^)。
5个回答

6
一般来说,这意味着您的程序行为不应依赖于任何类型的sizeof()(除非它们被制定为某个确切的大小),无论是显式还是隐式(这也包括可能的结构对齐)。指针只是其中的一部分,这也可能意味着您不应尝试依赖于能够在不相关的指针类型和/或整数之间进行转换,除非它们是专门用于此目的(例如intptr_t)。同样,您需要注意写入磁盘的内容,在那里您也永远不应该依赖于例如内置类型的大小,在各处都相同。每当您必须使用明确定义大小的类型(例如uint32_t)时,请小心处理(因为外部数据格式等原因)。

3
“指针假设”是指编写依赖于指针适合其他数据类型的代码,例如int copy_of_pointer = ptr; - 如果int是32位类型,则此代码将在64位机器上出现问题,因为只有部分指针将被存储。

只要指针仅存储在指针类型中,那么就没有问题。
通常,指针的大小为“机器字”,所以在32位架构上为32位,在64位架构上,所有指针都是64位。但是,有些体系结构并非如此。我自己从未在这样的机器上工作过[除了x86及其“far”和“near”指针之外-但让我们暂且不谈]。
大多数编译器会告诉您当您将指针转换为不适合其中的整数时,因此如果启用警告,大多数问题将变得显而易见-解决警告,您的代码很可能立即起作用。

3
对于一份良好的程序(即按照C++的语法和语义规则编写的程序,没有未定义的行为),C++标准保证您的程序将具有一组可观察的行为。由于程序中的未指定行为(包括实现定义的行为)而导致的可观察行为会有所不同。如果避免未指定的行为或解决它,您的程序将保证具有特定和确定的输出。如果以这种方式编写程序,则在32位或64位机器上运行程序时不会出现任何差异。
一个简单(强制)的示例程序可能具有不同的可能输出,如下所示:
int main()
{
  std::cout << sizeof(void*) << std::endl;
  return 0;
}

这个程序在32位和64位机器上的输出可能会不同(但不一定)。sizeof(void*)的结果是实现定义的。然而,可以编写包含实现定义行为但被解决为良好定义的程序。
int main()
{
  int size = sizeof(void*);
  if (size != 4) {
    size = 4;
  }
  std::cout << size << std::endl;
  return 0;
}

尽管此程序使用实现定义的行为,但它始终会打印出4。这是一个愚蠢的例子,因为我们本可以只做int size = 4;,但在编写平台无关代码时确实存在一些情况。
因此,编写可移植代码的规则是:避免或解决未指定的行为
以下是避免未指定行为的一些提示:
  1. 不要假设基本类型的大小除了C++标准规定的之外还有其他。也就是说,一个char至少为8位,shortint至少为16位,依此类推。

  2. 不要试图进行指针操作(在指针类型之间转换或在整数类型中存储指针)。

  3. 不要使用unsigned char*读取非char对象的值表示(用于序列化或相关任务)。

  4. 避免使用reinterpret_cast

  5. 在进行可能超出或低于范围的操作时要小心。在执行位移操作时要仔细思考。

  6. 在指针类型上进行算术运算时要小心。

  7. 不要使用void*

标准中还有许多未指定或未定义的行为。值得查阅一下。有一些出色的在线文章介绍了在32位和64位平台之间可能遇到的一些常见差异。

很棒的答案 :) 正是我在寻找的 :) - Fredrik Boston Westman

1
32位代码和64位代码没有区别,C/C++和其他编程语言的目标是可移植性,而不是汇编语言。唯一的区别在于编译代码的发行版,所有工作都由编译器/链接器自动完成,所以不要考虑这个问题。但是,如果你在64位发行版上进行编程,并且需要使用外部库(例如SDL),则外部库也必须编译为64位,如果想要编译你的代码。需要知道的一件事是,64位发行版上的ELF文件会比32位发行版上的ELF文件大,这是很正常的。指针的意义在于,当你增加/更改指针时,编译器将根据指针类型的大小来增加你的指针。包含的类型大小由处理器的寄存器大小/您正在使用的发行版定义。但你不用担心这个问题,编译会自动处理一切。综上所述:这就是为什么无法在32位发行版上执行64位ELF文件的原因。

当你说“disrib”时,你是指什么? - Fredrik Boston Westman
1
@FredrikBostonWestman:这个答案假设你使用的是现代的Linux/BSD操作系统。这种操作系统的“实现”被称为发行版;它们被设计成一个稳定的完整包,其中所有内容都是专门为其将要运行的架构编译的。显然,使用闭源技术很难获得这样的好处... - leftaroundabout

1
典型的32位/64位移植陷阱包括:
程序员默认sizeof(void*) == 4 * sizeof(char)。如果您做出这种假设,例如按照这种方式分配数组(“我需要20个指针,因此我分配80个字节”),则会在64位上导致缓冲区溢出。
"kitten-killer",int x = (int)&something(反之亦然,void* ptr = (void*)some_int)。再次假设sizeof(int) == sizeof(void*)。这不会导致溢出,但会丢失数据——指针的高32位。
这两个问题都属于类型别名类别(在二进制表示级别上假定两种类型之间的身份/可互换性/等同性),这种假设很常见;例如在UNIX上,假定time_t、size_t、off_t为int,在Windows上,HANDLE、void*和long可以互换等等...
假设数据结构/堆栈空间使用情况(另请参见下面的第5点)。在C/C++代码中,局部变量分配在堆栈上,并且由于以下原因和传递参数的不同规则而使用的空间在32位和64位模式下不同(32位x86通常在堆栈上,64位x86部分在寄存器中)。仅在32位默认堆栈大小上勉强运行的代码可能会在64位上导致堆栈溢出崩溃。这相对容易发现是崩溃的原因,但根据应用程序的可配置性可能难以修复。
32位和64位代码之间的时间差异(由于不同的代码大小/缓存足迹,或不同的内存访问特征/模式,或不同的调用约定)可能会破坏“校准”。例如,for (int i = 0; i < 1000000; ++i) sleep(0); 可能会对32位和64位产生不同的时间。

最后是ABI(应用程序二进制接口)。64位和32位环境之间的差异通常比指针大小更大... 目前,存在两个主要的“分支”64位环境,即IL32P64(Win64使用的内容 - int和long是int32_t,只有uintptr_t/void*是uint64_t,以大小整数为单位)和LP64(UN*X使用的内容 - int是int32_t,long是int64_t和uintptr_t/void*是uint64_t),但也有不同对齐规则的“细分” - 一些环境假定长,浮点或双精度在它们各自的大小上对齐,而其他环境则假定它们在四字节的倍数上对齐。在32位Linux中,它们都对齐到四个字节,在64位Linux中,float对齐到四个字节,long和double对齐到八字节的倍数。 这些规则的结果是,在许多情况下,即使数据类型声明完全相同,32位和64位环境中的sizeof(struct { ...})和结构/类成员的偏移量也会不同。 除了影响数组/向量分配之外,这些问题还影响数据的输入/输出,例如通过文件 - 如果32位应用程序将struct { char a; int b; char c, long d; double e }写入文件,然后重新编译为64位应用程序读取,则结果将不会如预期。 上述示例仅涉及语言基元(char,int,long等),但当然会影响各种平台相关/运行时库数据类型,无论是size_t,off_t,time_t,HANDLE,还是任何非平凡的struct/union/class...因此,这里的错误空间很大,

而且还有一些更低级别的差异,例如在手动优化汇编过程中出现,32位和64位有不同的寄存器数量和参数传递规则,所有这些都会极大地影响这些优化的表现,很可能需要重写或增强某些在32位模式下表现最佳的SSE2代码,在64位模式下表现最佳。此外,32位和64位的代码设计约束也非常不同,特别是关于内存分配/管理方面;对于“尽可能多地利用32位内存”的应用程序来说,它们将具有关于如何/何时分配/释放内存、使用内存映射文件、内部缓存等复杂逻辑,这些大多数在64位中都是不利的,因为你可以“简单地”利用可用的大地址空间。这样的应用程序可能可以成功重新编译为64位,但在那里的表现却比一些“古老简单的废弃版本”差。
因此,最终它也涉及到增强/收益,这就需要更多的工作,部分在编程中,部分在设计/需求中。即使您的应用程序在32位和64位环境下都可以干净地重新编译并经过验证,它是否实际受益于64位?是否有可以/应该对代码逻辑进行的更改,使其在64位上运行更快/更多?您能否在不破坏32位向后兼容性的情况下进行这些更改?没有对32位目标产生负面影响?增强将在哪里,您能获得多少收益? 对于一个大型商业项目,这些问题的答案通常是路线图上的重要标志,因为您的起点是一些现有的“赚钱机器”...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接