When debugging a crashed application from a crash dump, you often see that arguments to functions in the call stack are no data. Let’s say you want to get arguments for Hoge.Program.Test(Hoge.Foo, Hoge.Bar).
Since x64 calling convention use rcx/rdx/r8/r9 for the first 4 arguments, it’s not automatically pushed to the stack before the function call. However, sometimes the copy is stored in stack as below.
Exception:
0:000> !pe
Exception object: 000000e1000068e0
Exception type: System.Exception
Message: dummy exception
InnerException:
StackTrace (generated):
SP IP Function
000000E17AADEF00 00007FFB516702BC Hoge!Hoge.Program.TestSub()+0x3c
000000E17AADEF40 00007FFB51670243 Hoge!Hoge.Program.Test(Hoge.Foo, Hoge.Bar)+0x123
000000E17AADEFB0 00007FFB516700FE Hoge!Hoge.Program.Main(System.String[])+0x6e
StackTraceString:
HResult: 80131500
Callstack:
0:000> !clrstack
OS Thread Id: 0x1bd4 (0)
Child SP IP Call Site
000000e17aadee18 00007ffbc0718b9c [HelperMethodFrame: 000000e17aadee18]
000000e17aadef00 00007ffb516702bc Hoge.Program.TestSub() [c:\Users\sokoide\Projects\Spike\Hoge\Program.cs @ 45]
000000e17aadef40 00007ffb51670243 Hoge.Program.Test(Hoge.Foo, Hoge.Bar) [c:\Users\sokoide\Projects\Spike\Hoge\Program.cs @ 40]
000000e17aadefb0 00007ffb516700fe Hoge.Program.Main(System.String[]) [c:\Users\sokoide\Projects\Spike\Hoge\Program.cs @ 29]
000000e17aadf2d0 00007ffbb0caa7f3 [GCFrame: 000000e17aadf2d0]
0:000> !clrstack -a
OS Thread Id: 0x1bd4 (0)
Child SP IP Call Site
000000e17aadee18 00007ffbc0718b9c [HelperMethodFrame: 000000e17aadee18]
000000e17aadef00 00007ffb516702bc Hoge.Program.TestSub() [c:\Users\sokoide\Projects\Spike\Hoge\Program.cs @ 45]
PARAMETERS:
this =
000000e17aadef40 00007ffb51670243 Hoge.Program.Test(Hoge.Foo, Hoge.Bar) [c:\Users\sokoide\Projects\Spike\Hoge\Program.cs @ 40]
PARAMETERS:
this =
foo =
bar =
LOCALS:
000000e17aadefb0 00007ffb516700fe Hoge.Program.Main(System.String[]) [c:\Users\sokoide\Projects\Spike\Hoge\Program.cs @ 29]
PARAMETERS:
args =
If you see the JIT compiled code for Hoge.Program.Test(Hoge.Foo, Hoge.Bar), you can confirm that rcx is copied in rbi, and the rbi is pushed into the stack at 00007ffb`51670123.
0:000> !U 00007ffb51670243
Normal JIT generated code
Hoge.Program.Test(Hoge.Foo, Hoge.Bar)
Begin 00007ffb51670120, size 131
c:\Users\sokoide\Projects\Spike\Hoge\Program.cs @ 34:
00007ffb`51670120 53 push rbx
00007ffb`51670121 55 push rbp
00007ffb`51670122 56 push rsi
00007ffb`51670123 57 push rdi
00007ffb`51670124 4154 push r12
00007ffb`51670126 4155 push r13
00007ffb`51670128 4883ec38 sub rsp,38h
00007ffb`5167012c 498be8 mov rbp,r8
00007ffb`5167012f 488bfa mov rdi,rdx
00007ffb`51670132 488bf1 mov rsi,rcx
00007ffb`51670135 33db xor ebx,ebx
00007ffb`51670137 bab0000000 mov edx,0B0h
00007ffb`5167013c b901000000 mov ecx,1
If you can get stack base address when Test(Hoge.Foo, Hoge.Bar) is called, you can get those copied registers which have the Hoge.Foo/Bar arguments.
Since child-sp when the Test function is called is 000000E17AADEFB0, 000000E17AADEFB0-0x8 is the ret address, and 000000E17AADEFB0-0x10 is the base address as below. Then you can get rcx (this), rdx (1st arg), r8 (2nd arg) stored in rsi(<-rcx), rdi(<-rdx), rbp(<-r8) respectively.
0:000> dq 000000e1`7aadef40 000000e1`7aadefff
[7aadef40 – 7aadef78] = 0x38 … local vars
000000e1`7aadef40 000000e1`00002d38 00000000`00000000
000000e1`7aadef50 000000e1`00005b0c 00000000`00000000
000000e1`7aadef60 000000e1`00000009 000000e1`00002d38
000000e1`7aadef70 000000e1`00002d68 000000e1`7aadf300 (r13)
000000e1`7aadef80 000000e1`7aadf348(r12) 000000e1`00002d50 (rdi)
000000e1`7aadef90 000000e1`00002d38(rsi) 000000e1`7aadf010 (rbp)
000000e1`7aadefa0 000000e1`00002d68(rbx) 00007ffb`516700fe (ret)
000000e1`7aadefb0 000000e1`00002d38(child-sp) 000000e1`00002d50
000000e1`7aadefc0 000000e1`00002d68 000000e1`00000001
000000e1`7aadefd0 000000e1`7aadf2d0 000000e1`7aadf128
000000e1`7aadefe0 000000e1`7aadf1a0 00007ffb`b0caa7f3
000000e1`7aadeff0 000000e1`00002cc8 000000e1`7aadf500
this = rcx)
0:000> !do 000000e1`00002d38
Name: Hoge.Program
MethodTable: 00007ffb51554038
EEClass: 00007ffb516624a8
Size: 24(0x18) bytes
File: C:\Users\sokoide\Projects\Spike\Hoge\bin\Release\Hoge.exe
Fields:
None
1st arg = rdx)
0:000> !do 000000e1`00002d50
Name: Hoge.Foo
MethodTable: 00007ffb51554120
EEClass: 00007ffb51662520
Size: 24(0x18) bytes
File: C:\Users\sokoide\Projects\Spike\Hoge\bin\Release\Hoge.exe
Fields:
MT Field Offset Type VT Attr Value Name
00007ffbafad0e08 4000012 8 System.String 0 instance 000000e100002ce8 k__BackingField
2nd arg = r8)
0:000> !do 000000e1`7aadf010
Invalid object
… looks the pointer doesn’t have the class info. However, we know the type is Hoge.Bar!
0:000> !dumpheap -type Bar
Address MT Size
000000e100002d68 00007ffb51554208 24
Statistics:
MT Count TotalSize Class Name
00007ffb51554208 1 24 Hoge.Bar
Total 1 objects
then we can get the object detail using !dumpvc and the method table and the object pointer as below.
0:000> !dumpvc 00007ffb51554208 000000e1`7aadf010
Name: Hoge.Bar
MethodTable: 00007ffb51554208
EEClass: 00007ffb51662598
Size: 24(0x18) bytes
File: C:\Users\sokoide\Projects\Spike\Hoge\bin\Release\Hoge.exe
Fields:
MT Field Offset Type VT Attr Value Name
00007ffbafad0e08 4000013 0 System.String 0 instance 000000e17aadf150 k__BackingField
Here is the target application which was used to demonstrate it.
class Foo
{
public string Name { get; set; }
}
class Bar
{
public string Name { get; set; }
}
class Program
{
static void Main(string[] args)
{
Program p = new Program();
Foo foo = new Foo { Name = "hoge" };
Bar bar = new Bar { Name = "page" };
p.Test(foo, bar);
}
private void Test(Foo foo, Bar bar)
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Hello {0}", i);
}
Console.WriteLine(foo.Name);
Console.WriteLine(bar.Name);
TestSub();
}
private void TestSub()
{
throw new Exception("dummy exception");
}
}