调试调用IUnknown::Release的汇编代码:影子空间是否正确?
64位Windows Intel平台COM父对象IUnknown::Release汇编代码崩溃问题排查
原始问题代码
48 8B 41 08 ; MOV RAX, [RCX + 0x08] gets the refcount 48 FF C8 ; DEC RAX ; decrement refCount 48 89 41 08 ; MOV [RCX + 0x08], RAX ; update refCount in obj 48 83 F8 00 ; CMP RAX, 0 74 01 ; JE +1 ; (skips early return if RAX is zero) C3 ; RET ; (return from function) 51 ; PUSH RCX ; to cache it 48 8B 49 10 ; MOV RCX, [RCX + 0x10] ; Read the child object into RCX 48 83 F9 00 ; CMP RCX, 0 ; ensure the child object hasn't been set to Nothing 74 11 ; JE 0x11 ; jump the next 17 bytes to avoid calling release 48 8B 01 ; MOV RAX, [RCX] ; Get vtable pointer from [RCX] into RAX 48 8B 40 10 ; MOV RAX, [RAX + 0x10] ; Get IUnknown::Release function pointer from [RAX + 0x10] into RAX (QI + hex(ptr_size * 2) for Release) 48 83 EC 28 ; SUB RSP, 0x28 ; Allocate shadow space FF D0 ; CALL RAX ; Call the function pointed to by RAX 48 83 C4 28 ; ADD RSP, 0x28 ; Deallocate shadow space 59 ; POP RCX ; to restore it to the parent object 48 B8 {addrCoTaskMemFree} ; MOV RAX, addrCoTaskMemFree 48 83 EC 28 ; SUB RSP, 0x28 FF D0 ; CALL RAX 48 83 C4 28 ; ADD RSP, 0x28 48 31 C0 ; XOR RAX, RAX C3 ; RET
父对象内存布局
| Offset | Value | Size |
|---|---|---|
| 0x00 | vtablePointer | 0x08(64位) |
| 0x08 | refcount | 0x08(64位) |
| 0x10 | childObjectPtr | 0x08(64位) |
崩溃原因定位
1. 引用计数操作非原子,多线程环境下计数错误
当前代码通过MOV/DEC/MOV三步修改引用计数,不是原子操作。当多个线程同时调用Release时,会导致引用计数被错误覆盖,出现计数未正确递减、重复释放对象等问题。外部COM对象(如VBA.Collection)可能被多线程访问,更容易触发这类崩溃。
2. 调用子对象Release时违反x64调用约定的栈对齐要求
64位Windows x64调用约定明确要求:
- 调用函数前,RSP必须对齐到16字节边界(CALL指令会压入8字节返回地址,函数入口时RSP会是8字节不对齐,符合函数内部要求)
- 调用者需为被调用函数分配32字节(0x20)的影子空间
当前代码的栈操作逻辑:
PUSH RCX使RSP减少8字节,随后SUB RSP, 0x28再减少40字节,导致调用CALL RAX前RSP处于8字节不对齐状态。- 调用后子Release函数入口的RSP变为16字节对齐,违反约定。外部COM对象的
Release函数可能依赖正确的栈对齐(如使用XMM寄存器操作栈数据),从而触发崩溃。
3. 调用CoTaskMemFree时的影子空间分配冗余
CoTaskMemFree仅需32字节影子空间,当前代码分配0x28(40字节)属于冗余操作,虽不直接崩溃,但不符合调用约定规范。
修复后的汇编代码
; 原子递减引用计数,保证多线程安全 LOCK DEC QWORD PTR [RCX + 0x08] MOV RAX, [RCX + 0x08] ; 获取递减后的引用计数 CMP RAX, 0 JE @DestroyObject ; 计数为0,执行销毁逻辑 RET ; 计数不为0,直接返回 @DestroyObject: ; 保存父对象指针到影子空间,避免PUSH破坏栈对齐 MOV QWORD PTR [RSP - 0x20], RCX ; 加载子对象指针到RCX MOV RCX, [RCX + 0x10] CMP RCX, 0 JE @FreeParentObject ; 子对象为空,直接释放父对象 ; 准备调用子对象的Release:保证栈16字节对齐,分配标准32字节影子空间 SUB RSP, 0x28 ; 获取子对象的Release函数指针 MOV RAX, [RCX] MOV RAX, [RAX + 0x10] CALL RAX ; 调用子对象Release ADD RSP, 0x28 ; 恢复栈 @FreeParentObject: ; 恢复父对象指针到RCX MOV RCX, [RSP - 0x20] ; 调用CoTaskMemFree:分配标准32字节影子空间 SUB RSP, 0x20 MOV RAX, addrCoTaskMemFree CALL RAX ADD RSP, 0x20 XOR RAX, RAX ; 返回0(释放后的引用计数) RET
修复说明
- 原子引用计数:使用
LOCK DEC指令原子递减引用计数,确保多线程环境下计数准确。 - 栈对齐与影子空间:
- 调用子对象Release和CoTaskMemFree时,分配标准32字节(0x20)影子空间。
- 通过
SUB RSP,0x28确保调用前RSP处于16字节对齐状态,符合x64调用约定。 - 使用影子空间保存父对象指针,避免
PUSH操作破坏栈对齐。
- 逻辑简化:改用标签式分支跳转,避免偏移计算错误,提升代码可读性。
内容的提问来源于stack exchange,提问作者Greedo
相关产品推荐
相关产品推荐

