5

一个 c++ 特定的问题。所以我读了一个关于什么使程序成为 32 位/64 位的问题,它得到的答案是这样的(对不起,我找不到这个问题,前几天我看了它,我又找不到了:():只要你不做任何“指针假设”,你只需要重新编译它。所以我的问题是,什么是指针假设?据我了解,有 32 位指针和 64 位指针,所以我认为它与那。请显示它们之间的代码差异。在编写代码时要记住的任何其他好习惯,这有助于它轻松地在 to 之间进行转换,也欢迎 :) 请与他们分享示例

附言。我知道有这篇文章: 你如何编写同时兼容 32 位和 64 位的代码? 但我认为对于像我这样的新程序员来说,没有好的例子有点笼统。就像什么是 32 位存储单元等。有点想把它分解一下(不是双关语^^)ds。

4

5 回答 5

6

一般来说,这意味着您的程序行为不应该依赖于sizeof()任何类型(不具有某种精确大小),无论是显式还是隐式(这也包括可能的结构对齐)。

指针只是它们的一个子集,它可能还意味着您不应该尝试依赖能够在不相关的指针类型和/或整数之间进行转换,除非它们是专门为此而制作的(例如intptr_t)。

以同样的方式,您需要处理写入磁盘的内容,您也不应该依赖例如内置类型的大小,因为在任何地方都相同。

每当您必须(例如由于外部数据格式)使用显式大小的类型时,例如uint32_t.

于 2013-02-21T12:04:53.263 回答
3

“指针假设”是当您编写依赖于适合其他数据类型的指针的代码时,例如int copy_of_pointer = ptr;- 如果 int 是 32 位类型,那么此代码将在 64 位机器上中断,因为只有部分指针将被存储.

只要指针只存储在指针类型中,应该完全没有问题。

通常,指针是“机器字”的大小,因此在 32 位架构上是 32 位,而在 64 位架构上,所有指针都是 64 位。但是,有些架构并非如此。我自己从来没有在这样的机器上工作过[除了 x86 之外,它的“远”和“近”指针 - 但现在让我们忽略它]。

大多数编译器会在您将指针转换为指针不适合的整数时告诉您,因此如果您启用警告,大多数问题将变得明显 - 修复警告,您的代码很有可能立即工作.

于 2013-02-21T11:59:52.223 回答
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,尽管它使用实现定义的行为。这是一个愚蠢的例子,因为我们本来可以 done int size = 4;,但是在编写独立于平台的代码时确实会出现这种情况。

所以编写可移植代码的规则是:旨在避免或解决未指定的行为

以下是一些避免未指定行为的提示:

  1. 不要假设超出 C++ 标准规定的基本类型的大小。也就是说,achar至少为 8 位,short并且两者int都至少为 16 位,以此类推。

  2. 不要尝试做指针魔术(在指针类型之间转换或将指针存储在整数类型中)。

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

  4. 避免reinterpret_cast

  5. 执行可能上溢或下溢的操作时要小心。在进行位移操作时要仔细考虑。

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

  7. 不要使用void*.

标准中出现了更多未指定或未定义的行为。非常值得查找它们。有一些很棒的在线文章涵盖了您将在 32 位和 64 位平台之间遇到的一些更常见的差异。

于 2013-02-21T12:23:13.050 回答
1

32bit 代码和 64bit 代码没有区别,C/C++ 和其他编程语言的目标是它们的可移植性,而不是汇编语言。

唯一的区别是你将在哪个发行版上编译你的代码,所有的工作都是由你的编译器/链接器自动完成的,所以别想了。

但是:如果您在 64 位发行版上编程,并且您需要使用外部库(例如 SDL),那么如果您想要编译代码,则外部库也必须以 64 位编译。

要知道的一件事是,您的 ELF 文件在 64 位发行版上会比在 32 位发行版上更大,这只是逻辑。

指针有什么意义?当您增加/更改指针时,编译器将从指向类型的大小增加您的指针。

包含的类型大小由您的处理器的寄存器大小/您正在处理的分布定义。

但是你不必关心这个,编译会为你做一切。

Sum:这就是为什么您不能在 32 位发行版上执行 64 位 ELF 文件的原因。

于 2013-02-21T12:04:09.817 回答
1

32 位/64 位移植的典型缺陷是:

程序员的隐含假设 sizeof(void*) == 4 * sizeof(char)。如果您做出这个假设并且例如以这种方式分配数组(“我需要 20 个指针,所以我分配 80 个字节”),您的代码会在 64 位上中断,因为它会导致缓冲区溢出。

“小猫杀手”,int x = (int)&something; (反之亦然,void* ptr = (void*)some_int)。再次假设 sizeof(int) == sizeof(void*)。这不会导致溢出但会丢失数据 - 即指针的高 32 位。

这两个问题都属于称为类型别名的类(假设两种类型之间在二进制表示级别上具有同一性/可互换性/等效性),并且此类假设很常见;就像在 UN*X 上一样,假设 time_t、size_t、off_t 是 int,或者在 Windows 上,HANDLE、void* 和 long 是可互换的,等等......

关于数据结构/堆栈空间使用的假设(参见下面的 5.)。在 C/C++ 代码中,局部变量是在栈上分配的,32bit 和 64bit 模式使用的空间是不同的,这是由于以下几点,并且由于传递参数的规则不同(32bit x86 通常在栈上,64bit x86 部分在寄存器中)。在 32 位上使用默认堆栈大小的代码可能会导致 64 位上的堆栈溢出崩溃。这相对容易发现是导致崩溃的原因,但取决于应用程序的可配置性,可能难以修复。

32 位和 64 位代码之间的时序差异(由于不同的代码大小/缓存占用,或不同的内存访问特性/模式,或不同的调用约定)可能会破坏“校准”。比如说,对于 (int i = 0; i < 1000000; ++i) sleep(0); 32位和64位可能会有不同的时间......

最后是 ABI(应用程序二进制接口)。64 位和 32 位环境之间的差异通常比指针的大小更大……目前,存在 64 位环境的两个主要“分支”,IL32P64(Win64 使用的 - int 和 long 是 int32_t,只有 uintptr_t/vo​​id* 是 uint64_t,说就来自 ) 和 LP64 的大小整数而言(UN*X 使用的 - int 是 int32_t,long 是 int64_t 和 uintptr_t/vo​​id* 是 uint64_t),但也有不同对齐规则的“细分” - 有些环境假设 long , float 或 double 以它们各自的大小对齐,而其他人则假设它们以四个字节的倍数对齐。在 32 位 Linux 中,它们以 4 个字节对齐,而在 64 位 Linux 中,float 以 4 个字节对齐,long 和 double 以 8 个字节的倍数对齐。这些规则的结果是,在许多情况下,bith sizeof(struct { ...}) 和结构/类成员的偏移量在 32 位和 64 位环境之间是不同的,即使数据类型声明完全相同。除了影响数组/向量分配之外,这些问题还会影响数据输入/输出,例如通过文件 - 如果 32 位应用程序写入,例如 struct { char a; 诠释 b; 字符 c,长 d;double e } 到同一个应用程序重新编译为 64 位读入的文件中,结果将不是我们所希望的那样。刚刚给出的示例仅是关于语言原语(char、int、long 等),但当然会影响各种平台相关/运行时库数据类型,无论是 size_t、off_t、time_t、HANDLE,本质上是任何非平凡的结构/联合/类... - 所以这里的错误空间很大,}) 和结构/类成员的偏移量在 32 位和 64 位环境之间是不同的,即使数据类型声明完全相同。除了影响数组/向量分配之外,这些问题还会影响数据输入/输出,例如通过文件 - 如果 32 位应用程序写入,例如 struct { char a; 诠释 b; 字符 c,长 d;double e } 到同一个应用程序重新编译为 64 位读入的文件中,结果将不是我们所希望的那样。刚刚给出的示例仅是关于语言原语(char、int、long 等),但当然会影响各种平台相关/运行时库数据类型,无论是 size_t、off_t、time_t、HANDLE,本质上是任何非平凡的结构/联合/类... - 所以这里的错误空间很大,}) 和结构/类成员的偏移量在 32 位和 64 位环境之间是不同的,即使数据类型声明完全相同。除了影响数组/向量分配之外,这些问题还会影响数据输入/输出,例如通过文件 - 如果 32 位应用程序写入,例如 struct { char a; 诠释 b; 字符 c,长 d;double e } 到同一个应用程序重新编译为 64 位读入的文件中,结果将不是我们所希望的那样。刚刚给出的示例仅是关于语言原语(char、int、long 等),但当然会影响各种平台相关/运行时库数据类型,无论是 size_t、off_t、time_t、HANDLE,本质上是任何非平凡的结构/联合/类... - 所以这里的错误空间很大,除了影响数组/向量分配之外,这些问题还会影响数据输入/输出,例如通过文件 - 如果 32 位应用程序写入,例如 struct { char a; 诠释 b; 字符 c,长 d;double e } 到同一个应用程序重新编译为 64 位读入的文件中,结果将不是我们所希望的那样。刚刚给出的示例仅是关于语言原语(char、int、long 等),但当然会影响各种平台相关/运行时库数据类型,无论是 size_t、off_t、time_t、HANDLE,本质上是任何非平凡的结构/联合/类... - 所以这里的错误空间很大,除了影响数组/向量分配之外,这些问题还会影响数据输入/输出,例如通过文件 - 如果 32 位应用程序写入,例如 struct { char a; 诠释 b; 字符 c,长 d;double e } 到同一个应用程序重新编译为 64 位读入的文件中,结果将不是我们所希望的那样。刚刚给出的示例仅是关于语言原语(char、int、long 等),但当然会影响各种平台相关/运行时库数据类型,无论是 size_t、off_t、time_t、HANDLE,本质上是任何非平凡的结构/联合/类... - 所以这里的错误空间很大,

然后是较低级别的差异,例如手动优化装配(SSE/SSE2/...);32bit 和 64bit 有不同(数量)的寄存器,不同的参数传递规则;所有这些都会强烈影响此类优化的执行方式,并且很有可能例如在 32 位模式下提供最佳性能的 SSE2 代码将需要重写/需要增强以提供最佳性能 64 位模式。

还有 32 位和 64 位的代码设计约束非常不同,尤其是在内存分配/管理方面;一个经过仔细编码以“最大限度地利用它可以在 32 位中获得的内存”的应用程序将在如何/何时分配/释放内存、内存映射文件使用、内部缓存等方面具有复杂的逻辑 - 其中大部分将在 64 位中是有害的,您可以“简单地”利用巨大的可用地址空间。这样的应用程序可能会重新编译为 64 位就好了,但那里的性能比一些没有最大化 32 位窥视孔优化的“古老的简单弃用版本”更差。

所以,归根结底,它也是关于增强/增益的,这就是更多工作的地方,部分是编程,部分是设计/需求。即使你的应用程序在 32 位和 64 位环境中都干净地重新编译并在两者上都经过验证,实际上是受益于 64 位?是否可以/应该对代码逻辑进行更改以使其在 64 位中做得更多/运行得更快?您可以在不破坏 32 位向后兼容性的情况下进行这些更改吗?对 32 位目标没有负面影响?增强功能将在哪里,您可以获得多少?对于大型商业项目,这些问题的答案通常是路线图上的重要标志,因为您的起点是一些现有的“赚钱者”......

于 2013-02-21T12:24:25.403 回答