【问题标题】:What exactly is the value contained in an uninitialized local variable in C?C 中未初始化的局部变量中包含的值到底是什么?
【发布时间】:2018-10-02 21:51:13
【问题描述】:

如果我们在 C 语言中有一个函数,其中包含一个简单的单元化 ìnt 变量,我们知道这个变量可能并不总是初始化为零。相反,它可能包含一些“垃圾”值。

我的问题是:究竟能代表什么价值?可能是之前终止的进程留下的一些信息(未释放的内存)?

如果是,那么这不是一个极其严重的安全漏洞吗?因为通过这种方式,任何进程都可以读取与当前进程使用相同地址空间的进程留下的信息(密码、令牌等)。

我的假设是,对于每个新进程,内核会将分配给该新进程的内存归零(至少对于堆栈而言),然后将可执行文件加载到内存中。这些“垃圾”值实际上是由当前进程加载过程生成的值(因此无法访问使用相同进程的其他进程的任何剩余数据地址空间)。

我正在与一些人就这个话题争论,我真的想要一个清晰而全面的答案(我相信有一个)。我们假设内核是基于 debian/centos 的。很高兴知道不同内核/操作系统的行为是否存在差异。

非常感谢。

【问题讨论】:

  • 这样的变量会有一个indeterminate值。
  • 也许,也许不是。这就是不确定值的意义所在,您只是无法分辨它们是什么或它们来自什么。此外,对于其他纯整数类型,该值可能是 trap 表示,即使读取这些也可能导致崩溃。
  • 现代操作系统在后台任务被进程释放后将其归零。所以不,那个 indeterminate 值不是来自另一个进程的值。
  • 请区分OS和C标准。后者说一个未初始化的变量正是如此。然而,操作系统可能会做其他事情,但 C 代码不能试图对此进行猜测。如果 C 标准说一个变量是未初始化的:那么你必须这样对待它。

标签: c linux security process stack


【解决方案1】:

这应该分为两个问题:

  • C 标准对未初始化对象的值有何规定?
  • 调用main 时内存中的内容是什么?

第一个问题在其他 Stack Overflow 问题和答案中讨论。完整的答案很复杂,涉及对各种情况的讨论,而这个问题似乎并没有特别问这个问题,所以我将把它留给其他 Stack Overflow 问题。对于这个问题,只要说使用未初始化对象的值容易出现未定义的行为就足够了。此外,这不仅仅是因为对象的内存可能有麻烦的值,而是因为 C 标准允许 C 实现将读取未初始化值的程序以各种方式视为行为不端的程序,然后优化会进一步破坏程序.

就内存中的内容而言(假设我们有一种受支持的方式来检查它,可能使用汇编语言而不是 C),那么每个提供任何类型安全性的多用户系统都会擦除(或以其他方式初始化)内存在使其可用于进程之前。正如问题所考虑的那样,在调用main 时内存中的任何值都是加载过程或操作系统初始化的结果。 (请注意,加载过程的结果包括常量数据和程序文本的加载——因此我们希望在那里找到定义的值——以及加载代码完成的工作留下的任何数据——它的变量等等。 )

这个问题需要一个明确的答案,所以让我明确一点:为用户进程提供安全性的操作系统必须从内存中擦除以前进程的数据,然后才能将该内存提供给另一个进程过程。安全性不能通过信任程序不检查其提供的内存并对其进行任何操作来提供。

不适合不受信任的用户共享的基本系统当然可以在创建新进程并为其分配内存时跳过内存初始化。

【讨论】:

  • 这几乎就是我所期待的答案。谢谢!但是,您能否准确地说出这种行为(擦除先前进程的数据)是否可以被非常常用的 linux 发行版使用?喜欢 Ubuntu / Linux Mint / Fedora / CentOS?
  • @CosminIoniță:我没有针对特定操作系统版本或发行版的具体引用。让我解释一下,你的问题相当于问汽车工程师汽车是否有门锁。购买汽车时不要问经销商是否有锁,因为众所周知的标准功能是,每辆出售用于公共街道通用的新车都有锁。 (像赛车这样的特殊汽车可能不会。)同样,每个通用多用户系统都会保护内存。我已经编程了 40 年,我在 Digital Equipment Corporation 的各个操作系统组工作了 13 年,...
  • ……在 Apple 担任高级软件工程师 13 年。在将一个进程的数据分配给另一个进程之前从内存中擦除它是非常基础的;毫无疑问,任何通用多用户系统都可以做到这一点。也就是说,如果您真的想要特定操作系统的具体保证,您可以尝试提出诸如“在 Linux 源代码中,它会在将一个进程的数据重新分配给另一个进程之前从内存中的哪个位置擦除它?”之类的问题。我希望答案在虚拟内存子系统的某个地方。
  • 优秀!我很幸运能成为一个由像你这样的人组成的社区的一员!继续努力!
  • @CosminIoniță:我确实发现 this document 描述了 Linux 虚拟内存管理。在第 52 页上,它说为进程分配一个新页面,只需将其指向一个用零填充的只读共享(“全局可见”)页面。当进程第一次尝试写入页面时,会触发硬件页面错误,然后系统会创建一个新的(非共享)页面,用零填充它,并将其标记为进程可写。因此,每当一个进程分配一个新页面时,它都会得到一个零页面。
【解决方案2】:

好吧,局部变量存储在堆栈空间中,因此一旦您完成对当前例程的调用,堆栈指针就会向上移动以释放所有当前例程的局部变量,并且出于效率原因,不会擦除以前的内容(仅堆栈指针被移动)。

如果您进入一个新例程,编译器所做的就是将堆栈指针向下移动(它不会将任何内容压入局部变量空间,只是在该空间上移动以为新的局部变量集腾出空间) 并且在代码中需要局部变量之前不使用该空间。您要问的是如何解释堆栈段先前使用的位模式,这取决于堆栈在进入当前例程之前是如何使用的。这可以是:

  • 用于计算复杂表达式的其余临时数据。
  • 先前调用另一个例程的参数数据。
  • 以前调用的例程的返回地址。
  • 之前调用的例程的局部变量,在结束时不再使用。
  • 其他任何事情。

由于该内存现在以不同的方式使用(如当前例程的本地空间所指示的),因此对此类内存内容没有有效的解释,而是 旧代码中的垃圾数据

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-25
    • 2012-10-03
    • 2022-01-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多