为啥 GCC 生成的代码会从堆栈中读取垃圾?

Posted

技术标签:

【中文标题】为啥 GCC 生成的代码会从堆栈中读取垃圾?【英文标题】:Why does GCC-generated code read junk from stack?为什么 GCC 生成的代码会从堆栈中读取垃圾? 【发布时间】:2015-06-26 12:56:28 【问题描述】:

考虑以下代码(使用Eigen):

#include <Eigen/Dense>
#include <iostream>

template<int rows, int cols, int row, class R, class Rv, int N, class... Rs>
inline typename std::enable_if<sizeof...(Rs)==0>::type
    setRow(Eigen::Matrix<R,rows,cols>&)


template<int rows, int cols, int row, class R, class Rv, int N=0, class... Rs>
inline typename std::enable_if<sizeof...(Rs)==cols-N-1>::type
    setRow(Eigen::Matrix<R,rows,cols>& m, Rv val, Rs...args)

    m(row,N)=val;
    setRow<rows,cols,row,R,Rv,N+1>(m,args...);

template<class T, int R, int C, int CUR_ROW>
class MatrixConstructor

    Eigen::Matrix<T,R,C> m;
public:
    MatrixConstructor(const Eigen::Matrix<T,R,C>& m)
        : m(m)
    
    MatrixConstructor()
    
    template<class...Ts>
    typename std::enable_if<sizeof...(Ts)==C && CUR_ROW<R-1,
    MatrixConstructor<T,R,C,CUR_ROW+1>>::type operator()(Ts... vals)
    
        setRow<R,C,CUR_ROW>(m,vals...);                                                                                                                                                                                 
        return MatrixConstructor<T,R,C,CUR_ROW+1>(m);
    

    template<class...Ts>
    typename std::enable_if<sizeof...(Ts)==C && CUR_ROW==R-1,
    Eigen::Matrix<T,R,C>>::type operator()(Ts... vals)
    
        setRow<R,C,CUR_ROW>(m,vals...);
        return m;
    
;

void test()

    Eigen::Matrix<double,4,3> m=MatrixConstructor<double,4,3,0>()(1,2,3)
                                                                 (4,5,6)
                                                                 (7,8,9)
                                                                 (5,4,3);
    std::cout << m;


int main()
 test(); 

我用 gcc-4.8 编译它,并带有完整的优化和生成程序集列表的选项。这是我使用的命令:

g++ minitest.cpp -I/usr/include/eigen3 -std=c++0x -O3 -march=native -S -masm=intel

(我的 CPU 是 Intel(R) Xeon(R) CPU E3-1226 v3,运行 64 位 Linux 系统——希望现在-march=native 对读者有意义。)

让我感到奇怪的是,使用此命令行生成的一些指令似乎是荒谬的,甚至是多余的。参见例如设置堆栈后test() 函数如何启动(有关test()main() 的完整代码,请参阅here):

vmovsd   xmm4, QWORD PTR .LC6[rip] # 1.0
lea      rsi, [rsp+96]
vmovsd   xmm5, QWORD PTR .LC7[rip] # 2.0
vmovsd   QWORD PTR [rsp], xmm4
vmovapd  xmm3, XMMWORD PTR [rsp+16] # What does it read?!
vmovapd  xmm1, XMMWORD PTR [rsp]    # And this!
vmovsd   QWORD PTR [rsp+32], xmm5
vmovsd   xmm0, QWORD PTR .LC8[rip] # 3.0
vmovapd  XMMWORD PTR [rsp+304], xmm3 # And now even save this junk?!
vmovapd  XMMWORD PTR [rsp+192], xmm1
vmovapd  xmm3, XMMWORD PTR [rsp+48]
vmovapd  xmm1, XMMWORD PTR [rsp+32]
vmovsd   QWORD PTR [rsp+64], xmm0
vmovsd   xmm7, QWORD PTR .LC12[rip] # 7.0
vmovapd  XMMWORD PTR [rsp+336], xmm3
vmovapd  XMMWORD PTR [rsp+224], xmm1
vmovapd  xmm3, XMMWORD PTR [rsp+80]
vmovsd   QWORD PTR [rsp+304], xmm7   # Even stranger — overwrites the junk

我已经在调试器中逐步读取了这些垃圾,并确认在它们之后xmm3xmm1 寄存器确实具有无意义的值。看着这个未定义值的读数,我开始怀疑我的程序确实试图访问一些应该无法访问的内存。但为什么?我在某处介绍过 UB 吗?

我也尝试运行使用-fsanitize=address 编译的程序,但它运行时没有任何崩溃。

【问题讨论】:

这不只是英特尔组装订单吗?也就是说,vmovapd xmm3, XMMWORD PTR [rsp+16] 不会进入 xmm3吗? @Barry 完全正确,这就是之谜:它将[rsp+16] 的垃圾读入xmm3!然后将其保存到[rsp+304],然后用来自[rip+.LC12]7.0 覆盖这个保存的值。 【参考方案1】:

您的代码执行以下步骤:

    使用未初始化的 Eigen::Matrix 成员创建未初始化的 MatrixConstructor 对象 设置 Eigen::Matrix 成员的一行 创建一个新的 MatrixConstructor 对象,其 Eigen::Matrix 成员使用旧 MatrixConstruction 对象的 Eigen::Matrix 成员进行初始化

因此,在第 3 步中,您将复制一个仅设置了第一行的 Eigen::Matrix 对象。对象的其余值仍未初始化。由于这些都是临时对象,它们都分配在堆栈中,因此您看到从堆栈中读取垃圾也就不足为奇了。

请注意,这假定 Eigen::Matrix() 构造函数不会初始化对象,从快速查看源代码来看,默认情况下它似乎不会这样做。

【讨论】:

嗯,你似乎是对的。我将尝试在构造函数之间通过引用传递它并查看结果。现在仍然奇怪的是,冗余操作还没有在-O3 级别进行优化。 确实,存储对正在构造的矩阵的引用而不是副本会导致代码小几倍,这也利用了 AVX 四双指令。

以上是关于为啥 GCC 生成的代码会从堆栈中读取垃圾?的主要内容,如果未能解决你的问题,请参考以下文章

当 CPU 尝试读取由 GPU 初始化的托管内存时,为啥数据会从主机迁移到设备?

Mybatis为啥会从数据库读取出NULL来

为啥这个 fscanf() 读取垃圾值

为啥 gcc 4.x 在调用方法时默认为 linux 上的堆栈保留 8 个字节?

为啥 GCC 会在堆栈上推送一个额外的返回地址?

为啥 GCC 以这种方式对堆栈中的整数进行排序?