【问题标题】:Question about the behavior of uninitialized pointers to integers when used in the printf function关于在 printf 函数中使用时指向整数的未初始化指针的行为问题
【发布时间】:2020-10-10 08:24:12
【问题描述】:

我是这个论坛的新手,所以如果有任何令人震惊的格式选择,请告诉我,我会及时更新。

C Programming: A Modern Approach(由 K.N. King 撰写)一书中,写了以下段落:

如果指针变量p 尚未初始化,尝试以任何方式使用p 的值会导致未定义行为。在下面的例子中,printf 的调用可能会打印垃圾,导致程序崩溃,或者有一些其他影响

int *p;
printf("%d", *p);

据我了解指针以及编译器如何处理它们,声明 int *p 有效地说,“嘿,如果您将来取消引用 p,我将查看内存中四个连续字节的块,其起始地址是 p 中包含的值,并将这 4 个字节解释为有符号整数。"

至于是否正确……如果正确,那我有点疑惑为什么上面的代码块:

  1. 被归类为未定义行为
  2. 可能导致程序崩溃
  3. 可以有其他效果

对上述案例的评论:

我对未定义行为的理解是,在运行时,任何事情都可能发生。话虽如此,在我看来,在上面的代码中,只有非常明确的 子集 会发生。我知道p(由于缺乏初始化)正在存储一个随机地址,该地址可以指向内存中的任何位置。但是,当 printf 传递了取消引用的值 *p 时,编译器不会只查看内存的 4 个连续字节(从任意随机地址开始)并将这 4 个字节解释为有符号整数吗?

因此,printf 应该只做一件事:打印一个介于 -2,147,483,648 到 2,147,483,647 之间的数字。显然,这是很多不同可能的输出,但这真的可以称为“未定义的行为”。此外,这种“未定义的行为”如何导致“程序崩溃”或“产生其他影响”。

任何澄清将不胜感激!谢谢!

【问题讨论】:

  • "我将查看内存中四个连续字节的块" --> 不。UB 可以通过访问统一指针来发生 - 在调用 printf() 之前。 p 未定义为对随机地址进行排序。
  • “任何事情都可能发生”部分通常是因为允许编译器假设导致 UB 的事情不可能发生。因此,如果您在 if 条件中有 UB,编译器可能会忽略整个分支。
  • 这也可能是非常有用的阅读:stackoverflow.com/questions/2397984/….
  • 虽然“任何事情都可能发生”是对后果的唯一正确描述,但您可能需要考虑如果 p 的未初始化内容是一个不是有效地址的值会发生什么整数,要么是因为内存不存在,要么是因为机器体系结构只允许整数具有某些地址(例如,可被 4 整除),或者因为该部分地址空间存在但不允许您的进程读取它等等等等。
  • 或者p 中的地址恰好是内存映射I/O 控制器的一部分,并且从该地址读取被视为对硬盘驱动器重新格式化自身的命令。 (一些处理器,如 6502,通过内存映射地址执行所有 I/O。简单地从地址读取被认为是硬件命令。即使 x86 对某些硬件也使用锁存寻址,例如 EGA 视频卡。从锁存器读取会弄乱你的视频。)

标签: c pointers printf language-lawyer undefined-behavior


【解决方案1】:

未初始化值的值为不确定。它可以保存任何值(包括 0),甚至有可能在您每次尝试读取它时读取不同的值。该值也可能是陷阱表示,这意味着尝试读取它会触发处理器异常,从而导致程序崩溃。

假设您很幸运并且能够读取p 的值,由于大多数系统使用的虚拟内存模型,该值可能与映射到进程内存空间的地址不对应。因此,如果您尝试通过取消引用指针来读取该地址,则会触发可能导致程序崩溃的分段错误。

请注意,在这两种情况下,崩溃都发生在 printf 甚至被调用之前。

此外,编译器可以假设您的程序没有未定义的行为,并将根据该假设执行优化。这会使你的程序以你意想不到的方式运行。

至于为什么做这些事情是未定义的行为,因为C standard是这么说的。特别是,附录 J2 给出了未定义行为的示例:

具有自动存储时长的对象的值 在不确定时使用。 (6.2.4, 6.7.9, 6.8)

【讨论】:

  • 只是为了澄清(对所有这些东西有点新手),当你说“在这两种情况下,崩溃发生在 printf 甚至被调用之前”,那是因为编译器在将该值传递给printf 之前,将“计算”*p 对应的值。对吗?
  • @S.Cramer 正确。 printf 的第二个参数是表达式 *p,因此在调用函数之前必须对表达式进行完全计算。
【解决方案2】:

未定义的行为被定义为“我们没有指定必须发生什么,这取决于实施者。”

在实际意义上,*p 可能包含该内存区域最后保存的任何内容,可能是零,可能是更随机的东西,可能是以前使用的数据块。有时,为了安全起见,编译器会隐式将内存归零,从而牺牲一点时间来提供该功能。

值得注意的是,如果 p 被定义为 char*,并且您对其进行了 printf 处理,它会尝试打印内容,直到找到 0x00。如果这将您带到内存边界,您可能会遇到分段错误。

【讨论】:

  • 未定义的行为不是这样定义的。未定义行为具有不同于未指定行为和实现定义行为的特定含义。
  • 请指点?
  • 谢谢!我想我可能会删除这个,@dbush 写了一个更详细的答案。另一方面,这些链接是很好的补充信息。这里的最佳做法是什么?
  • @jamesdlin:这个神话是从哪里来的!?根据该标准的作者,“未定义的行为......还确定了可能的符合语言扩展的区域:实现者可以通过提供官方未定义行为的定义来扩充语言。”
猜你喜欢
  • 1970-01-01
  • 2013-07-24
  • 1970-01-01
  • 2017-10-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-08-18
相关资源
最近更新 更多