使用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可以准确地表示所有这些值)。
发布于 2015-05-09 09:25:23
好像是个虫子..。即使使用简化的代码
#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确实是8字节时,使用的值为48位)。
编辑
对此不确定,但问题可能与“树上的死代码消除”(-ftree-dce)有关。当在32位模式编译时,启用优化-O2,但禁用这个特定的-fno-tree-dce,程序输出会改变,并且是正确的,但是生成的代码不是。
更具体地说,doit的非内联版本不包含初始化代码,但是在main中生成的代码插入调用和优化器“知道”内存的值是3.14,并在输出中直接打印。
编辑2
确认为窃听器,已经在树干上更正了。
下一个版本的变通方法是-fno-tree-pta
发布于 2015-05-09 09:06:59
在UB的情况下,允许优化移除代码,但这里不应该这样做。
在Value *ptr = (Value *)malloc(sizeof(Value));中有一个不必要的强制转换,但这应该是无害的。
这行res.d = (unsigned long long) ptr;最好是res.d = (intptr_t) ptr;,因为显然允许intptr_t接收指针,然后可以在double变量中设置一个整数值:您可能会丢失精度,但不应该是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上的clangVersion3.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即使在3.14优化级别也进行-O3初始化( 3.14的转储十六进制是0x40091eb851eb851f)。
在阅读了其他评论和答案后,我认为问题的真正原因是gcc跳过中间转换,将return (double)((uintptr_t) ptr);读入return (double) ptr; --这并不完全是因为它将是语法错误,但仍然考虑到存在UB,因为指针值在结尾以双变量结尾。但是,如果我们用中间类型的分解行,则应该将其理解为(IMHO):
register intptr_t intermediate = (intptr_t) ptr; // valid conversion
return (double) intermediate; // valid conversion发布于 2015-05-09 23:16:07
C不是汇编程序。C可以调用未定义的行为,如果有人认为它是一个高级别的汇编程序看不到原因。例如:给定两个数组int a 10和int b 10,可能是巧合&a 10 == &b。但是,下面的代码
int a [10], b [10];
int* p = &a [10];
if (p == &b [0]) *p = 0;如果p == &b调用未定义的行为。两个指针,p和&b,比较相等,由相同的位组成,但行为不同。(如果您不同意,请查看“限制”指针,其中的要点是,比较相等的指针可以表现得不同)。
转换为uintptr_t的规则如下:每个有效指针都可以转换为uintptr_t,结果可以转换回指针,给出相同的指针。这些值是实现定义的,只是将空指针转换为uintptr_t必须给出零,而将0转换为指针必须给出空指针。没有什么要求转换应该是简单的,或者应该是您所认为的那样。
向uintptr_t的转换是定义的实现。如果体系结构中的指针被限制为n个<= 62位,那么转换完全可能是这样的:如果p是空指针,则将其转换为零。如果p不是空指针,则取n位,将其左移(63-n)位,或结果为0x8000 0000000 0001。这一结果保证不会在没有损失的情况下翻倍。当将uintptr_t转换为double时,其结果是不能再将其转换为有效的指针。
因此,如果(double)(uintptr_t)p是从p导出的唯一值,则不能重构p,则指针p丢失,并且由于不能再读取*p,所以可以优化对*p的赋值。
https://stackoverflow.com/questions/30137521
复制相似问题