我能否想要访问地址0?

60

在C和C ++中,常数0被用作空指针。但是在问题““指向特定固定地址的指针”中,似乎可以使用分配固定地址。在任何系统中,是否有任何可想象的需要,无论是什么低级任务,来访问地址0?

如果有,那么如何解决0是空指针的所有问题呢?

如果没有,是什么确保没有这样的需求?


2
有趣的问题!我想因为地址0只有1个字节,所以你可以不用它... - Hamish Grubijan
3
你认为这是一个有效的用途吗:让你的程序崩溃,执行任意代码并接管你的系统? :) - Paul Williams
6
一些嵌入式设备将中断等信息存储在地址0处。您是否考虑过使用嵌入式设备? - Anycorn
3
值得注意的是,PlayStation 1和PlayStation 2都非常乐意让您获取地址0处的值。没有人是故意这样做的,因为那里是控制台内核的位置。然而,如果某些代码意外地依赖于访问“空”指针时不崩溃,那么将其移植到其他系统就不太有趣了。 - Jim Buck
1
@JimBuck,谢谢你分享这个有趣的轶事! - Marc Claesen
显示剩余6条评论
17个回答

83
无论是在 C 还是 C++ 中,空指针值都不与物理地址“0”有任何关联。在源代码中使用常量“0”将指针设置为空指针值仅仅是语法上的一种简化方式。编译器需要将其转换为在特定平台上用作空指针值的实际物理地址。
换言之,在源代码中,“0”没有任何物理重要性。例如,它可以是“42”或“13”。也就是说,如果语言的作者愿意,他们可以使您必须执行“p = 42”才能将指针“p”设置为null-pointer值。同样,这并不意味着物理地址“42”必须用于保留空指针。编译器需要将源代码“p = 42”转换为将实际物理空指针值(“0x0000”或“0xBAAD”)塞入指针“p”的机器码。现在使用常量“0”就是这样做的。
此外,请注意,C 或 C++ 都没有提供严格定义的功能,允许将特定的物理地址分配给指针。因此,你关于“如何将0地址分配给指针”的问题在形式上没有答案。在 C/C++ 中,您无法将指针分配给特定地址。但是,在实现定义的特性领域,显式的整数到指针转换旨在具有该效果。因此,您可以按如下方式执行此操作:
uintptr_t address = 0;
void *p = (void *) address;

请注意,这与执行以下操作不同:

void *p = 0;

后者总是产生空指针值,而前者在一般情况下则不会。前者通常会产生一个指向物理地址0的指针,这个地址可能或可能不是给定平台上的空指针值。


很棒的答案。我希望我能给这个答案点赞两次 - Destructor
谢谢。我有一些问题不理解你的回答 https://stackoverflow.com/questions/52207010/do-the-following-statements-assign-physical-address-to-a-pointer - Tim
2
在 C/C++ 中,你无法直接将一个指针赋值为一个特定的地址。但是 void *p = (void*)0x12345678 呢?至少在某些平台上(比如我使用的 ARM 微控制器),这会将一个绝对、物理内存地址赋给指针。这通常用于 SFR(特殊功能寄存器,即内存映射 I/O)。 - jacobq
@iX3:“在实现定义特性的领域中” - Tim Sparkles
@Timbo,糟糕,不知道我怎么错过了那个; 抱歉。 - jacobq

23

顺便提一下:你可能会感兴趣地知道,使用微软的C++编译器,在32位机器上,一个空指针成员将被表示为位模式0xFFFFFFFF。也就是说:

struct foo
{
      int field;
};

int foo::*pmember = 0;     // 'null' member pointer

pmember将具有位模式“全为1”。这是因为您需要此值来与其他空指针值区分开来。

int foo::*pmember = &foo::field;

因为我们希望指向结构体foo的偏移量为0,所以位模式确实将是“全零位模式”。

其他C++编译器可能会选择不同的空成员指针的位模式,但关键观察点在于它不会是你可能期望的全零位模式。


2
有趣!+1 即使它与问题关系不大 :) - Logan Capaldo
等等,什么? 当我开始将该指针分配给不同类型的指针(如果需要,使用(void *)进行转换),会发生什么? 它会保持为全1还是会发生什么? 这是否意味着所有空指针在MS C++中都是“-1”? - Nas Banov
1
不允许将成员指针转换为“普通”指针。如果尝试这样做,MS C++会给出“无效类型转换”错误。此外,在MS C++中,只有空成员指针具有位模式“全1”,而指向对象的“普通”指针仍具有位模式“全0”。 - John Källén
1
G++ 也可以做到这一点。 - alecov
当然,这取决于微软不符合规范的“让我们使用最简单的成员指针,可能对这个特定类足够通用”的方式。尝试去实现完全的通用性,或者添加虚拟继承,你会看到更大的成员指针。 - Deduplicator

12

你的前提是错误的。当你将一个值为0的整数常量分配给一个指针时,它就变成了一个空指针常量。然而,这并不意味着一个空指针一定指向地址0。相反,C和C++标准都非常明确地说明,空指针可能指向除零以外的某个地址。

实际上,你需要设置一个空指针所指向的地址,但这个地址可以是任何你选择的地址。当你把0转换成一个指针时,它必须指向你选择的那个地址——但这才是真正所需的。例如,如果你决定将整数转换为指针意味着将0x8000加到整数上,那么空指针实际上将指向地址0x8000而不是地址0。

值得注意的是,对空指针进行间接引用会导致未定义行为。这意味着你不能在可移植的代码中这样做,但这并不意味着你根本无法这样做。当你编写针对小型微控制器等设备的代码时,通常会包含一些不可移植的代码片段。从某个地址读取可能会给你一些传感器的值,而写入同一地址可能会激活一个步进电机(仅仅是一个例子)。下一个设备(即使使用完全相同的处理器)可能会连接到普通RAM而不是这些地址。

即使空指针确实指向地址0,这也不能阻止你使用它来读取和/或写入该地址上的任何内容——只是不能以便于移植的方式这样做——但这并不是很重要。如果地址0被解码为连接到其他不同于普通存储的东西,那么你可能无法完全地可移植地使用它。


16
他的前提是正确的。他想知道:如何访问内存地址0,因为(void*)0是特殊的? - Daniel Stutzbach
2
@Daniel:(void *) 0是特殊的。int x = 0; (void *) x不是特殊的。它肯定不是可移植的,但这不是需要访问特定内存位置的人们所担心的问题。 - David Thornley
@PravasiMeet:虽然没有直接说明,但§4.10和§4.11讨论了将空指针常量存储到指针中涉及转换的事实,并非常小心地说结果指针的所有位都设置为0。 - Jerry Coffin
@JerryCoffin:发生了哪种类型的转换,为什么需要它? - Destructor
@PravasiMeet:在§4.10和§4.11中讨论的种类,是基于§4.10和§4.11中所述原因而进行的讨论。 - Jerry Coffin
显示剩余4条评论

11
编译器会为你处理这个问题(comp.lang.c FAQ):

如果一台机器在空指针中使用非零比特模式,则当程序员请求通过写入“0”或“NULL”来创建一个空指针时,编译器将生成该模式。因此,在内部空指针为非零的机器上将NULL定义为0与在其他任何机器上定义一样有效,因为编译器必须(并且可以)仍然针对指针上下文中看到的未装饰的0来生成机器的正确空指针。

您可以通过从非指针上下文中引用零来访问地址零。


8
实际上,C编译器会让你的程序尝试写入地址0。在运行时检查每个指针操作是否为NULL指针将会非常昂贵。在计算机上,程序将崩溃,因为操作系统禁止这样做。在没有内存保护的嵌入式系统中,程序确实会写入地址0,这通常会导致整个系统崩溃。
地址0可能在嵌入式系统中很有用(一般是指不在计算机中的CPU;它们可运行从音响到数码相机的所有东西)。通常,这些系统被设计成不需要写入地址0。在我所知道的每种情况下,它都是某种特殊地址。即使程序员需要写入它(例如设置中断表),他们也只需要在初始引导序列期间写入它(通常是一小段汇编语言来设置C环境)。

例如,在ARM体系结构中,地址0是复位向量。如果覆盖了那里的内容(应该是跳转到真正的复位代码),则会破坏软复位的功能。(但是,地址0通常映射到闪存,不太容易被覆盖。) - crazyscot
1
实际上,C编译器会让你的程序尝试写入地址0。我不认为这还准确。编译器将看到对地址0的天真尝试并优化UB,例如捕获,有关更多详细信息,请参见我的答案 - Shafik Yaghmour

7

内存地址0也被称为零页面。这是由BIOS填充的,包含关于系统上运行的硬件的信息。所有现代内核都保护该内存区域。您不应该需要访问此内存,但如果您想要访问,则需要从内核空间内进行访问,一个内核模块就可以解决问题。


7
对于x86计算机来说,这是正确的,但对于嵌入式系统则不然。 - Daniel Stutzbach
2
不能保证内存是否在页面中,也不能保证有内核。 - WhirlWind
1
@Daniel 这对于运行现代操作系统的x86计算机是正确的。也有嵌入式x86系统。 - Earlz
1
实际上,在x86架构中,您可以将(虚拟)零页面映射到用户模式下的任何位置,这取决于内核是否限制它。我猜测有一些操作系统允许这样做,Linux内核允许您选择进程可以使用的最低可用虚拟地址。 - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

6
在x86架构中,实模式下地址0(或者准确来说是0000:0000)及其附近是中断向量的位置。在早期,你通常会向中断向量写入值以安装中断处理程序(或者如果你更加遵循纪律,使用MS-DOS服务0x25)。为MS-DOS编译的C语言定义了一个far指针类型,当分配NULL或0时,其段部分和偏移部分都将接收到0000的位模式。
当然,一个行为不端的程序意外地写入了值为0000:0000的far指针,会在机器上引起非常严重的问题,通常会锁定它并强制重新启动。

3
实际上,在x86中写入0不会立即造成任何坏影响。0是用于除零异常的中断向量,而这种情况很少被触发。 - Earlz
据我所记,在Motorola 68K系列中,0是起始地址-启动时要执行的第一条指令从地址0处加载。虽然已经过了几年,所以我可能错了,但我对此有90%的信心,尽管我没有我的参考资料方便确认。 - Nathan Ernst
8080(因此Z80)在上电时从地址0开始执行,因此您的ROM通常会基于那里。通常不需要指向0000的指针。在我的TRS-80上尝试写入将没有任何效果。 - John Källén

5
在这个链接的问题中,人们正在讨论在微控制器中设置固定地址。当您编程微控制器时,所有内容都处于更低级别。
与桌面/服务器PC相比,您甚至没有操作系统,也没有虚拟内存等等。因此,在特定地址访问内存是可以接受甚至必要的。但在现代桌面/服务器PC上,则是无用且甚至危险的。

2
在微控制器中,地址0通常是程序入口点。因此,可以通过跳转到地址0来重置程序。 - Ponkadoodle

3
我使用gcc为没有MMU且0是完全有效地址的Motorola HC11编译了一些代码,但令人失望的是,发现要写入地址0,只需直接写入即可。在此架构上,NULL和地址0之间没有区别,这点我可以理解。因为在每个内存位置都可能有效的架构上,不可能定义一个唯一的NULL,所以我猜测gcc的作者们认为0足以作为NULL,无论它是否是有效地址。
      char *null = 0;
; Clears 8-bit AR and BR and stores it as a 16-bit pointer on the stack.
; The stack pointer, ironically, is stored at address 0.
1b:   4f              clra
1c:   5f              clrb
1d:   de 00           ldx     *0 <main>
1f:   ed 05           std     5,x

当我将其与另一个指针进行比较时,编译器会生成常规比较。这意味着它根本不认为char *null = 0是一个特殊的NULL指针,事实上,地址为0的指针和“NULL”指针是相等的。

; addr is a pointer stored at 7,x (offset of 7 from the address in XR) and 
; the "NULL" pointer is at 5,y (offset of 5 from the address in YR).  It doesn't
; treat the so-called NULL pointer as a special pointer, which is not standards
; compliant as far as I know.
37:   de 00           ldx     *0 <main>
39:   ec 07           ldd     7,x
3b:   18 de 00        ldy     *0 <main>
3e:   cd a3 05        cpd     5,y
41:   26 10           bne     53 <.LM7>

所以为了回答原始问题,我猜我的答案是要检查你的编译器实现,并找出它们是否费心去实现唯一值 NULL。如果没有,你就不必担心它。;)

(当然这个答案不符合标准。)


0 是 NULL 的独特值。它恰好是系统允许你写入的一个值。在其他系统上,它不是这样,所以会导致段错误。在其他系统上,你可以写入它,但你可能不应该这样做,因此会发生一些其他模糊的结果 [我想 HC11 就属于此类; 地址 0 是特殊的,但我不记得它代表什么]。所有三种行为都符合标准,只要标准未定义行为。至少在 C++ 中是这样,我认为 C 也是一样。 - Dennis Zickefoose
@Dennis Zickefoose:在标准兼容性方面,我指的是标准中提到保证NULL指针不会指向任何对象的部分,这意味着将任何其他指针与NULL进行比较将返回false。在HC11上,地址0只是普通的RAM,没有什么特别之处,你应该使用地址0来存储一些东西,因为你可以使用直接寻址来访问从0到255字节的RAM,从而得到更小的指令和更快的访问速度。 - indiv
1
如果编译器小心地不在那个地址分配对象,那么它就保证永远不会指向对象。这是其他平台的工作方式。空指针与其他指针一样处理,编译器只知道不让对象存在那里。 - Dennis Zickefoose

1

记住,在所有正常情况下,您实际上看不到特定的地址。 当您分配内存时,操作系统会向您提供该内存块的地址。

当您获取变量的引用时,变量已经在由系统确定的地址上分配。

因此,访问地址零并不是真正的问题,因为当您跟随指针时,您不关心它指向哪个地址,只要它是有效的:

int* i = new int(); // suppose this returns a pointer to address zero
*i = 42; // now we're accessing address zero, writing the value 42 to it

所以,如果你需要访问地址零,通常情况下它会正常工作。

0 == null 这个问题只有在某些情况下才会成为问题,比如你正在直接访问物理内存。也许你正在编写操作系统内核或类似的东西。在这种情况下,你将要写入特定的内存地址(尤其是那些映射到硬件寄存器的地址),因此你可能需要写入地址零。但是这时你真的绕过了 C++,依赖于你的编译器和硬件平台的具体实现。

当然,如果你需要写入地址零,这是可能的。只有常量 0 表示空指针。非常量整数值零不会产生空指针,如果被分配给一个指针。

所以你可以简单地这样做:

int i = 0;
int* zeroaddr = (int*)i;

现在,zeroaddr将指向地址零(*),但严格来说它不是一个空指针,因为零值不是常量。

(*):这并非完全正确。C++标准仅保证整数和地址之间存在“实现定义的映射”。它可以将0转换为地址0x1633de20或任何其他地址。但通常映射是直观和明显的,其中整数0被映射到地址零)


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