这个"不应该发生"的崩溃是 AMD Fusion CPU 的一个 Bug 吗?

70

我们公司最近接到了许多客户的电话,他们反映我们的程序在他们的系统上发生了访问冲突导致崩溃。

这个崩溃发生在 SQLite 3.6.23.1 中,我们将其作为应用程序的一部分提供(我们提供自定义版本,以便使用与应用程序其他部分相同的 VC++ 库,但它是基于原始的 SQLite 代码构建而成)。

pcache1Fetch 执行 call 00000000 时,程序会崩溃,WinDbg 堆栈跟踪显示如下:

0b50e5c4 719f9fad 06fe35f0 00000000 000079ad 0x0
0b50e5d8 719f9216 058d1628 000079ad 00000001 SQLite_Interop!pcache1Fetch+0x2d [sqlite3.c @ 31530]
0b50e5f4 719fd581 000079ad 00000001 0b50e63c SQLite_Interop!sqlite3PcacheFetch+0x76 [sqlite3.c @ 30651]
0b50e61c 719fff0c 000079ad 0b50e63c 00000000 SQLite_Interop!sqlite3PagerAcquire+0x51 [sqlite3.c @ 36026]
0b50e644 71a029ba 0b50e65c 00000001 00000e00 SQLite_Interop!getAndInitPage+0x1c [sqlite3.c @ 40158]
0b50e65c 71a030f8 000079ad 0aecd680 071ce030 SQLite_Interop!moveToChild+0x2a [sqlite3.c @ 42555]
0b50e690 71a0c637 0aecd6f0 00000000 0001edbe SQLite_Interop!sqlite3BtreeMovetoUnpacked+0x378 [sqlite3.c @ 43016]
0b50e6b8 71a109ed 06fd53e0 00000000 071ce030 SQLite_Interop!sqlite3VdbeCursorMoveto+0x27 [sqlite3.c @ 50624]
0b50e824 71a0db76 071ce030 0b50e880 071ce030 SQLite_Interop!sqlite3VdbeExec+0x14fd [sqlite3.c @ 55409]
0b50e850 71a0dcb5 0b50e880 21f9b4c0 00402540 SQLite_Interop!sqlite3Step+0x116 [sqlite3.c @ 51744]
0b50e870 00629a30 071ce030 76897ff4 70f24970 SQLite_Interop!sqlite3_step+0x75 [sqlite3.c @ 51806]

相关的C代码行为:

if( createFlag==1 ) sqlite3BeginBenignMalloc();
编译器会把定义为sqlite3BeginBenignMalloc的函数进行内联处理:
typedef struct BenignMallocHooks BenignMallocHooks;
static SQLITE_WSD struct BenignMallocHooks {
  void (*xBenignBegin)(void);
  void (*xBenignEnd)(void);
} sqlite3Hooks = { 0, 0 };

# define wsdHooksInit
# define wsdHooks sqlite3Hooks

SQLITE_PRIVATE void sqlite3BeginBenignMalloc(void){
  wsdHooksInit;
  if( wsdHooks.xBenignBegin ){
    wsdHooks.xBenignBegin();
  }
}

它的汇编代码如下:

719f9f99    mov     esi,dword ptr [esp+1Ch]
719f9f9d    cmp     esi,1
719f9fa0    jne     SQLite_Interop!pcache1Fetch+0x2d (719f9fad)
719f9fa2    mov     eax,dword ptr [SQLite_Interop!sqlite3Hooks (71a7813c)]
719f9fa7    test    eax,eax
719f9fa9    je      SQLite_Interop!pcache1Fetch+0x2d (719f9fad)
719f9fab    call    eax ; *** CRASH HERE ***
719f9fad    mov     ebx,dword ptr [esp+14h]

寄存器包括:

eax=00000000 ebx=00000001 ecx=000013f0 edx=fffffffe esi=00000001 edi=00000000
eip=00000000 esp=0b50e5c8 ebp=000079ad iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010202

如果eax是0(实际上它就是0),那么test eax, eax应该设置零标志位,但它未被设置为零。因为零标志位没有被设置,所以je不能跳转,然后应用程序在尝试执行call eax (00000000)时崩溃。

更新:这里的eax应该始终为0,因为sqlite3Hooks.xBenignBegin在我们的代码中未设置。我可以使用指定了SQLITE_OMIT_BUILTIN_TEST定义的选项重新构建SQLite,这将在代码中打开#define sqlite3BeginBenignMalloc()并完全省略此代码路径。这可能会解决问题,但感觉不像是一个“真正”的修复;还有什么能够阻止它在其他代码路径中发生呢?

到目前为止,所有客户的共同点都是运行“Windows 7 Home Premium 64-bit (6.1, Build 7601) Service Pack 1”,并拥有以下其中之一的CPU(根据DxDiag):

  • AMD A6-3400M APU with Radeon(tm) HD Graphics (4 CPUs), ~1.4GHz
  • AMD A8-3500M APU with Radeon(tm) HD Graphics (4 CPUs), ~1.5GHz
  • AMD A8-3850 APU with Radeon(tm) HD Graphics (4 CPUs), ~2.9GHz

根据维基百科的AMD Fusion文章,这些都是基于K10内核的“Llano”型AMD Fusion芯片,发布于2011年6月,这也是我们开始收到报告的时间。

最常见的客户系统是Toshiba Satellite L775D,但我们还有来自HP Pavilion dv6&dv7和Gateway系统的崩溃报告。

这个崩溃可能是由CPU错误(请参见AMD Family 12h Processors勘误表)引起的吗?还是我忽略了其他可能的解释?(根据Raymond,它可能是超频,但如果是这样,仅受到影响的是这个特定的CPU型号,这很奇怪。)

老实说,这似乎不可能是真正的CPU或操作系统错误,因为客户没有在其他应用程序中收到蓝屏或崩溃。一定有其他更可能的解释——但是是什么呢?

更新于8月15日:我得到了一台搭载AMD A6-3400M处理器的东芝L745D笔记本电脑,并可以在运行程序时稳定地复现崩溃。崩溃总是发生在相同的指令上; .time报告在崩溃前的用户时间为1m30s至7m之间。在原始帖子中我忽略了一个可能与问题相关的事实,即应用程序是多线程的,具有高CPU和I / O使用率。该应用程序默认生成四个工作线程,并发布80%以上的CPU使用率(在SQLite代码中还有一些阻塞以及互斥锁),直到它崩溃。我修改了应用程序,只使用两个线程,但它仍会崩溃(尽管需要更长的时间)。现在我正在运行一个仅使用一个线程的测试,但它还没有崩溃。

另请注意,它似乎并不纯粹是CPU负载问题;我可以在系统上运行Prime95而没有错误,并且它将使CPU温度升高到> 70°C,而当它正在运行时,我的应用程序几乎没有使温度超过50°C。

更新于8月16日:略微干扰指令会使问题“消失”。例如,用xor eax,eax替换内存负载(mov eax,dword ptr [SQLite_Interop!sqlite3Hooks(71a7813c)])可以防止崩溃。修改原始C代码以向if(createFlag == 1)语句添加额外的检查会更改编译代码中各种跳转的相对偏移量(以及test eax,eaxcall eax语句的位置),似乎也可以防止出现问题。

到目前为止我发现最奇怪的结果是,将jne719f9fa0更改为两个nop指令(因此控制始终落到test eax,eax指令上,无论createFlag / esi的值如何)允许程序在不崩溃的情况下运行。


3
几乎可以确定这不是CPU的错误。你考虑过制作一个更简单的测试用例吗? - Oliver Charlesworth
3
@Mehrdad:是的,有些代码可能只是跳到那里,但如果是这样,它确实非常成功地伪造了调用堆栈。 - Bradley Grainger
1
我必须同意Oli的观点。如果test设置了错误的标志位,这个问题非常基础,内部QA测试不可能没有发现。特别是因为“测试然后跳转”操作似乎是一种非常常见的编译器优化,在许多程序中都被使用。 - aroth
3
我想要插话一下,说这是一个非常好的问题。+1 - gahooa
1
@flolo:这是在64位Windows(WOW64)上运行的32位进程;这个输出是正常的。 - Bradley Grainger
显示剩余9条评论
3个回答

29
我在微软Build会议上与一位AMD工程师讨论了这个错误,并向他展示了我的复现步骤。他今天早上发邮件给我说:
“我们进行了调查,发现这是Llano APU家族中已知勘误的结果。根据OEM的情况,可以通过BIOS更新修复 - 如果可能,请向您的客户推荐它(即使您已经有解决方法)。”
“如果您感兴趣,该勘误是Family 12h修订指南(第45页)中的665号:http://support.amd.com/TechDocs/44739_12h_Rev_Gd.pdf#page=45。”
以下是该勘误的描述:

665整数除法指令可能导致不可预测行为

描述

在高度特定和详细的内部时序条件下,处理器核心可能会中止一条DIV或IDIV整数除法指令的推测执行(由于推测执行被重定向,例如由于错误预测的分支),但可能会挂起或过早完成非推测路径的第一条指令。

可能对系统造成的影响

不可预测的系统行为,通常会导致系统挂起。

建议的解决方法

BIOS应设置MSRC001_1029 [31]。

此解决方法更改了 AMD Family 10h和12h处理器软件优化指南, 订货号#40546中指定的DIV / IDIV指令延迟。使用此解决方法后,AMD Family 12h处理器的DIV / IDIV延迟与AMD Family 10h处理器的DIV / IDIV延迟类似。

计划修复

没有


1
“665整数除法”问题在Passmark论坛上有所讨论:http://www.passmark.com/forum/showthread.php?3656-AMD-llano-A-series-benchmark-and-CPU-bug其中一条评论指出,这个问题只会在使用双通道内存时发生。因此,如果您的电脑只有一根4GB的内存条和Llano CPU,则可能不需要进行BIOS修复。但是,如果您花费20美元升级到8GB,就会遇到问题——而您可能(错误地!)将其归咎于内存。不幸的是,“修复”BIOS会导致Passmark整数数学基准测试速度降低超过80%,Passmark得分降低超过30%。 - Dave Burton

1

我有点担心为if (wsdHooks.xBenignBegin)生成的代码不够通用。它假设唯一的真值是1,而实际上应该测试任何非零值。不过,MSVC有时候很令人困惑。这可能没什么大问题。算了吧:这些指令是针对未呈现的C代码。

鉴于eflag Z位清除且EAX为零,该代码没有通过执行指令到达此处。

719f9fa7    test    eax,eax

必须从其他地方跳转到以下指令(719f9fa9 je SQLite_Interop!pcache1Fetch+0x2d)或甚至call指令本身后面的指令。

另一个复杂之处在于,在x86系列中,无效跳转目标(如JE指令的第二个字节)通常可以执行无异常(没有故障)的多个指令,最终经常回到适当的指令对齐。换句话说,您可能不会寻找跳转到这些指令的任何一开始的跳转:跳转可能在它们的字节中间,导致执行不引人注意的操作,例如add [al+ebp],al,这 tend not to be noticed。

我预测,在test指令处设置断点将不会触发异常。找到此类原因的唯一方法就是非常幸运,或者怀疑一切并逐一证明它们的清白。


关于您的第一段:如果eax & eax等于零,那么test只会设置ZF,因此随后的je是相当安全的。 - Michael Foukarakis
你的汇编分析有误,检查 vs 1 是因为 C 代码正在检查 vs 1,因为它是针对行 if( createFlag==1 ) sqlite3BeginBenignMalloc(); 而不是 if (wsdHooks.xBenignBegin)(请参见 OP 对 sqlite3BeginBenignMalloc 被内联的评论)。 - Necrolis
@Michael Foukarakis:说得好,我已经撤回了我的评论。 - wallyk
2
我并不是想争辩,但我认为你的理论被我的发现所否定了。用nop指令替换test前面的jne似乎可以防止崩溃。(在没有这个更改的情况下100%可重现,在测试了一天后,使用该更改的可重现性为0%。)如果其他指令跳转到je的中间或直接跳转到call,那么它不会受到这种更改的影响。此外,如果有其他代码跳转到jecall,那么如何解释这种情况只发生在Llano APU上的理论呢? - Bradley Grainger

-1

在考虑CPU漏洞的可能性之前,请尝试排除更有可能的原因

  1. 调用指令的不同代码路径。使用uf命令反汇编函数并查找其他跳转/分支到调用指令的位置。

  2. 从钩子函数跳转/调用到0。dps SQLite_Interop!sqlite3Hooks l 2并验证它是否显示为null。


1.(我已在评论中回复了这个问题,但没有更新原始问题,所以不太明显;请见谅。)崩溃代码位于函数的0x2B字节处(几乎就在序言之后)。我反汇编了整个函数,没有跳回那么早的跳转;这个语句出现在函数体中的循环之前。当然,从函数外部跳转是可能的,但很难与堆栈相一致。 2. dps SQLite_Interop!sqlite3Hooks l 2 显示 00000000 00000000 - Bradley Grainger
我还设置了一个数据断点(ba w 4 SQLite_Interop!sqlite3Hooks),在崩溃之前没有对该地址进行写操作。 - Bradley Grainger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接