【问题标题】:Is it undefined behavior to read and compare padding bytes of a POD type?读取和比较 POD 类型的填充字节是未定义的行为吗?
【发布时间】:2018-05-06 08:21:28
【问题描述】:

今天遇到一些代码,大致如下sn-p。 valgrindUndefinedBehaviorSanitizer 都检测到未初始化数据的读取。

template <typename T>
void foo(const T& x)
{
    static_assert(std::is_pod_v<T> && sizeof(T) > 1);
    auto p = reinterpret_cast<const char*>(&x);

    std::size_t i = 1; 
    for(; i < sizeof(T); ++i)
    {
        if(p[i] != p[0]) { break; }
    }

    // ...
}

上述工具抱怨 p[i] != p[0] 比较时, 包含填充字节的对象已传递给foo。示例:

struct obj { char c; int* i; };
foo(obj{'b', nullptr});

从 POD 类型中读取填充字节并将它们与其他内容进行比较是未定义的行为吗?我在 Standard 和 StackOverflow 上都找不到明确的答案。

【问题讨论】:

    标签: c++ language-lawyer padding undefined-behavior standard-layout


    【解决方案1】:

    您的程序的行为是定义的实现在两个方面:


    1) 在 C++14 之前:由于您的 char 可能是 1 的补码或有符号幅度 signed 类型,您可能返回一个令人惊讶的结果,因为比较 + 0 和 -0。

    真正无懈可击的方法是使用const unsigned char* 指针。这消除了对现已废除的(来自 C++14)1 的补码或有符号幅度char 的任何担忧。


    由于 (i) 您拥有内存,(ii) 您正在获取指向 x 的指针,并且 (iii) unsigned char 不能包含陷阱表示,(iv) charunsigned char,并且signed char 被排除在严格的别名规则之外,使用const unsigned char* 读取未初始化内存的行为得到了很好的定义。


    2) 但是由于您不知道该未初始化内存中包含什么,因此读取它时的行为是未指定的,这意味着程序行为是实现定义的,因为 char 类型不能包含陷阱表示。

    【讨论】:

    • 不是UB吗?填充字节有定义值吗?
    • 不,UB 在通过unsigned char* 阅读时没有“机制”来表现自己。
    • 如果unsigned char *p 指向一个填充字节,if (*p==42) { } 会发生什么?分支是否会被占用?
    • 嗯,我以为从未初始化的内存读取是UB。也许我错了,我从来没有真正检查过这方面的标准。
    • ...,以及 (iv) char 类型从严格的别名规则中的明确豁免 已经使用 uint8_t 而不是 char 可以杀死你的好 -定义。
    【解决方案2】:

    这取决于条件。

    如果x 初始化为零,则填充位为零,因此这种情况定义明确(C++14 的 8.5/6):

    对 T 类型的对象或引用进行零初始化意味着:

    —如果 T 是标量类型 (3.9),则将对象初始化为该值 通过转换整数文字获得

    0(零)到 T;105

    ——如果 T 是(可能是 cv 限定的)非联合类类型,则每个 非静态数据成员和每个基类

    子对象初始化为零,填充初始化为零位

    ——如果 T 是一个(可能是 cv 限定的)联合类型,则对象的第一个 非静态命名数据成员为零-

    初始化和填充被初始化为零位;

    ——如果 T 是一个数组类型,每个元素都是零初始化的; — 如果 T 是 引用类型,不进行初始化。

    但是,如果 x 是默认初始化的,则未指定填充,因此它具有不确定的值(由此处未提及填充的事实推断)(8.5/7):

    默认初始化 T 类型的对象意味着:

    ——如果 T 是一个(可能是 cv 限定的)类类型(第 9 条),则默认 调用 T 的构造函数(12.1)(初始化是 如果 T 没有默认构造函数或重载决议,则格式错误 (13.3) 导致歧义或功能被删除或 无法从初始化的上下文中访问);

    ——如果 T 是一个数组类型,每个元素都是默认初始化的;

    ——否则,不执行初始化。

    比较不确定的值是在这种情况下是 UB,因为没有提到的例外适用,因为您将不确定的值与某物进行比较 (8.5/12):

    如果没有为对象指定初始化器,则该对象是 默认初始化。当使用自动或 获取动态存储时长,对象有一个不确定的 值,并且如果没有对对象执行初始化,则 对象保留一个不确定的值,直到该值被替换 (5.17)。 [注意:具有静态或线程存储持续时间的对象是 零初始化,见 3.6.2。 — 尾注] 如果一个不确定的值是 由评估产生的行为是未定义的,除非在 以下情况:

    - 如果不确定的无符号窄字符类型值 (3.9.1) 由以下评估产生:

    ……——条件表达式(5.16)的第二个或第三个操作数,

    ……——逗号表达式的右操作数(5.18),

    ......- 强制转换或转换为无符号窄字符类型(4.7、5.2.3、5.2.9、5.4)的操作数,

    ……——一个弃值表达式(第 5 条),然后是 operation 是一个不确定的值。

    —如果无符号窄字符类型的不确定值是 通过对简单赋值的右操作数求值产生 运算符(5.17),其第一个操作数是无符号窄的左值 字符类型,一个不确定的值替换 左操作数引用的对象。

    ——如果一个不确定的值 无符号窄字符类型是由评估产生的 初始化无符号对象时的初始化表达式 窄字符类型,该对象被初始化为不确定的 价值。

    【讨论】:

      【解决方案3】:

      Bathsheba 的回答正确地描述了 C++ 标准的字母。

      坏消息是,我测试过的所有现代编译器(GCC、Clang、MSVC 和 ICC)都忽略了这一点上的标准字母。相反,他们将Annex J.2 中的秃头语句视为 C 标准

      [行为未定义如果]具有自动存储持续时间的对象的值在不确定时被使用

      好像它是 100% 规范的,在 C 和 C++ 中,即使附件 J 不是规范的。这适用于所有对未初始化存储的可能读取访问,包括那些通过unsigned char * 仔细执行的读取访问,是的,包括对填充字节的读取访问。

      此外,如果您要提交错误报告,我相信您会被告知,如果标准的规范性文本与他们正在做的事情不一致,那就是 标准 那是有缺陷的。

      好消息是,如果您检查填充字节的内容,您只会在访问填充字节时招致 UB。复制它们是可以的。特别是,如果您初始化一个 POD 结构的所有命名字段,通过结构赋值和memcpy 复制它是安全的,但是将它与另一个比较是安全的这样的结构使用memcmp

      【讨论】:

      • 您能否链接到一些证据表明编译器将读取未初始化的填充视为unsigned char 视为UB? valgrind 报告不一定表示 UB,只是未指定的行为。
      • @MM: 考虑uint16_t a,b; unsigned char *ap=&amp;a, *bp=&amp;b,t0,t1; ...; t0=bp[0]^1; t1=bp[1]^1; ap[0]=t0^1; ap[1]=t1^1;clang 和gcc 都会将基于unsigned char 的操作序列视为等同于a=b;,即使后者未定义,并且可能生成代码这使得 a 的值超出 0-65535 范围。
      • @supercat 这个问题是关于结构中的填充字节,所以不存在诸如它们持有什么值之类的问题。你的例子有本质的不同
      • @MM:标准暗示使用unsigned char* 从任何对象读取数据应该始终产生值 0..CHAR_MAX,即使对象的值未初始化,但 gcc而clang不支持这一点。如果编写了自动存储中的结构,但没有对填充字节做出特别努力,我不知道编译器需要比结构中其他任何东西的行为更彻底地定义填充字节的行为。跨度>
      • @supercat 标准没有这样说。读取不确定值的对象会产生一个不确定的值,即 UB 除了少数情况外,所有这些情况都涉及丢弃该值或将其分配给另一个变量,从而也变得不确定。 (这些都在 C++14 dcl.init/12 中)
      猜你喜欢
      • 2018-04-03
      • 1970-01-01
      • 2014-08-21
      • 2019-04-20
      • 1970-01-01
      • 1970-01-01
      • 2011-09-25
      • 2013-01-22
      • 2017-01-28
      相关资源
      最近更新 更多