<2024年5月>
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

文章分类

导航

订阅

调试笔记之NvStreamUserAgent崩溃

昨日就见到这个崩溃,但是忙于写一篇文章,没时间理它,今日又见,略有闲暇,于是上调试器。

浏览所有线程,发现4号线程有异常,切换过去k(看)(在沈阳软件园讲课时有同行戏言k为看的缩写,乐而纳之)一下。果然是这个线程在发起WER(错误报告)。

异常发生在栈帧b,即RtlEnterCriticalSection。取栈帧a的栈帧基地址,使用.cxr命令让时光倒流,回到异常的第一现场:


引发异常的是著名的lock btr指令:

lock btr dword ptr [rcx+8],0

简单来说,这条指令会以互锁方式测试指定位,将结果放入CF,并将这一位复位为0,即:

CF ← Bit(BitBase, BitOffset);
Bit(BitBase, BitOffset) ← 0;

观察著名的关键区结构体,可以看到偏移8就是代表关键区状态的LockCount字段。

ntdll!_RTL_CRITICAL_SECTION

   +0x000 DebugInfo        : Ptr64 _RTL_CRITICAL_SECTION_DEBUG

   +0x008 LockCount        : Int4B

   +0x00c RecursionCount   : Int4B

   +0x010 OwningThread     : Ptr64 Void

   +0x018 LockSemaphore    : Ptr64 Void

   +0x020 SpinCount        : Uint8B

LockCount的默认值是0xffffffff(-1),不为-1即代表临界区忙碌。上面的btr指令即是做个关键的操作,如果位0本来1,那么便会被复位为0。

观察上面的寄存器上下文,可以看到rcx为0,看来关键区结构体为空。在x64时,RCX即第一个参数。当前函数中没有改变过rcx,看来父函数传递下来就有问题。

ntdll!RtlEnterCriticalSection:

00007ffd`1c2e0bd0 4883ec28        sub     rsp,28h

00007ffd`1c2e0bd4 65488b042530000000 mov   rax,qword ptr gs:[30h]

00007ffd`1c2e0bdd f00fba710800    lock btr dword ptr [rcx+8],0

观察父函数,非常简短,看来参数是从上一级来的。

NvStreamBase!NvMutexAcquire:

00007ffd`06974020 4883ec28        sub     rsp,28h

00007ffd`06974024 ff154ea10000    call    qword ptr [NvStreamBase!GetHashedRegKeyStatus+0x9a38 (00007ffd`0697e178)]

00007ffd`0697402a 33c0            xor     eax,eax

00007ffd`0697402c 4883c428        add     rsp,28h

00007ffd`06974030 c3              ret

继续追溯,看到再上一级是从另一个结构图的1788处读出来的:

0:004> ub 00007ff6`809eb196

NvStreamUserAgent+0xab17d:

00007ff6`809eb17d cc              int     3

00007ff6`809eb17e cc              int     3

00007ff6`809eb17f cc              int     3

00007ff6`809eb180 4053            push    rbx

00007ff6`809eb182 4883ec20        sub     rsp,20h

00007ff6`809eb186 488bd9          mov     rbx,rcx

00007ff6`809eb189 488b8988170000  mov     rcx,qword ptr [rcx+1788h]

00007ff6`809eb190 ff15423c4b00    call    qword ptr [NvStreamUserAgent+0x55edd8 (00007ff6`80e9edd8)]


当时的结构图是由rcx指向的(也是参数)。rcx值因为要给子函数传递参数而变化了,但是变化前复制到了rbx,而且在后来的子函数中,都没有改变过rbx,因此异常上下文中的rbx应该就是当初的结构体地址。

0:004> dq rbx+1788

00007ff6`8111ade8  00000253`c5119f60 00000000`00000000

00007ff6`8111adf8  00000000`00000000 00000000`00000000

00007ff6`8111ae08  00000000`00000000 00000000`00000000

其内容不为空,套用临界区结构体:

0:004> dt _RTL_CRITICAL_SECTION 00000253`c5119f60

ntdll!_RTL_CRITICAL_SECTION

   +0x000 DebugInfo        : 0xffffffff`ffffffff _RTL_CRITICAL_SECTION_DEBUG

   +0x008 LockCount        : 0n-1

   +0x00c RecursionCount   : 0n0

   +0x010 OwningThread     : (null) 

   +0x018 LockSemaphore    : (null) 

   +0x020 SpinCount        : 0xf

除了DebugInfo和spincount字段外,其它字段还很像。

在堆上搜索这个地址,可以找的到:

分配的大小也与关键区结构体的大小很符合:

0:004> ?? sizeof(ntdll!_RTL_CRITICAL_SECTION)

unsigned int64 0x28

如此看来,有矛盾了。从崩溃现场来看,临界区结构体为空,导致异常。从内存信息来看,至少我们调试时,结构体的成员却不为空。哪个可信呢?

在栈上寻找异常结构体:

0:004> s -d 000000b8`934fe130 L500 0xc0000005 

000000b8`934feef0  c0000005 00000000 00000000 00000000  ................

然后观察:

0:004> dt _EXCEPTION_RECORD 000000b8`934feef0  

ntdll!_EXCEPTION_RECORD

   +0x000 ExceptionCode    : 0n-1073741819

   +0x004 ExceptionFlags   : 0

   +0x008 ExceptionRecord  : (null) 

   +0x010 ExceptionAddress : 0x00007ffd`1c2e0bdd Void

   +0x018 NumberParameters : 2

   +0x020 ExceptionInformation : [15] 1

那个负数就是著名的c0000005,不知道是哪个前辈当年定义结构体时把无符号数定义成有符号,酿成千古之错。

0:004> ? 0n-1073741819

Evaluate expression: -1073741819 = ffffffff`c0000005

继续观察异常的附加信息:

0:004> dx -r1 (*((ntdll!unsigned __int64 (*)[15])0xb8934fef10))

(*((ntdll!unsigned __int64 (*)[15])0xb8934fef10))                 [Type: unsigned __int64 [15]]

    [0]              : 0x1 [Type: unsigned __int64]

    [1]              : 0x8 [Type: unsigned __int64]

根据MSDN:

EXCEPTION_ACCESS_VIOLATION

The first element of the array contains a read-write flag that indicates the type of operation that caused the access violation. If this value is zero, the thread attempted to read the inaccessible data. If this value is 1, the thread attempted to write to an inaccessible address. If this value is 8, the thread causes a user-mode data execution prevention (DEP) violation.

The second array element specifies the virtual address of the inaccessible data.

数组的第一个元素为1代表访问方式为写,第二个元素是试图访问的地址,即8,RCX为0,加8为8。如此看来,异常时关键区结构体为空有多个相互吻合的证据支撑。

顺便说一下,在Linux中,类似情况时错误码的定义有所不同:


大家基于的都是CPU手册,在IA手册中,基本定义为:

内存里的数据是调试时看到的,也应该是可信的。那么是怎么回事呢?CPU出bug了么?否也,这样的问题多半与多线程同时操作同一块内存有关。从栈回溯来看,异常发生在通信过程中,很可能是在访问+1788字段时,读到的数据确实为空,后来又有某个线程访问了这个字段,将其写为我们看到的值。

这样的问题接下来如何调试呢?要看身份了。这个问题发生在Nvidia显卡驱动的进程内,栈回溯中显示的几个模块都是Nividia程序包里的:

对于NV的同行,如果要做下一步的debug,设置好私有符号,找到出问问题的结构体,或许立刻看出问题,如果没有,那么下个硬件断点监视+1788字段......

对于老雷来说,这番调试越厨代庖,只为博得诸君一顾,就此打住了。


***********************************************************

正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生。

欢迎关注格友公众号

posted on 2017年5月27日 13:34 由 Raymond

Powered by Community Server Powered by CnForums.Net