【问题标题】:Why does a program accessing illegal pointer to pointer not crash?为什么访问非法指针指针的程序不会崩溃?
【发布时间】:2013-07-25 07:57:22
【问题描述】:

访问非法指针的程序不会因 SIGSEGV 崩溃。这不是一件好事,但我想知道这是怎么回事,以及这个过程是如何在生产中存活了很多天的。这让我感到困惑。

我已经在 Windows、Linux、OpenVMS 和 Mac OS 中试用过这个程序,他们从未抱怨过。

#include <stdio.h>
#include <string.h>

void printx(void *rec) { // I know this should have been a **
    char str[1000];
    memcpy(str, rec, 1000);
    printf("%*.s\n", 1000, str);
    printf("Whoa..!! I have not crashed yet :-P");
}

int main(int argc, char **argv) {
    void *x = 0; // you could also say void *x = (void *)10;
    printx(&x);
}

【问题讨论】:

  • 这是未定义的行为,因此不崩溃是一个非常好的结果。如果你想调试这类事情,请使用适当的内存检查工具。
  • 只是我传递了一个指向指针的指针,当 memcpy 尝试取消引用 printx() 函数中的指针并尝试复制一些垃圾 1000 字节时,它应该已经崩溃了
  • 内存检查器如 valgrind 尝试报告这类事情。否则没有保修。
  • 见:Hotel

标签: c undefined-behavior


【解决方案1】:

我对没有内存故障并不感到惊讶。该程序没有取消引用未初始化的指针。相反,它复制并打印从指针变量开始的内存内容,以及超出它的 996(或 992)字节。

由于指针是一个堆栈变量,它在堆栈顶部附近打印内存以向下移动。该内存包含main() 的堆栈帧:可能是一些保存的寄存器值、程序参数的计数、指向程序参数的指针、指向环境变量列表的指针以及用于返回的main() 保存的指令寄存器,通常在 C 运行时库启动代码中。在我研究过的所有实现中,下面的堆栈帧具有环境变量本身的副本、指向它们的指针数组以及指向程序参数的指针数组。在 Unix 环境(你暗示你正在使用)中,程序参数字符串将低于它。

所有这些内存都是“安全的”打印,除了一些不可打印的字符会出现可能会弄乱显示终端。

主要的潜在问题是是否有足够的堆栈内存分配和映射以防止在访问期间出现 SIGSEGV。如果环境数据太少,可能会发生段错误。或者,如果实现将该数据放在其他地方,以便这里只有几个字的堆栈。我建议通过清除环境变量并重新运行程序来确认。

如果任何 C 运行时约定不正确,这段代码就不会那么无害:

  • 架构使用堆栈
  • 在堆栈上分配了一个局部变量 (void *x)
  • 堆栈向编号较低的内存增长
  • 参数在堆栈上传递
  • main() 是否使用参数调用。 (一些轻型环境,如嵌入式处理器,调用 main() 时不带参数。)

在所有主流的现代实现中,所有这些通常都是正确的。

【讨论】:

  • @pavan.mankala:不客气。事实上,我已经涉足编写和维护了几个编译器,并且花了很多时间处理调用main() 的接口,主要是为了简化有限的内存环境。
  • +1:在大多数操作系统中,取消引用空指针肯定会产生段错误(。有依赖于此的软件。
  • @DevSolar,纯粹是学术上的说法是,发生的事情是未定义的行为,没有对实际观察到的行为进行解释。我会声称,能够解释机器在任何给定情况下的工作是一门好的工程和好的计算机科学,即使当或特别是当语言规范将某些决定的责任留给编译器和操作系统的实现者时也是如此。我们的程序不会在与实际问题隔离的真空中执行。
  • @Joni:所以你认为说指针是 32 位或 64 位,参数在堆栈上传递,堆栈向下扩展,在 argc / @987654328 旁边有一个指向环境变量的指针是一个很好的解释@,返回地址保存在堆栈中,yadda yadda,甚至没有将该语句限定为“...在 Linux 和 Windows 上”? 幸福地假设存在 目标机器上的SIGSEGV 之类的东西,并且非法访问不会使整个操作系统崩溃?对于这些陈述中的每一个,我都知道一个系统,该假设不成立。
  • @DevSolar,许多事情都可以从这个问题中推断出来。例如,OP 已经提到 SIGSEGV,暗示对某种 Unix 系统很熟悉,后来又提到了两个例子:Linux 和 OS X。至于指针是 32/64 位,这似乎不是必要的假设,但倾向于对于可以运行 Windows 和 OS X 的机器来说就是这种情况。至于实际上有一个堆栈和向下延伸的堆栈,这就是这些平台上的编译器通常组织内存的方式,这也是相当安全的事情认为。但是,是的,如果它符合假设,答案可能会更好。
【解决方案2】:

非法内存访问是未定义的行为。这意味着您的程序可能崩溃,但不能保证,因为确切的行为是未定义

(开发人员之间的一个笑话,尤其是在面对不关心此类事情的同事时,是“调用未定义的行为可能会格式化您的硬盘驱动器,但不能保证这样做”。;-))

更新:这里正在进行一些热门讨论。是的,系统开发人员应该知道实际上在给定系统上发生了什么。但这些知识与 CPU、操作系统、编译器等相关,而且通常用途有限,因为即使您使代码工作,它仍然质量很差。这就是为什么我将我的答案限制在最重要的一点上,而实际提出的问题(“为什么不崩溃”):

问题中发布的代码没有明确定义的行为,但这只是意味着您不能真正依赖它的功能,而不是它应该崩溃。

【讨论】:

  • 这里没有发生非法内存访问。虽然程序不小心大量访问堆栈内存,但只要有足够的堆栈数据,这并没有什么特别的问题。你的答案是正确的,但它没有解决上面的代码。
  • @wallyk:我不了解 C11,但 C99 标准在任何地方都没有提到“堆栈”。根据您的 实现 处理堆栈的方式,读取 1000 个字节将使您超越 xargvargc 进入虚无(非法),或者 youre trespassing from x` 进入str,并在重叠的内存区域上使用memcpy() 根据定义未定义的行为。无论哪种方式,非法代码。而你,先生,无意冒犯,只是“格式化硬盘驱动器”行中的类型编码器:这种类型的代码不会立即发现错误,因为它可能工作。
  • 我很确定 C11 和 C99 标准针对的是语言特性,而不是实现细节。但是,堆栈是经过验证的并且是真实的。我很久以前使用的大型机没有它们,但是结构语言为了方便而实现了堆栈。 memcpy() 没有内存重叠:分配了 1000 字节的目标地址,而它的源地址在别处。 (确实,我写过磁盘格式化代码,但它总是可以预见和有意的。)
  • @wallyk:同样,这段代码可能在机器 A 和机器 B 上工作,A 和 B 一起可能占全球所有系统的 99%,但 OP 发布的代码确实调用了 undefined 行为,defined 为“语言标准未定义的行为”。因为它没有被定义,谨慎的开发人员永远不应该依赖系统 X 上实际发生的事情,因为它会在某一天崩溃。我尊重您的实践经验,但我强烈认为太多有抱负的编码人员被展示了太多“幕后”的东西,而应用的“不要碰这个”警告太少了。
【解决方案3】:

如果您取消引用无效指针,您将调用未定义的行为。这意味着,程序可以崩溃,它可以运行,它可以煮一些咖啡,等等。

【讨论】:

    【解决方案4】:

    当你有

    int main(int argc, char **argv) {
        void *x = 0; // you could also say void *x = (void *)10;
        printx(&x);
    }
    

    您将x 声明为具有值0 的指针,并且该指针位于堆栈中,因为它是一个局部变量。现在,您将x地址 传递给printx,这意味着使用

    memcpy(str, rec, 1000);
    

    您正在将数据从堆栈上方(或者实际上从堆栈本身)复制到堆栈(因为堆栈指针地址在每次推送时都会减少)。源数据很可能被相同的页表条目覆盖,因为您只复制了 1000 个字节,因此您不会遇到分段错误。然而,最终,正如已经写过的,我们谈论的是未定义的行为。

    【讨论】:

      【解决方案5】:

      如果您写入到未访问区域,它很可能会崩溃。但是你在阅读,没关系。但行为仍是未定义的。

      【讨论】:

      • @pavan.mankala:这不是正在发生的事情。请看我的回答。
      • int main(void) { return *(int *)rand(); } – 分段错误。
      猜你喜欢
      • 1970-01-01
      • 2018-05-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-11-27
      • 2015-07-04
      相关资源
      最近更新 更多