1

Writing x64 Assembly code using MASM, we can use these directives to provide frame unwinding information. For example, from .SETFRAME definition:

These directives do not generate code; they only generate .xdata and .pdata.

Since these directives don't produce any code, I cannot see their effects in Disassembly window. So, I don't see any difference, when I write assembly function with or without these directives. How can I see the result of these directives - using dumpbin or something else?

How to write code that can test this unwinding capability? For example, I intentionally write assembly code that causes an exception. I want to see the difference in exception handling behavior, when function is written with or without these directives.

In my case caller is written in C++, and can use try-catch, SSE etc. - whatever is relevant for this situation.

Alex F
  • 42,307
  • 41
  • 144
  • 212
  • Probably your best bet is to have your asm function call another C++ function, and have your C++ function throw a *C++* exception. (Not like illegal memory from asm that will fault and might result in an SEH). So create a situation where a C++ exception needs to unwind the stack through your asm function; if it works then you got the metadata directives correct. – Peter Cordes Mar 27 '20 at 12:01
  • @PeterCordes: And use `try-catch` in C++ function that calls Assembly function? Such as: `try { asmfunction();} catch{...}` – Alex F Mar 27 '20 at 12:03
  • Yes, that's what I said. With the `throw` in another C++ function you call *from* asm. BTW, the non-Windows (e.g. GNU/Linux) equivalent of this metadata is DWARF `.cfi` directives which create a `.eh_frame` section. I don't know equivalent details for Windows, but I do know they use similar metadata that makes it possible to unwind the stack without relying on RBP frame pointers. – Peter Cordes Mar 27 '20 at 12:04
  • Maybe you missed the edit to my previous comment. I don't know how the Windows stack-unwind metadata details, just what purpose it serves. – Peter Cordes Mar 27 '20 at 12:09
  • 1
    @PeterCordes: I tested your answer, it behaves exactly as you described. – Alex F Mar 27 '20 at 15:05

2 Answers2

3

Answering your question:

How can I see the result of these directives - using dumpbin or something else?

You can use dumpbin /UNWINDINFO out.exe to see the additions to the .pdata resulting from your use of .SETFRAME.

The output will look something like the following:

00000054 00001530 00001541 000C2070
Unwind version: 1
Unwind flags: None
Size of prologue: 0x04
Count of codes: 2
Frame register: rbp
Frame offset: 0x0
Unwind codes:
  04: SET_FPREG, register=rbp, offset=0x00
  01: PUSH_NONVOL, register=rbp

A bit of explanation to the output:

  1. The second hex number found in the output is the function address 00001530
  2. Unwind codes express what happens in the function prolog. In the example what happens is:
    • RBP is pushed to the stack
    • RBP is used as the frame pointer

Other functions may look like the following:

000000D8 000016D0 0000178A 000C20E4
Unwind version: 1
Unwind flags: EHANDLER UHANDLER
Size of prologue: 0x05
Count of codes: 2
Unwind codes:
  05: ALLOC_SMALL, size=0x20
  01: PUSH_NONVOL, register=rbx
Handler: 000A2A50

One of the main differences here is that this function has an exception handler. This is indicated by the Unwind flags: EHANDLER UHANDLER as well as the Handler: 000A2A50.

Ayman Salah
  • 1,039
  • 14
  • 35
2

Probably your best bet is to have your asm function call another C++ function, and have your C++ function throw a C++ exception. Ideally have the code there depend on multiple values in call-preserved registers, so you can make sure they get restored. But just having unwinding find the right return addresses to get back into your caller requires correct metadata to indicate where that is relative to RSP, for any given RIP.

So create a situation where a C++ exception needs to unwind the stack through your asm function; if it works then you got the stack-unwind metadata directives correct. Specifically, try{}catch in the C++ caller, and throw in a C++ function you call from asm.

That thrower can I think be extern "C" so you can call it from asm without name mangling. Or call it via a function pointer, or just look at MSVC compiler output and copy the mangled name into asm.


Apparently Windows SEH uses the same mechanism as plain C++ exceptions, so you could potentially set up a catch for the exception delivered by the kernel in response to a memory fault from something like mov ds:[0], eax (null deref). You could put this at any point in your function to make sure the exception unwind info was correct about the stack state at every point, not just getting back into sync before a function-call.

https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-170&viewFallbackFrom=vs-2019 has details about the metadata.


BTW, the non-Windows (e.g. GNU/Linux) equivalent of this metadata is DWARF .cfi directives which create a .eh_frame section.

I don't know equivalent details for Windows, but I do know they use similar metadata that makes it possible to unwind the stack without relying on RBP frame pointers. This lets compilers make optimized code that doesn't waste instructions on push rbp / mov rbp,rsp and leave in function prologues/epilogues, and frees up RBP for use as a general-purpose register. (Even more useful in 32-bit code where 7 instead of 6 registers besides the stack pointer is a much bigger deal than 15 vs. 14.)

The idea is that given a RIP, you can look up the offset from RSP to the return address on the stack, and the locations of any call-preserved registers. So you can restore them and continue unwinding into the parent using that return address.

The metadata indicates where each register was saved, relative to RSP or RBP, given the current RIP as a search key. In functions that use an RBP frame pointer, one piece of metadata can indicate that. (Other metadata for each push rbx / push r12 says which call-preserved regs were saved in which order).

In functions that don't use RBP as a frame pointer, every push / pop or sub/add RSP needs metadata for which RIP it happened at, so given a RIP, stack unwinding can see where the return address is, and where those saved call-preserved registers are. (Functions that use alloca or VLAs thus must use RBP as a frame pointer.)

This is the big-picture problem that the metadata has to solve. There are a lot of details, and it's much easier to leave things up to a compiler!

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • 2
    on windows in *PE* exist `IMAGE_DIRECTORY_ENTRY_EXCEPTION` data directory, which is array of `RUNTIME_FUNCTION` structs. then we can look [*x64 exception handling*](https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64?view=vs-2019). possible found corresponded `RUNTIME_FUNCTION` by function *rva* and print unwind instructons and compare with asm code.something like [here](https://github.com/vtjnash/dbghelp2/blob/master/dlls/dbghelp/cpu_x86_64.c) but code can be much more small – RbMm Mar 27 '20 at 12:35
  • C++ exception IS SEH exception on Windows. It's not "easier". – ScienceDiscoverer Aug 06 '22 at 11:59
  • 1
    @ScienceDiscoverer: Thanks for the correction, didn't realize they use the same catch as well as unwind mechanism, whether the exception comes from the kernel or from a `throw`. I'd still suggest calling a C++ function that does a `throw` instead of `mov ds:[0], eax` or something, although I guess that would let you test whether your unwind directives are correct at every point, not just synced up for calls. – Peter Cordes Aug 06 '22 at 12:05
  • @PeterCordes _The idea is that given a RIP, you can look up the offset from RSP to the return address on the stack, and the locations of any call-preserved registers. So you can restore them and continue unwinding into the parent using that return address._ I'm trying to wrap my hand around this... Can you explain it a bit more or maybe there is some good article/tutorial on how this mechanism works? As far as I understood, compiler stores start and end of function addresses like this `$pdata$wmain DD imagerel $LN3 DD imagerel $LN3+24 DD imagerel $unwind$wmain` – ScienceDiscoverer Aug 06 '22 at 12:15
  • @PeterCordes and then pointer to some exception handler in .xdata (I guess its some default handler if there is no user defined one?) But how it can actually calculate the unwind metrics is beyond me... – ScienceDiscoverer Aug 06 '22 at 12:16
  • 1
    @ScienceDiscoverer: I think the general idea is similar to DWARF stack-unwind info for Linux / ELF executables. It's not about finding an exception handler, it's about being able to unwind the stack *through* a function, i.e. to restore the call-preserved registers this function saved, before looking at the parent function. – Peter Cordes Aug 06 '22 at 12:27
  • 1
    Function start/end addresses aren't sufficient; you also need to know where each register was saved, relative to RSP or RBP, given the current RIP as a search key. So in functions that don't use an RBP frame pointer and one piece of metadata to indicate that, every push / pop or sub/add RSP needs metadata for which RIP it happened at. (Functions that use alloca or VLAs thus must use RBP as a frame pointer.) – Peter Cordes Aug 06 '22 at 12:28
  • If anybody will be interested in the same matter, [here is article](https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-170&viewFallbackFrom=vs-2019) that explains how SEH works in detail. To be honest, to me it seems harder than rocket science... – ScienceDiscoverer Aug 06 '22 at 13:15