【发布时间】:2019-03-03 09:33:20
【问题描述】:
我正在一个名为 linux x86-64 的不起眼的系统上使用 gcc 对 C++ 进行编程。我希望可能有一些人使用过这个相同的特定系统(并且可能还能够帮助我理解什么是这个系统上的有效指针)。 我不关心访问指针指向的位置,只想通过指针算法计算出来。
根据标准第 3.9.2 节:
对象指针类型的有效值表示内存中字节的地址 (1.7) 或空指针。
并根据[expr.add]/4:
当具有整数类型的表达式被添加或减去时 从一个指针,结果具有指针操作数的类型。如果 表达式 P 指向具有 n 的数组对象 x 的元素 x[i] 元素,表达式 P + J 和 J + P(其中 J 的值为 j) 如果 0 ≤ i + j ≤ 则指向(可能是假设的)元素 x[i + j] n; 否则,行为未定义。同样,表达式 P - J 指向(可能是假设的)元素 x[i - j] 如果 0 ≤ i - j ≤ n;否则,行为未定义。
并根据stackoverflow question on valid C++ pointers in general:
0x1 是您系统上的有效内存地址吗?好吧,对于某些嵌入式系统来说确实如此。对于大多数使用虚拟内存的操作系统,从零开始的页面被保留为无效。
嗯,这很清楚!所以,除了NULL,一个有效的指针是内存中的一个字节,不,等等,它是一个数组元素,包括数组后面的元素,不,等等,它是一个虚拟内存页面,不,等等,它是超人!
(我想这里的“超人”是指“垃圾收集器”……不是我在任何地方读到过的,只是闻到了它的味道。但说真的,所有最好的垃圾收集器都不会以严重的方式破坏,如果你周围有虚假的指针;最糟糕的是,他们只是不时不时收集一些死对象。似乎没有什么值得搞乱指针算术的东西。)。
因此,基本上,一个合适的编译器必须支持上述所有类型的有效指针。我的意思是,一个假设的编译器仅仅因为指针 calculation 不好就大胆地生成未定义的行为,至少会躲避上面的 3 个项目符号,对吗? (好吧,语言律师,那是你的)。
此外,编译器几乎不可能知道其中的许多定义。 所以有很多方法可以创建一个有效的内存字节(想想懒惰的段错误陷阱微码,我将要访问数组的一部分的自定义页表系统的边带提示,...),映射一个页面,或者只是创建一个数组。
以我自己创建的一个较大的数组和我让默认内存管理器在其中创建的一个较小的数组为例:
#include <iostream>
#include <inttypes.h>
#include <assert.h>
using namespace std;
extern const char largish[1000000000000000000L];
asm("largish = 0");
int main()
{
char* smallish = new char[1000000000];
cout << "largish base = " << (long)largish << "\n"
<< "largish length = " << sizeof(largish) << "\n"
<< "smallish base = " << (long)smallish << "\n";
}
结果:
largish base = 0
largish length = 1000000000000000000
smallish base = 23173885579280
(不要问我怎么知道默认内存管理器会在另一个数组中分配一些东西。这是一个晦涩的系统设置。关键是我经历了数周的调试折磨此示例有效,只是为了向您证明不同的分配技术可以相互忽略)。
考虑到 linux x86-64 支持的内存管理和程序模块组合方式的数量,C++ 编译器确实无法了解所有数组和各种样式的页面映射.
最后,为什么我要特别提到gcc?因为它似乎经常将 any 指针视为有效指针......例如:
char* super_tricky_add_operation(char* a, long b) {return a + b;}
虽然在阅读了所有语言规范之后,您可能希望 super_tricky_add_operation(a, b) 的实现充满未定义的行为,但实际上它非常无聊,只是一个 add 或 lea 指令。太好了,因为我可以将它用于非常方便和实用的事情,例如 non-zero-based arrays,如果没有人使用我的 add 指令只是为了说明无效指针的问题。我爱 gcc.
总而言之,似乎任何支持 linux x86-64 上的标准链接工具的 C++ 编译器几乎都必须将 any 指针视为有效指针,而 gcc 似乎是那个俱乐部。但我不是 100% 确定(考虑到足够的小数精度)。
那么...谁能给出一个 gcc linux x86-64 中 invalid 指针的可靠示例?固体我的意思是导致未定义的行为。并解释是什么导致了语言规范允许的未定义行为?
(或提供gcc 证明相反的文档:所有指针都是有效的)。
【问题讨论】:
-
你知道什么是未定义的行为吗?这不是崩溃。它不会让您的计算机着火。不是报警,不是偷女朋友,不是发动核战争。或者所有这些东西。这只是标准拒绝谈论的行为,仅此而已。为什么又期待在
super_tricky_add_operation中找到特别有趣的汇编代码? -
“我所说的固体是指导致未定义的行为。”您打算如何识别未定义的行为?通过查看您的计算机并观察崩溃?你不能这样做。通过查看您的计算机并观察它是否着火了?你不能这样做。不是看着你的家被打,不是看着你的女朋友离开,不是看着世界在核灾难中终结。您只能通过阅读标准来识别UB。如果标准说你的程序有 UB,它就有 UB(参见前面评论中 UB 的定义)。
-
指针有效性绝对没有含糊之处。 [basic.compound] 指针类型的每个值都是以下之一: (3.1) — 指向对象或函数的指针(该指针被称为指向对象或函数),或 (3.2) — 一个超过对象末尾的指针 (8.7),或 (3.3) — 该类型的空指针值 (7.11),或 (3.4) — 无效的指针值。 编译器不需要解释这个是什么特殊的方式。它可以假设你做任何事情的所有指针都是有效的。
-
不,“我们”没有。您可以声明和定义一个对象,也可以使用 new 运算符创建一个。这使得,让我们用拇指数一数,一,二,这是创建对象的两种方式。您不会“发现”对象。你知道他们在哪里。总的来说,我的印象是你不知道你在问什么。是关于 UB 的症状吗?是关于创建对象吗?是关于指针有效性吗?这太宽泛了。请一次回答一个问题。
-
“新位置对于像 int 这样的 C 类型是可选的”不,它不是“因为 C++ 向后兼容 C”不,不是。
标签: c++ gcc language-lawyer x86-64 undefined-behavior