繁体   English   中英

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

[英]What is a valid pointer in gcc linux x86-64 C++?

我正在使用gcc在晦涩的系统linux x86-64上对C ++进行编程。 我希望可能有一些人在使用相同的特定系统(也可能能够帮助我了解该系统上的有效指针)。 我不在乎访问指针所指向的位置,只想通过指针算法来计算它。

根据标准的3.9.2节:

对象指针类型的有效值表示内存(1.7)中字节的地址或空指针。

并根据[expr.add] / 4

当将具有整数类型的表达式添加到指针或从指针中减去时,结果将具有指针操作数的类型。 如果表达式P指向具有n个元素的数组对象x的元素x [i],则表达式P + J和J + P(其中J的值为j)指向(可能是假设的)元素x [i + j]如果0≤i + j≤n; 否则,行为是undefined 同样,如果0≤i-j≤n,则表达式P-J指向(可能是假设的)元素x [i-j]; 否则,行为是不确定的。

根据一般有效C ++指针stackoverflow问题

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

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

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

因此,基本上,一个适当的编译器将必须支持上述所有类型的有效指针。 我的意思是,仅由于指针计算错误而假想的编译器就具有敢于生成未定义行为的胆识,至少可以避开上面的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 因为它通常似乎将任何指针都视为有效指针...例如:

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

阅读完所有语言规范后,您可能会期望super_tricky_add_operation(a, b)的实现充斥着未定义的行为,但实际上却很无聊,只是一条addlea指令。 真是太好了,因为如果没有人为了add有关无效指针的信息而塞入我的add指令,那么我可以将其用于非常方便和实用的事情,例如基于非零的数组 gcc

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

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

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

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

UB并不意味着它必须失败 只知道它是允许的 ,使程序的整个其余奇怪的行为在某些方面。 UB并不意味着仅指针比较结果可能是“错误的”,它意味着整个程序的整个行为都是不确定的。 这往往发生在依赖违背假设的优化中。

有趣的特殊情况是在虚拟地址空间的顶部包括一个数组:指向过去一末尾的指针将换为零,因此start < end将为假?!? 但是指针比较不必处理这种情况,因为Linux内核永远不会映射首页,因此指向它的指针不能指向或仅指向过去的对象。 请参阅为什么不能在64位内核的32位Linux进程中映射(MAP_FIXED)最高虚拟页面?


有关:

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

请参阅我在C语言中数组的最大大小是多少的评论 -这个限制让GCC实现的指针相减(以获得大小),而不从高位保持进位,为各类宽度大于char所在的C减的结果是对象,而不是字节,因此在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 */
  • RIP-相对或32位绝对( -fno-pie -no-pie )寻址不能到达被链接后的静态数据large[]中的BSS,默认代码模型( -mcmodel=small ,其中所有静态代码+数据假定适合2GB

     $ 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加载程序中映射程序,也不会在运行ld.so之前将其映射为PIE ld.so

     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可执行文件,我们会获得不同的错误代码,但仍在execve()尚未完成之前。)


使用asm("largish = 0");欺骗编译器+链接器+运行时asm("largish = 0"); 不是很有趣,并且会产生明显的不确定行为。

有趣的事实2:x64 MSVC不支持大于2 ^ 31-1字节的静态对象。 IDK(如果它具有-mcmodel=medium等效项)。 基本上,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'

同样,它指出long通常是错误的指针类型(因为Windows x64是LLP64 ABI,其中long是32位)。 您需要intptr_tuintptr_t ,或与printf("%p")等效的东西,它会打印原始的void*

除了实现通过静态,自动或线程持续时间的对象或使用标准库函数(如calloc提供的存储之外,该标准预计不会存在任何存储。 因此,它对实现如何处理指向此类存储的指针没有任何限制,因为从它的角度来看,这种存储不存在,有意义地标识不存在的存储的指针不存在,不存在的事物也不需要有关于它们的规则。

这并不意味着委员会的人员并不十分了解许多执行环境提供了C实现可能不了解的存储形式。 但是,可以预期实际使用各种平台的人员比委员会更有资格确定委员会程序员需要使用此类“外部”地址进行何种操作,以及如何最好地满足此类需求。 该标准无需担心此类问题。

碰巧的是,在某些执行环境中,编译器像处理整数数学一样处理指针算术比做其他任何事情都更方便,并且许多用于此类平台的编译器即使在不需要它们的情况下也可以有效地处理指针算术。这样做。 对于32位和64位x86和x64,我认为无效的非空地址没有任何位模式,但是有可能形成的指针不能作为指向它们所寻址对象的有效指针。

例如,给出如下所示:

char x=1,y=2;
ptrdiff_t delta = (uintptr_t)&y - (uintptr_t)&x;
char *p = &x+delta;
*p = 3;

即使以这样的方式定义指针表示,即使用整数算术将delta加到x的地址将产生y ,也绝不能保证编译器会认识到*p上的操作可能会影响y ,即使p保持y的地址。 即使位模式与y的地址匹配,指针p也会有效地表现为地址无效。

以下示例表明,GCC专门至少假定以下条件:

  • 全局数组不能位于地址0。
  • 数组不能环绕地址0。

在gcc linux x86-64 C ++中对无效指针进行算术引起的意外行为的示例(感谢melpomene):

  • largish == NULL在问题程序中的计算结果为false
  • unsigned n = ...; if (ptr + n < ptr) { /*overflow */ } unsigned n = ...; if (ptr + n < ptr) { /*overflow */ }可以优化为if (false)
  • int arr[123]; int n = ...; if (arr + n < arr || arr + n > arr + 123) int arr[123]; int n = ...; if (arr + n < arr || arr + n > arr + 123)可以优化为if (false).

注意,这些示例都涉及无效指针的比较 ,因此可能不会影响基于非零数组的实际情况。 因此,我提出了一个更实际的新问题

谢谢大家在聊天中帮助缩小问题范围。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM