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

h4c*_*k3d 41 c null operating-system pointers

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

int* ptr = NULL;
*ptr = 10;
Run Code Online (Sandbox Code Playgroud)

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

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

Ada*_*eld 65

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

答案很长(x86和x86-64):让我们下到最低级别:CPU.在x86和x86-64上,该代码通常会编译成如下所示的指令或指令序列:

movl $10, 0x00000000
Run Code Online (Sandbox Code Playgroud)

其中说"将常数整数10存储在虚拟内存地址0".在英特尔®64和IA-32架构软件开发手册详细描述了当该指令被执行会发生什么,所以我要总结一下你.

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

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

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

内核重新​​获得控制权,并根据异常和进程页面表中的信息,确定发生了什么.在这种情况下,它意识到用户级进程访问了无效的内存位置,然后它会做出相应的反应.在Windows上,它将调用结构化异常处理以允许用户代码处理异常.在POSIX系统上,操作系统将向SIGSEGV进程发送信号.

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

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

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

另请参阅关于NULL的很多内容:利用内核NULL取消引用,以获取攻击者如何利用内核内部的空指针解除引用错误以获取Linux机器上的root权限的示例.

  • 虚拟地址"0"当然不总是无效的; AIX将只读页面映射为"0".见http://engineering-software.web.cern.ch/engineering-software/Products/Purify/purify/docs/html/purify/html/ht_m_zpr.htm (5认同)

Who*_*aig 6

作为旁注,为了强制体系结构的差异,由一家以三字母缩写名称而闻名的公司开发和维护的某个操作系统通常被称为大型原色,其确定性最强.

它们在一个巨大的"东西"中利用128位线性地址空间来存储所有数据(内存和磁盘).根据它们的OS,"有效"指针必须放在该地址空间内的128位边界上.这个,顺便说一下,对于结构,包装或不包装,引起迷人的副作用.无论如何,隐藏在每个进程专用页面中的是一个位图,它为进程地址空间中的每个有效位置分配一个,其中有效指针可以放置.其硬件和操作系统上可以生成并返回有效内存地址并将其分配给指针的所有操作码将设置表示该指针(目标指针)所在的内存地址的位.

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

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.
}
Run Code Online (Sandbox Code Playgroud)

真正有趣的是这个.

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.
}
Run Code Online (Sandbox Code Playgroud)

它是你必须要相信的东西.我甚至无法想象为维护该位图所做的内务处理,特别是在复制指针值或释放动态内存时.