在.NET中调用约定和堆栈遍历



我正在学习《Inside Windows Debugging》一书,无法完全理解《Listing parameters and Locals for System Code》一章中使用的技术。顾名思义,我正试图获得一个传递给某个函数的参数列表。堆栈示例:

0:012:x86> k
*** Stack trace for last set context - .thread/.cxr resets it
# ChildEBP RetAddr  
WARNING: Frame IP not in any known module. Following frames may be wrong.
0e 053eecec 04561d6e 0x4562f2e 
0f 053eed30 04561c52 0x4561d6e
10 053eed54 72617118 0x4561c52
11 053eed60 72616cc0 mscorlib_ni!System.Threading.Tasks.Task.InnerInvoke()$##6003FA0+0x28
12 053eed84 726170ea mscorlib_ni!System.Threading.Tasks.Task.Execute()$##6003F91+0x30
13 053eedec 72633fd6 mscorlib_ni!System.Threading.Tasks.Task.ExecutionContextCallback(System.Object)$##6003F9F+0x1a
14 053eee00 72616f68 mscorlib_ni!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)$##6003AD3+0x16
15 053eee6c 72616e72 mscorlib_ni!System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef)$##6003F9E+0xd8
16 053eee7c 7268ac9c mscorlib_ni!System.Threading.Tasks.Task.ExecuteEntry(Boolean)$##6003F9D+0xb2
17 053eee8c 726340c5 mscorlib_ni!System.Threading.Tasks.ThreadPoolTaskScheduler.LongRunningThreadWork(System.Object)$##60040E8+0x1c
18 053eeef0 72633fd6 mscorlib_ni!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)$##6003AD4+0xe5
19 053eef04 72633f91 mscorlib_ni!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)$##6003AD3+0x16
1a 053eef20 72688c8e mscorlib_ni!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)$##6003AD2+0x41
1b 053eef38 736eebf6 mscorlib_ni!System.Threading.ThreadHelper.ThreadStart(System.Object)$##6003BE3+0x4e

一般来说,我有一个完整的堆栈,但尽管如此,我还是希望能够处理k:等命令的输出

0:012:x86> !clrstack
OS Thread Id: 0x2518 (12)
Child SP       IP Call Site
053eecac 04562f2e Namespace.ProcessingManager.B(System.String, System.Threading.CancellationToken, System.Collections.Generic.List`1, Int32)
053eed00 04561d6e Namespace.ProcessingManager.A(System.String, System.Threading.CancellationToken, Int32)
053eed40 04561c52 Namespace.ProcessingManager+c__DisplayClass25_0.b__0()
053eed5c 72617118 System.Threading.Tasks.Task.InnerInvoke()
053eed68 72616cc0 System.Threading.Tasks.Task.Execute()
053eed8c 726170ea System.Threading.Tasks.Task.ExecutionContextCallback(System.Object)
053eed90 726340c5 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
053eedfc 72633fd6 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
053eee10 72616f68 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef)
053eee74 72616e72 System.Threading.Tasks.Task.ExecuteEntry(Boolean)
053eee84 7268ac9c System.Threading.Tasks.ThreadPoolTaskScheduler.LongRunningThreadWork(System.Object)
053eee88 726070e3 System.Threading.ThreadHelper.ThreadStart_Context(System.Object)
053eee94 726340c5 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
053eef00 72633fd6 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
053eef14 72633f91 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
053eef2c 72688c8e System.Threading.ThreadHelper.ThreadStart(System.Object)

我对具有以下签名的方法B的自变量感兴趣:

void B (string code, CancellationToken token, List<SomeObj> items, int taskCount)

我的研究是在假设.NET使用__stdcall调用约定的情况下进行的:

0:012:x86> dd 053eecec
053eecec  053eed30 04561d6e 00000002 1259e288 
053eecfc  0163dae4 12592918 01698868 01639394
053eed0c  01639394 015fe3b4 00000000 00000000
053eed1c  00000000 00000000 053eed30 01791aa4
053eed2c  01791abc 053eed54 04561c52 00000005
053eed3c  0163dae4 016da0e4 0163dae4 0167402c
053eed4c  01791b04 053eedc8 053eed60 72617118
053eed5c  01791b04 053eed84 72616cc0 0163db2c

053eed30:保存的EBP(上一帧指针-A(。

04561d6e:RetAddr指向下一段实际代码。

00000002:Arg:taskCount(我认为像00000002这样的值表示基元类型的值(。

1259e288:Arg:项目(通过!do 1259e288检查(。

0163dae4:Arg:token(通过!do 0163dae4检查(。

12592918:假设给定地址将包含string code参数,但像!dodu这样的命令不会返回任何内容。通常,在该值之后,会找到string code(01639394(,但它似乎指的是以前的方法,因为01698868处的值在方法A处创建。

我如何得到第一个论点?

UPD

从评论中提到的答案来看:

__clrcall是托管代码的调用约定。它是其他指针的混合,这个指针像__thiscall一样传递,经过优化参数传递类似__fastcall,参数顺序类似__cdecl调用方清理类似于__stdcall。

来自__cdecl描述:

参数传递顺序:从右到左。堆栈维护责任:调用函数从堆栈中弹出参数。

这与我得到的类似。

0:012:x86> .frame /c /r 0e
0e 053eecec 04561d6e 0x4562f2e
eax=00000000 ebx=19223de4 ecx=00000000 edx=00000000 esi=0000000a edi=0000000a
eip=04562f2e esp=053eeca8 ebp=053eecec iopl=0         nv up ei pl nz ac po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000212
04562f2e eb75            jmp     04562fa5

来自__fastcall描述:

参数传递顺序:第一个两个DWORD或更小的参数从左到右在参数列表中找到的在ECX中传递和EDX寄存器;所有其他参数都是从从右到左。堆栈维护责任:调用的函数弹出堆栈中的参数。

eipespebp是";服务";寄存器,据我所知,参数不会通过它们传递。ebx

您最好在可重复性最小的示例上尝试,而不是在成熟的应用程序上。我使用的代码是

using System;
using System.Text;
namespace CallingConvention
{
class Program
{
static void Main(string[] args)
{
var ecx = new Program(); // any object ok for testing
var edx = new int[5]; // any object ok for testing
var stack1 = new object(); // any object ok for testing
var stack2 = new AccessViolationException(); // any object ok for testing
Method1(ecx, edx, stack1, stack2);
}
private static void Method1(Program ecx, int[] edx, object stack1, AccessViolationException stack2)
{
Console.WriteLine(ecx);
Console.WriteLine(edx);
Console.WriteLine(stack1);
Console.WriteLine(stack2);
var ecx2 = new ASCIIEncoding();
var edx2 = new ArgumentException();
Method2(ecx2, edx2);
}
private static void Method2(ASCIIEncoding ecx2, ArgumentException edx2)
{
Console.WriteLine(ecx2);
Console.WriteLine(edx2);
Console.WriteLine("ECX and EDX possibly destroyed.");
Console.ReadLine();
}
}
}

但它看起来甚至可以更短。调试会话:

[...]
ntdll!LdrpDoDebuggerBreak+0x2b:
773d1a62 cc              int     3
0:000> sxe ld clrjit
0:000> g
[...]
ModLoad: 56be0000 56c6a000   C:WindowsMicrosoft.NETFrameworkv4.0.30319clrjit.dll
[...]
0:000> .loadby sos clr
0:000> .load B:...sosex.dll
0:000> !mbm *!*Program.Main
0:000> !mbm *!*Method1
0:000> !mbm *!*Method2
0:000> !mbl
0 e : disable *!*PROGRAM.MAIN ILOffset=0: pass=1 oneshot=false thread=ANY
CallingConvention!CallingConvention.Program.Main(string[]) (PENDING JIT)
1 e : disable *!*METHOD1 ILOffset=0: pass=1 oneshot=false thread=ANY
CallingConvention!CallingConvention.Program.Method1(CallingConvention.Program, int[], object, System.AccessViolationException) (PENDING JIT)
2 e : disable *!*METHOD2 ILOffset=0: pass=1 oneshot=false thread=ANY
CallingConvention!CallingConvention.Program.Method2(System.Text.ASCIIEncoding, System.ArgumentException) (PENDING JIT)

0:000> g

到目前为止,这只是为了设置必要的扩展和断点。

0:000> !clrstack -a
OS Thread Id: 0x44cc (0)
Child SP       IP Call Site
001fee28 007c084f *** WARNING: Unable to verify checksum for CallingConvention.exe
CallingConvention.Program.Main(System.String[]) [C:...Program.cs @ 10]
PARAMETERS:
args (<CLR reg>) = 0x02452430
LOCALS:
<no data>
<no data>
<no data>
001fefa4 5813f036 [GCFrame: 001fefa4] 
0:000> r ecx
ecx=02452430

在主要方法开始时,还没有当地人。但我们已经可以看到args[]在ECX寄存器中传递:!clrstack表示<CLR reg>,我们在ECX中找到了相同的值。

0:000> !mt
0:000> !clrstack -a
OS Thread Id: 0x44cc (0)
Child SP       IP Call Site
001fee28 007c085b CallingConvention.Program.Main(System.String[]) [C:...Program.cs @ 11]
PARAMETERS:
args = <no data>
LOCALS:
<no data>
<no data>
<no data>
001fefa4 5813f036 [GCFrame: 001fefa4] 

仅仅1行托管之后,ECX的值就被覆盖了,所以我们不再知道args[]是什么。我们不需要,因为它从未被使用过。

尽管已经调用了new Program(),但!clrstack无法将其标识为局部变量。

0:000> !dumpheap -type Program
Address       MT     Size
0245243c 00774d78       12  
[...]
0:000> r
eax=0245243c ebx=0245243c ecx=00774d78 edx=00546998 esi=00000000 edi=001fee50

但这个例子就在那里,它似乎在EAX和EBX中。

0:000> !mt
eax=02452474 ebx=0245243c ecx=02452474 edx=00546998 esi=02452474 edi=02452468
eip=007c0887 esp=001fee28 ebp=001fee38 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
007c0887 ff15c03dd256    call    dword ptr [mscorlib_ni+0xb3dc0 (56d23dc0)] ds:002b:56d23dc0=5714f2e0
0:000> !clrstack -a
OS Thread Id: 0x44cc (0)
Child SP       IP Call Site
001fee28 007c0887 CallingConvention.Program.Main(System.String[]) [C:...Program.cs @ 13]
PARAMETERS:
args = <no data>
LOCALS:
0x001fee28 = 0x02452448
<no data>
<no data>
001fefa4 5813f036 [GCFrame: 001fefa4] 
0:000> r
eax=02452474 ebx=0245243c ecx=02452474 edx=00546998 esi=02452474 edi=02452468
[...]

稍后的另一个托管调用,我们在堆栈上看到int[]。EAX已更改,但EBX仍保留Program对象。

0:000> !mt
0:000> *** inside AccessViolation constructor
0:000> !mgu
0:000> *** get out of constructor
0:000> !mt
0:000> *** right before Method1() call
0:000> r
eax=02456f00 ebx=0245243c ecx=0245243c edx=02452448 esi=02452474 edi=02452468
eip=007c0894 esp=001fee20 ebp=001fee38 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
007c0894 ff15604d7700    call    dword ptr ds:[774D60h] ds:002b:00774d60=007c043d
0:000> !do ebx
Name:        CallingConvention.Program
[...]
0:000> !do ecx
Name:        CallingConvention.Program
[...]
0:000> !do edx
Name:        System.Int32[]
[...]
  • EBX=程序对象,来自之前
  • ECX=程序对象,为方法调用准备
  • EDX=int[],为方法调用准备

我不知道为什么,但其他两个参数在EDI和ESI:中

0:000> !muf
[...]
007c088d 57              push    edi
007c088e 56              push    esi
[...]
0:000> !do edi
Name:        System.Object
[...]
0:000> !do esi
Name:        System.AccessViolationException
[...]
0:000> r edi
edi=02452468
0:000> r esi
esi=02452474
0:000> dd esp-8 L2
001fee18  02452474 02452468

0:000> !mt
Breakpoint 7 hit
[...]
0:000> !clrstack -a
OS Thread Id: 0x44cc (0)
Child SP       IP Call Site
001fee10 007c08b7 CallingConvention.Program.Method1(CallingConvention.Program, Int32[], System.Object, System.AccessViolationException) [C:...Program.cs @ 19]
PARAMETERS:
ecx (<CLR reg>) = 0x0245243c
edx (<CLR reg>) = 0x02452448
stack1 (0x001fee18) = 0x001fee38
stack2 (0x001fee14) = 0x02452468
LOCALS:
<no data>
[...]
0:000> !do ecx
Name:        CallingConvention.Program
[...]
0:000> !do edx
Name:        System.Int32[]
[...]
0:000> ? esp+8
Evaluate expression: 2092568 = 001fee18
0:000> dd esp+8 L1
001fee18  001fee38
0:000> ? esp+4
Evaluate expression: 2092564 = 001fee14
0:000> dd esp+4 L1
001fee14  02452468

CCD_ 35似乎认为推送的参数处于ESP+8和ESP+4。但这是不对的。0x001fee38不是有效对象。这是堆栈上的一个地址。

0:000> !do poi(esp)
Name:        System.AccessViolationException
[...]
0:000> !do poi(esp+4)
Name:        System.Object
[...]

相反,他们似乎在ESP和ESP+4。

0:000> kb L1
# ChildEBP RetAddr      Args to Child              
00 001fee18 007c089a     02452474 02452468 02452448 0x7c08b7
0:000> !mt
0:000> *** inside Console.WriteLine()
0:000> !clrstack -a
OS Thread Id: 0x44cc (0)
Child SP       IP Call Site
001fee10 007c08bc CallingConvention.Program.Method1(CallingConvention.Program, Int32[], System.Object, System.AccessViolationException) [C:UsersTsourcereposCallingConventionCallingConventionProgram.cs @ 20]
PARAMETERS:
ecx = <no data>
edx (<CLR reg>) = 0x02452448
stack1 (0x001fee24) = 0x02452468
stack2 (0x001fee20) = 0x02452474
LOCALS:
<no data>
[...]
0:000> r ecx
ecx=024578f4
0:000> !do ecx
Name:        System.IO.TextWriter+SyncTextWriter
[...]

此时,通过ECX传递的参数已被覆盖。

结论:.NET使用了__clrcall。它将前两个参数作为ECX和EDX传递,然后在堆栈上从右向左传递。

如何获得第一个参数?

您可以从ECX寄存器中获取第一个参数,只要该寄存器未被重用即可。

eip,esp,ebp是"服务";寄存器,据我所知,参数不会通过它们传递。ebx?

不,EBX没有参与调用约定,但它可能为局部变量服务。

最新更新