gcc优化跳过初始化分配的内存

时间:2015-05-09 08:01:03

标签: c gcc strict-aliasing

使用gcc 4.9.2 20150304 64位我碰到了这个看似奇怪的行为:

double doit() {
    double *ptr = (double *)malloc(sizeof(double));
    ptr[0] = 3.14;
    return (double)((uintptr_t) ptr);
}

在代码中,我在堆上分配double,初始化它,然后返回另一个double初始化的第一个转换为intptr_t的地址。通过优化-O2,这将在32位模式下生成以下汇编代码:

sub    $0x28,%esp
push   $0x8                   ;; 8 bytes requested
call   8048300 <malloc@plt>   ;; malloc 'em
movl   $0x0,0x14(%esp)        ;; store zeros in upper 32bits
mov    %eax,0x10(%esp)        ;; store address in lower 32bits
fildll 0x10(%esp)             ;; convert a long long to double
add    $0x2c,%esp
ret    

令人惊讶的是,分配的double的初始化完全消失了。

使用-O0生成代码时,一切都按预期工作,相反的代码是:

push   %ebp
mov    %esp,%ebp
sub    $0x28,%esp
sub    $0xc,%esp
push   $0x8                    ;; 8 bytes requested
call   8048300 <malloc@plt>    ;; malloc 'em
add    $0x10,%esp
mov    %eax,-0xc(%ebp)
mov    -0xc(%ebp),%eax
fldl   0x8048578               ;; load 3.14 constant
fstpl  (%eax)                  ;; store in allocated memory
mov    -0xc(%ebp),%eax
mov    %eax,-0x28(%ebp)        ;; store address in low 32 bits
movl   $0x0,-0x24(%ebp)        ;; store 0 in high 32 bits
fildll -0x28(%ebp)             ;; convert the long-long to a double
fstpl  -0x20(%ebp)
fldl   -0x20(%ebp)
leave  
ret    

问题

我做了什么无效的事情(我正在考虑别名规则,即使在我看来跳过初始化没有理由)或者这只是一个gcc错误?

请注意,编译为64位代码时会出现同样的问题(在64位模式下正式intptr_t为8字节,因此ad double无法准确表示它。但这并不会发生,因为在x86-64上只使用了64位地址中的48位,而double可以完全代表所有这些值。

5 个答案:

答案 0 :(得分:4)

在UB的情况下允许优化删除代码,但在这里它不应该。

Value *ptr = (Value *)malloc(sizeof(Value));中有不必要的演员,但这应该是无害的。

此行res.d = (unsigned long long) ptr;最好是res.d = (intptr_t) ptr;,因为intptr_t明确允许接收指针,然后您可以在double变量中设置一个整数值:you可能会失去精确度,但它不应该是UB。

我无法测试它(因为我没有gcc 4.9)但如果你遇到同样的问题:

#include <stdint.h>

...

Value doit() {
    Value *ptr = malloc(sizeof(Value));
    ptr[0].u = 7;
    Value res; res.d = (double) ((intptr_t) ptr);
    return res;
}

我会结束一个gcc错误。

我可以尝试在FreeBSD 10.1上使用clang版本3.4.1编译简化版本的代码

cc -O3 -S doit.c给出(条带化到代码部分):

doit:                                   # @doit
# BB#0:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-8, %esp
    subl    $16, %esp
    movl    $8, (%esp)
    calll   malloc
    movl    $1074339512, 4(%eax)    # imm = 0x40091EB8
    movl    $1374389535, (%eax)     # imm = 0x51EB851F
    movl    %eax, 8(%esp)
    movl    $0, 12(%esp)
    fildll  8(%esp)
    movl    %ebp, %esp
    popl    %ebp
    ret

它与gcc的编译方式不同,但clang甚至在-O3优化级别进行3.14初始化(3.14的转储十六进制为0x40091eb851eb851f

在阅读其他评论和答案之后,我认为问题的真正原因是gcc 跳过中间演员并将return (double)((uintptr_t) ptr);读作return (double) ptr; - 不完全是因为它会出现语法错误,但仍然会考虑UB,因为在结尾指针值结束为双变量。但是如果我们用中间演员分解那么它应该被读取(恕我直言):

register intptr_t intermediate = (intptr_t) ptr; // valid conversion
return (double) intermediate;  // valid conversion

答案 1 :(得分:2)

我觉得这里没什么奇怪的。您从未读过7您写的malloc,而是将double的结果写入Value *ptr = (Value*) malloc(sizeof(Value)); ptr[0].u = 7; Value res; res.d = (uintptr_t) ptr; // ptr is a result of malloc return res; // ptr is lost here which probably makes // GCC think that it is no longer accessible // so "7" is lost here too

.u

将指针转换为double很可能会失去精度,从而使内存无法访问(UB)。

但是,如果将指针保存为整数(Value res; res.u = (uintptr_t) ptr; // Saving to .u, not .d ),GCC会将其视为别名内存并保持初始化:

0x0000000000400570 <+0>:     sub    $0x8,%rsp
0x0000000000400574 <+4>:     mov    $0x8,%edi
0x0000000000400579 <+9>:     callq  0x400460 <malloc@plt>
0x000000000040057e <+14>:    movq   $0x7,(%rax)
0x0000000000400585 <+21>:    add    $0x8,%rsp
0x0000000000400589 <+25>:    retq   

编译到

(double)ptr

所以问题是您将指针保存为double。

BTW,Send_Email ( $BeforeScript, $AfterScript) 是编译错误,标准要求:

  

6.5.4投注运营商

     

[...]

     

4指针类型不得转换为任何浮点类型。浮动类型不应转换为任何指针类型。

截至N1548草案

答案 2 :(得分:1)

这似乎是一个错误...即使使用简化代码

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

double doit() {
    double *ptr = (double *)malloc(sizeof(double));
    ptr[0] = 3.14;
    uintptr_t ip = (uintptr_t)ptr;
    return (double)ip;
}

int main(int argc, const char *argv[]) {
    double v = doit();
    double *p = (double *)((intptr_t)v);
    printf("sizeof(uintptr_t) = %i\n", (int)sizeof(uintptr_t));
    printf("*p = %0.3f\n", *p);
    return 0;
}

使用-O2编译时不会初始化内存。

代码正常返回intptr_t(或unsigned long long);但转换为double后返回它不起作用gcc显然假设在这种情况下你将无法再访问内存。

这在32位模式下显然是错误的(其中intptr_t为4个字节,double为整数提供53位精度),但对于64位模式,其中uintptr_t-ftree-dce确实是8个字节,使用的值是48位)。

修改

对此不确定,但问题可能与“树上的死代码消除”(-O2)有关。在32位模式下进行编译时,启用优化-fno-tree-dce但禁用具有doit的特定版本,程序输出会更改并且正确但生成的代码不是

更具体地说,main的非内联版本不包含初始化代码,但3.14中生成的代码内联调用,优化器“知道”内存的值为{{1}并直接在输出中打印。

编辑2

确认为一个错误,已在行李箱中更正。

下一版本的解决方法是-fno-tree-pta

答案 3 :(得分:0)

C不是汇编程序。 C可以调用未定义的行为,其中将其视为高级汇编程序的人无法查看原因。作为一个例子:给定两个数组int [10]和int b [10],有可能是巧合&amp; a [10] ==&amp; b [0]。但是,以下代码

int a [10],  b [10];
int* p = &a [10];
if (p == &b [0]) *p = 0;
如果p ==&amp; b [0],

将调用未定义的行为。两个指针p和&amp; b [0]比较相等,并且由相同的位组成,但表现不同。 (如果你不同意,请查看“限制”指针,其中整点是比较相等的指针可以表现不同)。

转换为uintptr_t的规则如下:每个有效指针都可以转换为uintptr_t,结果可以转换回指针,给出相同的指针。这些值是实现定义的,除了将空指针转换为uintptr_t必须给出零,并且将0转换为指针必须给出空指针。没有什么要求转换应该简单,或者应该是您认为的应该是。

转换为uintptr_t是实现定义的。如果体系结构中的指针被限制为n <= 62位,则完全有可能转换如下:如果p是空指针,则将其转换为零。如果p不是空指针,则取n位,将它们向左移位(63-n)位,或将结果移位到0x8000 0000 0000 0001.结果保证不能转换为double而不丢失。当uintptr_t转换为double时,结果是不能再将其转回有效指针。

结果,如果(double)(uintptr_t)p是从p导出的唯一值,那么p不能重建,指针p丢失,并且* p的赋值可以被优化掉,因为* p不能再读一遍。

答案 4 :(得分:0)

我相信gcc是正确的。您没有使用该值并且没有返回指针,因此它认为该值已无法访问。

你应该已经返回指针并在其他地方强制转换为double,或者你需要一个联合,以便gcc知道指向该值的指针仍然存在:

require_once('/path/to/Zend/Json/Expr.php')