前言-显示调用栈
在分析崩溃时候,经常会查看调用栈,正确理解调用中的各字段的含义对于排查问题至关重要,所以本篇重点介绍下,如何查看调用栈。
查看调用栈,kb 如下图
调用栈命令,可以观看官方文档 :https://docs.microsoft.com/zh-cn/windows-hardware/drivers/debugger/k--kb--kc--kd--kp--kp--kv--display-stack-backtrace-
||2:2:196> kb
*** Stack trace for last set context - .thread/.cxr resets it
# ChildEBP RetAddr Args to Child
00 00cf8dec 002da5ca 96b6b062 3951c6b0 40f28218 lec_teacher!GoodsPreviewWidget::getShowMode [d:\shendun_lec_teacher\origin\version\1.5.7\src\plugin\activity\goods\goodspreviewwidget.cpp @ 22]
01 00cf8e38 66a0c9ba 00000001 3951c6b0 1e7cb010 lec_teacher!GraphicsToolManager::onCountDownAccepted+0x7a [d:\shendun_lec_teacher\origin\version\1.5.7\src\tool\graphics\manager\graphicstoolmanager.cpp @ 739]
02 (Inline) -------- -------- -------- -------- Qt5Core!QtPrivate::QSlotObjectBase::call+0x17 [c:\users\qt\work\qt\qtbase\src\corelib\kernel\qobjectdefs_impl.h @ 394]
03 00cf8ed0 66a0cc8e 40f26bc8 66d48844 66d48844 Qt5Core!QMetaObject::activate+0x40a [c:\users\qt\work\qt\qtbase\src\corelib\kernel\qobject.cpp @ 3774]
04 00cf8ee4 644c8218 40f26bc8 646197f4 00000001 Qt5Core!QMetaObject::activate+0x1e [c:\users\qt\work\qt\qtbase\src\corelib\kernel\qobject.cpp @ 3646]
05 (Inline) -------- -------- -------- -------- Qt5Widgets!QDialog::rejected+0xb [c:\users\qt\work\qt\qtbase\src\widgets\.moc\release\moc_qdialog.cpp @ 241]
06 (Inline) -------- -------- -------- -------- Qt5Widgets!QDialogPrivate::finalize+0x23 [c:\users\qt\work\qt\qtbase\src\widgets\dialogs\qdialog.cpp @ 178]
07 00cf8f0c 644843da 00000001 002e5485 96b6b10a Qt5Widgets!QDialog::done+0x38 [c:\users\qt\work\qt\qtbase\src\widgets\dialogs\qdialog.cpp @ 632]
08 00cf8f14 002e5485 96b6b10a 00000000 00000002 Qt5Widgets!QAbstractSpinBox::stepUp+0xa [c:\users\qt\work\qt\qtbase\src\widgets\widgets\qabstractspinbox.cpp @ 617]
09 00cf8f50 003518b2 40f26bc8 00000000 00000002 lec_teacher!CountDownDialog::on_okBtn_clicked+0x165 [d:\shendun_lec_teacher\origin\version\1.5.7\src\tool\graphics\widgets\countdowndialog.cpp @ 204]
0a 00cf8f74 669f5808 00000000 0000002b 00cf9054 lec_teacher!CountDownDialog::qt_metacall+0x32 [d:\shendun_lec_teacher\origin\version\1.5.7\release\moc_countdowndialog.cpp @ 180]
0b 00cf8f84 66a0cacf 40f26bc8 00000000 0000002b Qt5Core!QMetaObject::metacall+0x28 [c:\users\qt\work\qt\qtbase\src\corelib\kernel\qmetaobject.cpp @ 304]
0c 00cf9018 66a0cc8e 32be0c10 66d4871c 66d4871c Qt5Core!QMetaObject::activate+0x51f [c:\users\qt\work\qt\qtbase\src\corelib\kernel\qobject.cpp @ 3813]
0d 00cf902c 643fdd80 32be0c10 6460966c 00000002 Qt5Core!QMetaObject::activate+0x1e [c:\users\qt\work\qt\qtbase\src\corelib\kernel\qobject.cpp @ 3646]
在打印的调用栈中,基本展示了调用栈的层级,并且指明了一些参数和具体的代码文件、函数、行数。
内容解析
首先解释下,打印的信息内容
- ChildEBP: a pointer to a memory location which stores the address of the previous function on the stack ("stack frame").
- RetAddr: The "return address" where processing will resume once this function returns (finishes what it had to do).
ChildEBP:表示是每个函数的ebp,也就是基地址,如果是内联函数没有基地址
RetAddr:返回值地址,通过下图可以清晰的看到RetAddr的含义,可以理解为函数运行后的下一条指令。也就是执行的上层调用者的调用函数的下一条指令。
例如下面的示例:
QDialog::done的RetAddr是644843da 指向的就是 setUp的调用done的下一条指令。
由于编译程序会对代码进行优化,所以不是内联函数的一些代码会被优化成内联函数,所以上面的2个字段内容就无效了,例如在栈帧05,06帧
调用栈参数传递
在推导调用栈时候,一些基础知识是需要知道的,例如:栈帧是如何存储,参数是如何传递。
我先来看一下参数传递的几种方式,调用参数的顺序。
约定类型 | __cdecl | stdcall | PASCAL | fastcall |
---|---|---|---|---|
参数传递顺序 | 从右到左 | 从右到左 | 从左到右 | 使用寄存器 |
平衡堆栈者 | 调用者 | 函数自身 | 函数自身 | 函数自身 |
__cdecl 是c++ 默认的调用方式,
stdcall是Win32中绝大多数 API函数的约定方式,也有少部分使用__cdcel约定方式。
例如:int add(int a, int b)
__cdecl ,调用者处理栈,调整
push b ;参数按从右到左传递
push a
call add
add esp, 8 ;调用者在函数外部平衡堆栈
stdcall,在函数内部把栈顶调整
push b ;参数按从右到左传递
push a
call add
例如:看一下实例
X86 体系结构具有多个不同的调用约定。 幸运的是,它们都遵循相同的寄存器保留和函数返回规则:
-
函数必须保留所有寄存器, eax、 ecx 和 edx 除外,可以在函数调用中更改这些寄存器 ,并且必须 根据调用约定对其进行更新。
-
如果结果为32位或更小,则 eax 寄存器接收函数返回值。 如果结果为64位,则结果存储在 edx: eax 对中。
下面是用于 x86 体系结构的调用约定的列表:
-
Win32 (_ _ stdcall)
函数参数在堆栈上传递,从右到左推送,被调用方清理堆栈。
-
本机 c + + 方法调用 (也称为 thiscall)
函数参数将在堆栈上传递,从右到左推送,在 ecx 寄存器中传递 "this" 指针,被调用方清理堆栈。
-
C + + 方法调用的 COM (_ _ stdcall)
函数参数在堆栈上传递,从右到左,然后将 "this" 指针推送到堆栈上,然后调用函数。 被调用方清理堆栈。
-
__fastcall
在 ecx 和 edx 寄存器中传递前两个 DWORD 或更小的参数。 剩余的参数在堆栈上传递(从右到左)。 被调用方清理堆栈。
-
__cdecl
函数参数在堆栈上传递,从右到左推送,并且调用方清理堆栈。 _ _ Cdecl 调用约定用于具有可变长度参数的所有函数。
分析反编译
由于编译器会对代码进行优化,将一些简单函数直接去掉,例如:accepted 和 rejected 由于最终调用activate 只是后2个参数不同,所以生成的汇编直接调用了activate
void QDialog::done(int r)
{
Q_D(QDialog);
d->hide(r);
d->finalize(r, r);
}
void QDialogPrivate::finalize(int resultCode, int dialogCode)
{
Q_Q(QDialog);
if (dialogCode == QDialog::Accepted)
emit q->accepted(); // 调用QMetaObject::activate
else if (dialogCode == QDialog::Rejected)
emit q->rejected(); // 调用QMetaObject::activate
emit q->finished(resultCode);
}
void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index,
void **argv)
{
activate(sender, QMetaObjectPrivate::signalOffset(m), local_signal_index, argv);
}
void QDialog::done(int r) 反汇编解析
Qt5Widgets!QDialog::done:
644c81e0 83ec08 sub esp, 8 //1个变量,需要减8
644c81e3 53 push ebx //保存当前ebx
644c81e4 8b5c2410 mov ebx, dword ptr [esp+10h] //获取参数大小是8字节,1
644c81e8 56 push esi //保存栈顶
644c81e9 8b7104 mov esi, dword ptr [ecx+4] //重置栈顶
644c81ec 8bce mov ecx, esi //ecx 保存栈顶
644c81ee 57 push edi //保存edi
644c81ef 53 push ebx //保存ebx 避免函数调用被覆盖
644c81f0 e8eb020000 call Qt5Widgets!QDialogPrivate::hide (644c84e0)
644c81f5 8b7604 mov esi, dword ptr [esi+4]
644c81f8 8b3de0655f64 mov edi, dword ptr [Qt5Widgets!_imp_?activateQMetaObjectSAXPAVQObjectPBU1HPAPAXZ (645f65e0)]
//判断是否等于1,accepted
644c81fe 83fb01 cmp ebx, 1 // 比较ebx 是否为1
644c8201 7506 jne Qt5Widgets!QDialog::done+0x29 (644c8209) //如果不相等跳转到644c8209
644c8203 6a00 push 0 // 右侧第一参数, argv = 0
644c8205 6a01 push 1 // 右侧第二个参数,local_signal_index = 1
644c8207 eb07 jmp Qt5Widgets!QDialog::done+0x30 (644c8210)
//不相等跳转到此位置,也就是else if的位置rejected
644c8209 85db test ebx, ebx //使用test指令判断ebx是否为0,但不保存结果
644c820b 750e jne Qt5Widgets!QDialog::done+0x3b (644c821b)
644c820d 53 push ebx //右侧第一个参数, argv = ebx
644c820e 6a02 push 2 //右侧第二个参数,local_signal_index = 2 也就是第三个个信号
//添加从右侧开始3、4的参数,QMetaObject *m 和 QObject *sender
644c8210 68f4976164 push offset Qt5Widgets!QDialog::staticMetaObject (646197f4)
644c8215 56 push esi
644c8216 ffd7 call edi //调用edi的指向的函数,就是644c81f8 那行的activate
644c8218 83c410 add esp, 10h
//最后调用finished
644c821b 8d442418 lea eax, [esp+18h]
644c821f 895c2418 mov dword ptr [esp+18h], ebx
644c8223 89442410 mov dword ptr [esp+10h], eax
644c8227 8d44240c lea eax, [esp+0Ch]
644c822b 50 push eax
644c822c 6a00 push 0
644c822e 68f4976164 push offset Qt5Widgets!QDialog::staticMetaObject (646197f4)
644c8233 56 push esi
644c8234 c744241c00000000 mov dword ptr [esp+1Ch], 0
644c823c ffd7 call edi
644c823e 83c410 add esp, 10h
644c8241 5f pop edi
小结
1、使用kv显示调用栈
2、认识了调用的字段含义
3、大概讲解了汇编指令中参数是如何传递的