How does Windbg recover function arguments in x64 crash dumps? Or can it?

Whenever I use windbg on a x64 kernel dump, and go through the local variables for each of the callstack functions, I see a lot of junk values.

My question is, is this because the arguments to functions are lost in x64 crash dumps (since most of them are passed in registers) ?
If not, then why I see so many junk values in Local variables of x64 crash dumps, and how can Windbg recover the function arguments passed in register in call stack functions?

Local as in dv /v output, Locals window or kv callstack “parameters”?
The first two are the same and won’t be junk.
Kv output won’t show “parameters”, because even if they are passed on the
stack (more than 4 params needed), the local stack value may get reused or
the value changed. This is useless for any parameter viewing.

You can inspect the entire stack frame for values manually and guess more
than you can be sure.

Dejan.

Long answer:

In the Windows ABI the first four arguments are passed in RCX, RDX, R8, and R9. There is an area on the stack called the Home Space* that can be used to “home” the arguments onto the stack. This is always the first 32 bytes on the stack after the return address*. The remaining arguments to the function are then passed on the stack after the Home Space.

In the debug build of your code the compiler always homes the arguments onto the stack. This allows the debugger to always show you the arguments even if the registers get reused. You can see this behavior very clearly if you look at KeBugcheckEx, which is always compiled without optimizations:

3: kd> u nt!KeBugCheckEx
nt!KeBugCheckEx:
fffff800`02a7ef00 mov     qword ptr [rsp+8],rcx
fffff800`02a7ef05 mov     qword ptr [rsp+10h],rdx
fffff800`02a7ef0a mov     qword ptr [rsp+18h],r8
fffff800`02a7ef0f mov     qword ptr [rsp+20h],r9
fffff800`02a7ef14 pushfq

However, in the optimized build the function is free to use that Home Space however it wants. Some functions ignore it, some functions store non-volatile registers, some even home random arguments for some reason…For example:

3: kd> u nt!IoGetDmaAdapter
nt!IoGetDmaAdapter:
fffff800`02ee3610 mov     qword ptr [rsp+10h],rbx
fffff800`02ee3615 mov     qword ptr [rsp+18h],rbp
fffff800`02ee361a mov     qword ptr [rsp+20h],rsi
fffff800`02ee361f push    rdi

The debugger is dumb very trusting so it just always assumes the arguments are in the Home Space. So, sometimes you get junk and sometimes you get the right values. This is also why the parameters are always garbage if you break at the start of the function but magically fix themselves once you single step.

-scott

  • This is also called the Shadow Space or Spill Space…I like Home Space

  • Note that there’s always 32-bytes of Home Space, even if the function doesn’t take any arguments…

2 Likes

@“Scott_Noone_(OSR)” said:
Long answer:

In the Windows ABI the first four arguments are passed in RCX, RDX, R8, and R9. There is an area on the stack called the Home Space* that can be used to “home” the arguments onto the stack. This is always the first 32 bytes on the stack after the return address*. The remaining arguments to the function are then passed on the stack after the Home Space.

In the debug build of your code the compiler always homes the arguments onto the stack. This allows the debugger to always show you the arguments even if the registers get reused. You can see this behavior very clearly if you look at KeBugcheckEx, which is always compiled without optimizations:

3: kd> u nt!KeBugCheckEx
nt!KeBugCheckEx:
fffff800`02a7ef00 mov     qword ptr [rsp+8],rcx
fffff800`02a7ef05 mov     qword ptr [rsp+10h],rdx
fffff800`02a7ef0a mov     qword ptr [rsp+18h],r8
fffff800`02a7ef0f mov     qword ptr [rsp+20h],r9
fffff800`02a7ef14 pushfq

However, in the optimized build the function is free to use that Home Space however it wants. Some functions ignore it, some functions store non-volatile registers, some even home random arguments for some reason…For example:

3: kd> u nt!IoGetDmaAdapter
nt!IoGetDmaAdapter:
fffff800`02ee3610 mov     qword ptr [rsp+10h],rbx
fffff800`02ee3615 mov     qword ptr [rsp+18h],rbp
fffff800`02ee361a mov     qword ptr [rsp+20h],rsi
fffff800`02ee361f push    rdi

The debugger is dumb very trusting so it just always assumes the arguments are in the Home Space. So, sometimes you get junk and sometimes you get the right values. This is also why the parameters are always garbage if you break at the start of the function but magically fix themselves once you single step.

-scott

  • This is also called the Shadow Space or Spill Space…I like Home Space

  • Note that there’s always 32-bytes of Home Space, even if the function doesn’t take any arguments…

Wow, even after so many years of reading assembly I never knew this shadow space exists! Thanks for the detailed answer.

So basically in the release builds, the arguments to the functions are junks (since I assume the compiler will never pass arguments to the shadow stack in release builds when optimization is on), correct?

Also another question: Sometimes when I view the local variables on a crash dump, some of the local functions are straight up missing. So are these optimized out? if so, then does this mean that If i see a local (not function argument) variable in the local variables view of windbg, then it should always be correct (since in this case I at least know that it was not optimized out) ? And if it’s a junk value, then it means that it was overwritten with a junk value which means a stack overflow?

1 Like

Where are you looking, exactly?

I never knew this shadow space exists!

This is all part of the Windows x64 calling convention. Linux does it a little bit differently.

… the arguments to the functions are junks … the compiler will never pass arguments to the shadow stack in release builds …

The first 4 arguments go in rcx, rdx, r8, and r9. They are not stored on the stack, in either release or debug builds, even though space is allocated for them. In the debug build, the function that RECEIVES the parameters tucks them away on the stack to help the debugger.

… some of the local functions are straight up missing … then it should always be correct …

I assume you mean local variables. In general, you should not make any “should always be” assumptions with the debugger. It does the best that it can, but sometimes the information it needs is simply not present.

@Dejan_Maksimovic said:
Where are you looking, exactly?

Well, almost all of the crash dumps that I analyze contain a lot of junk values in Local view.
My main goal is to understand whether these junk values mean an overflow happened, or it’s because of loss of information (because of optimization, argument passing, etc).

@Tim_Roberts said:
I assume you mean local variables. In general, you should not make any “should always be” assumptions with the debugger. It does the best that it can, but sometimes the information it needs is simply not present.

I apologize, yes I meant local variables, not local functions.

So is there anyway that I can be 100% sure whether or not a local variable having a junk value is because of optimization/registry argument passing, or because of some overflow/corruption?

100% sure? No. Since the spill region happens BEFORE the function call, if there is a stack overflow, it will usually have trashed the return address as well. It would be a very specific corruption that hops over the return address.

1 Like

Locals, or " dv /v" command will not show random values if PDBs are present.
Both will show “variable is not available” for optimized away variables.

Unless corruption happened or Mosmatched PDBs, the values are very likely
to be correct here.
It is the STACK output (kv command or Stack window) that are not likely to
show any correct values for arguments.

@Dejan_Maksimovic said:
Locals, or " dv /v" command will not show random values if PDBs are present.
Both will show “variable is not available” for optimized away variables.

Unless corruption happened or Mosmatched PDBs, the values are very likely
to be correct here.
It is the STACK output (kv command or Stack window) that are not likely to
show any correct values for arguments.

But I have seen random values for some of the locals in the kernel crash dumps many many times, Specially in x64 crash dumps, and almost always they had nothing to do with the crash dump so I always assumed these random values/NULLs that happen in local variables in x64 is because of the fact that function arguments are lost since they are passed in registers (assuming its a release build)

Also if there is a pdb mismatch, then the symbols wont even load to begin with.

I asked 3 times now where you are looking and you repeat a reply that
doesn’t answer :slight_smile: