**Writing a custom Unhandled Exception Filter for Maya**
# Introduction #
Hello (again, if you've read any of my previous tutorials).
Today's tutorial will focus on creating what is known as an Unhandled Exception
Filter (a.k.a custom crash dump handler), that will allow us to bypass the
default Maya crash dump handler and capture more detailed information that we
can then access in a debugger such as WinDbg (or through writing our own parser
for the dump files, which we'll also go over in this tutorial). We'll also go
over some scenarios where a simple unhandled exception filter isn't enough to
intercept some types of crashes, and discuss possible strategies to accomodate
those situations.
!!! note Just Maya? I'm outta here...
Even though this tutorial is going to be targeted towards writing the filter
for Maya, the information here could really be applied to any application
executable, and so if you're interested in something like installing your
own crash handler in Blender, or Houdini, or even your own game engine,
you might still find this of interest!
# Code repository #
All the source code for this tutorial is available
[here](https://github.com/sonictk/maya_custom_unhandled_exception_filter_tutorial).
Perfunctory disclaimer: the code is written in a very tutorial-focused,
non-production manner. This means that there is very little
error-checking/handling going on, with brevity and readability favoured over
other options that might be more performant and/or scalable. The code is
available as a reference, and should not be taken as an indicator of what good
robust code shipped to end-users should look like.
If you are going to use the example code provided in production directly, do so
at your own risk.
# Why are we doing this? #
Ok, so let's talk a little bit about the current state of crash handling in
general when it comes to our day-to-day work in Maya.
The default Autodesk Maya crash handler is pretty dismal, all things
considered. It's essentially there to write out a minidump to disk, attempt to
invoke some code to attempt recovery of the scene (which, in my professional
experience, just causes more confusion when attempting to actually debug the
real crash dump). It also generally _crashes_ in the middle of its own crash
handling, in which case WER takes over (which can be a good thing sometimes,
since at least you can [easily customize what WER does in the event of a
crash](https://docs.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps).)
I've also never had a case in my professional experience where Autodesk actually
took advantage of their own crash handler to identify our issues; in every
single case where a dump was involved in triaging and identifying a root cause,
I was the one providing a reproducible case, or the dump and initial analysis
for our designated representative to forward to the engineering teams
instead. For this reason, among others (which I can't publicly go into here), I
don't feel too bad about ignoring the existing machinery surrounding the capture
of crashes.
For obvious reasons, this tutorial will only focus on Windows. Linux and macOS
have different facilities for handling exceptions and abnormal process
termination, and the techniques described in this document won't really apply to
those platforms. However, where I find certain information to be of use even in
a cross-platform environment, it will appear in the following manner:
!!! tip Crossing the platforms
Platform-specific information goes here.
# Requirements #
Let's go over what you'll be required to have with you before following along in
this tutorial.
## What you should know ##
- **Proficiency with C/C++**. This tutorial will not focus on basics, and is
meant to be more of a quickstart guide for programmers already experienced
with C++ to dive into the specifics of developing a custom unhandled
exception filter for Maya, or any other application out there. I have provided
sample code along with a basic `build.bat` script in the example code
accompanying this tutorial, which should be a welcome sight for those of you
battle-scarred by overly-convoluted build systems.
- **A good understanding of debugging on Windows**. I expect that you should, by
now, be able to use WinDbg to debug your code, and understand the basic
operations involved in operating the debugger. You should also have basic
comprehension skills when reading x64 assembly and know how to use a
disassembler for basic inspection of binaries.
- **Competency with Maya and the OpenMaya SDK**. This tutorial will not focus on
getting you up to speed with the specifics of the OpenMaya API and how Maya
works in general; there will not be detailed explanations of how Maya plug-ins
work, nor how the callback system works either. You are expected to read up on
these subjects on your own if you are unfamiliar with the material covered.
## What you will need ##
- Windows 10 and _a_ version of Autodesk Maya. This tutorial was
written against Maya 2020, but theoretically should work with any version of
Maya, as you'll see why later on.
- A **C/C++ development environment** set up and ready to go. (If you want to see
what my Emacs setup looks like, it's
available [here](https://github.com/sonictk/lightweight-emacs).)
- Visual Studio (For the compiler).
!!! warning MSVC Compiler versions
You should use the recommended MSVC compiler that Maya was built against.
Information on this is available in the SDK documentation and also on
Cyrille Fauvel's blog: https://around-the-corner.typepad.com/adn/2019/12/maya-2020-api-update-guide.html
- The Maya Devkit for your particular Maya version. These are (currently)
available at:
https://www.autodesk.com/developer-network/platform-technologies/maya
Again, if you're familiar with building Maya plug-ins, you'd probably already
have this lying around somewhere.
- [WinDbg](https://www.microsoft.com/en-us/p/windbg-preview/9pgjgd53tn86?activetab=pivot:overviewtab).
I recommend getting WinDbg Preview, but if you detest the Microsoft Store as
much as I do and insist on having local, non-DRM binaries, you can use the
[legacy version](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools) as well.
- A good disassembler if you want to follow along in some of the theory sections
of this tutorial. I make use of
both [IDA](https://www.hex-rays.com/products/ida/support/download_freeware/)
or [radare2/Cutter](https://rada.re/n/), but you're welcome to use any
disassembler you'd like, as long as it has x86/x64 architecture support. If
you don't have knowledge of x64 assembly, don't worry; I have [the tutorial
just for that](https://sonictk.github.io/asm_tutorial/). No guru knowledge
required here, just a basic working knowledge of assembly will do.
If you're experienced with writing Maya plug-ins, you probably would have these
already anyway, so there's not really that much extra prep you need to do.
Don't worry about knowing Win32 specifics, I'll go over what you need to know as
we go along. Let's begin!
# Exceptions, how do they even work, like #
Before we begin writing any code, however, let's go into detail about what
exactly we're dealing with here. The subject of the tutorial today is about
crash handling, so naturally, we're going to have to talk about exceptions, and
more specifically, how they're implemented on Windows.
!!! note x86 vs x64 exception handling
Because most modern environments make use of x64 Windows and it's getting
rarer to find anyone still running on x86 Windows, I'll only be going over
how exception handling works on x64, which is surprisingly different from
x86 (table-based vs frame-based). If you're interested about how SEH was
originally implemented, I recommend reading the excellent resource
[A Crash Course on the Depths of Win32 Structured Exception Handling](https://bytepointer.com/resources/pietrek_crash_course_depths_of_win32_seh.htm)
which goes over the topic in a much better fashion than I can.
## Win64: Structured Exception Handling (SEH) ##
Let's talk a little about how exceptions are handled at a high-level first on
Windows these days. First of all, when your code runs, unexpected behaviour
happens in one of two ways: either your code triggers a **software exception**,
or a **hardware exception** gets initiated by the CPU itself. A software
exception could be something like an application raising an exception when it
cannot allocate memory, or the OS itself detecting when an invalid parameter is
passed to a function (or the application itself simply calling
`RaiseException`). A hardware exception, on the other hand, can be triggered by
things such as a divide-by-zero operation, or a floating point overflow, or even
the most common type of exception that I'm sure most of us by now are innately
familiar with: an access violation (i.e. reading/writing to inaccessible
memory).
The Structured Exception Handling (SEH) mechanism has been around in Windows
forever (since at least Windows '95), and though its form has changed over the
decades since, fundamentally its purpose remains the same: it acts as an
abstraction over both hardware _and_ software-triggered exceptions, and allows
for programmers to have some degree of control over what happens in those
events, and, if they choose not to, to have the OS take over instead. It also
provides the standard for tooling around (i.e. debuggers, compilers) and thus
allows us not to have to worry about using bare interrupts or traps separately
to handle hardware exceptions.
So, how exactly is this implemented in modern-day Windows? Well, when your
executable gets compiled, a table is created by the compiler that contains the
metadata describing _all_ of the exception-handling code within the _entirety_
of the module, and is stuffed into the [PE header](https://docs.microsoft.com/en-us/windows/win32/debug/pe-format)
of the `.exe`. Upon triggering an exception, the OS will look at the table, find
the appropriate exception handler associated with it, and then execute it.
For example, taking the following [sample C++ code from MSDN](https://docs.microsoft.com/en-us/cpp/cpp/writing-an-exception-filter?view=vs-2019):
```cpp
#include
#include
int main() {
int Eval_Exception( int );
__try {}
__except ( Eval_Exception( GetExceptionCode( ))) {
printf("Exception handled");
}
}
BOOL SafeDiv(INT32 dividend, INT32 divisor, INT32 *pResult)
{
__try
{
*pResult = dividend / divisor;
}
__except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
return FALSE;
}
return TRUE;
}
void ResetVars( int ) {
printf("Clean up code");
}
int Eval_Exception ( int n_except ) {
if ( n_except != STATUS_INTEGER_OVERFLOW &&
n_except != STATUS_FLOAT_OVERFLOW ) // Pass on most exceptions
return EXCEPTION_CONTINUE_SEARCH;
// Execute some code to clean up problem
ResetVars( 0 ); // initializes data to 0
return EXCEPTION_CONTINUE_EXECUTION;
}
```
Compiles to this with `/Fa /Od` using x64 MSVC:
```text
?filt$0@?0??SafeDiv@@YAHHHPEAH@Z@4HA PROC ; `SafeDiv'::`1'::filt$0
push rbp
mov rbp, rdx
$LN7@filt$0:
; Line 22
mov QWORD PTR $T2[rbp], rcx
mov rax, QWORD PTR $T2[rbp]
mov rax, QWORD PTR [rax]
mov eax, DWORD PTR [rax]
mov DWORD PTR $T1[rbp], eax
mov eax, DWORD PTR $T1[rbp]
cmp eax, -1073741676 ; c0000094H
jne SHORT $LN4@filt$0
mov DWORD PTR tv67[rbp], 1
jmp SHORT $LN5@filt$0
$LN4@filt$0:
mov DWORD PTR tv67[rbp], 0
$LN5@filt$0:
mov eax, DWORD PTR tv67[rbp]
$LN9@filt$0:
pop rbp
ret 0
int 3
?filt$0@?0??SafeDiv@@YAHHHPEAH@Z@4HA ENDP ; `SafeDiv'::`1'::filt$0
```
There's no trace of our exception handling code in there. If we look at the
`.pdata` section of the compiled `.exe`, however:
```text
; Segment type: Pure data
; Segment permissions: Read
_pdata segment para public 'DATA' use64
assume cs:_pdata
;org 140085000h
ExceptionDir RUNTIME_FUNCTION
RUNTIME_FUNCTION
RUNTIME_FUNCTION
; The exception filter table entry
RUNTIME_FUNCTION
RUNTIME_FUNCTION
RUNTIME_FUNCTION
RUNTIME_FUNCTION
RUNTIME_FUNCTION
...
```
We see the table of metadata that the compiler stuffed inside the `.exe`, with
our exception filter `Eval_Exception` listed. And if we look at what a
`RUNTIME_FUNCTION`` is decompiled:
```text
RUNTIME_FUNCTION struc ; (sizeof=0xC, mappedto_1270)
; XREF: .rdata:000000014007F054/r
; .rdata:000000014007F068/r ...
FunctionStart dd ? ; offset rva
FunctionEnd dd ? ; offset rva pastend
UnwindInfo dd ? ; offset rva
RUNTIME_FUNCTION ends
```
Which lines up nicely with what we see from the MSDN documentation:
```cpp
typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY {
DWORD BeginAddress;
DWORD EndAddress;
union {
DWORD UnwindInfoAddress;
DWORD UnwindData;
} DUMMYUNIONNAME;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION, _IMAGE_RUNTIME_FUNCTION_ENTRY, *_PIMAGE_RUNTIME_FUNCTION_ENTRY;
```
!!! tip
Keep in mind that even though this is on an x64 architecture, for
_reasons__, the offsets are actually from the _base_ of the image, and are
not addresses or pointers.
The `UnwindInfoAddress` member points to an `UNWIND_INFO` structure, which looks
like the following in IDA:
```text
UNWIND_INFO struc ; (sizeof=0x4, mappedto_1271)
; XREF: .rdata:stru_140079FC0/r
; .rdata:stru_140079FC8/r ...
Ver3_Flags db ? ; base 16
PrologSize db ? ; base 16
CntUnwindCodes db ? ; base 16
FrReg_FrRegOff db ? ; base 16
UNWIND_INFO ends
; ---------------------------------------------------------------------------
UNWIND_CODE struc ; (sizeof=0x2, mappedto_1272)
; XREF: .rdata:0000000140079FC4/r
; .rdata:0000000140079FCC/r ...
PrologOff db ? ; base 16
OpCode_OpInfo db ? ; base 16
UNWIND_CODE ends
; ---------------------------------------------------------------------------
C_SCOPE_TABLE struc ; (sizeof=0x10, mappedto_1273)
Begin dd ? ; offset rva
End dd ? ; offset rva pastend
Handler dd ? ; offset rva
Target dd ? ; offset rva
C_SCOPE_TABLE ends
```
And again, lines up nicely with what we see according to MSDN:
```cpp
typedef struct _UNWIND_INFO {
UBYTE Version : 3;
UBYTE Flags : 5;
UBYTE SizeOfProlog;
UBYTE CountOfCodes;
UBYTE FrameRegister : 4;
UBYTE FrameOffset : 4;
UNWIND_CODE UnwindCode[1];
/* UNWIND_CODE MoreUnwindCode[((CountOfCodes + 1) & ~1) - 1];
* union {
* OPTIONAL ULONG ExceptionHandler;
* OPTIONAL ULONG FunctionEntry;
* };
* OPTIONAL ULONG ExceptionData[]; */
} UNWIND_INFO, *PUNWIND_INFO;
```
So, basically, if the `UnwindData` from the `RUNTIME_FUNCTION` structure has the
`UNW_FLAG_EHANDLER` bit set, the `FunctionStart` and `FunctionEnd` members tell
us where the function is in the `.exe` that makes use of SEH, and the
`UnwindInfoAddress` points to a `_UNWIND_INFO_` struct that describes where the
exception-handling code is within the procedure and its exception handlers.
The `ExceptionData` is stored as an offset to a pointer to a `_SCOPE_TABLE`
struct, which definition we can also find on MSDN:
```cpp
typedef struct _SCOPE_TABLE {
ULONG Count;
struct
{
ULONG BeginAddress;
ULONG EndAddress;
ULONG HandlerAddress;
ULONG JumpTarget;
} ScopeRecord[1];
} SCOPE_TABLE, *PSCOPE_TABLE;
```
Finally, we find it: the `HandlerAddress` specifies the offset to the exception
filter itself within the scope of the ``__except()`` statement itself. As you
can see, MSVC does a _lot_ behind the scenes whenever you use SEH to make sure
that the debugger is able to understand the context whenever an exception is
encountered, and to invoke the appropriate behaviour.
!!! tip
For more information,
[MSDN](https://docs.microsoft.com/en-us/cpp/build/exception-handling-x64?view=vs-2019)
has a great reference on this subject with more specific implementation
details, including the actual memory layout of the table and unwind
structures used to store the pointers to the exception handlers. You can
also take a look at [the x86 version](https://docs.microsoft.com/en-us/previous-versions/ms253960(v=vs.90)?redirectedfrom=MSDN)
as well for more information on the subject.
This is in direct contrast to how exception handling was handled on x86; there,
the compiler would build exception-handling structures and stuff it into each
function's prolog as they were compiled, effectively forming a linked-list of
exception handlers that would be stored on the stack at runtime. For security
and performance reasons (i.e. [buffer overflow attacks targeting the exception
information directly](https://www.exploit-db.com/docs/english/17505-structured-exception-handler-exploitation.pdf),
along with the fact that this approach necessitated overhead in the common case
since extra instructions would have to be executed to set up each function's
prolog even if no exceptions were triggered), this approach fell out of favour
with the move to the x64 calling conventions and architecture.
!!! note Oh, so exceptions perform better?
This doesn't mean that exceptions are a good thing now. With the move
to a table-based storage, exceptions on x64 incur more overhead in terms of
the amount of memory required to store the exception handler information as
opposed to the linked-list stack-based scheme. Additionally, while overhead
in the normal execution path is now reduced drastically, the overhead
involved when an exception is _actually_ triggered now goes up dramatically,
since there is a lot more indirection involved in executing each
handler. This is (partially) why you'll notice the usual arguments regarding
whether exceptions should be used in general under high-performance
scenarios. We're not going to get into that discussion right now, but
suffice to say that to deal with writing crash handlers on Windows, you're
going to have to deal with exceptions, or SEH at the very least.
So what does this mean for us, really? Well, we've learned:
1. Windows uses SEH on x64, and has first-class support for it both in the
kernel, debuggers and compilers.
2. Anytime we make use of `__try`, `__except`, `__finally` for MSVC,
the compiler creates a table in the PE header that contains data about all
the exception filters/handlers that are associated with each function scope
in your application, and where they are stored in the executable and when
they should be executed.
Since we don't have the source code for Maya itself, we need a way to be able to
tell the OS to use our own custom exception handler at runtime. Luckily, Windows
provides us a few facilities to do so, and we'll be making use of them
below. The first one we'll be going over is the **unhandled exception filter**,
which, as its name implies, handles all exceptions that have been unhandled up
to that point.
## `SetUnhandledExceptionFilter` and what it does ##
In addition to having special compiler-specific syntax to invoke SEH, Windows
has an additional trick up its sleeve by also allowing each thread of a process
to have its own **top-level exception handler**, that is, an exception handler
that will be invoked if no other appropriate exception handlers can be found by
the OS when one is triggered.
!!! tip Different top-level exception filters per-thread?
Technically, while each thread can potentially have its own top-level
exception filter, calling `SetUnhandledExceptionFilter` will replace the
filter for _all_ threads in the process (and any newly-spawned ones as
well). However, it's important to note that when the filter is executed, it
executes only in the thread that _actually_ triggered the exception, and
only the context of that thread is available to the handler.
Thus, we'll be making use of this in order to execute our exception-handling
code, since it allows us to register our function at runtime. As a matter of
fact, Maya (and most other win32 applications) already do this to allow them to
register their own crash handlers for capturing dumps from their customers in
order to help them analyze what went wrong.
## Windows Error Reporting (WER) ##
The next part of the exception handling workflow on Windows is **Windows Error
Reporting**. By default, in modern Windows (starting around Vista),
applications will now trigger WER when crashing, which basically spawns an
out-of-process application (i.e. `werfault.exe`) that is responsible, if
configured, to [collect user-mode dumps](https://docs.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps)
from your application that just crashed in order to help you troubleshoot the
issue. The out-of-process aspect means that this is much more robust against
dealing with certain types of crashes (stack corruption, hangs, etc.). By
default, if an SEH is triggered, no appropriate frame-based exception handlers
were found, and the application has not registered any top-level unhandled
exception filters, the OS will invoke WER after the application is terminated by
the OS, allowing for writing a minidump (by default) for diagnostic
purposes.
However, there are several disadvantages to us working with WER and its exposed API:
- It sends your data to Microsoft, and then you have to request it from
Microsoft after. Security and privacy notwithstanding, this is just ridiculous
in order to get information that really should be accessible directly to
developers without jumping through all these hoops (Yes, account registration
and approval from Microsoft is required).
- The WER API itself is fairly limited and you don't really have control over
the actions taken by WER itself; you can essentially register blocks of memory
or ask for specific files to be included in your dump, but you can't really do
more than that. It's alright, but definitely not the same as being able to
execute arbitrary code from within an unhandled exception filter itself.
!!! tip Possible usage of WER for this purpose...?
Capturing information from out-of-process rather than attempting to handle
exceptions and writing out a minidump from within the same process makes
total sense, and I do wish Microsoft hadn't designed the system to require
their explicit involvement in order to use.
However while I won't be investigating WER in this tutorial, I do
encourage you to read [this excellent series of articles](https://peteronprogramming.wordpress.com/2016/05/29/crashes-you-cant-handle-easily-1-seh-failure-on-x64-windows/)
on the subject of WER and potential workarounds for the problems listed above.
That doesn't mean we're going to ignore WER, however; another nice trait about
it is that if an unhandled exception filter _crashes_ while it is itself
running, WER will _still_ get invoked and automatically write out a user-mode
dump for us. That way, we can still get a callstack of what was involved in the
crash. Let's go ahead and set it up to do so as-per [the instructions from MSDN](https://docs.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps).
We'll want to specify the `DumpCount` to something more reasonable, like `30` or
higher, capture a mini dump (By setting `DumpType` to `1`), and making sure that
`DumpFolder` points to somewhere convenient and have access to.
!!! tip Why not capture full dumps?
I actually normally do (and I recommend you do too when the situation calls
for it), but for the purposes of this tutorial, minidumps are faster to
write out, and in the first place, this tutorial is about putting our own
information that we want in a custom dump of our own that wouldn't be easily
accessible in a full dump anyway.
As mentioned in MSDN, feel free to specialize the settings for just `maya.exe`
if you'd like; otherwise, the settings you put in the registry will affect _all_
programs that crash on your system, not just Maya itself.
# Making Maya crash on-demand #
Before we start writing code for our unhandled exception filter, let's write the
code for our actual use case first: crashing Maya.
## Types of exceptions ##
Today, I'm just going to go over some of the more common ones that I run
into. These are by no means comprehensive, and not all types of crashes might
trigger SEH filters either (which I'll also demonstrate), so keep that in
mind. The crashes that we'll tackle here are:
1. Null pointer deference (By far, the #1 cause of concern)
2. Out-of-bounds-access in STL containers
3. Stack corruption
4. Pure virtual function calls
5. Stack overflow
6. Calling `std::abort` directly
We're going to implement all of this behaviour shortly, but before that, let's
talk a little bit more about exceptions and how they work with debuggers.
## First/second-chance exceptions ##
There's another concept we should probably go over before getting into writing
code, which is the concept of **first-chance/second-chance** exceptions. On
Windows, if you are running your program under the control of a debugger and
encounter an exception, the OS will first notify the debugger _even before
walking the exception table_ to find the appropriate exception handler, in order
to give the debugger a chance to handle breakpoints or single-step
exceptions. This is what's called the **first-chance notification**. After that,
if ther OS cannot find any exception handlers to handle the exception, the OS
will notify the debugger _again_ to allow it to possibly handle the exception or
grab data from the process, before finally terminating the process being
debugged itself.
What does this concept mean for us? It means that if we want to test our
exception handler, we _shouldn't do it with the debugger attached_, since the
first-chance notification will kick in and stop us from further debugging the
exception filter code itself. One hacky workaround is to have the exception
filter raise a message dialog at the beginning of its execution either through
`MessageBoxA` or similar (which will halt the application), so that the
first-chance notification would already have passed, wait for the message box to
display, _then_ attach the debugger to it in order to debug the code running in
the exception filter itself.
There are other ways to get around this, such as running the filter from
_within_ an `__except` statement itself, like so:
```cpp
__try
{
char *p = NULL;
*p = 5;
}
__except(mayaCustomUnhandledExceptionFilter(GetExceptionInformation()))
{
printf("Handling exception.");
}
```
This will cause the OS to _still_ notify the debugger (assuming we single-step
or set a breakpoint on the print statement within the `__except`) after the
first-chance when the exception is raised and not skip the `__except` clause,
thus allowing us to debug the code within the filter itself.
## Writing the command plug-in ##
The boilerplate for the command should be pretty familiar to anyone who has
written anything in Maya before, so I won't go over it in detail. We're
basically going to make a new command called `mayaForceCrash`, which takes a
`-crashType` argument and allows us to bring Maya down in various ways.
Header file for our `MPxCommand` is as follows:
```cpp
#ifndef MAYA_CUSTOM_UNHANDLED_EXCEPTION_FILTER_CMD_H
#define MAYA_CUSTOM_UNHANDLED_EXCEPTION_FILTER_CMD_H
#include
#include
#include
#define MAYA_FORCE_CRASH_CMD_NAME "mayaForceCrash"
#define MAYA_CRASH_CMD_HELP_FLAG_SHORTNAME "-h"
#define MAYA_CRASH_CMD_HELP_FLAG_NAME "-help"
#define MAYA_CRASH_CMD_CRASH_TYPE_FLAG_SHORTNAME "-ct"
#define MAYA_CRASH_CMD_CRASH_TYPE_FLAG_NAME "-crashType"
#define MAYA_CRASH_CMD_HELP_TEXT "Triggers a crash for debugging purposes."
enum MayaForceCrashType
{
MayaForceCrashType_NoCrash = 0,
MayaForceCrashType_NullPtrDereference,
MayaForceCrashType_Abort,
MayaForceCrashType_OutOfBoundsAccess,
MayaForceCrashType_StackCorruption,
MayaForceCrashType_PureVirtualFuncCall,
MayaForceCrashType_StackOverflow
};
struct MayaForceCrashCmd : public MPxCommand
{
static void *creator();
MStatus doIt(const MArgList &args);
MStatus redoIt();
MStatus undoIt();
bool isUndoable() const;
static MSyntax newSyntax();
MStatus parseArgs(const MArgList &args);
bool flagHelp;
int crashType;
};
#endif /* MAYA_CUSTOM_UNHANDLED_EXCEPTION_FILTER_CMD_H */
```
And the implementation:
```cpp
#include "maya_custom_unhandled_exception_filter_cmd.h"
#include
void *MayaForceCrashCmd::creator()
{
MayaForceCrashCmd *cmd = new MayaForceCrashCmd();
cmd->flagHelp = false;
cmd->crashType = MayaForceCrashType_NoCrash;
return cmd;
}
MSyntax MayaForceCrashCmd::newSyntax()
{
MSyntax syntax;
syntax.enableQuery(false);
syntax.enableEdit(false);
syntax.useSelectionAsDefault(false);
syntax.addFlag(MAYA_CRASH_CMD_HELP_FLAG_SHORTNAME,
MAYA_CRASH_CMD_HELP_FLAG_NAME);
syntax.addFlag(MAYA_CRASH_CMD_CRASH_TYPE_FLAG_SHORTNAME,
MAYA_CRASH_CMD_CRASH_TYPE_FLAG_NAME,
MSyntax::kLong);
return syntax;
}
MStatus MayaForceCrashCmd::parseArgs(const MArgList &args)
{
MStatus result;
MArgDatabase argDb(this->syntax(), args, &result);
CHECK_MSTATUS_AND_RETURN_IT(result);
if (argDb.isFlagSet(MAYA_CRASH_CMD_HELP_FLAG_SHORTNAME)) {
MGlobal::displayInfo(MAYA_CRASH_CMD_HELP_TEXT);
this->flagHelp = true;
return MStatus::kSuccess;
}
if (argDb.isFlagSet(MAYA_CRASH_CMD_CRASH_TYPE_FLAG_SHORTNAME)) {
result = argDb.getFlagArgument(MAYA_CRASH_CMD_CRASH_TYPE_FLAG_SHORTNAME, 0, this->crashType);
CHECK_MSTATUS_AND_RETURN_IT(result);
}
return result;
}
#pragma warning(disable : 4717)
__declspec(noinline) void StackOverflow1 (volatile unsigned int* param)
{
volatile unsigned int dummy[256];
dummy[*param] %= 256;
StackOverflow1 (&dummy[*param]);
}
MStatus MayaForceCrashCmd::redoIt()
{
MStatus result = MStatus::kSuccess;
switch (this->crashType) {
case MayaForceCrashType_NullPtrDereference:
{
break;
}
case MayaForceCrashType_Abort: // TODO: (sonictk) This doesn't seem to call it either.
{
break;
}
case MayaForceCrashType_OutOfBoundsAccess:
{
break;
}
case MayaForceCrashType_StackCorruption:
{
break;
}
case MayaForceCrashType_PureVirtualFuncCall:
{
break;
}
case MayaForceCrashType_StackOverflow:
{
break;
}
case MayaForceCrashType_NoCrash:
default:
MGlobal::displayWarning("Invalid crash type specified.");
break;
}
return result;
}
MStatus MayaForceCrashCmd::doIt(const MArgList &args)
{
this->clearResult();
MStatus stat = this->parseArgs(args);
CHECK_MSTATUS_AND_RETURN_IT(stat);
if (this->flagHelp == true) {
return MStatus::kSuccess;
}
return this->redoIt();
}
MStatus MayaForceCrashCmd::undoIt()
{
return MStatus::kSuccess;
}
bool MayaForceCrashCmd::isUndoable() const
{
return false;
}
```
Pretty basic skeleton setup. So let's go ahead and start filling in the gaps for
the crash logic, starting with the simplest (and yet most common) case: a `NULL`
pointer deference.
```cpp
case MayaForceCrashType_NullPtrDereference:
{
char *p = NULL;
*p = 5;
break;
}
```
Abort and out-of-bounds access are pretty straightforward as well:
```cpp
case MayaForceCrashType_Abort:
{
abort();
break;
}
case MayaForceCrashType_OutOfBoundsAccess:
{
std::vector v;
v[0] = 5;
break;
}
```
Simulating stack corruption is a little more tricky. Taking a trick
from [peteronprogramming](https://peteronprogramming.wordpress.com/) here, we
use a special MSVC intrinsic,
[`_AddressOfReturnAddress`](https://docs.microsoft.com/en-us/cpp/intrinsics/addressofreturnaddress?view=vs-2019),
that provides the return address of the current procedure. By overwriting what
it returns with a garbage address, we essentially corrupt the stack ourselves.
```cpp
case MayaForceCrashType_StackCorruption:
{
*(uintptr_t *)_AddressOfReturnAddress() = 0x1234;
break;
}
```
Another common problem in OOP-heavy codebases (which thankfully I don't deal
much with personally, but professionally it is a never-ending nightmare) is
inheritance and lots of interfaces. As such, pure virtual function calls are an
unfortunate reality of most C++ codebases which we should simulate as well.
```cpp
case MayaForceCrashType_PureVirtualFuncCall:
{
struct A
{
A() { bar(); }
virtual void foo() = 0;
void bar() { foo(); }
};
struct B: A
{
void foo() {}
};
A *a = new B;
a->foo();
break;
}
```
The final crash type we're going to test is stack overflow. Ha, ha. As it turns
out, simulating it is just slightly tricky: we basically disable MSVC's stack
overflow warnings, and also ensure that our function won't get inlined in order
to take up as much stack space as possible.
```cpp
#pragma warning(disable : 4717)
__declspec(noinline) void StackOverflow1 (volatile unsigned int* param)
{
volatile unsigned int dummy[256];
dummy[*param] %= 256;
StackOverflow1 (&dummy[*param]);
}
case MayaForceCrashType_StackOverflow: // TODO: (sonictk) Also doesn't get caught by the handlers.
{
unsigned int initial = 3;
StackOverflow1(&initial);
break;
}
```
With that, we should be able to run our `mayaForceCrash` command within Maya,
and force Maya to exit in a few different ways. Great! Let's actually start
writing the exception filter itself now.
# Handling the crashes ourselves #
The basic form of an unhandled exception filter is exceedingly simple. From
MSDN:
```cpp
LONG WINAPI UnhandledExceptionFilter(
LPEXCEPTION_POINTERS exceptionInfo
);
```
It is basically a callback function that returns a value to indicate to the
system whether to continue searching for additional handlers, or whether to
execute the handler directly. We pass it a pointer to an `_EXCEPTION_POINTERS`
structure, which contains information about the exception that occurred along
with the thread context in which it was triggered. Thus:
```cpp
LONG WINAPI mayaCustomUnhandledExceptionFilter(LPEXCEPTION_POINTERS exceptionInfo)
{
return EXCEPTION_EXECUTE_HANDLER;
}
```
Will be the basic skeleton of the handler, and we register this when we first
initialize the Maya plugin:
```cpp
static LPTOP_LEVEL_EXCEPTION_FILTER gPrevFilter = NULL;
MStatus initializePlugin(MObject obj)
{
MFnPlugin plugin(obj, PLUGIN_AUTHOR, PLUGIN_VERSION, PLUGIN_REQUIRED_API_VERSION);
gPrevFilter = ::SetUnhandledExceptionFilter(mayaCustomUnhandledExceptionFilter);
mstat = plugin.registerCommand(MAYA_FORCE_CRASH_CMD_NAME,
MayaForceCrashCmd::creator,
MayaForceCrashCmd::newSyntax);
CHECK_MSTATUS_AND_RETURN_IT(mstat);
return mstat;
}
```
Since `SetUnhandledExceptionFilter` returns the previous top-level exception
filter that was in use, we can make use of that to ensure that when we unload
our plug-in, we also restore the original Maya one as well:
```cpp
MStatus uninitializePlugin(MObject obj)
{
::SetUnhandledExceptionFilter(gPrevFilter);
MFnPlugin plugin(obj);
mstat = plugin.deregisterCommand(MAYA_FORCE_CRASH_CMD_NAME);
return mstat;
}
```
That's it! Technically, after writing this code, any time you load your plug-in
within Maya now, you should have overridden Maya's in-built crash handler, and
it will now execute our own code when Maya encounters any unhandled exceptions
under SEH. Obviously, however, our filter doesn't really do anything at the
moment. Let's start changing that, and at the same time, plan out what exactly
it is our filter needs to do.
## What our filter will do ##
So, now that we have a basic skeleton of our filter written out, what exactly do
we want it to do? Well, first of all, it would be nice to be able to store some
additional information in a minidump that would normally not be possible through
either WER or that the default Maya crash handler doesn't do for us. For
example, what if I could store the last MEL proc that executed, or the last
change to the DAG that an end-user did? Or heck, even storing basic information
such as the Maya scene that the user had open, along with the Maya referenced
files and/or textures in that scene would already be useful.
For starters, we're going to be focusing on implementing the following:
***********************************************************************************
* *
* custom plugin dll *
* +--------------------------------------+ *
* | | *
* | +-------------------------------+ | 3. *
* | | |----------------------. *
* | | Custom unhandled exception| | | *
* | | filter proc |<------------------. | *
* | +-------------------------------+ | | | *
* | | | | | | *
* | | Storage for useful information| | | | *
* | | captured during the Maya | | | | *
* | | session (scene filename, | | | | *
* | | DAG changes, MEL procs, etc.) | | | | *
* | +-------------------------------+ | | | *
* +-------------+------------------------+ | | *
* | | | *
* SetUnhandledExceptionFilter | 1. | | *
* overrides Maya's default | +------------------------+ | | *
* top-level unhandled | | | | | *
* exception handler | | maya.exe | | | *
* '----------> | | | *
* +----------+-------------+ | | *
* | | | *
* Unhandled exception is thrown | 2. | | *
* and OS invokes the top-level | | | *
* filter to handle it '-----------------' | *
* (no debugger) | *
* | *
* | *
* | *
* | *
* | *
* .---------------------------------------' *
* | *
* | *
* | *
* | *
* v *
* +--------------------------------+ *
* | Writes out minidump to disk | *
* | with additional Maya-specific | *
* | context | *
* | | *
* | | *
* | | *
* +--------------------------------+ *
* *
***********************************************************************************
[Figure [diagram]: The initial implementation.]
We already implemented a little bit of Step 1, so let's go ahead and implement
a few examples of storing some Maya-specific context within our plug-in's `.bss`
data segment that could be useful when we're debugging our crash dump later on.
!!! tip Why store in the `.bss` segment?
No particular reason: you could also pre-allocate some memory from the heap
and that would essentially serve the same purpose as storing it as global
variables. However, what I would advise against is calling into Maya
functions or even doing anything with the heap _at_ the time of the crash
itself, as you can't reason fully about the state of the program at that
point, and it's far less risky to just read from `.bss` data or some other
pre-allocated memory location rather than enter new function calls or make
syscalls to allocate new pages of memory, etc.
Let's start with something simple: storing the path to the current Maya scene
(if any).
```cpp
static char gMayaCurrentScenePath[MAX_PATH] = {0};
void mayaSceneAfterOpenCB(void *unused)
{
(void)unused;
const MString curFileNameMStr = MFileIO::currentFile();
const unsigned int lenCurFileName = curFileNameMStr.length();
memcpy(gMayaCurrentScenePath, curFileNameMStr.asChar(), lenCurFileName);
memset(gMayaCurrentScenePath + lenCurFileName, 0, 1);
return;
}
MStatus initializePlugin(MObject obj)
{
MFnPlugin plugin(obj, PLUGIN_AUTHOR, PLUGIN_VERSION, PLUGIN_REQUIRED_API_VERSION);
gPrevFilter = ::SetUnhandledExceptionFilter(mayaCustomUnhandledExceptionFilter);
MStatus mstat;
gMayaSceneAfterOpen_cbid = MSceneMessage::addCallback(MSceneMessage::kAfterOpen, mayaSceneAfterOpenCB, NULL, &mstat);
CHECK_MSTATUS_AND_RETURN_IT(mstat);
mayaSceneAfterOpenCB(NULL);
mstat = plugin.registerCommand(MAYA_FORCE_CRASH_CMD_NAME,
MayaForceCrashCmd::creator,
MayaForceCrashCmd::newSyntax);
CHECK_MSTATUS_AND_RETURN_IT(mstat);
return mstat;
}
MStatus uninitializePlugin(MObject obj)
{
::SetUnhandledExceptionFilter(gPrevFilter);
MStatus mstat = MMessage::removeCallback(gMayaSceneAfterOpen_cbid);
CHECK_MSTATUS_AND_RETURN_IT(mstat);
MFnPlugin plugin(obj);
mstat = plugin.deregisterCommand(MAYA_FORCE_CRASH_CMD_NAME);
return mstat;
}
```
Again, very straightforward stuff with the Maya API: we register a callback to
record the scene filename every time a new scene is opened, and update the
global variable each time that happens. That way, when we do hit a crash, we
don't need to call any functions, and just need to read from the already-written
memory to get our string.
Very good; we now are able to save the most basic of information about the scene
state when switching Maya scenes, and storing it somewhere readily accessible
when we hit a crash. Let's go ahead and take on Step 3, where we actually now
write this information to disk.
## Writing the Unhandled Exception Filter ##
Let's fill out the handler a little bit with some rudimentary logic to prepare
for writing the dump file:
```cpp
#define MINIDUMP_FILE_NAME "MayaCustomCrashDump.dmp"
#define DEFAULT_TEMP_DIRECTORY "C:/temp"
#define TEMP_ENV_VAR_NAME "TEMP"
LONG WINAPI mayaCustomUnhandledExceptionFilter(LPEXCEPTION_POINTERS exceptionInfo)
{
char tempDirPath[MAX_PATH] = {0};
DWORD lenTempDirPath = ::GetEnvironmentVariable((LPCTSTR)TEMP_ENV_VAR_NAME, (LPTSTR)tempDirPath, (DWORD)MAX_PATH);
if (lenTempDirPath == 0) {
static const size_t lenDefaultTempDirPath = strlen(DEFAULT_TEMP_DIRECTORY);
memcpy(tempDirPath, DEFAULT_TEMP_DIRECTORY, lenDefaultTempDirPath);
memset(tempDirPath + lenDefaultTempDirPath, 0, 1);
}
char dumpFilePath[MAX_PATH] = {0};
snprintf(dumpFilePath, MAX_PATH, "%s\\%s", tempDirPath, MINIDUMP_FILE_NAME);
HANDLE hFile = CreateFile(dumpFilePath, GENERIC_READ|GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == NULL || hFile == INVALID_HANDLE_VALUE) {
::MessageBoxA(NULL, MSG_UNABLE_TO_WRITE_DUMP, MSG_UNHANDLED_EXCEPTION, MB_OK|MB_ICONSTOP);
return EXCEPTION_CONTINUE_SEARCH;
}
CloseHandle(hFile);
return EXCEPTION_EXECUTE_HANDLER;
}
```
Even if you're familiar with Win32 API code, this should be pretty
self-explanatory: we find the system's temporary directory as defined by the
`%TMP%` environment variable and open a file handle to a new minidump file,
`MayaCustomCrashDump.dmp`, where we can write into in the next step, falling
back appropriately on defaults if the system-provided environment variables
aren't available. If we can't open the file handle, we just raise an error
dialog, and tell the system to proceed with normal execution (which, since we
are already the top-level filter, should pop up the infamous "Application Error"
dialog box depending on the behaviour of the app via `SetErrorMode`.) by
returning `EXCEPTION_CONTINUE_SEARCH`. If we _do_ succeed, we tell the system
to return from our handler, which usually just means that the system will
terminate the application straightaway after since again, we are the top-level
exception filter for the application at this point.
Great! Let's go ahead and take a look at the structure of a minidump on Windows,
and how we can stuff the information we want about the Maya scene into it.
### Custom user streams ###
Minidumps start with a `MINIDUMP_EXCEPTION_INFORMATION` structure, which looks
like the following:
```cpp
typedef struct _MINIDUMP_EXCEPTION_INFORMATION {
DWORD ThreadId;
PEXCEPTION_POINTERS ExceptionPointers;
BOOL ClientPointers;
} MINIDUMP_EXCEPTION_INFORMATION, *PMINIDUMP_EXCEPTION_INFORMATION;
```
This is trivial to fill out as the exception information is already passed into
our filter when it is invoked, as shown below:
```cpp
MINIDUMP_EXCEPTION_INFORMATION dumpExceptionInfo = {0};
dumpExceptionInfo.ThreadId = ::GetCurrentThreadId();
dumpExceptionInfo.ExceptionPointers = exceptionInfo;
dumpExceptionInfo.ClientPointers = TRUE;
```
The `ClientPointers` member merely indicates if the memory regions referred to
by `ExceptionPointers` resides in the same process being debugged by the
debugger. The answer is yes (since we are going to be debugging `maya.exe` when
inspecting our minidump, not the debugger process itself), thus we set it to
`TRUE`.
The next step is storing our own data within the dump. For that, we need a
`MINIDUMP_USER_STREAM` structure instead:
```cpp
typedef struct _MINIDUMP_USER_STREAM {
ULONG32 Type;
ULONG BufferSize;
PVOID Buffer;
} MINIDUMP_USER_STREAM, *PMINIDUMP_USER_STREAM;
```
Again, nothing complicated. I'm liking this a lot already! We basically have a
variable-length buffer that has a size indicated, and has a
`MINIDUMP_STREAM_TYPE` to indicate its type. For our purposes, we're going to
specify a type of `CommentStreamA`, which means we're just storing an ANSI
string (i.e. the path to our Maya scene) in the dump for later review during debugging.
!!! tip
If you have Unicode file paths, you can modify the code above to store
`wchar_t` file paths and specify `CommentStreamW` if you'd prefer. Remember
to use the correct string handling functions and account for the increased
buffer size as well!
```cpp
MINIDUMP_USER_STREAM dumpMayaFileInfo = {0};
dumpMayaFileInfo.Type = CommentStreamA;
dumpMayaFileInfo.BufferSize = MAX_PATH;
dumpMayaFileInfo.Buffer = gMayaCurrentScenePath;
```
We then need to stuff our stream into another structure. From MSDN:
```cpp
typedef struct _MINIDUMP_USER_STREAM_INFORMATION {
ULONG UserStreamCount;
PMINIDUMP_USER_STREAM UserStreamArray;
} MINIDUMP_USER_STREAM_INFORMATION, *PMINIDUMP_USER_STREAM_INFORMATION;
```
Again, nothing complicated, just a structure that points to an array of
`MINIDUMP_USER_STREAM` strucutres:
```cpp
#define ARRAY_SIZE(x) sizeof(x) / sizeof(x[0])
MINIDUMP_USER_STREAM streams[] = {
dumpMayaFileInfo
};
MINIDUMP_USER_STREAM_INFORMATION dumpUserInfo = {0};
dumpUserInfo.UserStreamCount = ARRAY_SIZE(streams);
dumpUserInfo.UserStreamArray = streams;
```
And that's it! We're now ready to write the actual dump out to disk.
### Writing the minidump ###
Windows helpfully provides the `MiniDumpWriteDump` function for us to be able to
write out dumps to disk from the current process. It's also fairly
straightforward:
```cpp
BOOL MiniDumpWriteDump(
HANDLE hProcess,
DWORD ProcessId,
HANDLE hFile,
MINIDUMP_TYPE DumpType,
PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
PMINIDUMP_CALLBACK_INFORMATION CallbackParam
);
```
Thus, we can do the following to write out our own dump from within our
exception handler:
```cpp
// ... previous code in the exception handler
static const DWORD miniDumpFlags = MiniDumpNormal;
BOOL dumpWritten = ::MiniDumpWriteDump(::GetCurrentProcess(), ::GetCurrentProcessId(), hFile, (MINIDUMP_TYPE)miniDumpFlags, &dumpExceptionInfo, &dumpUserInfo, NULL);
if (dumpWritten == false) {
::MessageBoxA(NULL, MSG_UNABLE_TO_WRITE_DUMP, MSG_UNHANDLED_EXCEPTION, MB_OK|MB_ICONSTOP);
return EXCEPTION_CONTINUE_SEARCH;
} else {
char msg[MAX_PATH] = {0};
snprintf(msg, MAX_PATH, "An unrecoverable error has occured and the application will now close.\nA minidump file has been written to the following location for debugging purposes:\n%s", dumpFilePath);
::MessageBoxA(NULL, msg, MSG_UNHANDLED_EXCEPTION, MB_OK|MB_ICONSTOP);
}
CloseHandle(hFile);
return EXCEPTION_EXECUTE_HANDLER;
```
This should all be pretty obvious. We attempt to write out the dump file using
the structures that we have set up previously, check if it succeeds, and either
show an error message with the path to the dump file that was written out for
the end-user's convenience, or, if an error occurred while attempting to write
out the dump, exit our handler and allow normal execution to resume (which
should have Windows show the Application Error pop-up dialog). Finally, we close
the file handle to the dump file and we're done!
Go ahead and try now. Enter `mayaForceCrash -ct `, and see our crash
handler get invoked instead of Maya's own!

If you open the crash in WinDbg and analyze it (`!analyze -v`), you should also
now see your scene name that we previously stored in a custom user stream now
show up in the debugger output as well:
```text
COMMENT: C:/Users/sonictk/Documents/maya/projects/default/untitled
```
Great, so we're done! Right? Well, no.
Try executing a crash that involves stack corruption. For reference, that's this
bit of code here that we typed earlier:
```cpp
case MayaForceCrashType_StackCorruption:
{
*(uintptr_t *)_AddressOfReturnAddress() = 0x1234;
break;
}
```
Go on, I'll wait.
...
Back yet? Did you notice that our exception handler didn't get executed?
However, because we have WER enabled, you should now be able to go to the
directory described earlier in your `DumpsFolder` registry key and find your
crash dump there. Open it in WinDbg and print the stack (If you're new to WinDbg,
the command is `kP`). You should see the following (additional tables removed
for brevity):
```text
0:000> kP
# Call Site
00 0x1234
01 0x0000008d`08ffa648
02 0x0000008d`08ffa648
03 0x0000008d`08ffa670
04 OpenMayaRender!Autodesk::Maya::OpenMaya20200000::MFnAdskMaterial::type+0x11130
05 0x000001bc`1301ff00
06 0x4b082205`5ec939ec
07 0x000001bc`1320c860
08 OpenMaya!THcommandObject::doIt+0xb1
09 CommandEngine!TmetaCommand::doCommand+0x68
0a CommandEngine!Tjournal::operator=+0x5f2
0b CommandEngine!setMelGlobalStackLevelPtr+0x218
0c CommandEngine!TmelVariableList::`default constructor closure'+0xbc97
0d CommandEngine!TmelVariableList::`default constructor closure'+0xbde4
0e CommandEngine!SophiaExecutable::evaluate+0x47
0f CommandEngine!TcommandEngine::executeCommand+0x3bd
10 ExtensionLayer!QmayaCommandScrollFieldExecuter::doCommandCompletion+0x2f1
11 ExtensionLayer!TidleAction::preDoIdleAction+0xf
12 ExtensionLayer!TidleAction::destroyAfterRunning+0x23
13 ExtensionLayer!TidleLicenseAction::~TidleLicenseAction+0x16f
14 ExtensionLayer!TeventHandler::doIdles+0x371
15 ExtensionLayer!TeventHandler::suspendIdleEvents+0x10f
16 Qt5Core!QObject::event+0x91
17 Qt5Widgets!QApplicationPrivate::notify_helper+0x13d
18 Qt5Widgets!QApplication::notify+0x1ba7
19 ExtensionLayer!QmayaApplication::currentMousePos+0x6fa
1a Qt5Core!QCoreApplication::notifyInternal2+0xb9
1b Qt5Core!QEventDispatcherWin32::event+0xef
1c Qt5Widgets!QApplicationPrivate::notify_helper+0x13d
1d Qt5Widgets!QApplication::notify+0x1ba7
1e ExtensionLayer!QmayaApplication::currentMousePos+0x6fa
1f Qt5Core!QCoreApplication::notifyInternal2+0xb9
20 Qt5Core!QCoreApplicationPrivate::sendPostedEvents+0x228
21 qwindows!qt_plugin_query_metadata+0x1ebf
22 Qt5Core!QEventDispatcherWin32::processEvents+0xde9
23 user32!UserCallWinProcCheckWow+0x2bd
24 user32!DispatchMessageWorker+0x1e2
25 Qt5Core!QEventDispatcherWin32::processEvents+0x5db
26 qwindows!qt_plugin_query_metadata+0x1e99
27 Qt5Core!QEventLoop::exec+0x1fb
28 Qt5Core!QCoreApplication::exec+0x15e
29 ExtensionLayer!Tapplication::start+0xd8
2a maya!TiteratorWrapperFwd >::operator+++0xfe5
2b maya!TiteratorWrapperFwd >::operator+++0x130c7
2c maya!TiteratorWrapperFwd >::operator+++0x12116
2d kernel32!BaseThreadInitThunk+0x14
2e ntdll!RtlUserThreadStart+0x21
```
Oh, what's that at the top of the stack? Isn't that our randomly chosen address
that we used to corrupt the stack with, why yes it is! More curiously, what does
the auto-analysis (invoked with `!analyze -v`) try to tell us happened?
```text
0:000> !analyze -v
*******************************************************************************
* *
* Exception Analysis *
* *
*******************************************************************************
*** WARNING: Unable to verify checksum for mayaOpenImageIO.dll
KEY_VALUES_STRING: 1
Key : AV.Fault
Value: Execute
Key : Analysis.CPU.mSec
Value: 2140
Key : Analysis.DebugAnalysisProvider.CPP
Value: Create: 8007007e on
Key : Analysis.DebugData
Value: CreateObject
Key : Analysis.DebugModel
Value: CreateObject
Key : Analysis.Elapsed.mSec
Value: 31614
Key : Analysis.Memory.CommitPeak.Mb
Value: 412
Key : Analysis.System
Value: CreateObject
Key : Timeline.Process.Start.DeltaSec
Value: 83190
Key : WER.OS.Branch
Value: 19h1_release
Key : WER.OS.Timestamp
Value: 2019-03-18T12:02:00Z
Key : WER.OS.Version
Value: 10.0.18362.1
Key : WER.Process.Version
Value: 21.0.0.524
ADDITIONAL_XML: 1
OS_BUILD_LAYERS: 1
NTGLOBALFLAG: 0
PROCESS_BAM_CURRENT_THROTTLED: 0
PROCESS_BAM_PREVIOUS_THROTTLED: 0
APPLICATION_VERIFIER_FLAGS: 0
CONTEXT: (.ecxr)
rax=0000008d08ffa670 rbx=0000008d08ffa648 rcx=00007ffa47e522e5
rdx=00007ffa47e50000 rsi=000001bc1321a770 rdi=0000008d08ffa670
rip=0000000000001234 rsp=0000008d08ffa5e0 rbp=0000008d08ffa729
r8=00000000000000ff r9=0000000000000001 r10=0000000000008000
r11=0000008d08ffa5d0 r12=0000000000000001 r13=0000000000000001
r14=0000000000000001 r15=000001bc1320c520
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010200
00000000`00001234 ?? ???
Resetting default scope
EXCEPTION_RECORD: (.exr -1)
ExceptionAddress: 0000000000001234
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 0000000000000008
Parameter[1]: 0000000000001234
Attempt to execute non-executable address 0000000000001234
PROCESS_NAME: maya.exe
EXECUTE_ADDRESS: 1234
FAILED_INSTRUCTION_ADDRESS:
+0
00000000`00001234 ?? ???
ERROR_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%p referenced memory at 0x%p. The memory could not be %s.
EXCEPTION_CODE_STR: c0000005
EXCEPTION_PARAMETER1: 0000000000000008
EXCEPTION_PARAMETER2: 0000000000001234
IP_ON_HEAP: 0000000000001234
The fault address in not in any loaded module, please check your build's rebase
log at \bin\build_logs\timebuild\ntrebase.log for module which may
contain the address if it were loaded.
IP_ON_STACK:
+0
0000008d`08ffa648 f0 ???
FRAME_ONE_INVALID: 1
...
SYMBOL_NAME: openmayarender!Autodesk::Maya::OpenMaya20200000::MFnAdskMaterial::type+11130
MODULE_NAME: OpenMayaRender
IMAGE_NAME: OpenMayaRender.dll
STACK_COMMAND: ~0s ; .ecxr ; kb
FAILURE_BUCKET_ID: SOFTWARE_NX_FAULT_CODE_c0000005_OpenMayaRender.dll!Autodesk::Maya::OpenMaya20200000::MFnAdskMaterial::type
OS_VERSION: 10.0.18362.1
BUILDLAB_STR: 19h1_release
OSPLATFORM_TYPE: x64
OSNAME: Windows 10
IMAGE_VERSION: 21.0.0.524
FAILURE_ID_HASH: {c7389fa3-e6f6-36c4-c2c4-ad5882805143}
Followup: MachineOwner
---------
```
Yea, so if you weren't paying attention and just looking at the auto-analysis,
you might be thinking "huh, it's a bug somewhere in Maya". Luckily, this time we
used a pretty obvious address. What if it was something that was far less
discernable, especially in a production setting?
## Adding a Vectored Exception Filter ##
One way to help us catch some of these types of crashes (though not always) is
using an extension to Structured Exception Handling, known as **Vectored
Exception Filters**. They are [basically filters that are executed _before_ any
of the SEH filters](https://docs.microsoft.com/en-us/windows/win32/debug/vectored-exception-handling),
and unlike SEH filters, are not frame-based and not tied to any specific
function. However, they still behave the same way as SEH filters do when it
comes to debuggers being present, which we'll discuss later on near the end of
this document. One other advantage they have, unlike SEH filters, is that you
can add multiple handlers, unlike `SetUnhandledExceptionFilter`, which only
allows you to set a single handler as the top-level one for the entire process.
!!! tip So what's the catch?
Well, because vectored exception handlers are implemented as a linked-list
of registered handlers, walking the list if you have a lot of these handlers
registered is potentially a hit on performance when triggering an
exception. There's also
[other esoteric issues](https://chromium.googlesource.com/native_client/src/native_client/+/bradnelson/pnacl-in-pnacl/documentation/windows_ntdll_patch.txt) involved
with VEHs. Fortunately for us, we shouldn't run into any of these with our
plug-in specifically, but make a decision for yourself based on your general
codebase and the style chosen within it: are you exception-heavy, or not?
Let's go ahead and add one to our plug-in in the `initializePlugin` when it is loaded:
```cpp
static PVECTORED_EXCEPTION_HANDLER gpVectoredHandler = NULL;
LONG WINAPI mayaCustomVectoredExceptionHandler(PEXCEPTION_POINTERS exceptionInfo)
{
LONG result = mayaCustomUnhandledExceptionFilter(exceptionInfo);
return result;
}
MStatus initializePlugin(MObject obj)
{
MFnPlugin plugin(obj, PLUGIN_AUTHOR, PLUGIN_VERSION, PLUGIN_REQUIRED_API_VERSION);
gpVectoredHandler = (PVECTORED_EXCEPTION_HANDLER)::AddVectoredExceptionHandler(1, mayaCustomVectoredExceptionHandler);
gPrevFilter = ::SetUnhandledExceptionFilter(mayaCustomUnhandledExceptionFilter);
// ...more code below
```
As you can see, we just call the existing SEH filter that we've already made.
And let's make sure we remove it in `uninitializePlugin` as well:
```cpp
if (gpVectoredHandler != NULL) {
ULONG stat = ::RemoveVectoredExceptionHandler(gpVectoredHandler);
if (stat == 0) {
MGlobal::displayError("Could not remove the vectored exception handler.");
return MStatus::kFailure;
}
}
```
Go ahead and try the same crash now. You should see our custom crash
`MessageBox` show up.
Now try a normal simple null pointer deference crash:
```cpp
char *p = NULL;
*p = 5;
```
Uh oh. The dialog showed up twice! If you step through the code, _both_ the
vectored exception filter _and_ the unhandled exception filter are being run one
after the other. Let's stop that by basically storing a global to check if the
filter has already run before, and if so, not to run it again:
```cpp
static bool gHandlerCalled = false;
LONG WINAPI mayaCustomVectoredExceptionHandler(PEXCEPTION_POINTERS exceptionInfo)
{
LONG result = mayaCustomUnhandledExceptionFilter(exceptionInfo);
gHandlerCalled = true;
return result;
}
LONG WINAPI mayaCustomUnhandledExceptionFilter(LPEXCEPTION_POINTERS exceptionInfo)
{
if (gHandlerCalled == true) {
return EXCEPTION_EXECUTE_HANDLER;
}
char tempDirPath[MAX_PATH] = {0};
DWORD lenTempDirPath = ::GetEnvironmentVariable((LPCTSTR)TEMP_ENV_VAR_NAME, (LPTSTR)tempDirPath, (DWORD)MAX_PATH);
if (lenTempDirPath == 0) {
static const size_t lenDefaultTempDirPath = strlen(DEFAULT_TEMP_DIRECTORY);
memcpy(tempDirPath, DEFAULT_TEMP_DIRECTORY, lenDefaultTempDirPath);
memset(tempDirPath + lenDefaultTempDirPath, 0, 1);
}
char dumpFilePath[MAX_PATH] = {0};
snprintf(dumpFilePath, MAX_PATH, "%s\\%s", tempDirPath, MINIDUMP_FILE_NAME);
...
```
Now if you run it again, we should be catching both `NULL` pointer deferences,
``std::vector`` out of bounds accesses, _and_ stack corruption as well for the
obvious cases! Great!
Ok, let's try something that you'll probably have had to deal with at some point
in your life (or maybe a lot, if you're unfortunate enough to be working on a
heavily OOP-ed codebase):
```cpp
struct A
{
A() { bar(); }
virtual void foo() = 0;
void bar() { foo(); }
};
struct B: A
{
void foo() {}
};
A *a = new B;
a->foo();
```
Yep, the dreaded pure virtual function call. Go ahead and run this code; spoiler
alert, it won't get picked up. Why is this?
Well, it turns out that exceptions generated from the C runtime libraries (CRT)
have their own error handling mechanisms on Windows. The actual implementation
isn't well-documented, and this has changed over the years (for example, the
Section Walking the Import Address Table (IAT) technique works differently when
compiled under MSVC 2008 versus a more modern compiler, which I found to my
absolute dismay during the course of writing the sample code for this
article). What we do have though, in 2020, are a bunch of ways to register
different types of exception handlers for the various different types of CRT
exceptions that can be triggered.
Let's go over a few of them, starting, obviously, with the case we just
identified above.
## Catching CRT exceptions ##
Whenever a CRT exception is raised on Windows, invariably, `terminate()`
will get called at some point. There even exists a function in the standard,
`set_terminate()` for setting the error handler function.
### Dealing with pure virtual function calls ###
However, for pure virtual function calls, MSVC provides a special intrinsic,
`_set_purecall_handler`, that will allow us to override the default error
handler function that the compiler generates normally for such calls.
The function signature for the handler itself is pretty basic:
```cpp
void purecall_handler(void);
```
However, this presents us with an issue: we don't get the `EXCEPTION_POINTERS`
structure here, so how are we going to fire off the exception?
The answer is, surprisingly fairly straightforward: we'll create the exception
record by ourselves. Thankfully, thanks to
[some sleuthing done by CrashRpt for us already](http://crashrpt.sourceforge.net/docs/html/exception_handling.html#getting_exception_context),
we see that this is not as ardous as it might sound like: the Windows CRT
implementation already has a solution for us:
```cpp
_CRTIMP void __cdecl _invoke_watson(
const wchar_t *pszExpression,
const wchar_t *pszFunction,
const wchar_t *pszFile,
unsigned int nLine,
uintptr_t pReserved
)
{
/* Fake an exception to call reportfault. */
EXCEPTION_RECORD ExceptionRecord;
CONTEXT ContextRecord;
EXCEPTION_POINTERS ExceptionPointers;
BOOL wasDebuggerPresent = FALSE;
DWORD ret = 0;
(pszExpression);
(pszFunction);
(pszFile);
(nLine);
(pReserved);
#ifdef _X86_
__asm {
mov dword ptr [ContextRecord.Eax], eax
mov dword ptr [ContextRecord.Ecx], ecx
mov dword ptr [ContextRecord.Edx], edx
mov dword ptr [ContextRecord.Ebx], ebx
mov dword ptr [ContextRecord.Esi], esi
mov dword ptr [ContextRecord.Edi], edi
mov word ptr [ContextRecord.SegSs], ss
mov word ptr [ContextRecord.SegCs], cs
mov word ptr [ContextRecord.SegDs], ds
mov word ptr [ContextRecord.SegEs], es
mov word ptr [ContextRecord.SegFs], fs
mov word ptr [ContextRecord.SegGs], gs
pushfd
pop [ContextRecord.EFlags]
}
ContextRecord.ContextFlags = CONTEXT_CONTROL;
#pragma warning(push)
#pragma warning(disable:4311)
ContextRecord.Eip = (ULONG)_ReturnAddress();
ContextRecord.Esp = (ULONG)_AddressOfReturnAddress();
#pragma warning(pop)
ContextRecord.Ebp = *((ULONG *)_AddressOfReturnAddress()-1);
#elif defined (_IA64_) || defined (_AMD64_)
/* Need to fill up the Context in IA64 and AMD64. */
RtlCaptureContext(&ContextRecord);
#else /* defined (_IA64_) || defined (_AMD64_) */
ZeroMemory(&ContextRecord, sizeof(ContextRecord));
#endif /* defined (_IA64_) || defined (_AMD64_) */
ZeroMemory(&ExceptionRecord, sizeof(ExceptionRecord));
ExceptionRecord.ExceptionCode = STATUS_INVALID_PARAMETER;
ExceptionRecord.ExceptionAddress = _ReturnAddress();
ExceptionPointers.ExceptionRecord = &ExceptionRecord;
ExceptionPointers.ContextRecord = &ContextRecord;
wasDebuggerPresent = IsDebuggerPresent();
/* Make sure any filter already in place is deleted. */
SetUnhandledExceptionFilter(NULL);
ret = UnhandledExceptionFilter(&ExceptionPointers);
// if no handler found and no debugger previously attached
// the execution must stop into the debugger hook.
if (ret == EXCEPTION_CONTINUE_SEARCH && !wasDebuggerPresent) {
_CRT_DEBUGGER_HOOK(_CRT_DEBUGGER_INVALIDPARAMETER);
}
TerminateProcess(GetCurrentProcess(), STATUS_INVALID_PARAMETER);
}
```
Modifying it to suit our needs, we get:
```cpp
#if _MSC_VER >= 1300
#include
#endif
#ifndef _AddressOfReturnAddress
// Taken from: http://msdn.microsoft.com/en-us/library/s975zw7k(VS.71).aspx
#ifdef __cplusplus
#define EXTERNC extern "C"
#else
#define EXTERNC
#endif
// _ReturnAddress and _AddressOfReturnAddress should be prototyped before use
EXTERNC void *_AddressOfReturnAddress(void);
EXTERNC void *_ReturnAddress(void);
#endif
// The following function retrieves exception info
void GetExceptionPointers(DWORD dwExceptionCode,
EXCEPTION_POINTERS **ppExceptionPointers) {
// The following code was taken from VC++ 8.0 CRT (invarg.c: line 104)
EXCEPTION_RECORD ExceptionRecord;
CONTEXT ContextRecord;
memset(&ContextRecord, 0, sizeof(CONTEXT));
#ifdef _X86_
__asm {
mov dword ptr [ContextRecord.Eax], eax
mov dword ptr [ContextRecord.Ecx], ecx
mov dword ptr [ContextRecord.Edx], edx
mov dword ptr [ContextRecord.Ebx], ebx
mov dword ptr [ContextRecord.Esi], esi
mov dword ptr [ContextRecord.Edi], edi
mov word ptr [ContextRecord.SegSs], ss
mov word ptr [ContextRecord.SegCs], cs
mov word ptr [ContextRecord.SegDs], ds
mov word ptr [ContextRecord.SegEs], es
mov word ptr [ContextRecord.SegFs], fs
mov word ptr [ContextRecord.SegGs], gs
pushfd
pop [ContextRecord.EFlags]
}
ContextRecord.ContextFlags = CONTEXT_CONTROL;
#pragma warning(push)
#pragma warning(disable : 4311)
ContextRecord.Eip = (ULONG)_ReturnAddress();
ContextRecord.Esp = (ULONG)_AddressOfReturnAddress();
#pragma warning(pop)
ContextRecord.Ebp = *((ULONG *)_AddressOfReturnAddress() - 1);
#elif defined(_IA64_) || defined(_AMD64_)
/* Need to fill up the Context in IA64 and AMD64. */
RtlCaptureContext(&ContextRecord);
#else /* defined (_IA64_) || defined (_AMD64_) */
ZeroMemory(&ContextRecord, sizeof(ContextRecord));
#endif /* defined (_IA64_) || defined (_AMD64_) */
ZeroMemory(&ExceptionRecord, sizeof(EXCEPTION_RECORD));
ExceptionRecord.ExceptionCode = dwExceptionCode;
ExceptionRecord.ExceptionAddress = _ReturnAddress();
EXCEPTION_RECORD *pExceptionRecord = new EXCEPTION_RECORD;
memcpy(pExceptionRecord, &ExceptionRecord, sizeof(EXCEPTION_RECORD));
CONTEXT *pContextRecord = new CONTEXT;
memcpy(pContextRecord, &ContextRecord, sizeof(CONTEXT));
*ppExceptionPointers = new EXCEPTION_POINTERS;
(*ppExceptionPointers)->ExceptionRecord = pExceptionRecord;
(*ppExceptionPointers)->ContextRecord = pContextRecord;
}
```
I'm not going to pretend that I inspected the inline assembly on a x86 build of
Windows, but I do know at least what the function does give me at the end of the
day: a nicely-filled `EXCEPTION_POINTERS` structure that has the necessary
context I need. With that, we can go ahead and implement a rudimentary way to
invoke our exception filter using the `_set_purecall_handler` intrinsic:
```cpp
typedef void (__cdecl* _purecall_handler)(void);
static _purecall_handler gOrigPurecallHandler = NULL;
MStatus initializePlugin(MObject obj)
{
// ...
gOrigPurecallHandler = _set_purecall_handler([]() {
EXCEPTION_POINTERS *ppExceptionPointers = NULL;
GetExceptionPointers(EXCEPTION_NONCONTINUABLE, &ppExceptionPointers);
mayaCustomVectoredExceptionHandler(ppExceptionPointers);
::TerminateProcess(0);
});
// ...
return mstat;
}
```
You can see that we store the original handler when we substitute our own lambda
function with it.
!!! tip Making things easier to debug
One thing to note: because CRT exceptions don't really have corresponding
Win32 exception codes defined for them, if you just invoke the SEH filter as
is, the generated crash dump's error code will just be 0x00000001
(`ERROR_INVALID_FUNCTION`), which doesn't really tell us much. It might be
worth having a separate exception handler that specifically captures more
information about the pure virtual function call-site, or at least letting
the developer looking at the crash dump know that they _are_ dealing with a
such a situation.
Go ahead and execute the same crash code again, and you should now see your
exception handler get invoked, and the dump file written out to the custom
location specified with our own user streams.
Cool! Let's also do the same thing we did with our unhandled exception filter,
where we preserve the original handler when unloading our plug-in:
```cpp
MStatus uninitializePlugin(MObject obj)
{
// ...
_set_purecall_handler(gOrigPurecallHandler);
// ...
return mstat;
}
```
Great! We now are able to catch pure virtual function calls. Let's move on.
### Catching ``std::abort`` and other signals ###
By now, we've learned that there are a bunch of exception mechanisms on Windows:
the Structured Exceptions, and the ones thrown from the C runtime library. We've
also learned about `_set_purecall_handler`, which sets the handler for
exceptions thrown for the CRT when it comes to pure virtual function
calls. However, there is another way that programs can be forced to be
terminated on Windows, and that is via the use of `std::abort`, which sends the
`SIGABRT` signal to the program.
!!! note Light the Batsignal... So what is a signal exactly? It's basically a
software-side interrupt that tells the system that "hey, something
_exceptional_ has occurred in the execution of this program" and the system
will take over to decide what to do from there. The concept comes from UNIX,
but is ported over to Windows (although there are exceptions here and there,
such as `SIGINT` not being supported due to Win32 generating new threads to
handle software-side interrupts), so we can make use of the CRT functions to
accomplish our goals here.
So, assuming we want to be able to also invoke our custom crash handler if
`std::abort` is called in code, what do we do?
```cpp
typedef void (* abort_handler)(int sig);
static abort_handler gOrigAbortHandler = NULL;
gOrigAbortHandler = signal(SIGABRT, [](int signal){ (void)signal; ::RaiseException(SIGABRT, EXCEPTION_NONCONTINUABLE, 0, NULL); });
```
As you can see, the answer is trivially simple when making use of what's
available in the CRT. However, there's one minor difference between what we're
doing here and what we did earlier on where we formatted the exception record
ourselves; here, we just rely on `RaiseException` which will automatically do
the equivalent of raising a structured exception, which will in turn
automatically invoke our custom exception filter, giving it the error code of
`EXCEPTION_NONCONTINUABLE` (which will also show up in the dump file generated.)
You can make use of either method to generate the crash dump with the
information you want; both work fine in practice.
Don't forget to also re-set the `std::abort` handler after when we uninitialize
our plug-in:
```cpp
signal(SIGABRT, gOrigAbortHandler);
```
!!! tip Catching other UNIX signals?
If you're interested, you can also attempt to catch all the other traditional
UNIX signals such as `SIGFPE`, `SIGILL`, `SIGTERM` through the same method as
well; just swap the parameter that you pass to `signal`.
With that, we've got a pretty good grasp on making a custom crash handler thus
far. We can now catch most types of exceptions and other crash-y behaviour,
invoke our exception filter to execute, and write out the data we want.
...Now imagine that someone else has read this tutorial, and both of you deploy
your plug-ins to the same person. Recall that `SetUnhandledExceptionFilter` sets
the top-level exception handler for the _entire process and all its threads_.
I'm sure you can see where this is going: how do we stop people from overwriting
our own exception handler with their own?
## Detouring the CRT's exception handler ##
So, how we're going to do this is make use of a technique called _detouring_,
where we essentially patch over the original function in process memory to point
to an empty function. The order of operations looks like this:
1. On load of our plug-in, we call `SetUnhandledExceptionFilter` to set our
handler as the top-level handler for the `maya.exe` process.
2. We detour `SetUnhandledExceptionFilter` to an empty function that doesn't do
anything, so that any future calls to it by anyone else will not be able to
un-set our handler.
3. And, of course, upon unloading our plug-in, we restore the original
`SetUnhandledExceptionFilter` function to its original state for everyone
else to use as per-normal again.
We've already done part 1. So before we implement Part 2, we need to talk a
little (again) about the Portable Executable (PE) format that Windows uses for
the memory layout of executables.
### The Import Address Table (IAT) ###
So, just a quick re-cap: the format that all executables are expected to have on
Windows is the Portable Executable, or PE format. The PE format itself has a
very well-defined specification (as expected), and the Windows loader will map
parts of the file into memory based on where they are in the file itself. One of
these regions is the **Import Address Table**, which, as the name implies, is
essentially a lookup table that stores the entries for symbols that are being
_imported_ from other libraries during the binding process. What essentially
happens is:
0. During compilation of the program, when the compiler generates the IAT
section of the PE executable, it writes the entries for every function that
is imported from every implicitly-linked DLL at compile-time into that table
as a bunch of function pointers. The entries in the table can either be an
address, or an actual function name itself, and how the dynamic linker is
supposed to know is based off the fields of each entry in the IAT itself. (If
bit 63 is set to `1`, the function is to be imported by ordinal, otherwise
there are 31 bits reserved for indexing into _another_ table, known as the
Hint/Name Table, that contains the string of the function to be imported.)
These tables are stored in the various sections as defined by the PE format.
1. The program to be executed is loaded into the VMAS and mapped into memory by
the OS loader.
2. The dynamic linker parses the IAT and as it loads other modules, it patches
over the IAT table (known as "fixing up") entries with the addresses to the
_actual_ functions in those other modules. (For example,
`SetUnhandledExceptionFilter`!).
3. The last entry in the IAT is set to `NULL` to indicate the table's end.
4. Thus, when the entry in the IAT is called upon at runtime, we execute an
indirect jump to the fixed-up address specified by the function pointer, thus
executing the correct function we wanted in our original source code.
!!! tip More information about the PE format and IAT
The
[official specification](https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#import-address-table)
regarding the IAT in the PE format is available on MSDN. Although, quite
honestly speaking, the information there is rather terse and you might be
better off reading the
[Wikipedia article](https://en.wikipedia.org/wiki/Portable_Executable#Import_table)
for once.
Cool. So how does any of this relate to what we're trying to accomplish here?
Well, if we know that `SetUnhandledExceptionFilter` is something that we want to
prevent other callers from executing (at least not until we've unloaded our
plug-in), and we know that it's an imported syscall, and we know that the
address of it is actually set in our executable at runtime...
Can you guess where we're going with this? If it hasn't twigged for you yet,
just read on and see if that light bulb comes on suddenly for you!
### The Import Directory Table ###
Well, since we know that the function pointer exists "somewhere" in our process'
memory, let's take a stab at finding it among the table. This is where things
get a little hairy, because almost all of what we're doing here isn't really
documented well on MSDN: the best resource I can find on this strangely enough
comes from
[a Microsoft article that isn't even well-hosted on their own site anymore.](http://www.delphibasics.info/home/delphibasicsarticles/anin-depthlookintothewin32portableexecutablefileformat-part2).
While the article there gives a pretty good overview of each section of the PE
format in its entirety, I'll try to summarize the important bits here regarding
the IAT and Import Table sections themselves.
We start with the basic `IMAGE_IMPORT_DESCRIPTOR` structure, of which there
exists one for each imported PE executable (i.e. DLL). Looking directly at the
code from `winnt.h`:
```cpp
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
```
The structure above describes what is known as the [Import Directory Table](https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#import-directory-table),
where it contains the information to the **Import Address Table** in the member
called `FirstThunk`. (Ignore the `OriginalFirstThunk` section, which points to
the **Import Name Table** instead. Yes, yes, legacy/historical conventions and
all that.) The `FirstThunk` member stores what is known as the **relative
virtual address** to the IAT that we were previously talking about.
### Relative Virtual Addressing (RVA) ###
If you're not familiar with this term (or haven't read my [previous
tutorial](https://sonictk.github.io/asm_tutorial/#hello,worldrevisted/breakingthecodedown/rip-relativeaddressing)
talking about `rip`-relative addressing), basically, on modern x64
architecture, the OS no longer loads programs at fixed memory addresses;
the entire memory space of an executable is mapped to virtual memory
addresses by the **Virtual Memory Address System**, which means that we can
no longer compile code with **fixed displacements** (i.e. fixed memory
addresses to locations within the executable's address space.) This gets
even more complicated with DLLs, since they can load just about anywhere
within the host process's address space based on the VMAS's
disposition.
Therefore, RVA was introduced: it basically allows us to specify _offsets_
within the executable to find memory locations, thus sidestepping the
problem of not having a guaranteed load address of the start of the
executable. In practice at runtime, this turns out to be not so bad in terms
of a performance hit, since part of the loader's job is to fix-up all the
relative addresses upon loading the executable and patch them over with the
"real" addresses (the reason _real_ is in quotes is because the final
addresses are still technically virtual addresses that will get translated
by the VMAS to the still-not-real-but-more-real physical addresses that
point to the actual RAM)
An example of RVA is the following:
**********************************************************
* load address RVA *
* +-----------------+ +-----------------+ *
* | 0x20000000 | | 0x00000004 | *
* +-------+---------+ +---------+-------+ *
* | | *
* '--------------. .---------------' *
* | *
* +--------v---------+ *
* | 0x20000004 | *
* +------------------+ *
* final address *
* *
**********************************************************
[Figure [diagram]: How a relative virtual address (RVA) is constructed.]
As you can see, the concept itself is trivially simple.
### Memory layout of the IAT ###
So that now we understand that the Import Directory Table contains the entries
for each DLL (PE) imported into our process, we can focus our attention on the
Import Address Table itself. Each entry in the IAT mimics the following
structure, again from `winnt.h`:
```cpp
typedef struct _IMAGE_THUNK_DATA64 {
union {
ULONGLONG ForwarderString; // PBYTE
ULONGLONG Function; // PDWORD
ULONGLONG Ordinal;
ULONGLONG AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA64;
typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64;
#define IMAGE_ORDINAL_FLAG64 0x8000000000000000
#define IMAGE_ORDINAL_FLAG32 0x80000000
#define IMAGE_ORDINAL64(Ordinal) (Ordinal & 0xffff)
#define IMAGE_ORDINAL32(Ordinal) (Ordinal & 0xffff)
#define IMAGE_SNAP_BY_ORDINAL64(Ordinal) ((Ordinal & IMAGE_ORDINAL_FLAG64) != 0)
#define IMAGE_SNAP_BY_ORDINAL32(Ordinal) ((Ordinal & IMAGE_ORDINAL_FLAG32) != 0)
```
!!! note x86/x64 differences
If you look in `winnt.h` yourself, you'll no doubt notice the presence of
the `IMAGE_THUNK_DATA32` struct definition as well; which was used during
the days of x86 architecture. The sizes are different and the bitmasks are
slightly different as well, but otherwise their purposes are the same.
If you were paying attention in the earlier section, you can see here that this
structure should start to make some sense: we have the `ForwarderString` member,
which `IMAGE_ORDINAL_FLAG64` acts as the bitflag to determine if the import is
imported by ordinal or name. The `Function` member points to the address of the
function being imported itself, which is what we'll be primarily interested in,
while the `Ordinal` and `AddressOfData` members correspond to the **Ordinal Number** and the
[Hint/Name Table RVA](https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#hintname-table)
respectively as detailed in the MSDN documentation.
Overall, if the above was a little confusing, you can think of the relationships
between the Import Directory Tables and IATs along with how they relate to the
DLLs being imported in the following manner:
*******************************************************
* *
* main.exe *
* +---------------------------------------------+ *
* |///////// Other stuff ///////////////////////| *
* |/////////////////////////////////////////////| *
* +---------------------------------------------+ *
* | Import directory table | *
* | `IMAGE_IMPORT_DESCRIPTOR` for `foo.dll` | *
* +---------------------------------------------+ *
* | Import address table | *
* | +----------------------------------------+ | *
* | | `IMAGE_THUNK_DATA` | | *
* | | for `bar()` | | *
* | +----------------------------------------+ | *
* | | `IMAGE_THUNK_DATA` | | *
* | | for `bazz()` | | *
* | +----------------------------------------+ | *
* | | `IMAGE_THUNK_DATA` | | *
* | | for `jazz()` | | *
* | +----------------------------------------+ | *
* | |0000000000000000000000000000000000000000| | *
* | |00000000 End of table (null) 00000000000| | *
* | |0000000000000000000000000000000000000000| | *
* | |0000000000000000000000000000000000000000| | *
* | +----------------------------------------+ | *
* +---------------------------------------------+ *
* | foo.dll | *
* | +--------------------------------------+ | *
* | | Export Address Table | | *
* | | | | *
* | +--------------------------------------+ | *
* | | Export bar() | | *
* | | | | *
* | +--------------------------------------+ | *
* | | Export bazz() | | *
* | | | | *
* | +--------------------------------------+ | *
* | | Export jazz() | | *
* | | | | *
* | +--------------------------------------+ | *
* +---------------------------------------------+ *
* *
*******************************************************
[Figure [diagram]: A _very_ rough overview of how the IAT works.]
We see that `foo.dll` exports `bar`, `bazz` and `jazz`, which creates a seperate
**Export Address Table** containing the addresses and data about those functions
for the dynamic linker to use when patching over the addresses in our IAT within
`main.exe` (This is what happens what you use `__declspec(export)`, for example,
to tell the compiler to generate those entries in the EAT). We also see that
there is one entry in the Import Directory Table for `foo.dll` being imported,
and that there are 3 IAT entries, one for each function being imported from
`foo.dll`.
So hopefully that makes more sense now; it really isn't that complicated, but
I'll be the first to admit that gathering information about this topic was a
little annoying and hard to piece together. Now that we have that sorted out,
let's actually start writing the code to walk the IAT(s) and find all instances
of `SetUnhandledExceptionFilter`.
Have you caught on to what we're planning to do yet? If you haven't, here's the
answer: because we know now where the import entries are stored in our
executable `maya.exe` (and for that matter, _any_ other DLLs loaded within Maya
itself), we can walk all of them, _fix up the addresses to
`SetUnhandledExceptionFilter` in all of them_ to point to an empty function that
does nothing, and from that point on, while other callers may be able to call
`SetUnhandledExceptionFilter`, they won't be able to actually _do_ anything with
it!
!!! warning There be dragons where we're going...
While I'll be talking about this later, this is explicitly _not_ recommended
by [Raymond Chen](https://devblogs.microsoft.com/oldnewthing/20180606-00/?p=98925) himself,
for good reason. However, for the reasons already discussed above regarding
WER, we're going to move on ahead for now until Microsoft offers some better
exposed functionality from WER and abandons their "online-only" philosophy
of error reporting.
Now that we know what we're going to be implementing next, let's go ahead and
start the first part of this process: finding all instances of where
`SetUnhandledExceptionFilter` might be lurking within the IATs of the DLLs
loaded within Maya.
### Searching the IAT ###
Let's start with what exactly we're looking to do: we want to replace the
default `SetUnhandledExceptionFilter` with our own detoured function that does
nothing. Let's start by writing the detoured function first, which effectively
amounts to a no-op:
```cpp
LONG WINAPI detouredSetUnhandledExceptionFilter(LPEXCEPTION_POINTERS exceptionInfo)
{
(void)exceptionInfo;
return 0;
}
```
In order to start walking the Import Directory Table for the process and all its
loaded modules, we basically need to write some Win32 code:
```cpp
bool patchOverUnhandledExceptionFilter(PROC pFnFilterReplace, FARPROC *pOrigFilter)
{
HMODULE hKernel32Mod = NULL;
::GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, "kernel32.dll", &hKernel32Mod);
if (hKernel32Mod == NULL) {
return false;
}
if (*pOrigFilter == NULL) {
*pOrigFilter = ::GetProcAddress(hKernel32Mod, "SetUnhandledExceptionFilter");
if (*pOrigFilter == NULL) {
return false;
}
}
DWORD currentProcId = ::GetCurrentProcessId();
HANDLE hProcSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, currentProcId);
MODULEENTRY32 modEntry = {0};
modEntry.dwSize = sizeof(modEntry);
for (BOOL bStat = Module32First(hProcSnapshot, &modEntry); bStat == TRUE; bStat = Module32Next(hProcSnapshot, &modEntry)) {
// TODO: Gotta patch over here...
}
}
```
Ok, let's break this down a little bit:
```cpp
HMODULE hKernel32Mod = NULL;
::GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, "kernel32.dll", &hKernel32Mod);
if (hKernel32Mod == NULL) {
return false;
}
if (*pOrigFilter == NULL) {
*pOrigFilter = ::GetProcAddress(hKernel32Mod, "SetUnhandledExceptionFilter");
if (*pOrigFilter == NULL) {
return false;
}
}
```
This is pretty obvious; we're getting a handle to `kernel32.dll`, which
`SetUnhandledExceptionFilter` resides in, and then getting the address of the
function itself by name.
```cpp
DWORD currentProcId = ::GetCurrentProcessId();
HANDLE hProcSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, currentProcId);
MODULEENTRY32 modEntry = {0};
modEntry.dwSize = sizeof(modEntry);
```
This part is a little more complex, but if we look at the documentation of
`CreateToolhelp32Snapshot`, things start to make more sense: we are basically
snapshotting the entire process and all the modules currently being used so that
we can walk over it without fear of DLLs being unloaded in the middle of our
walk (possible, if other threads are executing at the same time).
!!! tip Why not `EnumProcessModules`?
We could also use `EnumProcessModules` to achieve the same thing. However,
there's a bunch of additional memory management involved, and since
`CreateToolhelp32Snapshot` manages that for us (since this patch operation
isn't particularly expensive), I'm electing to use that for simplicity and
safety (since I also don't know if `EnumProcessModules` executes in an
atomic fashion).
Once we have the snapshot of process memory, we can begin walking it, as the for
loop demonstrates:
```cpp
for (BOOL bStat = Module32First(hProcSnapshot, &modEntry); bStat == TRUE; bStat = Module32Next(hProcSnapshot, &modEntry)) {
// TODO: Gotta patch over here...
}
```
As you can see, we make use of the Win32 utility functions `Module32First` and
`Module32Next` to iterate over the loaded modules within the memory snapshot
that we just took. Now we just need to actually write the code within the loop
to find our desired function...
To do that, let's abstract a little bit here and have a single function
responsible for patching over a single IAT entry that we can continue calling
for each IAT entry:
```cpp
bool patchOverIATEntryInOneModule(const char *calleeModName, PROC pfnCurrent, PROC pfnNew, HMODULE hmodCaller)
{
ULONG importSectionSize = 0;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)::ImageDirectoryEntryToDataEx(hmodCaller, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &importSectionSize, NULL);
if (pImportDesc == NULL) {
return false;
}
bool foundEntry = false;
while (pImportDesc->Name != NULL && pImportDesc->Characteristics != NULL) {
PSTR pszImportName = (PSTR)((PBYTE)hmodCaller + pImportDesc->Name);
if (_stricmp(pszImportName, calleeModName) == 0) {
foundEntry = true;
break;
}
++pImportDesc;
}
if (foundEntry != true) {
return false;
}
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((PBYTE)hmodCaller + pImportDesc->FirstThunk);
while (pThunk->u1.Function != NULL) {
PROC *pFunc = (PROC *)&pThunk->u1.Function;
if (*pFunc == *pfnCurrent) {
// TODO: Found! ...Now how do we patch over it?
return true;
}
++pThunk;
}
return false;
}
```
Again, let's break this down bit-by-bit:
```cpp
ULONG importSectionSize = 0;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)::ImageDirectoryEntryToDataEx(hmodCaller, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &importSectionSize, NULL);
if (pImportDesc == NULL) {
return false;
}
```
As mentioned earlier, the Import Directory Table takes the form of the
`IMAGE_IMPORT_DESCRIPTOR` structure, and we make use of the Win32 function
`ImageDirectoryEntryToDataEx` to get the address of that section within our
current module being iterated over, by specifying the
`IMAGE_DIRECTORY_ENTRY_IMPORT` parameter.
```cpp
bool foundEntry = false;
while (pImportDesc->Name != NULL && pImportDesc->Characteristics != NULL) {
PSTR pszImportName = (PSTR)((PBYTE)hmodCaller + pImportDesc->Name);
if (_stricmp(pszImportName, calleeModName) == 0) {
foundEntry = true;
break;
}
++pImportDesc;
}
```
This code here is essentially checking if the Import Directory Table's name
entry matches that of what we're looking for, which is this case is
`kernel32.dll`. The reason for the expression `(PSTR)((PBYTE)hmodCaller +
pImportDesc->Name)` is that if you recall, the member `Name` within the
`IMAGE_IMPORT_DESCRIPTOR` is not the _actual_ name itself, it's an RVA yet again
to the ASCII string within the executable where the string data is actually
stored; therefore, to get the proper pointer to that string, we offset from the
start of the image base and add the amount that `Name` specifies.
Next, once we've located the Import Directory Table that we're interested in, we
have to walk the IAT entries within it to find the function that we're looking
for:
```cpp
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((PBYTE)hmodCaller + pImportDesc->FirstThunk);
while (pThunk->u1.Function != NULL) {
PROC *pFunc = (PROC *)&pThunk->u1.Function;
if (*pFunc == *pfnCurrent) {
// TODO: Found! ...Now how do we patch over it?
return true;
}
++pThunk;
}
```
Recall again that each IAT entry is represented by the struct `IMAGE_THUNK_DATA`
(with slightly different sizes for x86/x64 architecture), and that the
`FirstThunk` member within the Import Directory Table is actually the RVA to the
beginning of the IAT. Thus, we can again offset starting from the image base to
find the address of the IAT entry, cast that to a pointer to a
`IMAGE_THUNK_DATA`, and then start checking within it for the `Function` member,
which points to the address of the function it is importing. We then check if it
matches the function we're looking for (i.e. `SetUnhandledExceptionFilter`), and
if it is...
Well, now what? We need to somehow replace that address with the address of our
detoured function, but how do we go about overwriting this bit of process memory
without triggering an access violation?
### Overwriting process memory ###
When Windows deals with memory, it doesn't just allocate or de-allocate memory
in terms of bytes or bits, it does so in _pages_: larger blocks of memory that
the VMAS uses in memory management. This is the smallest unit of data that is
usually used when the VMAS allocates/de-allocates memory for use by
applications, and
[each page has its own read/write/execute permissions](https://docs.microsoft.com/en-us/windows/win32/memory/memory-protection)
for making sure that rogue processes don't go about crashing other processes on
the same machine. However, by that same vein, it also provides us the necessary
syscalls to be able to modify those protections.
Thus, we can write the following code:
```cpp
MEMORY_BASIC_INFORMATION memDesc = {0};
::VirtualQuery(pFunc, &memDesc, sizeof(MEMORY_BASIC_INFORMATION));
if (!::VirtualProtect(memDesc.BaseAddress, memDesc.RegionSize, PAGE_READWRITE, &memDesc.Protect)) {
return false;
}
HANDLE hProcess = GetCurrentProcess();
::WriteProcessMemory(hProcess, pFunc, &pfnNew, sizeof(pfnNew), NULL);
DWORD dwOldProtect = 0;
::VirtualProtect(memDesc.BaseAddress, memDesc.RegionSize, memDesc.Protect, &dwOldProtect);
return true;
```
Voila! We basically get the information about the page where the function in
question is currently located, disable the page protections on it to allow us to
write to it, write to it using `WriteProcessMemory`, and then restore the
original page protections after without anyone else being the wiser.
With a bit of abstraction, our final code for patching over looks like this:
```cpp
bool patchOverIATEntryInOneModule(const char *calleeModName, PROC pfnCurrent, PROC pfnNew, HMODULE hmodCaller)
{
ULONG importSectionSize = 0;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)::ImageDirectoryEntryToDataEx(hmodCaller, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &importSectionSize, NULL);
if (pImportDesc == NULL) {
return false;
}
bool foundEntry = false;
while (pImportDesc->Name != NULL && pImportDesc->Characteristics != NULL) {
PSTR pszImportName = (PSTR)((PBYTE)hmodCaller + pImportDesc->Name);
if (_stricmp(pszImportName, calleeModName) == 0) {
foundEntry = true;
break;
}
++pImportDesc;
}
if (foundEntry != true) {
return false;
}
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((PBYTE)hmodCaller + pImportDesc->FirstThunk);
while (pThunk->u1.Function != NULL) {
PROC *pFunc = (PROC *)&pThunk->u1.Function;
if (*pFunc == *pfnCurrent) { // NOTE: (sonictk) Found! Now we can patch over it.
MEMORY_BASIC_INFORMATION memDesc = {0};
::VirtualQuery(pFunc, &memDesc, sizeof(MEMORY_BASIC_INFORMATION));
if (!::VirtualProtect(memDesc.BaseAddress, memDesc.RegionSize, PAGE_READWRITE, &memDesc.Protect)) {
return false;
}
HANDLE hProcess = GetCurrentProcess();
::WriteProcessMemory(hProcess, pFunc, &pfnNew, sizeof(pfnNew), NULL);
DWORD dwOldProtect = 0;
::VirtualProtect(memDesc.BaseAddress, memDesc.RegionSize, memDesc.Protect, &dwOldProtect);
return true;
}
++pThunk;
}
return false;
}
bool patchOverIATEntriesInAllModules(const char *calleeModName, PROC pfnCurrent, PROC pfnNew)
{
DWORD currentProcId = ::GetCurrentProcessId();
HANDLE hProcSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, currentProcId);
MODULEENTRY32 modEntry = {0};
modEntry.dwSize = sizeof(modEntry);
for (BOOL bStat = Module32First(hProcSnapshot, &modEntry); bStat == TRUE; bStat = Module32Next(hProcSnapshot, &modEntry)) {
patchOverIATEntryInOneModule(calleeModName, pfnCurrent, pfnNew, modEntry.hModule);
}
::CloseHandle(hProcSnapshot);
return true;
}
bool patchOverUnhandledExceptionFilter(PROC pFnFilterReplace, FARPROC *pOrigFilter)
{
HMODULE hKernel32Mod = NULL;
::GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, "kernel32.dll", &hKernel32Mod);
if (hKernel32Mod == NULL) {
return false;
}
if (*pOrigFilter == NULL) {
*pOrigFilter = ::GetProcAddress(hKernel32Mod, "SetUnhandledExceptionFilter");
if (*pOrigFilter == NULL) {
return false;
}
}
bool bStat = patchOverIATEntriesInAllModules("kernel32.dll", *pOrigFilter, pFnFilterReplace);
return bStat;
}
```
Go ahead and call `patchOverUnhandledExceptionFilter` in `initializePlugin`
after you've already set the new top-level exception handler to our custom one,
then try setting `SetUnhandledExceptionFilter` after, whether in the plug-in or
in another one after. You'll find that it ends up calling the
`detouredSetUnhandledExceptionFilter` function instead, and our original handler
still runs on any of our Maya crashes!
# Parsing our dump #
So now we have a pretty good setup for capturing our dumps, and we're able to
stop other "bad" actors from removing our crash handler once we have it
loaded. However, right now, we only write the scene file name into the dump; we
could do a lot more here to help ourselves in the event of a Maya crash. Let's
go ahead and try to add more information about the environment context at the
time of a crash now.
## Adding a _more_ customized user stream ##
The first thing we want to do is make sure we decide on what exactly we're going
to store in memory, and what we will write to the dump file in the event of a
crash. For now, I'm deciding on the following for our example:
1. The last DAG parent touched in the scene
2. The last DAG child touched in the scene
3. The last DG node added to the scene
4. The last DAG operation type
5. The Maya API version
6. Any custom API version (for custom Maya builds, which is common in studios
that have a working relationship with Autodesk)
7. The version of Maya that was used to create the Maya scene file open
8. And whether the Y-axis is determined as the global "up" axis
Which translates nicely to the following struct:
```cpp
#define MAYA_DAG_PATH_MAX_NAME_LEN 512
#define MAYA_DG_NODE_MAX_NAME_LEN 512
#pragma pack(1)
typedef struct MayaCrashDumpInfo
{
char lastDagParentName[MAYA_DAG_PATH_MAX_NAME_LEN];
char lastDagChildName[MAYA_DAG_PATH_MAX_NAME_LEN];
char lastDGNodeAddedName[MAYA_DG_NODE_MAX_NAME_LEN];
int verAPI;
int verCustom;
int verMayaFile;
short lastDagMessage;
bool isYUp;
} MayaCrashDumpInfo;
```
We pack this struct manually, because this is the _actual_ data that's going to
be written inside the dump file, and if we want to be able to parse it, we need
to make sure that we don't get any funny padding surprises from the compiler.
Storing this data within the Maya session as the user interacts and works is a
simple exercise in the Maya API, and really shouldn't require any explanation:
```cpp
static MayaCrashDumpInfo gMayaCrashDumpInfo = {0};
void mayaSceneAfterOpenCB(void *unused)
{
(void)unused;
memset(&gMayaCrashDumpInfo, 0, sizeof(gMayaCrashDumpInfo));
gMayaCrashDumpInfo.verAPI = MGlobal::apiVersion();
gMayaCrashDumpInfo.verCustom = MGlobal::customVersion();
gMayaCrashDumpInfo.verMayaFile = MFileIO::latestMayaFileVersion();
gMayaCrashDumpInfo.isYUp = MGlobal::isYAxisUp();
return;
}
void mayaAllDAGChangesCB(MDagMessage::DagMessage msgType, MDagPath &child, MDagPath &parent, void *unused)
{
(void)unused;
gMayaCrashDumpInfo.lastDagMessage = (short)msgType;
MString childName = child.partialPathName();
const char *childNameC = childName.asChar();
size_t lenChildName = strlen(childNameC);
lenChildName = lenChildName > MAYA_DAG_PATH_MAX_NAME_LEN ? MAYA_DAG_PATH_MAX_NAME_LEN : lenChildName;
MString parentName = parent.partialPathName();
const char *parentNameC = parentName.asChar();
size_t lenParentName = strlen(parentNameC);
lenParentName = lenParentName > MAYA_DAG_PATH_MAX_NAME_LEN ? MAYA_DAG_PATH_MAX_NAME_LEN : lenParentName;
memcpy(gMayaCrashDumpInfo.lastDagChildName, childNameC, lenChildName);
memset(gMayaCrashDumpInfo.lastDagChildName + lenChildName, 0, 1);
memcpy(gMayaCrashDumpInfo.lastDagParentName, parentNameC, lenParentName);
memset(gMayaCrashDumpInfo.lastDagParentName + lenParentName, 0, 1);
return;
}
void mayaNodeAddedCB(MObject &node, void *unused)
{
(void)unused;
if (!node.hasFn(MFn::kDependencyNode)) {
return;
}
MStatus mstat;
MFnDependencyNode fnNode(node, &mstat);
if (mstat != MStatus::kSuccess) {
return;
}
MString nodeName;
if (!fnNode.hasUniqueName()) {
nodeName = fnNode.absoluteName(&mstat);
if (mstat != MStatus::kSuccess) {
return;
}
} else {
nodeName = fnNode.name(&mstat);
}
const char *nodeNameC = nodeName.asChar();
size_t lenNodeName = strlen(nodeNameC);
if (lenNodeName == 0) {
return;
}
memcpy(gMayaCrashDumpInfo.lastDGNodeAddedName, nodeNameC, lenNodeName);
memset(gMayaCrashDumpInfo.lastDGNodeAddedName + lenNodeName, 0, 1);
return;
}
```
After that, it's just a matter of registering the callbacks when the plug-in is
initialized and removing them when the plug-in gets unloaded:
```cpp
static MCallbackId gMayaSceneAfterOpen_cbid = 0;
static MCallbackId gMayaAllDAGChanges_cbid = 0;
static MCallbackId gMayaNodeAdded_cbid = 0;
MStatus initializePlugin(MObject obj)
{
//...
gMayaSceneAfterOpen_cbid = MSceneMessage::addCallback(MSceneMessage::kAfterOpen, mayaSceneAfterOpenCB, NULL, &mstat);
CHECK_MSTATUS_AND_RETURN_IT(mstat);
gMayaAllDAGChanges_cbid = MDagMessage::addAllDagChangesCallback(mayaAllDAGChangesCB, NULL, &mstat);
CHECK_MSTATUS_AND_RETURN_IT(mstat);
gMayaNodeAdded_cbid = MDGMessage::addNodeAddedCallback(mayaNodeAddedCB, "dependNode", NULL, &mstat);
CHECK_MSTATUS_AND_RETURN_IT(mstat);
//...
}
MStatus uninitializePlugin(MObject obj)
{
// ...
MStatus mstat = MMessage::removeCallback(gMayaSceneAfterOpen_cbid);
CHECK_MSTATUS_AND_RETURN_IT(mstat);
mstat = MMessage::removeCallback(gMayaAllDAGChanges_cbid);
CHECK_MSTATUS_AND_RETURN_IT(mstat);
mstat = MMessage::removeCallback(gMayaNodeAdded_cbid);
CHECK_MSTATUS_AND_RETURN_IT(mstat);
// ...
}
```
Again, very trivial. So now we have a struct that's filled with all the good
information we want in our dump file during a crash. Let's take a look again at
the code for our crash handler to see how we can write that into the dump. If
you recall in the earlier Section Custom user streams, we had the following
structures to deal with when writing out our scene file name into the minidump:
```cpp
LONG WINAPI mayaCustomUnhandledExceptionFilter(LPEXCEPTION_POINTERS exceptionInfo)
{
// ...
MINIDUMP_EXCEPTION_INFORMATION dumpExceptionInfo = {0};
dumpExceptionInfo.ThreadId = ::GetCurrentThreadId();
dumpExceptionInfo.ExceptionPointers = exceptionInfo;
dumpExceptionInfo.ClientPointers = TRUE;
MINIDUMP_USER_STREAM dumpMayaFileInfo = {0};
dumpMayaFileInfo.Type = CommentStreamA;
dumpMayaFileInfo.BufferSize = MAX_PATH;
dumpMayaFileInfo.Buffer = gMayaCurrentScenePath;
MINIDUMP_USER_STREAM streams[] = {
dumpMayaFileInfo,
};
MINIDUMP_USER_STREAM_INFORMATION dumpUserInfo = {0};
dumpUserInfo.UserStreamCount = ARRAY_SIZE(streams);
dumpUserInfo.UserStreamArray = streams;
//...
```
Now that we have a `MayaCrashDumpInfo` structure of our very own, we can go
ahead and make a new user stream for it:
```cpp
#define MAYA_CRASH_INFO_STREAM_TYPE LastReservedStream + 1
MINIDUMP_USER_STREAM dumpMayaCrashInfo = {0};
dumpMayaCrashInfo.Type = MAYA_CRASH_INFO_STREAM_TYPE;
dumpMayaCrashInfo.BufferSize = sizeof(gMayaCrashDumpInfo);
dumpMayaCrashInfo.Buffer = &gMayaCrashDumpInfo;
```
If you're wondering what `LastReservedStream` is, it's the [enum defined](https://docs.microsoft.com/en-us/windows/win32/api/minidumpapiset/ne-minidumpapiset-minidump_stream_type)
for user streams to indicate that any stream types after that will be ignored by
the debuggers and allows us to store whatever custom types of user streams we
want in the minidump. Perfect! We can now add this new user stream to the dump
that we're writing out, like so:
```cpp
//...
MINIDUMP_USER_STREAM streams[] = {
dumpMayaFileInfo,
dumpMayaCrashInfo
};
//...
```
And that's it! We can add as many or as few streams as we'd like to the dump
this way, but generally I prefer keeping all of the information that I need
specifically in one giant blob struct (for fixed-size buffers) so that it's
faster to both write and access. Depending on your needs, you can utilize
different strategies here for writing out your own custom information into the
minidumps, espeically if you have a lot of variable-sized buffers to store.
!!! tip More examples
The code in the repository accompanying this tutorial shows additional
examples of storing different types of Maya context information in the
minidump, including the scene timing information along with the last MEL
procedure executed (which is _especially_ useful for debugging 3rd-party
plug-in crashes). I would recommend you try to analyze the use cases for
your own pipeline and decide on what you need to store in the minidump.
Ok, now that we have our own custom information in the dump in a custom memory
layout, if you try to open the dump up in WinDbg now, you'll likely notice that
we still don't see anything indicating its presence, let alone being able to
read from it and get the information we desire. Obviously, WinDbg knows nothing
about our custom user stream now, nor how to represent it to us, and certainly
we'd like to know about it (otherwise, what would the entire point of
this tutorial be?).
So, how do we get human-readable information about our structure within the
minidump? Let's first take a simpler approach to doing this outside of WinDbg,
and just write a small little command-line program to parse the minidump file,
read the user stream, and print out information about it.
## Extracting our custom user stream ##
Windows helpfully provides a function, `MiniDumpReadDumpStream`, that will do
all the heavy lifting and give us the binary data within the user stream that
we're interested in:
```cpp
#include
#include
#include "common.h"
#include
void parseAndPrintCustomStreamFromMiniDump(const char *dumpFilePath)
{
if (dumpFilePath == NULL) {
return;
}
HANDLE hFile = CreateFile(dumpFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("ERROR: Could not open the dump file requested.\n");
return;
}
HANDLE hMapFile = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (hMapFile == NULL) {
printf("ERROR: Could not create the file mapping for the dump.\n");
return;
}
PVOID pFileView = MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, 0);
if (pFileView == NULL) {
printf("ERROR: Failed to map view of the dump file.\n");
return;
}
PMINIDUMP_DIRECTORY miniDumpDirPath = NULL;
PVOID pUserStream = NULL;
ULONG streamSize = 0;
BOOL bStat = MiniDumpReadDumpStream(pFileView,
MAYA_CRASH_INFO_STREAM_TYPE,
&miniDumpDirPath,
&pUserStream,
&streamSize);
if (bStat != TRUE) {
printf("ERROR: Failed to find stream in dump file. Check if it was generated correctly.\n");
return;
}
if (streamSize != sizeof(MayaCrashDumpInfo)) {
printf("ERROR: Stream size mismatch. Check if the dump file was written correctly.\n");
return;
}
MayaCrashDumpInfo *dumpInfo = (MayaCrashDumpInfo *)pUserStream;
printf("Maya API version: %d\n"
"Custom API version: %d\n"
"Maya file version: %d\n"
"Y is up: %d\n"
"Last DAG parent: %s\n"
"Last DAG child: %s\n"
"Last DAG message: %d\n"
"Last DG node added: %s\n"
"End of crash info.\n",
dumpInfo->verAPI,
dumpInfo->verCustom,
dumpInfo->verMayaFile,
dumpInfo->isYUp,
dumpInfo->lastDagParentName,
dumpInfo->lastDagChildName,
dumpInfo->lastDagMessage,
dumpInfo->lastDGNodeAddedName);
return;
}
```
Again, this code should be pretty trivial for anyone who has done a modicum of
Win32 programming: we create a file handle to the dump file, create a file
mapping of the file (since that's what the minidump functions generally expect),
and pass it to `MiniDumpReadDumpStream` with our custom dump stream type
`MAYA_CRASH_INFO_STEAM_TYPE` as the parameter for `StreamNumber`, cast the
result to a pointer to a `MayaCrashDumpInfo` structure, and then just print out
the information within it.
Now all we need is just the entry point, which really is just reading the dump
file and passing that to `parseAndPrintCustomStreamFromMiniDump`:
```cpp
int main(int argc, char *argv[])
{
if (argc == 1) {
char tempDirPath[MAX_PATH] = {0};
DWORD lenTempDirPath = GetEnvironmentVariable((LPCTSTR)TEMP_ENV_VAR_NAME, (LPTSTR)tempDirPath, (DWORD)MAX_PATH);
if (lenTempDirPath == 0) {
const size_t lenDefaultTempDirPath = strlen(DEFAULT_TEMP_DIRECTORY);
memcpy(tempDirPath, DEFAULT_TEMP_DIRECTORY, lenDefaultTempDirPath);
memset(tempDirPath + lenDefaultTempDirPath, 0, 1);
}
char dumpFilePath[MAX_PATH] = {0};
snprintf(dumpFilePath, MAX_PATH, "%s\\%s", tempDirPath, MINIDUMP_FILE_NAME);
parseAndPrintCustomStreamFromMiniDump(dumpFilePath);
} else {
for (int i=1; i < argc; ++i) {
char *path = argv[i];
parseAndPrintCustomStreamFromMiniDump(path);
}
}
return 0;
}
```
Since `MiniDumpReadDumpStream` comes from the `Dbghelp.h` header, we just make
sure that we link against `Dbghelp.lib` when building our `.exe`, and that's it:
```text
set DumpReaderCommonCompilerFlags=/nologo /W4 /WX /Fe:"%BuildDir%\dump_reader.exe"
set DumpReaderDebugCompilerFlags=%DumpReaderCommonCompilerFlags% /Zi /Od
set DumpReaderReleaseCompilerFlags=%DumpReaderCommonCompilerFlags% /O2
set DumpReaderCommonLinkerFlags=/nologo /machine:x64 /incremental:no /subsystem:console /defaultlib:Kernel32.lib /defaultlib:Dbghelp.lib /pdb:"%BuildDir%\dump_reader.pdb"
set DumpReaderDebugLinkerFlags=%DumpReaderCommonLinkerFlags% /opt:noref /debug
set DumpReaderReleaseLinkerFlags=%DumpReaderCommonLinkerFlags% /opt:ref
set DumpReaderEntryPoint=%~dp0src\maya_read_custom_dump_user_streams_main.c
if "%BuildType%"=="debug" (
set DumpReaderBuildCmd=cl %DumpReaderDebugCompilerFlags% "%DumpReaderEntryPoint%" /link %DumpReaderDebugLinkerFlags%
) else (
set DumpReaderBuildCmd=cl %DumpReaderReleaseCompilerFlags% "%DumpReaderEntryPoint%" /link %DumpReaderReleaseLinkerFlags%
)
echo Compiling custom dump file reader (command follows)...
echo %DumpReaderBuildCmd%
%DumpReaderBuildCmd%
```
Once you compile this, you should be able to get the following output in your
command prompt:
```text
dump_reader.exe
Maya API version: 20200000
Custom API version: 20200000
Maya file version: 250
Y is up: 1
Last DAG parent: pSphere1
Last DAG child: pCube1
Last DAG message: 0
Last DG node added: pCube1
End of crash info.
```
Brilliant! But of course, while this is good enough for running on several dumps
at once, wouldn't it be nice if we had this same functionality within WinDbg
itself when we're looking at the dump file as well? How do we give ourselves the
same functionality there as well?
## Writing a WinDbg extension ##
The answer lies in the extensibility that WinDbg provides to us in the form of
[WinDbg extensions.](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/writing-wdbgexts-extensions)
These are basically similar to Maya plug-ins, but for WinDbg: they're, again,
DLLs with fixed exported entry points for being loaded into WinDbg. However,
unlike Maya plug-ins, there are some convenience macros for declaring the
equivalent of `MPxCommand`s that allow us to execute arbitrary code from within
the debugger engine. With writing an extension, we then have the ability to
write a special debugger command to basically extract the customized user stream
from our dump file, parse it, and display human-readable information about it.
Great! So let's go ahead and get started. Writing a WinDbg extension, as you
will soon see, is trivially simple once you understand the documentation.
!!! note Why not write a `DbgEng` extension instead?
Those of you who are already familiar with WinDbg might be wondering why
I'm electing to write what is known as a `WdbgExts` extension, rather than
utilizing the newer [`DbgEng`](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/writing-dbgeng-extension-code)
API for writing WinDbg extensions, or the even-newer [JavaScript API](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/javascript-debugger-scripting).
The answer is because, quite frankly, we don't need the features of `DbgEng`
and the overhead of COM involved for our uses, and JavaScript debugger
extensions...well...let's not talk about the overhead there.
All WinDbg extensions start with the entry point, along with some boilerplate
functions. In a new file called `windbg_custom_ext_main.c`, have the following
code pasted in there:
```cpp
#ifndef _WIN32
#error "Unsupported platform for compilation."
#endif // _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include
#ifndef DLL_EXPORT
#define DLL_EXPORT __declspec(dllexport)
#endif
#define KDEXT_64BIT
#include
#include
#include
#include
static EXT_API_VERSION gApiVersion = { 1, 0, EXT_API_VERSION_NUMBER64, 0 };
WINDBG_EXTENSION_APIS64 ExtensionApis = {0};
static USHORT gSavedMajorVersion = 0;
static USHORT gSavedMinorVersion = 0;
DLL_EXPORT BOOL DllMain(HINSTANCE hInstDLL, DWORD dwReason, DWORD dwReserved)
{
(void)hInstDLL;
(void)dwReserved;
switch (dwReason) {
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
case DLL_PROCESS_ATTACH:
default:
break;
}
return TRUE;
}
DLL_EXPORT VOID WinDbgExtensionDllInit(PWINDBG_EXTENSION_APIS64 lpExtApis, USHORT majorVer, USHORT minorVer)
{
ExtensionApis = *lpExtApis;
gSavedMajorVersion = majorVer;
gSavedMinorVersion = minorVer;
return;
}
DLL_EXPORT LPEXT_API_VERSION ExtensionApiVersion()
{
return &gApiVersion;
}
DLL_EXPORT VOID CheckVersion()
{
if (gApiVersion.MajorVersion != gSavedMajorVersion) {
dprintf("WARNING: The major version of the debugger and extension are mismatched. %d %d\n", gApiVersion.MajorVersion, gSavedMajorVersion);
}
if (gApiVersion.MinorVersion != gSavedMinorVersion) {
dprintf("WARNING: The minor version of the debugger and extension are mismatched. %d %d\n", gApiVersion.MinorVersion, gSavedMinorVersion);
}
return;
}
```
As far as the includes go, there's really only one point of interest here:
```cpp
#define KDEXT_64BIT
```
This may seem like a pretty random macro, but it's actually used by the
`wdbgext` APIs to switch to 64-bit addresses instead. Additionally, this _has_
to defined before all your other structure definitions as well in order to
actually be effective, which is why we place it earlier in the source.
After the usual suspects (includes), we then have basic code defining our WinDbg
extension version, and some global variables to hold the storage for the actual
version of WinDbg that we are running. Then...that's a bunch of
mysterious-looking, though simple functions. Let's go over them in turn and see
what they do for us:
The first one, `DLLMain`, should be pretty familiar to anyone with any Win32
programming experience: this is the default entry point called by Windows upon
loading our DLL (or starts/terminates a thread within the process). While we
don't need to specify one explicitly, I've gotten into the habit of doing so to
make it _explicitly_ clear what I intend to happen: nothing, as I've been bitten
by this a few times now.
The second one, `WinDbgExtensionDllInit`, is also another entry point, but this
one, like Maya's `initializePlugin`, is WinDbg-specific. It's called by WinDbg
right when our extension is loaded, and as you can see, all we're doing is just
setting the stored global variables to that of the current build version (major
and minor) of the Windows OS that the end-user is running our extension on.
`ExtensionApiVersion` just returns our hard-coded extension version, and is also
used by the debugger to determine what version of our extension we are
running. In the same vein, `CheckVersion` is called by the debugger each time
any time we run any of the commands in our extension (i.e. anything starting
with a `!` mark, like `!analyze`), which we'll use for just checking against the
debugger version (though really nothing much untoward should happen with what
we're planning to do). One thing to note is that you are _required_ to implement
`ExtensionApiVersion`, while `CheckVersion` is optional.
We can now focus on compiling the plug-in first, before adding our own
functionality. Thankfully, compiling a WinDbg extension is trivially simple,
especially since we're writing it in C. These are the relevant lines from my
`build.bat` build script:
```text
set WinDbgExtCommonCompilerFlags=/nologo /W4 /WX
set WinDbgExtDebugCompilerFlags=%WinDbgExtCommonCompilerFlags% /Zi /Od
set WinDbgExtReleaseCompilerFlags=%WinDbgExtCommonCompilerFlags% /O2
set WinDbgExtCommonLinkerFlags=/nologo /dll /machine:x64 /incremental:no /subsystem:windows /out:"%BuildDir%\windbg_%ProjectName%.dll" /pdb:"%BuildDir%\windbg_%ProjectName%.pdb" /defaultlib:Kernel32.lib /defaultlib:User32.lib
set WinDbgExtDebugLinkerFlags=%WinDbgExtCommonLinkerFlags% /opt:noref /debug
set WinDbgExtReleaseLinkerFlags=%WinDbgExtCommonLinkerFlags% /opt:ref
set WinDbgExtEntryPoint=%~dp0src\windbg_custom_ext_main.c
if "%BuildType%"=="debug" (
set WinDbgExtBuildCmd=cl %WinDbgExtDebugCompilerFlags% "%WinDbgExtEntryPoint%" /link %WinDbgExtDebugLinkerFlags%
) else (
set WinDbgExtBuildCmd=cl %WinDbgExtReleaseCompilerFlags% "%WinDbgExtEntryPoint%" /link %WinDbgExtReleaseLinkerFlags%
)
echo Compiling WinDbg extension (command follows)...
echo %WinDbgExtBuildCmd%
%WinDbgExtBuildCmd%
```
Yep, as you can see, it's no different than compiling any other sort of
DLL. Deploying it is even simpler:
```text
if not "%_NT_DEBUGGER_EXTENSION_PATH%"=="" (
echo Copying WinDbg extension to deployment location %_NT_DEBUGGER_EXTENSION_PATH% ...
echo .
copy /Y "%BuildDir%\windbg_%ProjectName%.dll" "%_NT_DEBUGGER_EXTENSION_PATH%"
)
```
The environment variable `__NT_DEBUGGER_EXTENSION_PATH` is known to all the
Microsoft debuggers (WinDbg Preview/WinDbg/Visual Studio) and all DLL files
located at the location specified will be automatically available to load
withiin WinDbg, which is why we copy our built binary there.
## Parsing our dump manually ##
Now that we're here, it's time for the real meat of what our extension should
do, which is to extract our custom user stream and parse it. However, there's a
problem: unlike what we did when writing our standalone user stream parser, we
can't make use of `MiniDumpReadDumpStream` here:
!!! warning Writing WdbgExts Extension Code
You must not attempt to call any DbgHelp or ImageHlp routines from a
debugger extension. This is not supported and may cause a variety of problems.
https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/writing-wdbgexts-extension-code
Whelp. Even though during my personal testing, I could call
`MiniDumpReadDumpStream` just fine, I wouldn't bet against the MSDN
documentation here since it's pretty much spelled out in black-and-white. What
can we do then to get our information instead?
Thanks to an [obscure post on Stack Overflow](https://stackoverflow.com/questions/5487279/how-do-i-extract-a-user-stream-from-a-windbg-extension),
the answer is, quite simply, to do the work ourselves:
```cpp
DLL_EXPORT DECLARE_API(readMayaDumpStreams)
{
char tempDirPath[MAX_PATH] = {0};
DWORD lenTempDirPath = GetEnvironmentVariable((LPCTSTR)TEMP_ENV_VAR_NAME, (LPTSTR)tempDirPath, (DWORD)MAX_PATH);
if (lenTempDirPath == 0) {
const size_t lenDefaultTempDirPath = strlen(DEFAULT_TEMP_DIRECTORY);
memcpy(tempDirPath, DEFAULT_TEMP_DIRECTORY, lenDefaultTempDirPath);
memset(tempDirPath + lenDefaultTempDirPath, 0, 1);
}
char dumpFilePath[MAX_PATH] = {0};
snprintf(dumpFilePath, MAX_PATH, "%s\\%s", tempDirPath, MINIDUMP_FILE_NAME);
HANDLE hFile = CreateFile(dumpFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
dprintf("ERROR: Could not open the dump file requested.\n");
return;
}
PMINIDUMP_HEADER pDumpHeader = NULL;
PMINIDUMP_DIRECTORY pDumpDirPath = NULL;
PMINIDUMP_USER_STREAM pDumpUserStream = NULL;
static const ULONG gBufSize = 0x1000;
void *pBuf = malloc(gBufSize);
DWORD bytesRead = 0;
BOOL bStat = ReadFile(hFile, pBuf, sizeof(MINIDUMP_HEADER), &bytesRead, NULL);
if (bStat == 0) {
dprintf("ERROR: File read failure.\n");
return;
}
ULONG numUserStreams = 0;
if (bytesRead == sizeof(MINIDUMP_HEADER)) {
pDumpHeader = (PMINIDUMP_HEADER)pBuf;
numUserStreams = pDumpHeader->NumberOfStreams;
for (ULONG i=0; i < numUserStreams; ++i) {
bStat = ReadFile(hFile, pBuf, sizeof(MINIDUMP_DIRECTORY), &bytesRead, NULL);
if (bStat == 0) {
dprintf("ERROR: Failed to read minidump directory.\n");
break;
}
pDumpDirPath = (PMINIDUMP_DIRECTORY)pBuf;
if (pDumpDirPath->StreamType != MAYA_CRASH_INFO_STREAM_TYPE) {
continue;
}
pDumpUserStream = (PMINIDUMP_USER_STREAM)pBuf;
ULONG streamBufSize = pDumpUserStream->BufferSize;
if (streamBufSize != sizeof(MayaCrashDumpInfo)) {
dprintf("ERROR: The stream size does not match that of the known crash dump structure.\n");
continue;
}
ULONG streamType = pDumpUserStream->Type;
PCHAR pUserStreamBuf = (PCHAR)pDumpUserStream->Buffer;
DWORD curPos = SetFilePointer(hFile, 0, NULL, FILE_CURRENT);
SetFilePointer(hFile, (LONG)pUserStreamBuf, NULL, FILE_BEGIN);
MayaCrashDumpInfo crashInfo = {0};
bStat = ReadFile(hFile, &crashInfo, streamBufSize, &bytesRead, NULL);
if (bStat == 0 || bytesRead != streamBufSize) {
dprintf("ERROR: Failed to read user stream.\n");
break;
}
dprintf("\n"
"-------------------------------------------------\n"
"Maya dump file information is as follows:\n"
"Stream type %d:\n"
"Maya API version: %d\n"
"Custom API version: %d\n"
"Maya file version:: %d\n"
"Is Y-axis up: %d \n"
"Last DAG parent: %s \n"
"Last DAG child: %s \n"
"Last DAG message: %d \n"
"Last DG node added: %s \n"
"\nEnd of crash info. \n"
"-------------------------------------------------\n"
"\n\n", streamType, crashInfo.verAPI, crashInfo.verCustom, crashInfo.verMayaFile, crashInfo.isYUp, crashInfo.lastDagParentName, crashInfo.lastDagChildName, crashInfo.lastDagMessage, crashInfo.lastDGNodeAddedName);
SetFilePointer(hFile, curPos, NULL, FILE_BEGIN);
}
} else {
dprintf("ERROR: Unable to read minidump header.\n");
}
CloseHandle(hFile);
free(pBuf);
return;
}
```
Ok, that was all very scary, I'm sure. Dealing with raw bytes! Think of the
children! So let's go over it, line-by-line.
The first part is the declaration of a new command using the `DECLARE_API` macro
from the WdbgExts API:
```cpp
DLL_EXPORT DECLARE_API(readMayaDumpStreams)
```
This will allow us to execute the WinDbg command `!readMayaDumpStreams` which
will then run our code.
After some initial boilerplate to get the path to our dump file and create a
file handle to it, we get to this part:
```cpp
PMINIDUMP_HEADER pDumpHeader = NULL;
PMINIDUMP_DIRECTORY pDumpDirPath = NULL;
PMINIDUMP_USER_STREAM pDumpUserStream = NULL;
```
"Interesting", you might think. "Where on earth would I know about these
structures had I not read that Stack Overflow post?"
The answer lies in `minidumpapiset.h`, which is strangely not documented very
well on MSDN. Essentially, we see that the memory layout of a minidump, like any
other file format, is pretty well-explained just by looking at the structs:
```cpp
typedef struct _MINIDUMP_HEADER {
ULONG32 Signature;
ULONG32 Version;
ULONG32 NumberOfStreams;
RVA StreamDirectoryRva;
ULONG32 CheckSum;
union {
ULONG32 Reserved;
ULONG32 TimeDateStamp;
};
ULONG64 Flags;
} MINIDUMP_HEADER, *PMINIDUMP_HEADER;
```
The `StreamDirectoryRva` member is again, a relative virtual address to the
start of `MINIDUMP_DIRECTORY` structures, which look like the following:
```cpp
typedef struct _MINIDUMP_DIRECTORY {
ULONG32 StreamType;
MINIDUMP_LOCATION_DESCRIPTOR Location;
} MINIDUMP_DIRECTORY, *PMINIDUMP_DIRECTORY;
```
Look familiar? Turns out that `StreamType` member _is_ the user stream type! And
in there, the `Location` member struct looks like this:
```cpp
typedef struct _MINIDUMP_LOCATION_DESCRIPTOR64 {
ULONG64 DataSize;
RVA64 Rva;
} MINIDUMP_LOCATION_DESCRIPTOR64;
```
...Which basically points to the location of our custom user stream data that
we're interested in (again, as a relative virtual address), along with its
stored size (but we know that already, since we packed the struct ourselves)
Thus! We have all the information we need to get to our user stream. Well,
almost:
```cpp
static const ULONG gBufSize = 0x1000;
void *pBuf = malloc(gBufSize);
DWORD bytesRead = 0;
BOOL bStat = ReadFile(hFile, pBuf, sizeof(MINIDUMP_HEADER), &bytesRead, NULL);
if (bStat == 0) {
dprintf("ERROR: File read failure.\n");
return;
}
```
Where is the magic number `0x1000` coming from? Well, apparently, that's the
[size of the minidump header, even though the struct would have you believe otherwise.](https://computer.forensikblog.de/en/2006/03/dmp-file-structure.html)
4096 bytes. The size of a single memory page on x64 Windows. Who would have guessed?
Continuing on, we read the file header into a buffer, and start to parse the
header itself:
```cpp
ULONG numUserStreams = 0;
if (bytesRead == sizeof(MINIDUMP_HEADER)) {
pDumpHeader = (PMINIDUMP_HEADER)pBuf;
numUserStreams = pDumpHeader->NumberOfStreams;
for (ULONG i=0; i < numUserStreams; ++i) {
bStat = ReadFile(hFile, pBuf, sizeof(MINIDUMP_DIRECTORY), &bytesRead, NULL);
if (bStat == 0) {
dprintf("ERROR: Failed to read minidump directory.\n");
break;
pDumpDirPath = (PMINIDUMP_DIRECTORY)pBuf;
if (pDumpDirPath->StreamType != MAYA_CRASH_INFO_STREAM_TYPE) {
continue;
}
pDumpUserStream = (PMINIDUMP_USER_STREAM)pBuf;
ULONG streamBufSize = pDumpUserStream->BufferSize;
if (streamBufSize != sizeof(MayaCrashDumpInfo)) {
dprintf("ERROR: The stream size does not match that of the known crash dump structure.\n");
continue;
}
ULONG streamType = pDumpUserStream->Type;
PCHAR pUserStreamBuf = (PCHAR)pDumpUserStream->Buffer;
DWORD curPos = SetFilePointer(hFile, 0, NULL, FILE_CURRENT);
SetFilePointer(hFile, (LONG)pUserStreamBuf, NULL, FILE_BEGIN);
MayaCrashDumpInfo crashInfo = {0};
bStat = ReadFile(hFile, &crashInfo, streamBufSize, &bytesRead, NULL);
if (bStat == 0 || bytesRead != streamBufSize) {
dprintf("ERROR: Failed to read user stream.\n");
break;
}
dprintf("\n"
"-------------------------------------------------\n"
"Maya dump file information is as follows:\n"
"Stream type %d:\n"
"Maya API version: %d\n"
"Custom API version: %d\n"
"Maya file version:: %d\n"
"Is Y-axis up: %d \n"
"Last DAG parent: %s \n"
"Last DAG child: %s \n"
"Last DAG message: %d \n"
"Last DG node added: %s \n"
"\nEnd of crash info. \n"
"-------------------------------------------------\n"
"\n\n", streamType, crashInfo.verAPI, crashInfo.verCustom, crashInfo.verMayaFile, crashInfo.isYUp, crashInfo.lastDagParentName, crashInfo.lastDagChildName, crashInfo.lastDagMessage, crashInfo.lastDGNodeAddedName);
SetFilePointer(hFile, curPos, NULL, FILE_BEGIN);
```
As you can see, with those structures now in the back of our minds, this code
becomes less mysterious in what it's doing. It boils down essentially to:
1. Inspecting the header, finding the number of user streams within the dump.
2. We then proceed to walking each stream in turn (i.e. `MINIDUMP_DIRECTORY`),
checking the stream type to see if it's the one we're interested in (i.e.
our custom stream type `MAYA_CRASH_INFO_STREAM_TYPE`).
3. We then cast that to our custom stream memory layout and again read the bytes
into another buffer.
4. Finally, we print the information from the bytes to human-readable format
using `dprintf`, which is a WdbgExts API function for printing to the output
view of the WinDbg application.
If you compile this DLL now and load it in WinDbg (using the `.load` command)
and then fire the command `!readMayaDumpStreams`, you should get the exact same
output that we had in the previous chapter.
With that, congratulations! You've accomplished quite a feat here: not only have
you overriden the Autodesk crash handler in Maya, a tactic you could apply to
almost any application, you've also learned a little more about how error
handling and crashes are handled on Win32, and how to write some tooling to help
yourself and others debug those crashes via either your own standalone tools, or
integrating them into WinDbg. I hope this has been a useful and interesting
journey.
# Epilogue: potential problems #
Now, let's discuss some potential cons of the approaches discussed in this
tutorial, and some solutions for those.
## Runtime overhead and performance ##
Well, the most obvious one has been staring at us in the face all this
while. For this plug-in to work effectively in production, we'd essentially have
to register a ton of additional callbacks within Maya, which adds a runtime
cost. We also have to consider, depending on what we're storing into memory
(i.e. perhaps storing the _entire scene_, or parts of it, so that we can
reconstruct it from the dump file), how much overhead we're going to be adding
there as well.
However, in my opinion, this is a reasonable cost when weighed against the value
of the data you'd be getting. The way I see this working is that when you have
a problem that is occuring on an end-user's machine that defies all explanation
and occurs without reproducibility, you deploy this plug-in to them and get more
information while they take a minor hit in performance. The amount of data
captured could also be tweaked with plug-in options in order to mitigate against
performance as well. Ultimately, this is a solveable problem with some careful
pipelining work.
## Safety and guarantees? Don't count on it ##
Another thing that we should be aware of here is that this solution is _by no
means 100% bulletproof_. Just because the example code here places the data in
the `.bss` section doesn't mean that it's going to be safe from buffer overruns,
or other types of memory corruption issues. There's a bunch of ways around this
issue, depending on how far you're willing to take it:
1. Periodically flush the buffers to other parts of reserved process
memory and set the page protections appropriately. This will allow for a (more)
permanant record of what you've done and you can just read from them when
writing the dump to disk instead.
2. Continuously write to the minidump file handle, either manually or by
re-writing the _entire dump_ each time a callback is executed, which would be
_incredibly expensive_, but at least guarantee a more timely update of
information before your crash occurs.
All options really depend on your scenario and what kind of crashes you are
expecting to occur within your pipeline. Adjust accordingly.
Another issue, on a more humanistic side, is that we don't necessarily guarantee
that the information we capture will be helpful in diagnosing the _actual_ root
cause. However, on this point, I like to think of debugging as a case within a
Sherlock Holmes story: all the data we're getting here is just that, data. As
long as we use this data to help us in making theories about root causes suit
_them_, rather than the other way round, it's better than the normal information
you get from a minidump.
Yet another problem is that the method of storing the information to be written
to the dump outlined here assumes thread-safe writes; there are no
synchronization primitives being used here to guarantee that multiple threads
don't attempt to update the same memory location. You can certainly throw in a
couple of mutexes (or more likely,
[critical section objects](https://docs.microsoft.com/en-us/windows/win32/sync/critical-section-objects))
here and there to try and safeguard access, but that would likely come with its
own share of problems and further compound the performance issues.
Finally, we don't guarantee that _all_ types of exceptions will be caught by
this, even if you go ahead and implement all the VS intrinsics, the CRT signal
handlers, etc. There are some types of exceptions that are just _really_ hard to catch, as
[this article explains](https://peteronprogramming.wordpress.com/2016/05/29/crashes-you-cant-handle-easily-1-seh-failure-on-x64-windows/).
If you find yourself wanting of a solution to tackle one of the issues he brings
up in the series of articles, I encourage you to read the WER solution offered
there and see if works for you.
# Closing thoughts #
Well, if you've stuck around till this point, perhaps this has given you an
insight into how exceptions are handled on Windows in the modern x64 era. Even
if you don't necessarily implement this the exact same way, hopefully you'll
have learned a thing or two along the way to help yourself make it easier to
debug your own applications in the future. The tools for debugging user-mode
crashes have never been more powerful than in our current generation of hardware
and software, and I am of the firm belief that there is little excuse these days
to plead ignorance when it comes to identifying root causes.
Fixing them, on the other hand...that'll have to be a tale (or two) for another time.
# Credits #
The work I've done here would not be possible (or at the least, would have been
incredibly difficult) without the prior work of some amazing authors:
* [Cristian Adam](https://www.codeproject.com/Articles/154686/SetUnhandledExceptionFilter-and-the-C-C-Runtime-Li)'s
article on his original implementation of this approach.
* [Jérémie Laval](https://blog.neteril.org/blog/2016/12/23/diverting-functions-windows-iat-patching/),
for more information on how the Import Address Table works on Win32 and
providing valuble sample code for reference.
* [peteronprogramming](https://peteronprogramming.wordpress.com/2016/05/29/crashes-you-cant-handle-easily-1-seh-failure-on-x64-windows/),
for his series of articles regarding the shortfalls of various approaches on
Windows to dealing with this problem.
* [Morgan Mcguire](https://casual-effects.com/markdeep/), for the Markdeep
format used to write this article.
* [Windows via C/C++, Fifth Edition](https://www.amazon.com/Windows-via-softcover-Developer-Reference/dp/0735663777)
by Jeffrey Richter and Christophe Nasarre, whose code and explanation in
Chapter 22: DLL Injection and API Hooking was invaluable as a reference when
writing this implementation.
* Aras Pranckevicius, for writing the original Markdeep theme used in this tutorial.
# Legal disclaimer & License #
All work done here is of a personal nature and opinions expressed here are
purely my own and do not reflect any of my current/previous employers. No code
demonstrated here is indicative of my _actual_ professional work (which usually
tends to be a lot more boring and mundane in the first place), and should not be
viewed as representative of the companies I work/worked for/with, nor an
endorsement of any coding practices or otherwise in an official capacity.
The license for this tutorial is available in the code repository that
accompanies this, in the file named `LICENSE`, which also governs the code
samples provided, except where otherwise noted.