【问题标题】:Restricting field access between two objects of the same type in gcc限制gcc中相同类型的两个对象之间的字段访问
【发布时间】:2015-05-22 18:07:34
【问题描述】:

假设我们有以下代码:

typedef struct {
  int f1;
  int f2;
} t_str;

int f(t_str* p, t_str* q)
{
  p[0].f1++;
  q[0].f2++;
  p[0].f1++;
  q[0].f2++;
  p[0].f1++;
  q[0].f2++;
  p[0].f1++;
  q[0].f2++;
  p[0].f1++;
  q[0].f2++;
  p[0].f1++;
  q[0].f2++;
  p[0].f1++;
  q[0].f2++;
  p[0].f1++;
  q[0].f2++;
  p[0].f1++;
  q[0].f2++;

return 0;
}

当我们用 -O3 选项编译它(我使用 gcc-5.1.0)时,编译器得到以下汇编程序:

f:
.LFB0:
    .cfi_startproc
    movl    8(%esp), %edx
    movl    4(%esp), %ecx
    movl    4(%edx), %eax
    addl    $9, (%ecx)
    addl    $9, %eax
    movl    %eax, 4(%edx)
    xorl    %eax, %eax
    ret
    .cfi_endproc

这意味着 gcc 决定访问 p 的字段 f1 和访问 q 的字段 f2 从不别名。我猜这来自假设相同类型的两个对象永远不会重叠或者它们是相同的。但我在标准中没有发现问题。

请问,任何人都可以在标准中找到这个问题,或者为什么 gcc 限制字段访问,或者评论发生了什么?

更新:

嗯,我也想到了第 6.5 节的第 7 段,但对我来说,对所有对象都有这样的明确形式会更舒服:

6.5.16.1 简单赋值

3 如果存储在一个对象中的值是从另一个对象中读取的 以任何方式与第一个对象的存储重叠,然后 重叠应准确且两个对象应具有合格或 兼容类型的不合格版本;否则,行为是 未定义。

很遗憾,这里不能使用这条规则。

现在看看,如果上面的代码我做了以下函数:

void main()
{
    char * c = malloc(12);
    memset(c, 0, 12);
    f((t_str *)(c + 4), (t_str *)c);
    printf("%d %d %d\n", ((t_str *)c)->f1, ((t_str *)c)->f2, ((t_str *)(c + 4))->f2);
}

现在我在执行过程中得到以下信息:

$ gcc-5.1.0 test1.c -O3 && ./a.out
0 9 0
$ gcc-5.1.0 test1.c -O0 && ./a.out
0 18 0

那么您认为这段代码的有效性如何?因为我不确定它是否符合第 6.5 节第 7 段。

PS:有趣的事情:

$ gcc-5.1.0 test1.c -O3 -fwhole-program && ./a.out
0 10 0
$ gcc-5.1.0 test1.c -O3 -flto && ./a.out
0 10 0

【问题讨论】:

  • 我不确定我是否理解您的问题。 “别名”和“重叠”是什么意思?编译器所做的似乎是正确的,只要 p 和 q 在访问之间不改变(例如,通过处理中断等)。我认为您可以使用关键字“volatile”来强制编译器假设它们可能已经更改。
  • 为了更有趣,看看如果您声明 int*pf1=&p->f1;int*pf2=&p->f2; 等然后现在将 p->f1++ 替换为 (*pf1)++ 会发生什么。
  • alexanius 想知道标准是否明确排除了q 指向p->f2 的可能性。由于t_str 结构只有2 个int 成员,因此可以将2 个此类结构的数组视为在地址&p->f2 处包含第三个此类结构。我假装它被排除在外,但是为typedef struct { int f[2]; } t_str;做类似的案例可能会更困难
  • @Marc Glisse:看看gcc 在这种情况下优化了什么会很有趣,但我认为生成相同的代码应该仍然是正确的,或者甚至在没有负载的情况下更紧凑的代码/通过%eax存储
  • clang 在 32 位和 64 位 x86 代码中生成的完全相同的代码(ABI 差异除外)

标签: c gcc optimization alias


【解决方案1】:

C11 最新草案 (N1570) 第 6.5 节第 7 段内容如下:

对象的存储值只能由具有以下类型之一的左值表达式访问:88) — 与对象的有效类型兼容的类型, — 与对象的有效类型兼容的类型的限定版本, — 与对象的有效类型相对应的有符号或无符号类型, — 对应于对象有效类型的限定版本的有符号或无符号类型, — 在其成员中包含上述类型之一的聚合或联合类型(递归地,包括子聚合或包含联合的成员),或 — 一种字符类型。

我将此解释为意味着pq 指向的对象不能重叠,除非它们是同一个对象,因为t_str 对象应该被正确的指针访问。

标准不够精确,无法明确指出&p->f2 不是指向t_str 对象的有效指针,该对象由p[0]p[1] 之间共享的2 个int 组成。然而这似乎不正确,因为编译器可能会在f1f2 之间或实际上在f2 和结构末尾之间插入填充。

顺便说一句,&p->f2 - &p->f1 不是一个有效的表达式,因为第 6.5.6 节的第 9 节加法运算符声明了这个约束:当两个指针相减时,两个指针都应该指向同一个数组对象的元素,或者一个过去数组对象的最后一个元素;

如果函数f() 将指向char 的指针作为参数并通过该指针访问数据,则gcc 不能假定此数据与p 指向的结构的int 成员不同和q。这个有点违反直觉的例外是为什么这么多 C 库函数原型在许多指针参数上都有 restrict 限定符的原因。 (函数原型中的这些限定符只是给程序员的一个提示,并没有真正告诉编译器任何事情。

【讨论】:

    【解决方案2】:

    正如 chqrlie 所提到的,gcc 不太可能让您创建&p[0].f1 == &q[0].f2 的情况。话虽如此,它不应该有所作为。这些操作在数学上是通勤的。唯一会在代码中崩溃的方法是如果存在整数溢出,那么顺序再次无关紧要。我对此进行了测试,对于我的编译器(gcc 3.4.6),只要我打开任何优化,就会出现这种优化。

    作为对比,考虑一下当我们将结构更改为

    时会发生什么
    typedef struct {
        double f1;
        double f2;
    } t_str;
    

    在这种情况下,即使使用 -O3,我们也看不到增量组合在一起,因为由于下溢,我们可能会得到不同的答案。

    【讨论】:

    • 虽然我同意 C 源代码中的操作通勤,但 gcc 生成的 asm 通过在递增 (%ecx) 之前加载 %eax 并在其后存储 %eax+9 来打破这一点。带有addl $9, (%edx) 的更常规的汇编不会假设*p*q 不重叠。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-02-22
    • 2012-12-25
    • 1970-01-01
    • 2021-01-02
    • 2012-09-10
    • 2011-01-30
    • 2020-01-14
    相关资源
    最近更新 更多