c++ - 在gcc linux x86-64 C++中有效的指针是什么?

标签 c++ gcc language-lawyer x86-64 undefined-behavior

我正在使用gcc在晦涩的系统linux x86-64上对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-
  如果0≤i-j,则J指向(可能是假想的)元素x [i-j]
  ≤n;否则,行为是不确定的。


并根据stackoverflow question on valid C++ pointers in general


  0x1是系统上的有效内存地址吗?好吧,对于某些嵌入式系统而言。对于大多数使用虚拟内存的操作系统,从零开始的页面被保留为无效。


好吧,这很清楚!因此,除了NULL之外,有效指针是内存中的一个字节,不,等待,它是一个数组元素,包括紧接该数组之后的元素,不,等待,它是虚拟内存页面,不,等待,它是超人!

(我想这里的“超人”是指“垃圾收集器”……不是我在任何地方都读过,只是闻到了气味。但是,认真地说,如果您有假冒,所有最好的垃圾收集器都不会严重损坏。指针四处乱放;最糟糕的是,它们不时不时收集一些死对象。似乎没有什么值得弄乱指针算法的东西。)

因此,基本上,一个适当的编译器将必须支持上述所有类型的有效指针。我的意思是,仅由于指针计算错误而假想的编译器就具有敢于生成未定义行为的胆识,至少可以避开上面的3个项目符号,对吗? (好的,语言律师,那是你的)。

此外,对于编译器而言,其中许多定义几乎是不可能的。创建有效内存字节的方法有很多(想想懒惰的segfault陷阱微代码,自定义页表系统的边带提示,我将要访问数组的一部分,...),映射页面或简单地创建数组。

例如,以我自己创建的一个大型数组和一个让我默认内存管理器在其中创建的较小数组为例:

#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?因为它通常似乎将任何指针都视为有效指针...例如:

char* super_tricky_add_operation(char* a, long b) {return a + b;}


阅读完所有语言规范后,您可能希望super_tricky_add_operation(a, b)的实现充斥着未定义的行为,但实际上,这很无聊,仅是addlea指令。太好了,因为如果没人用我的add指令只是为了指出无效指针,我就可以将它用于non-zero-based arrays这样非常方便和实用的事情。我爱gcc

总之,似乎所有在Linux x86-64上支持标准链接工具的C ++编译器都几乎必须将任何指针视为有效指针,并且gcc似乎是该俱乐部的成员。但是我不太确定100%(就是说,有足够的分数精度)。

那么...谁能举一个可靠的例子说明gcc linux x86-64中的无效指针?所谓固体,是指导致不确定的行为。并解释一下是什么引起了语言规范所允许的未定义行为?

(或提供gcc文档证明相反:所有指针均有效)。

最佳答案

通常,无论指针是否指向对象,指针数学都可以完全满足您的期望。

UB并不意味着它必须失败。只有这样才能使整个程序的其余部分以某种方式表现出异常。 UB并不意味着仅指针比较结果可能是“错误的”,它意味着整个程序的整个行为都是不确定的。这往往发生在依赖违背假设的优化中。

有趣的极端情况是在虚拟地址空间的最顶端包括一个数组:指向最后一句的指针将换为零,因此start < end将为假?!但是指针比较不必处理这种情况,因为Linux内核永远不会映射首页,因此指向它的指针不能指向或仅指向过去的对象。见Why can't I mmap(MAP_FIXED) the highest virtual page in a 32-bit Linux process on a 64-bit kernel?



有关:

GCC的最大对象大小为PTRDIFF_MAX(这是带符号的类型)。因此,例如,在32位x86上,虽然您可以mmap一个,但并非所有代码源都完全支持大于2GB的数组。

请参阅我对What is the maximum size of an array in C?的评论-对于比char宽的类型(对于C减法结果是对象而不是字节),此限制使gcc可以进行指针减法(以获取大小)而不会保留高位的进位值,因此在asm中为(a - b) / sizeof(T)




  不要问我怎么知道默认的内存管理器会在另一个数组中分配一些东西。这是一个晦涩的系统设置。关键是我经历了数周的调试折磨,以使此示例正常工作,只是向您证明了不同的分配技术可以相互忽略)。


首先,您实际上从未为large[]分配空间。您使用了内联汇编,使其从地址0开始,但实际上没有进行映射。

new使用brkmmap从内核获取新内存时,内核不会重叠现有的映射页,因此,实际上静态和动态分配不会重叠。

其次,char[1000000000000000000L]〜= 2 ^ 59字节。当前的x86-64硬件和软件仅支持规范的48位虚拟地址(符号扩展为64位)。这将随着下一代英特尔硬件的出现而改变,后者将添加更高级别的页表,使我们最多可使用48 + 9 = 57位地址。 (仍然使用内核使用的上半部分,中间使用一个大洞。)

您从0到〜2 ^ 59的未分配空间覆盖了x86-64 Linux上可能的所有用户空间虚拟内存地址,因此,您分配的任何内容(包括其他静态数组)当然都将位于该虚假数组的“内部”。



从声明中删除extern const(因此实际上分配了数组,https://godbolt.org/z/Hp2Exc)会遇到以下问题:

//extern const 
char largish[1000000000000000000L];
//asm("largish = 0");

/* rest of the code unchanged */



使用默认代码模型(-fno-pie -no-pie where all static code+data is assumed to fit in 2GB),相对RIP或32位绝对(large[])寻址无法到达BSS中-mcmodel=small之后链接的静态数据。

$ g++ -O2 large.cpp
/usr/bin/ld: /tmp/cc876exP.o: in function `_GLOBAL__sub_I_largish':
large.cpp:(.text.startup+0xd7): relocation truncated to fit: R_X86_64_PC32 against `.bss'
/usr/bin/ld: large.cpp:(.text.startup+0xf5): relocation truncated to fit: R_X86_64_PC32 against `.bss'
collect2: error: ld returned 1 exit status

使用-mcmodel=medium进行编译会将large[]放在大数据段中,在该段中它不会干扰对其他静态数据的寻址,但是它本身是使用64位绝对寻址来寻址的。 (或者-mcmodel=large对所有静态代码/数据执行此操作,因此每个调用都是间接movabs reg,imm64 / call reg而不是call rel32的。)

这样我们就可以进行编译和链接,但是可执行文件将无法运行,因为内核知道仅支持48位虚拟地址,并且在运行程序之前不会在ELF加载程序中映射程序,也不会在运行就可以了。

peter@volta:/tmp$ g++ -fno-pie -no-pie -mcmodel=medium -O2 large.cpp
peter@volta:/tmp$ strace ./a.out 
execve("./a.out", ["./a.out"], 0x7ffd788a4b60 /* 52 vars */) = -1 EINVAL (Invalid argument)
+++ killed by SIGSEGV +++
Segmentation fault (core dumped)
peter@volta:/tmp$ g++ -mcmodel=medium -O2 large.cpp
peter@volta:/tmp$ strace ./a.out 
execve("./a.out", ["./a.out"], 0x7ffdd3bbad00 /* 52 vars */) = -1 ENOMEM (Cannot allocate memory)
+++ killed by SIGSEGV +++
Segmentation fault (core dumped)



(有趣的是,对于PIE可执行文件和非PIE可执行文件,我们会得到不同的错误代码,但仍未完成ld.so。)



execve()欺骗编译器+链接器+运行时不是很有趣,并且会产生明显的未定义行为。

有趣的事实2:x64 MSVC不支持大于2 ^ 31-1字节的静态对象。 IDK(如果它具有asm("largish = 0");等效项)。基本上,GCC无法针对所选内存模型警告对象太大。

<source>(7): error C2148: total size of array must not exceed 0x7fffffff bytes

<source>(13): warning C4311: 'type cast': pointer truncation from 'char *' to 'long'
<source>(14): error C2070: 'char [-1486618624]': illegal sizeof operand
<source>(15): warning C4311: 'type cast': pointer truncation from 'char *' to 'long'


另外,它指出-mcmodel=medium通常是错误的指针类型(因为Windows x64是LLP64 ABI,其中long是32位)。您需要longintptr_t,或者与打印原始uintptr_tprintf("%p")等效的东西。

关于c++ - 在gcc linux x86-64 C++中有效的指针是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54967429/

相关文章:

c++ - 为什么传递右值引用 (X&&) 就像传递左值引用 (X&)?

c++ - 原子的 0 初始化是否保证将值成员设置为 0?

c++ - getline() 与 ifstream 的意外行为

c++ - 逗号在数字中的含义

objective-c - 如何限制使用的预处理定义的范围?

c++ - 修改嵌套lambda中捕获的参数 : gcc vs clang?

c - 6.3.1.1p2 的含义,要点 2

c++ - 在 cpp 文件中定义模板函数 - 不适用于 VC6

c++ - 如何使用带有 `--gcov-tool` 标志的 lcov?

c - MinGW编译错误