允许 struct 字段溢出到下一个字段

Ami*_*nen 8 c gcc struct compiler-optimization loop-unrolling

考虑以下简单示例:

struct __attribute__ ((__packed__)) {
 int code[1];
 int place_holder[100];
} s;

void test(int n)
{
 int i;

 for (i = 0; i < n; i++) {
  s.code[i] = 1;
 }
}
Run Code Online (Sandbox Code Playgroud)

for 循环正在写入code大小为 1 的字段 。之后的下一个字段codeplace_holder
我希望在 的情况下n > 1,写入code数组会溢出并1写入place_holder.

但是,在使用-O2(在 gcc 4.9.4 上但也可能在其他版本上)进行编译时,会发生一些有趣的事情。
编译器识别出代码可能溢出数组code,并将循环展开限制为 1 次迭代

很容易看出,在编译-fdump-tree-all和查看最后一个树传递(“t.optimized”)时:


;; Function test (test, funcdef_no=0, decl_uid=1366, symbol_order=1)

Removing basic block 5
test (int n)
{
  <bb 2>:
  # DEBUG i => 0
  # DEBUG i => 0
  if (n_4(D) > 0)
    goto <bb 3>;
  else
    goto <bb 4>;

  <bb 3>:
  s.code[0] = 1;
  # DEBUG i => 1
  # DEBUG i => 1

  <bb 4>:
  return;

}
Run Code Online (Sandbox Code Playgroud)

因此,在这种情况下,编译器将循环完全展开为一次迭代。

我的问题是:

  1. 从 C 规范的角度来看,从一个结构成员到下一个结构成员(故意)溢出是非法或未定义的行为吗?
    让我们假设我知道内存中的结构布局,并且知道我在故意溢出code数组时在做什么。
  2. 在这种情况下,有没有办法防止 gcc 展开循环?我知道我可以完全防止循环展开,但是我仍然对其他情况下的循环展开感兴趣。我还怀疑编译器正在进行的分析可能会影响循环展开以外的传递。
    gcc 假设我在访问我的数组时不会溢出,所以我真正想要的是告诉编译器不要接受这个假设(通过提供一些编译器选项)。

我知道编写从一个字段溢出到另一个字段的代码是一种不好的做法,而且我不打算编写这样的代码。
我也知道将数组(可能为零大小)作为最后一个结构字段以允许它溢出的做法,编译器很好地支持这一点,而在这种情况下,数组code不是最后一个字段。
所以这不是“如何修复代码”的问题,而是理解编译器假设并影响它们的问题。

当我观察已经以这种方式编写的现有代码并对其进行调试以找出为什么它的行为不像原始开发人员所期望的那样时,这些问题就出现了。
风险在于代码中还有其他地方存在此类问题。静态分析工具可以帮助找出答案,但我也想知道是否有办法让编译器容忍此类代码并仍然生成我们期望的结果。

更新

我得到了上面问题(1)的明确答案,但问题(2)没有。

  • 通过一些编译选项,gcc 可以允许它作为扩展吗?
  • 有没有办法至少在 gcc 识别出警告时得到警告?(并且它通过优化事物清楚地识别它)。
    这对于在大型现有代码库中识别此类情况很重要。

Lun*_*din 5

从 C 规范的角度来看,从一个结构成员到下一个结构成员(故意)溢出是非法或未定义的行为吗?

这是未定义的行为。该arr[i]运营商是语法糖左右*(arr + i)。因此数组访问归结+为指针算术的二元运算符,C17 6.5.6 加法运算符,来自 §7 和 §8:

对于这些运算符而言,指向不是数组元素的对象的指针的行为与指向长度为 1 的数组的第一个元素的指针的行为相同,该数组的元素类型为对象的类型。

当具有整数类型的表达式与指针相加或相减时,结果具有指针操作数的类型。/--/
如果指针操作数和结果都指向同一个数组对象的元素,或者超过数组对象最后一个元素,则求值不会产生溢出;否则,行为未定义。如果结果指向数组对象的最后一个元素,则不应将其用作被评估的一元 * 运算符的操作数。

正如您所注意到的,优化编译器可能会利用这些规则来生成更快的代码。


在这种情况下,有没有办法防止 gcc 展开循环?

有一个特殊的例外规则可以使用,C17 6.3.2.3/7:

当指向对象的指针转换为指向字符类型的指针时,结果指向对象的最低寻址字节。结果的连续增量,直到对象的大小,产生指向对象剩余字节的指针。

此外,严格别名不适用于字符类型,因为 C17 6.5 §7 中的另一条特殊规则

对象的存储值只能由具有以下类型之一的左值表达式访问: ...字符类型。

这两个特殊规则和谐共存。所以假设我们在指针转换期间没有搞乱对齐等,这意味着我们可以这样做:

unsigned char* i;
for(i = (unsigned char*)&mystruct; i < (unsigned char*)(&mystruct + 1); i++)
{
  do_something(*i);
}
Run Code Online (Sandbox Code Playgroud)

然而,这可能会读取填充字节等,因此它是“实现定义的”。但理论上您可以按字节访问结构体字节,并且只要结构体偏移量是按字节计算的,您就可以以这种方式遍历结构体(或任何其他对象)的多个成员。


据我所知,这个看起来很可疑的代码应该是明确定义的:

#include <stdint.h>
#include <string.h>
#include <stdio.h>

struct __attribute__ ((__packed__)) {
 int code[1];
 int place_holder[100];
} s;

void test(int val, int n)
{
  for (unsigned char* i = (unsigned char*)&s; 
       i < (unsigned char*)&s + n*sizeof(int); 
       i += _Alignof(int)) 
  {
    if((uintptr_t)i % _Alignof(int) == 0) // not really necessary, just defensive prog.
    {
      memcpy(i, &val, sizeof(int));
      printf("Writing %d to address %p\n", val, (void*)i);
    }
  }
}

int main (void)
{
  test(42, 3);
  printf("%d %d %d\n", s.code[0], s.place_holder[0], s.place_holder[1]);
}
Run Code Online (Sandbox Code Playgroud)

这在 gcc 和 clang (x86) 上运行良好。它的效率如何,那是另一回事了。不过,请不要写这样的代码。


Kam*_*Cuk 1

从 C 规范的角度来看,从一个结构成员(故意)溢出到下一个是非法或未定义的行为吗?

越界访问数组是未定义的行为。来自C11 J.2

在以下情况下,该行为是未定义的:

[...]

数组下标超出范围 [...]

在这种情况下有没有办法阻止 gcc 展开循环?

code指针volatile的别名。但即使使用中间指针似乎也有效。神螺栓链接


归档时间:

查看次数:

538 次

最近记录:

5 年,5 月 前