【问题标题】:Lowest possible memory address on modern OS现代操作系统上可能的最低内存地址
【发布时间】:2012-12-08 22:59:39
【问题描述】:

最近有人指出我的一个 C 程序,如果内存块的起始地址足够低,我的一个测试将由于绕零而失败,从而导致崩溃。

起初我认为“这是一个令人讨厌的潜在错误”,但后来,我想知道:这种情况会发生吗?我从来没有见过。公平地说,这个程序已经在无数系统上运行了数百万次,而且到目前为止还没有发生过。

因此,我的问题是: 对malloc() 的调用可能返回的最低内存地址是多少?据我所知,我从未见过像 0x00000032 这样的地址。

我只对“现代”环境感兴趣,例如 Linux、BSD 和 Windows。此代码不适用于 C64 或任何爱好/研究操作系统。

【问题讨论】:

    标签: c memory malloc memory-address


    【解决方案1】:

    首先,既然这是您所要求的,我只会考虑现代系统。这意味着他们正在使用分页内存并且在 0 处有一个错误页面来处理空指针取消引用。

    现在,我所知道的任何真实系统上的最小页面大小是 4k(4096 字节)。这意味着你永远不会有低于 0x1000 的有效地址;任何较低的内容都将是包含零地址的页面的一部分,因此会排除空指针取消引用错误。

    在现实世界中,好的系统实际上可以防止你走得那么低;现代 Linux 甚至阻止应用程序有意将页面映射到低于可配置默认值(我相信是 64k)。这个想法是,您甚至希望从空指针(例如p[n],其中p 恰好是空指针)到故障(在Linux的情况下,他们希望内核空间中的代码在尝试时出错)访问这些地址以避免内核空指针取消引用错误,这些错误可能导致特权提升漏洞)。

    话虽如此,在指针指向的数组边界之外执行指针算术是未定义的行为。即使地址没有换行,编译器也可能会做各种各样的事情(无论是为了强化你的代码,还是为了优化),其中未定义的行为可能会导致你的程序中断。好的代码应该遵循其编写语言的规则,即不调用未定义的行为,即使您希望 UB 是无害的。

    【讨论】:

    • 谢谢,显然我的代码看起来是安全的。但是,我有兴趣了解应该更好地避免哪种“不良做法”。我知道指针不应该超出数组,否则它是未定义的行为,但是如果我的测试的目的是检测到这一点呢?
    • 如果你的意思是你正在做类似if (ptr+n < begin || ptr+n >= end) ... 的事情,那么你应该做的是(假设n 已签名并且ptr 是数组中某处的有效指针)@ 987654326@。换句话说,以代数方式重新排列您的测试,以确保不会发生溢出(对于整数)或越界指针算术(对于指针)。这是一个非常值得学习的安全编程技能,即使你在这里并不严格“需要”它。
    • 谢谢。这是我应该在代码的许多其他部分中使用的建议,即使它们今天运行良好。
    • 快速说明:不幸的是,测试指针正确性的建议方法似乎不如简单的指针比较快。实施时性能明显下降。
    • 通过确保您的代码从不开始越界索引来避免进行比较可能会花费更少的时间......除此之外,我认为您应该采取查看生成的 asm 并找出它变慢的原因。它实际上可能表明存在错误。有很多方法可以使这种比较与错误的签名类型等混淆......
    【解决方案2】:

    您可能的意思是您正在计算 &a - 1 或类似的东西。

    请不要这样做,即使指针比较目前在大多数架构上都实现为无符号比较,并且您知道(uintptr_t)&a 大于当前系统上的某些任意界限。编译器利用未定义的行为进行优化。他们现在就这样做,如果他们现在不利用它,他们将来会这样做,而不管您对指令集或平台可能期望的“保证”如何。

    请参阅this well-told anecdote 了解更多信息。

    在一个完全不同的寄存器中,您可能会认为 C 中未定义有符号溢出,因为它曾经是有不同的硬件选择,例如 1 的补码和符号幅度。因此,如果您知道平台是 2 的补码,则 (x+1) > x 之类的表达式将检测到 MAX_INT

    这可能是历史原因,但推理不再成立。现代编译器将表达式(x+1) > x(带有x 类型int)优化为1,因为有符号溢出是未定义的。编译器作者并不关心未定义的最初原因是可用架构的多样性。你用指针做的任何未定义的事情都是他们列表中的下一个。 如果你调用未定义的行为,你的程序明天就会崩溃,不是因为架构发生了变化,而是因为编译器在优化方面越来越激进。

    【讨论】:

    • 感谢您的详细解释。我正在比较2个指针。其中一个是“受信任的”(由我的函数创建和修改),另一个不受信任,因为它是由“外部来源”提供的一个参数的操作的结果。两个指针都应该指向同一个“有效”内存块,除非“外部参数”故意为假(测试试图检测的攻击场景)。
    • @Cyan 出于修辞原因,我在回答中描绘了一幅黯淡的画面,但据我所知,比较 (uintptr_t)trusted(uintptr_t)outside 应该是安全的。其中的转换是实现定义的,这限制了编译器的优化能力。
    【解决方案3】:

    heap 上执行动态分配。 Heap 驻留在进程 address space 中,就在 text(程序代码)、initialized datauninitialized data 部分之后,请参见此处:http://www.cprogramming.com/tutorial/virtual_memory_and_heaps.html。所以堆中可能的最小地址取决于这 3 个段的大小,因此没有绝对的答案,因为它取决于特定的程序。

    【讨论】:

    • 感谢您提供非常有趣的链接。
    猜你喜欢
    • 2012-10-29
    • 1970-01-01
    • 2012-03-16
    • 2015-03-10
    • 2010-09-12
    • 1970-01-01
    • 2019-09-20
    • 2018-05-28
    • 2018-10-29
    相关资源
    最近更新 更多