Reputation: 895
I am in the process of studying the book Inside Windows Debugging
and cannot fully understand the technique used in the chapter Listing parameters and Locals for System Code
. As the name suggests, I'm trying to get a list of arguments passed to some function. An example of a stack:
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
In general, I have a full stack, but nevertheless I would like to be able to work with the output of commands like 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)
I am interested in the arguments of method B
having the following signature:
void B (string code, CancellationToken token, List<SomeObj> items, int taskCount)
My research is conducted on the assumption that .NET
uses the __stdcall calling convention:
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
: Saved EBP
(Previous Frame Pointer - A).
04561d6e
: RetAddr
points to the next piece of actual code.
00000002
: Arg: taskCount (I suppose that values like 00000002
represent values of primitive types).
1259e288
: Arg: items (Checked via !do 1259e288
).
0163dae4
: Arg: token (Checked via !do 0163dae4
).
12592918
: It was assumed that the given address will contain the string code
argument, but commands like !do
, du
do not return anything. In general, after this value, string code
(01639394
) is found, but it seems to refer to the previous method, because the value at 01698868
creates at method A
.
How do I get the first argument?
UPD
From the answer mentioned in the comments:
__clrcall is the calling convention for managed code. It is a blend of the other ones, this pointer passing like __thiscall, optimized argument passing like __fastcall, argument order like __cdecl and caller cleanup like __stdcall.
From __cdecl description:
Argument-passing order: Right to left. Stack-maintenance responsibility: Calling function pops the arguments from the stack.
This is similar to what I got.
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
From __fastcall description:
Argument-passing order: The first two DWORD or smaller arguments that are found in the argument list from left to right are passed in ECX and EDX registers; all other arguments are passed on the stack from right to left. Stack-maintenance responsibility: Called function pops the arguments from the stack.
eip
, esp
, ebp
are "service" registers, as I understand it, parameters are not passed through them. ebx
?
Upvotes: 1
Views: 378
Reputation: 59513
You best try this on a minimum reproducible example instead of a fully fledged application. The code I used was
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();
}
}
}
but it seems it can even be shorter. The debug session:
[...]
ntdll!LdrpDoDebuggerBreak+0x2b:
773d1a62 cc int 3
0:000> sxe ld clrjit
0:000> g
[...]
ModLoad: 56be0000 56c6a000 C:\Windows\Microsoft.NET\Framework\v4.0.30319\clrjit.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
So far that's just for setting up the necessary extensions and breakpoints.
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
At the beginning of the main method, there are no locals yet. But we can already see that args[]
is passed in the ECX register: !clrstack
says <CLR reg>
and we find the same value in 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]
Just 1 managed line later, the value of ECX has been overwritten, so we don't know what args[]
is any longer. And we don't need to, because it's never used.
Although new Program()
has been called, !clrstack
is unable to identify it as a local variable.
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
But the instance is there and it seems to be in EAX and 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
[...]
Another managed call later, we see the int[]
on the stack. EAX has changed, but EBX still holds the Program object.
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[]
[...]
I don't exactly know why, but the other two parameters are in EDI and 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
!clrstack
seems to think that the pushed parameters are at ESP+8 and ESP+4. But that's not correct. 0x001fee38
is not a valid object. It's an address on the stack.
0:000> !do poi(esp)
Name: System.AccessViolationException
[...]
0:000> !do poi(esp+4)
Name: System.Object
[...]
Instead they seem to be at ESP and 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:\Users\T\source\repos\CallingConvention\CallingConvention\Program.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
[...]
At this point, the parameter passed via ECX has been overwritten.
Conclusion: .NET uses __clrcall. It passes the first two parameters as ECX and EDX, then from right to left on the stack.
How do I get the first argument?
You can get the first argument from the ECX register as long as that register has not been reused.
eip, esp, ebp are "service" registers, as I understand it, parameters are not passed through them. ebx?
No, EBX is not involved in the calling convention, but it might have served for a local variable.
Upvotes: 1