Windbg如何高效查看函数参数?

99ANYc3cd6
预计阅读时长 18 分钟
位置: 首页 参数 正文

核心概念

在 WinDbg 中,查看函数参数的关键在于理解函数调用在 x86 和 x64 架构下的不同约定。

windbg 查看函数参数
(图片来源网络,侵删)
  1. x86 (32位) 架构:

    • 栈帧: 函数参数通过 push 指令压入栈中,然后调用函数,函数返回地址也压入栈。
    • 查看位置: 假设函数有 3 个参数,它们相对于栈帧指针 (EBP) 的位置是固定的:
      • [EBP+4]: 返回地址
      • [EBP+8]: 第一个参数
      • [EBP+C]: 第二个参数
      • [EBP+10]: 第三个参数
    • 寄存器: 前几个整数/指针参数也可能通过寄存器传递(如 ECX, EDX)。
  2. x64 (64位) 架构):

    • 寄存器优先: 前 4 个整数/指针参数通过寄存器传递,效率更高。
      • RCX: 第一个参数
      • RDX: 第二个参数
      • R8: 第三个参数
      • R9: 第四个参数
    • 栈补充: 如果参数超过 4 个,多余的参数才会通过栈传递。
    • 查看位置: 对于通过栈传递的参数,它们位于 [RSP+0x28] 之后的位置。

使用 k 命令(最常用、最直接)

k (kb, kn) 命令是查看调用堆栈的利器,WinDbg 能够根据调试符号(PDB)自动解析并显示每个函数的参数值。

步骤:

windbg 查看函数参数
(图片来源网络,侵删)
  1. 触发断点: 在函数入口或你感兴趣的代码行设置断点。

    // 在 MyFunction 函数入口断住
    bp MyFunction
    g // 继续执行,直到断点命中
  2. 查看调用堆栈: 当断点命中后,使用 k 命令。

    k

示例分析:

x64 示例

windbg 查看函数参数
(图片来源网络,侵删)

假设我们有如下 C++ 代码:

void MyFunction(int a, int b, int c, int d, int e) {
    // 函数体
}
void Caller() {
    MyFunction(10, 20, 30, 40, 50);
}

MyFunction 的入口断住后,执行 k 命令:

0:000> k
 # Child-SP          RetAddr               Call Site
00 fffffa80`00001be8 fffff80e`xxxxxxxx MyFunction+0x0
01 fffffa80`00001bf0 fffff80e`xxxxxxxx Caller+0x20
02 fffffa80`00001bf8 fffff80e`xxxxxxxx some_other_function+...
...

看起来 k 命令没有直接显示参数,这是因为你需要一个更详细的 k 命令变体。

// 使用 kb 命令,它会显示参数
0:000> kb
 # Child-SP          RetAddr               Call Site
00 fffffa80`00001be8 fffff80e`xxxxxxxx MyFunction+0x0
    [a] 0000000c
    [b] 00000014
    [c] 0000001e
    [d] 00000028
    [e] 00000032
01 fffffa80`00001bf0 fffff80e`xxxxxxxx Caller+0x20
02 fffffa80`00001bf8 fffff80e`xxxxxxxx some_other_function+...
...

解释:

  • kb 命令会尝试解析当前栈帧(MyFunction)的参数。
  • [a] 0000000c: 这是第一个参数 a 的值(十进制是 12,但传进来的是 0xC),WinDbg 显示的是十六进制。
  • [b] 00000014: 第二个参数 b 的值(十进制 20)。
  • 以此类推,前 4 个参数来自寄存器,第 5 个参数 e 来自栈。

x86 示例

同样的代码在 32 位环境下:

void MyFunction(int a, int b) {
    // ...
}
void Caller() {
    MyFunction(100, 200);
}

MyFunction 入口断住,执行 kb

0:001> kb
 # Child-SP          RetAddr               Call Site
0012ff88 0040101a MyFunction+0x0
    [a] 00000064
    [b] 000000c8
0012ff8c 0040100d Caller+0xd
...

解释:

  • [a] 00000064: 第一个参数 a 的值(十进制 100)。
  • [b] 000000c8: 第二个参数 b 的值(十进制 200)。
  • 在 x86 中,参数值直接显示在 kb 的输出中。

手动检查寄存器和栈(适用于无符号或符号损坏时)

kb 无法显示参数(没有 PDB 符号,或符号已损坏)时,你需要手动查看。

x64 手动查看步骤:

  1. 查看寄存器: 前 4 个参数在寄存器里。

    0:000> r rcx, rdx, r8, r9
    rcx=0000000c 0000000c
    rdx=00000014 00000014
    r8=0000001e 0000001e
    r9=00000028 00000028

    这和 kb 的输出结果一致。

  2. 查看栈: 第 5 个及以后的参数在栈上。

    // RSP 是当前栈顶,参数通常在 RSP+0x28 之后
    0:000> dq rsp L5
    fffffa80`00001be8  00000000`00000032  fffffa80`00001bf0  <-- 第5个参数 e=0x32
    fffffa80`00001bf0  fffff80e`xxxxxxxx  fffffa80`00001bf8  <-- 返回地址
    ...

x86 手动查看步骤:

  1. 查看栈: 参数相对于 EBP 的位置是固定的。

    // 假设 EBP 指向当前栈帧
    0:001> r ebp
    ebp=0012ff88
    // 参数在 EBP+8 和 EBP+C
    0:001> dd ebp+8 L2
    0012ff88  0040101a 00000064  <-- 返回地址 0x0040101a, 参数 a=0x64
    0012ff8c  0040100d 000000c8  <-- 参数 b=0xc8
    • dd 是以双字(32位)格式查看内存。
    • L2 表示显示 2 个双字。

使用 dt 命令查看结构体/类参数

如果函数的参数是一个结构体或 C++ 对象,kb 可能只显示其地址,你可以使用 dt (Display Type) 命令来查看该地址处的具体内容。

示例:

struct MY_STRUCT {
    int x;
    int y;
};
void ProcessData(MY_STRUCT* pData) {
    // ...
}

ProcessData 入口断住:

0:000> kb
 # Child-SP          RetAddr               Call Site
00 fffffa80`00001be8 fffff80e`xxxxxxxx ProcessData+0x0
    [pData] fffffa80`00001234  <-- 参数是一个指针
01 fffffa80`00001bf0 fffff80e`xxxxxxxx Caller+0x20
...

kb 只告诉我们 pData 的地址是 fffffa8000001234,现在用 dt 查看这个地址的内容:

0:000> dt MY_STRUCT fffffa80`00001234
MY_STRUCT
   +0x000 x : 0x1
   +0x004 y : 0x2

这样你就能看到结构体内部的值了。


使用 uf 命令反汇编并查看

如果你想看到函数调用指令本身,uf (Unassemble Function) 是个好工具,它能显示函数的完整汇编代码,包括调用者是如何传递参数的。

示例:

// 反汇编 Caller 函数
0:000> uf Caller
...
0040100c 8b442410       mov     eax,dword ptr [esp+10h]  // 获取参数 e
00401010 8b4c2414       mov     ecx,dword ptr [esp+14h]  // 获取参数 d
00401014 51             push    ecx                     // 将参数 d 压栈
00401015 50             push    eax                     // 将参数 e 压栈
00401016 6a1e           push    1eh                     // 将参数 c 压栈 (0x1e = 30)
00401018 6a14           push    14h                     // 将参数 b 压栈 (0x14 = 20)
0040101a 6a0c           push    0ch                     // 将参数 a 压栈 (0x0c = 10)
0040101c e8xxxxxxxx     call    MyFunction               // 调用 MyFunction
...

通过这个输出,你可以清晰地看到:

  1. push 0ch: 第一个参数 a (10) 被压入栈。
  2. push 14h: 第二个参数 b (20) 被压入栈。
  3. ...以此类推。
  4. call MyFunction: 执行函数调用。

总结与最佳实践

方法 优点 缺点 适用场景
kb / k 最简单、最直接,自动解析符号 依赖 PDB 符号,符号损坏时无效 日常调试的首选,90% 的情况都用这个。
手动检查 不依赖符号,最可靠 需要手动计算地址,繁琐 符号缺失、损坏或想深入理解调用机制时。
dt 能查看结构体/对象的内部细节 需要知道类型定义 函数参数是复杂结构体时。
uf 能看到汇编层面的调用细节 信息量可能过大,不够直观 想要精确分析参数传递过程或优化代码时。

工作流程建议:

  1. 首先尝试 kb,这是最快、最干净的方法。
  2. kb 输出不理想(只显示地址),检查符号是否已加载(使用 .sym 命令)。
  3. 如果符号确实有问题或不存在,切换到手动检查方法,根据是 x64 还是 x86 查看 RCX/RDX/R8/R9[EBP+8] 等位置。
  4. 如果参数是结构体/对象,使用 dt 来展开其内容。
  5. 如果需要底层细节,使用 uf 来反汇编调用者代码。

掌握这些方法,你就能在 WinDbg 中游刃有余地查看和分析函数参数了。

-- 展开阅读全文 --
头像
深圳智能家居装修公司哪家强?
« 上一篇 今天
ThinkPad E15参数有哪些值得关注的亮点?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

最近发表

标签列表

目录[+]