8

这似乎是一个相当常见的模式,例如在 hexchat 中(可能无法编译,另请参阅插件文档。另请注意,它hexchat_plugin_get_info并没有永远使用,所以为了简单起见,我将其省略):

static hexchat_plugin *ph;
static int timer_cb(void *userdata) {
    if (hexchat_set_context(ph, userdata)) { /* <-- is this line UB? */
        /* omitted */
    }
    return 0;
}
static int do_ub(char *word[], char *word_eol[], void *userdata) {
    void *context = hexchat_get_context(ph);
    hexchat_hook_timer(ph, 1000, timer_cb, context);
    hexchat_command(ph, "close"); /* free the context - in practice this would be done by another plugin or by the user, not like this, but for the purposes of this example this simulates the user closing the context. */
    return HEXCHAT_EAT_ALL;
}
int hexchat_plugin_init(hexchat_plugin *plugin_handle, char **plugin_name, char **plugin_desc, char **plugin_version, char *arg) {
    *plugin_name = "do_ub";
    *plugin_desc = "does ub when you /do_ub";
    *plugin_version = "1.0.0";
    ph = plugin_handle;
    /* etc */
    hexchat_hook_command(ph, "do_ub", 0, do_ub, "does UB", NULL);
    return 1;
}

in 中的行timer_cb导致 hexchat 将(可能已释放 - 在此示例中肯定已释放,请参阅 中的注释do_ub)指针与另一个指针进行比较,如果您从此处 (plugin.c#L1089, hexchat_set_context) 开始,您将结束在这里 (hexchat.c#L191, is_session)。要调用此代码,/do_ub请在 hexchat 中运行。

相关代码:

int
hexchat_set_context (hexchat_plugin *ph, hexchat_context *context)
{
    if (is_session (context))
    {
        ph->context = context;
        return 1;
    }
    return 0;
}

int
is_session (session * sess)
{
    return g_slist_find (sess_list, sess) ? 1 : 0;
}

这种东西是UB吗?

4

3 回答 3

7

如C11 标准草案 6.2.4p2(对象的存储持续时间)(重点是我的)中所述,在它指向的对象已达到其生命周期结束之后使用指针的值是不确定的:

对象的生命周期是程序执行期间保证为其保留存储的部分。一个对象存在,有一个不变的地址,并在其整个生命周期中保留其最后存储的值。如果对象在其生命周期之外被引用,则行为未定义。当指针指向(或刚刚过去)的对象到达其生命周期的末尾时,指针的值变得不确定。

并且使用它的值(只是为了任何东西)是一个明确的未定义行为,如附件 J.2(未定义行为)中所述:

在以下情况下行为未定义: [...] 使用指向其生命周期已结束的对象的指针的值 (6.2.4)。

于 2018-10-03T14:18:55.467 回答
3

是的,严格来说,使用已经为任何东西释放的指针值——即使是看似无害的比较——是未定义的行为。在实践中不太可能引起任何实际问题,但我认为这是值得避免的。

另见C FAQ列表,问题 7.21

于 2018-10-03T14:37:40.623 回答
1

tl; dr:执行某些操作(例如比较指针而不考虑由此识别的对象的生命周期)的能力是一种流行的扩展,绝大多数编译器可以配置为支持禁用优化。然而,标准并未强制要求对它的支持,并且积极的优化器可能会破坏依赖它的代码。

在编写标准时,有一些分段内存平台尝试将指针加载到寄存器中会导致系统检索有关指针所在内存区域的信息。如果此类信息不再可用,则尝试检索它可能会产生超出标准管辖范围的任意后果。标准要求涉及此类指针的比较除了产生 0 或 1 之外没有副作用,这会使该语言在此类平台上不切实际。

虽然该标准的作者毫无疑问地意识到能够使用与任意指针的比较(但需要注意的是结果可能不是特别有意义)是每个针对传统硬件的实现都支持的有用功能,但他们认为没有必要将其视为比“流行扩展”更重要的东西,只要这样做是有用和实用的,质量实现就支持它。

从 C89 基本原理,第 11 页第 23 行:

术语未指定的行为、未定义的行为和实现定义的行为用于对编写标准没有或不能完全描述其属性的程序的结果进行分类。采用这种分类的目的是允许实现之间的某种多样性,从而允许实现质量成为市场上的积极力量,并允许某些流行的扩展,而不会消除符合标准的声望。标准的信息性附录 J 对属于这三类之一的行为进行了分类。

不幸的是,尽管当今使用的几乎所有平台都可以以基本上零成本 (*) 支持这种语义,但一些编译器编写者认为他们希望假设代码永远不会使用释放的指针做任何事情,这比程序员可以从中获得的任何值更重要在传统平台上基本上是普遍支持的扩展。除非可以保证任何使用自己的代码的人都会禁用那些试图摆脱有用扩展的语言的过度渴望优化器的作者强加的虚假“优化”,否则可能不得不编写额外的代码来解决缺少此类扩展的问题.

(*) 在某些情况下,函数向外部代码公开多个指向它已分配和释放的存储区域的指针,编译器必须坚持行为保证,即这些指针将比较相等,因此需要实际执行存储会泄漏指针的操作;将指针视为不确定将允许消除存储。然而,在人为的场景之外,通过使用泄露到外部世界的指针来消除此类存储所节省的成本很少会对性能产生任何有意义的影响。

于 2018-10-03T17:00:29.493 回答