【问题标题】:Is it UB to access 'padded' bytes?是 UB 访问“填充”字节吗?
【发布时间】:2013-01-22 04:51:24
【问题描述】:

如果我有这样的对象:

struct {
    uint32_t n;
    uint8_t c;
} blob {};

那么将有 3 个“填充”字节。

访问填充字节是 UB 吗?例如:

uint8_t * data = reinterpret_cast<uint8_t*>(&blob);
std::cout << data[4] << data[5] << data[6] << data[7];

我首先假设这可能是 UB,但如果这是真的,那么 memcpy 也将是 UB:

memcpy(buf, &blob, sizeof(blob));

我的具体问题是:

  • 访问填充字节是 UB 吗?
  • 如果不是,那是否意味着也定义了值?

【问题讨论】:

  • 由于这是一个非常规矩的问题,我不确定最终的答案是什么。我怀疑它会被定义(只要 uint8_t* 是 unsigned char*),但初始值将保持未指定,就像任何其他未初始化的成员变量一样。现在,如果你想要规范引用,我把它留给有更多时间查找的人:)
  • 我假设 uint32_tsizeof(uint32_t) 对齐。
  • @PeterG: 标准要求sizeof(blob) 至少为5。由于uint8_t 存在,因此CHAR_BIT == 8sizeof(uint32_t) == 4 存在。然后,sizeof(blob) 是 5 还是 8 是对齐问题。允许故意将其设置为 6 或 7。
  • @PeterG。如果sizeof(blob) 包含uint32_tuint8_t,它怎么可能是4 个字节?我可以看到 5 个字节,我可以看到 8 个字节。但是4?怎么样?
  • @SteveJessop 我认为他的意思是在4 + 1 = 4 的系统上,您不能指望任何东西都可以很好地定义。

标签: c++ alignment language-lawyer


【解决方案1】:

不,当整个对象被零初始化(标准在 §8.5/5 中说,当对象被零初始化时,填充被初始化为 0 位)或值初始化时,访问填充不是 UB不是具有用户定义构造函数的类。

【讨论】:

  • 所以如果对象不是零初始化但分配了成员,那么访问填充字节是UB吗? (memcpy 也会如此?)
  • @StackedCrooked 如果未初始化但已分配成员,则可以复制成员但不能复制整个对象。
  • @StackedCrooked:如果它没有被初始化,同样的事情也会发生在任何其他未初始化的内存块上:如果你的程序依赖于你有 UB 的未初始化内存的值,如果该内存的值与您的程序无关,您没有UB(即memcpy 完全可以,只要您的程序不依赖于填充中字段的值)。未初始化并不意味着未定义的行为,int main() {int a; int b=a; return 0; }中没有未定义的行为
  • @DavidRodríguez-dribeas 实际上我很确定这个例子是未定义的,不是吗?
  • @DavidRodríguez-dribeas:在 C++ 中,您的示例 int b=a 是 UB。 4.1/1,“如果对象未初始化,则需要 [左值到右值] 转换的程序具有未定义的行为。”
【解决方案2】:

我认为,在适当的情况下,您最终可能会为此选择 UB。我在想的是你有ECC或奇偶校验检查的内存,其中ecc /奇偶校验位是通过写入内存来设置的。如果在 [never was write to AT ALL] 之前没有使用过一块内存,并且您在填充字段中读取了未初始化的字节,那么当尚未写入的内存时,它可能会导致 ecc/parity 错误正在阅读。

当然,在这样的系统中,您可以通过在引导期间的某个时间点简单地执行“填充所有内存”来避免一大堆痛苦,因为这样做是不合时宜的:

struct Blob 
{
    uint32_t n;
    uint8_t c;
};

Blob *b = malloc(sizeof(Blob)*10);

for(int i = 0; i < 10; i++)
{
   b[i].n = i;
   b[i].c = i;
 }


 ...

 Blob a[3];

 memcpy(a, &b[1], sizeof(a));    // Copies 3 * Blob objects, including padding. 

现在,由于并非 b[x] 的所有位都已设置,它可能无法复制 memcpy 中的数据,因为奇偶校验/ecc 错误。那会很糟糕。但同时,不能强制编译器“设置”所有的填充区域。

我的结论是,这是UB,但除非有特殊情况,否则不太可能引起问题。当然,您会在很多代码中看到上述类型的memcpy 代码。

【讨论】:

  • 读取任何未初始化的内存都是UB,不仅仅是在奇怪的系统上。
  • 是的,如果您使用 malloc 不会填满分配的内存,那么使用 memcpy 复制您无法写入每个字节的结构会很糟糕。然而,我们经常看到这种代码,你不同意吗?它可以工作,因为几乎总是,内存被操作系统填充,无论是在引导期间,还是稍后在向应用程序提供内存的操作系统阶段。但是我确定是UB覆盖了内存没有初始化的情况。
【解决方案3】:

如果不是未定义的行为,它肯定是实现定义的。虽然 C++ 标准不能保证你的程序做什么,但你的系统的 ABI 规范——SysV,如果你使用的是 Linux——会。我怀疑,如果您在填充位上胡闹,您可能更感兴趣的是您的程序将如何在您的系统上运行,而不是它在任何符合 C++ 的任意系统上的运行方式。

【讨论】:

    【解决方案4】:

    一个 POD 结构将存在于至少 sizeof(struct) 字节(包括任何填充字节)的连续内存块中。访问填充字节(如果它们存在)只有在它没有被初始化时才会是 UB。

    memset(&s, 0, sizeof(s));
    

    这将初始化所有字节,包括填充。之后从填充中读取不会是 UB。

    当然,memset() 是 C 主义,我们永远不会在 C++ 中这样做,对吧?

    【讨论】:

      【解决方案5】:

      在 C 中它不是未定义的行为。唯一一次访问未初始化的东西(例如对象中的填充)会导致未定义的行为,即对象具有自动存储持续时间并且从未占用其地址:

      6.3.2.1.2: 如果 左值指定一个自动存储持续时间的对象,该对象可能已经 使用寄存器存储类声明(从未占用其地址),以及该对象 未初始化(未使用初始化程序声明且未对其赋值 在使用之前执行),行为未定义。

      但在这种情况下,您使用的是地址(带有&amp;),因此行为是明确定义的(不会发生错误),但您可能会得到任何随机值。

      在 C++ 中,所有的赌注都没有,通常情况下。

      【讨论】:

      • 正如 Steve Jessop 提到的,4.1/1 表示如果程序需要对未初始化的对象进行左值到右值的转换,那么它就是 UB。
      • 啊,错过了 C++ 标记——在 C 中它是明确定义的。 C++ 规范在这里被破坏了。
      猜你喜欢
      • 2022-11-15
      • 1970-01-01
      • 1970-01-01
      • 2019-07-18
      • 1970-01-01
      • 1970-01-01
      • 2015-12-01
      • 2018-01-14
      • 1970-01-01
      相关资源
      最近更新 更多