当我们在 C 中取消引用 NULL 指针时,操作系统会发生啥?

Posted

技术标签:

【中文标题】当我们在 C 中取消引用 NULL 指针时,操作系统会发生啥?【英文标题】:What happens in OS when we dereference a NULL pointer in C?当我们在 C 中取消引用 NULL 指针时,操作系统会发生什么? 【发布时间】:2012-09-20 16:06:48 【问题描述】:

假设有一个指针,我们用 NULL 初始化它。

int* ptr = NULL;
*ptr = 10;

现在,程序将崩溃,因为 ptr 没有指向任何地址,我们正在为其分配一个值,这是一个无效的访问。那么,问题是,操作系统内部发生了什么?是否发生页面错误/分段错误?内核甚至会在页表中搜索吗?还是在此之前发生崩溃?

我知道我不会在任何程序中做这样的事情,但这只是想知道在这种情况下操作系统或编译器内部会发生什么。这不是一个重复的问题。

【问题讨论】:

真正的答案需要您指定您正在谈论的操作系统和 CPU。 @TJD Linux。拿任何CPU。我说的是 GENERAL。 @sTEAK.:你不能“笼统地”谈论这个。该语言说取消引用空指针会导致未定义的行为。您给定的处理器和操作系统将对此做什么是特定于实现的。不存在“一般” @sTEAK:行为绝对取决于我们所讨论的操作系统和 CPU。 “占用任何 CPU。我说的是 GENERAL”。不,关键是 CPU 有不同的机制来处理这个问题。在某些情况下,这没有机制 【参考方案1】:

简答:这取决于很多因素,包括编译器、处理器架构、特定处理器型号和操作系统等。

长答案(x86 和 x86-64):让我们深入到最底层:CPU。在 x86 和 x86-64 上,该代码通常会编译成这样的指令或指令序列:

movl $10, 0x00000000

这表示“将常量整数 10 存储在虚拟内存地址 0 处”。 Intel® 64 and IA-32 Architectures Software Developer Manuals详细描述了这条指令执行时会发生什么,所以我来给大家总结一下。

CPU 可以在几种不同的模式下运行,其中一些是为了向后兼容更旧的 CPU。现代操作系统以一种称为保护模式的模式运行用户级代码,该模式使用paging 将虚拟地址转换为物理地址。

对于每个进程,操作系统都有一个页表,它规定了地址的映射方式。页表以 CPU 理解的特定格式存储在内存中(并受到保护,以便用户代码无法修改它们)。对于发生的每一次内存访问,CPU 都会根据页表对其进行转换。如果翻译成功,则对物理内存位置执行相应的读/写操作。

地址转换失败时会发生有趣的事情。并非所有地址都是有效的,如果任何内存访问产生无效地址,处理器将引发页面错误异常。这会触发从 用户模式(又名 x86/x86-64 上的当前权限级别 (CPL) 3)到 内核模式(又名 CPL)的转换0) 到内核代码中的特定位置,由中断描述符表 (IDT) 定义。

内核重新获得控制权,并根据来自异常的信息和进程的页表,找出发生了什么。在这种情况下,它意识到用户级进程访问了一个无效的内存位置,然后它会做出相应的反应。在 Windows 上,它将调用 structured exception handling 以允许用户代码处理异常。在 POSIX 系统上,操作系统将向进程传递SIGSEGV 信号。

在其他情况下,操作系统将在内部处理页面错误并从当前位置重新启动进程,就好像什么都没发生一样。例如,guard pages 被放置在堆栈的底部,以允许堆栈按需增长到极限,而不是为堆栈预先分配大量内存。类似的机制用于实现copy-on-write内存。

在现代操作系统中,通常设置页表以使地址 0 成为无效的虚拟地址。但有时可以改变这一点,例如在 Linux 上通过将 0 写入伪文件 /proc/sys/vm/mmap_min_addr,之后可以使用 mmap(2) 映射虚拟地址 0。在这种情况下,取消引用空指针不会导致页面错误。

上面的讨论都是关于当原始代码在用户空间运行时会发生什么。但这也可能发生在内核内部。内核可以(并且肯定比用户代码更有可能)映射虚拟地址 0,因此这样的内存访问是正常的。但是如果它没有被映射,那么接下来发生的事情大致相似:CPU 引发一个页面错误错误,该错误会陷入内核的预定义点,内核检查发生了什么,并做出相应的反应。如果内核无法从异常中恢复,它通常会通过打印输出以某种方式出现恐慌(kernel panickernel oops 或 Windows 上的 BSOD)一些调试信息到控制台或串行端口,然后停止。

另请参阅Much ado about NULL: Exploiting a kernel NULL dereference ,了解攻击者如何利用内核内部的空指针取消引用错误来获得 Linux 计算机上的 root 权限的示例。

【讨论】:

虚拟地址0当然不总是无效的; AIX 在0 映射一个只读页面。见engineering-software.web.cern.ch/engineering-software/Products/… 另外,您需要考虑到您的代码可能在内部内核中运行,其中通常映射虚拟地址0 的页面。 更多关于 x86 分页的信息:***.com/questions/18431261/how-does-x86-paging-work 在这种情况下,它意识到用户级进程访问了无效的内存位置。 “它”如何意识到地址无效而不是页面错误? 添加 Q:进程地址空间的信息存储在哪里?因为如果翻译未命中是页面错误(从磁盘引入代码/数据)或分段错误(终止进程),这是需要检查的信息。【参考方案2】:

作为旁注,只是为了强调架构上的差异,由一家以其三字母首字母缩写词名称而闻名的公司开发和维护的特定操作系统具有最令人着迷的 NULL 确定。

他们在一个巨大的“事物”中为所有数据(内存和磁盘)使用 128 位线性地址空间。根据他们的操作系统,“有效”指针必须放置在该地址空间内的 128 位边界上。顺便说一句,这会对包含指针的结构(无论是否打包)造成迷人的副作用。无论如何,隐藏在每个进程专用页面中的是一个位图,它为进程地址空间中可以放置有效指针的每个有效位置分配一个 bit。硬件和操作系统上所有可以生成和返回有效内存地址并将其分配给指针的操作码都会设置表示该指针(目标指针)所在的内存地址的位。

那么为什么要有人关心呢?原因很简单:

int a = 0;
int *p = &a;
int *q = p-1;

if (p)

// p is valid, p's bit is lit, this code will run.


if (q)

   // the address stored in q is not valid. q's bit is not lit. this will NOT run.

真正有趣的是这个。

if (p == NULL)

   // p is valid. this will NOT run.


if (q == NULL)

   // q is not valid, and therefore treated as NULL, this WILL run.


if (!p)

   // same as before. p is valid, therefore this won't run


if (!q)

   // same as before, q is NOT valid, therefore this WILL run.

你必须亲眼所见才能相信。我什至无法想象为维护该位图而进行的内务处理,尤其是在复制指针值或释放动态内存时。

【讨论】:

抱歉 - 我在解析您在此处所说的内容时遇到了一些问题。 128 位地址空间中的 128 位边界 - 您是指面向字节的 128 位地址空间吗?边界区域中的 128 位数据?如果一切都是字节,那么 128 位地址空间中只有 1 128 位区域,因此没有边界。那是错字吗?你能详细说明或修复吗? @TedMiddleton,没有错字。 128 位是 16 个字节。 128 位边界意味着对齐到 16 字节的倍数。 128位地址空间是指指针长度为128位(16字节)的地址空间。【参考方案3】:

典型情况下,int *ptr = NULL; 将设置ptr 指向地址 0。C 标准(和 C++ 标准)非常小心需要,但它非常很常见。

当您执行*ptr = 10; 时,CPU 通常会在地址线上生成 0,在数据线上生成10,同时设置 R/W 线以指示写入(并且,如果总线有这样的事情,断言内存与 I/O 线以指示写入内存,而不是 I/O)。

假设 CPU 支持内存保护(并且您正在使用启用它的操作系统),CPU 将在发生之前检查(尝试的)访问。例如,现代 Intel/AMD CPU 将使用将虚拟地址映射到物理地址的分页表。在典型情况下,地址 0 不会映射到任何物理地址。在这种情况下,CPU 将产生访问冲突异常。对于一个相当典型的示例,Microsoft Windows 未映射前 4 兆字节,因此该范围内的 任何 地址通常会导致访问冲突。

在较旧的 CPU(或未启用 CPU 保护功能的较旧操作系统)上,尝试写入通常会成功。例如,在 MS-DOS 下,通过 NULL 指针写入将简单地写入地址零。在小型或中型模型(数据的 16 位地址)中,大多数编译器会将一些已知模式写入数据段的前几个字节,当程序结束时,他们会检查该模式是否保持不变(并且如果失败,请做一些事情以表明您已通过 NULL 指针写入)。在紧凑型或大型模型(20 位数据地址)中,它们通常只会写入地址零而不会发出警告。

【讨论】:

【参考方案4】:

在支持虚拟内存的 CPU 上,如果尝试读取内存地址 0x0,通常会发出页面错误异常。操作系统页面错误处理程序将被调用,然后操作系统将确定该页面无效并中止您的程序。

请注意,在某些 CPU 上,您还可以安全地访问内存地址 0x0

正如 C 标准所说,取消引用空指针是未定义的,如果编译器能够在编译时(甚至运行时)检测到您正在取消引用空指针,它可以做任何它想做的事情,比如用 a 中止程序详细的错误消息。

(C99, 6.5.3.2.p4) "如果给指针赋值了一个无效值,一元 * 操作符的行为是未定义的。87)"

87):“一元 * 运算符取消引用指针的无效值包括空指针、与指向的对象类型不恰当对齐的地址,以及对象在其生命周期结束后的地址。 "

【讨论】:

【参考方案5】:

我想这取决于平台和编译器。 NULL 指针可以通过使用 NULL 页面来实现,在这种情况下您会遇到页面错误,或者它可能低于向下扩展段的段限制,在这种情况下您会遇到分段错误。

这不是一个确定的答案,只是我的猜想。

【讨论】:

以上是关于当我们在 C 中取消引用 NULL 指针时,操作系统会发生啥?的主要内容,如果未能解决你的问题,请参考以下文章

C中的棘手指针算法:** k

如何通过 Cppcheck 解释 Null 指针取消引用?

C语言visual studio警告:取消对NULL指针“p”的引用

引用还是指针?

当对象在运行时释放时,确保nil或null值

“dynamic_cast”之后的 NULL 指针实际上可以被取消引用吗?