虚拟方法错误(0x0)地址

时间:2018-12-03 15:11:45

标签: c++ assembly gdb cpu-registers vtable

在一些调用虚拟成员函数的代码中出现了一些奇怪的段错误。 Segfault大约平均每30k调用一次。

我正在使用虚拟方法来实现模板方法模式。

它出现的代码行是

的第一行
GenericDevice::updateValue()
{
     ...
     double tmpValue=getValue();
     Value=tmpValue;
     ...
}

使用

class GenericDevice
{
    public: 
    void updateValue();
    void print(string& result);
    ...
    protected:
    virtual double getValue()const=0;
    ...
    private:
    std::atomic<double> Value;
    ...
}

稍后通过在运行时加载动态库来提供GenericDevice类

class SpecializedDeviced : public
{
    ...
    virtual double getValue()const final;
    ... 
}

发生问题时,我能够获取coredump并查看汇编代码:

0x55cd3ef036f4 GenericDevice::updateValue()+92   mov    -0x38(%rbp),%rax   
0x55cd3ef036f8 GenericDevice::updateValue()+96   mov    (%rax),%rax 
0x55cd3ef036fb GenericDevice::updateValue()+99   add    $0x40,%rax  
0x55cd3ef036ff GenericDevice::updateValue()+103  mov    (%rax),%rax 
0x55cd3ef03702 GenericDevice::updateValue()+106  mov   -0x38(%rbp),%rdx
0x55cd3ef03706 GenericDevice::updateValue()+110  mov   %rdx,%rdi         
0x55cd3ef03709 GenericDevice::updateValue()+113  callq  *%rax
0x55cd3ef0370b <GenericDevice::updateValue()+115>  movq   %xmm0,%rax          
0x55cd3ef03710 <GenericDevice::updateValue()+120>  mov    %rax,-0x28(%rbp) 
0x55cd3ef03714 <GenericDevice::updateValue()+124>  mov    -0x38(%rbp),%rax  
0x55cd3ef03718 <GenericDevice::updateValue()+128>  lea    0x38(%rax),%rdx     
0x55cd3ef0371c <GenericDevice::updateValue()+132>  mov    -0x28(%rbp),%rax    
0x55cd3ef03720 <GenericDevice::updateValue()+136>  mov    %rax,-0x40(%rbp)    
0x55cd3ef03724 <GenericDevice::updateValue()+140>  movsd  -0x40(%rbp),%xmm0  

预计段错误已发生在0x55cd3ef03709 GenericDevice :: updateValue()+ 113中。

where
#0  0x000055cd3ef0370a in MyNamespace::GenericDevice::updateValue (this=0x55cd40586698) at ../src/GenericDevice.cpp:22
#1  0x000055cd3ef038d2 in MyNamespace::GenericDevice::print (this=0x55cd40586698,result="REDACTED"...) at ../src/GenericDevice.cpp:50
...

函数GenericDevice :: updateValue()已按预期方式调用

<GenericDevice::print(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)+301>  callq  0x55cd3ef03698 <GenericDevice::updateValue()>

将rax设为0x0的原因。

Register group: general
rax            0x0              0  
rbx            0x5c01b8a2       1543616674  
rcx            0x2              2  
rdx            0x28             40  
rsi            0x2              2  
rdi            0x55cd40586630   94340036191792  
rbp            0x7ffe39086e60   0x7ffe39086e60  
rsp            0x7ffe39086e20   0x7ffe39086e20  
r8             0x7fbb06e7e8a0   140441251473568  
r9             0x3              3  
r10            0x33             51  
r11            0x206            518                       
r12            0x55cd3ef19438   94340012676152  
r13            0x7ffe39089010   140729855283216   
r14            0x0              0   
r15            0x0              0  
rip            0x55cd3ef0370a  0x55cd3ef0370a<GenericDevice::updateValue()+114>                     eflags         0x10206  [ PF IF RF ]               
cs             0x33     51
ss             0x2b     43
ds             0x0      0  
es             0x0      0  
fs             0x0      0   
gs             0x0      0 

通过执行汇编摘录I中的计算,可以确认汇编代码和它使用的数据与预期的虚函数调用匹配并以正确的数据开头:

  1. 使用该对象的指针

    (gdb) x /g $rbp-0x38  
    0x7ffe39086e28: 0x000055cd40586698   
    (gdb) p this  
    $1 = (GenericDevice * const) 0x55cd40586698
    
  2. 指向vtable的指针正确(* this的第一个元素)

    (gdb) x 0x000055cd40586698  
    0x55cd40586698: 0x00007fbb070c1aa0
    (gdb) info vtbl this  
    vtable for 'GenericDevice' @ 0x7fbb070c1aa0 (subobject @ 0x55cd40586698):
    
  3. vtable包含我们正在寻找的方法的地址。

    (gdb) info vtbl this  
    vtable for 'GenericDevice' @ 0x7fbb070c1aa0 (subobject @ 0x55cd40586698):  
    ...  
    [8]: 0x7fbb06e7bf50 non-virtual thunk to MyNamespace::SpecializedDevice::getValue() const.
    
  4. 使用了正确的vtable偏移量

    (gdb) x 0x00007fbb070c1aa0+0x40  
    0x7fbb070c1ae0 <_ZTVN12MyNamespace11SpecializedDeviceE+168>: 0x00007fbb06e7bf50
    

到目前为止的结论: 通过逐步执行汇编代码,验证了正确数据和指令的使用。

  • 使用了正确的数据:可以排除内存损坏。
  • 汇编指令似乎正确:可以排除编码/编译错误
  • vtable看起来不错:可以排除在运行时加载库时的错误:该函数通常可以正常运行数万次。

请随时指出我的推理中的任何错误。

但是寄存器rax中的值仍为零,而不是预期的0x7fbb070c1ae0

  • 这是否可以指示一个(很少使用)cpu内核中的硬件错误? 将解释罕见和随机发生的情况,但我希望其他程序和OS也出现问题。

处理器型号为Intel(R)Core(TM)i7-4770 CPU @ 3.40GHz

谢谢!

更新: 我找到了$ RIP标记
0x55cd3ef0370a MyNamespace::GenericDevice::updateValue()+114 shlb 0x48(%rsi)

gdb显示的程序集在滚动后似乎已更改。这就是为什么我在第一次尝试中没有看到标记的原因。启动gdb并输入布局asm后,我得到:

>0x55cd3ef0370a <MyNamespace::GenericDevicer::updateValue()+114>  shlb   0x48(%rsi)           
0x55cd3ef0370d <MyNamespace::GenericDevicer::updateValue()+117>  movd   %mm0,%eax            
0x55cd3ef03710 <MyNamespace::GenericDevicer::updateValue()+120>  mov    %rax,-0x28(%rbp)     
0x55cd3ef03714 <MyNamespace::GenericDevicer::updateValue()+124>  mov    -0x38(%rbp),%rax     
0x55cd3ef03718 <MyNamespace::GenericDevicer::updateValue()+128>  lea    0x38(%rax),%rdx   
0x55cd3ef0371c <MyNamespace::GenericDevicer::updateValue()+132>  mov    -0x28(%rbp),%rax
0x55cd3ef03720 <MyNamespace::GenericDevicer::updateValue()+136>  mov    %rax,-0x40(%rbp)
0x55cd3ef03724 <MyNamespace::GenericDevicer::updateValue()+140>  movsd  -0x40(%rbp),%xmm0  

...

在gdb中滚动ams之后,我得到了原始问题中发布的代码。原始问题中的代码与可执行文件中的代码匹配。上面发布的代码确实部分偏离了可执行文件。

shlb指令对我来说毫无意义。甚至无法在 Intel® 64 and IA-32 Architectures Software Developer’s Manual。 最接近的比赛是shl。

2 个答案:

答案 0 :(得分:1)

@Jester指出,您的其他寄存器值与您说崩溃发生的代码不匹配。

  

发生问题时,我能够获得coredump并查看汇编代码:...段错误发生在汇编摘录的最后一行。

你怎么知道的? where的输出是什么?

通常,应该有一个当前的$RIP标记,如下所示:

   0x55cd3ef036f4 GenericDevice::updateValue()+92   mov    -0x38(%rbp),%rax   
   0x55cd3ef036f8 GenericDevice::updateValue()+96   mov    (%rax),%rax 
   0x55cd3ef036fb GenericDevice::updateValue()+99   add    $0x40,%rax  
   0x55cd3ef036ff GenericDevice::updateValue()+103  mov    (%rax),%rax 
   0x55cd3ef03702 GenericDevice::updateValue()+106  mov   -0x38(%rbp),%rdx
   0x55cd3ef03706 GenericDevice::updateValue()+110  mov   %rdx,%rdi         
   0x55cd3ef03709 GenericDevice::updateValue()+113  callq  *%rax
=> 0x55cd3ef0370e GenericDevice::updateValue()+118  ....

您看到那个标记了吗?

否则,您的崩溃可能发生在其他地方(但是很好地分析了数据)。

如果您确实看到了标记,则其他细节(例如 exact 处理器的品牌和型号)可能很重要(例如,请参见this问答)。

答案 1 :(得分:1)

在执行被调用函数之前,调用语句将返回地址压入堆栈。资源 Intel® 64 and IA-32 Architectures Software Developer’s Manual第225页。 另一个线程在同一堆栈上保留对变量的无效引用,并将其递减,该变量是存储的返回地址。 基本上,该线程应该持有对计数器的引用,该计数器对GenericDevice :: updateValue()仍有多少待处理的作业进行计数。超时后,计数器将超出范围,但正在执行的线程仍保留现在无效的引用。超时很少发生,并且仅在读取设备而不是模型的情况下发生。因此,存储在堆栈中的返回地址被偶尔破坏了。