The x64dbg threading model
20 Oct 2016, by mrexodiaIn a recent post about the architecture of x64dbg there was a comment to explain the “little bit of a mess” that is the threading model of x64dbg. It is indeed not particularly pretty, but everything has a purpose.
Command loop thread
The first thread of interest is the command thread. This thread is created during the initialization phase and it infinitely waits and executes commands that are given to it through the so-called CommandProvider. In the very first version of x64dbg the command thread was the main thread and since the introduction of the GUI this has been moved to a separate thread in favor of the GUI thread.
Debug thread
The debug thread runs the TitanEngine debug loop. It still works the same as two years ago:
Between WaitForDebugEvent
and ContinueDebugEvent
, the debuggee is in a paused state. The event handlers use event objects to communicate with the GUI. When you click the ‘Run’ button it will set an event object and continue the debug loop and in that way also continue the debuggee.
Here is a simplified version of the cbDebugRun command callback (running on the command thread):
bool cbDebugRun(int argc, char* argv[])
{
// Don't "run" twice if the program is already running.
if(dbgisrunning())
return false;
//Set the event, which makes calls to wait(WAITID_RUN) return.
unlock(WAITID_RUN);
return true;
}
On the debug loop thread we have the cbPauseBreakpoint breakpoint event handler that waits for the user to resume the debug loop (again, simplified):
void cbPauseBreakpoint()
{
//Unset (reset) the event.
lock(WAITID_RUN);
//Wait for the event to be set, a call to unlock(WAITID_RUN).
wait(WAITID_RUN);
}
Here is a simple diagram giving you an overview of what’s going on with the basic threads.
- A block represents a thread;
- A dashed arrow represents starting a new thread;
- A red arrow represents thread termination;
- A circle contains the termination condition.
Some challenging areas are properly signaling the termination of the debuggee. Issues #303, #323 and #438 were, with the great help and patience of wk-952, fixed and this signaling appears to be working now!
Script thread
When dealing with scripting, you usually want to simulate user interaction. This means that the expectation is that the following x64dbgpy (Python) script should be equivalent to:
- Setting a breakpoint on
__security_init_cookie
- Pressing the run button
- Stepping five times
- Setting
RAX
to0x2b992ddfa232
- Stepping out of the function
from x64dbgpy.pluginsdk import *
debug.SetBreakpoint(x64dbg.DbgValFromString("__security_init_cookie"))
debug.Run()
for _ in range(0,5):
debug.StepIn()
register.SetRAX(0x2b992ddfa232)
debug.StepOut()
There has to be some sort of synchronization at the end of debug.Run
and debug.StepOut
to make sure the debuggee is paused before the next command is executed. The implementation for this is in _plugin_waituntilpaused and looks like this:
PLUG_IMPEXP bool _plugin_waituntilpaused()
{
while(DbgIsDebugging() && dbgisrunning()) //wait until the debugger paused
Sleep(1);
return DbgIsDebugging();
}
The implementation of dbgisrunning is a check if lock(WAITID_RUN)
has been called.
Worker threads
There are various threads that just do periodic background work. These include:
- Refreshing the memory map
- Refreshing the dumps
- Refreshing the time-wasted counter
Other threads are triggered once to fulfill a specific purpose. These include:
- Command animation
- Executing asynchronous Script DLLs
- Loading databases from disk
- Executing Scylla
- Querying the name of a handle
- Loading a script from disk
TaskThread
For interaction with the GUI, performance is very important. For this purpose jdavidberger has implemented TaskThread. It’s some variadic templates that basically allow you to trigger an arbitrary function from a different thread to then quickly return to the real work.
The actual thread runs in an infinite loop, waiting for the TaskThread
instance to receive a WakeUp
(trigger). Once awake, the specified function is executed and after that the thread is being delayed for a configurable amount of time. This ignores all triggers (except the last one) within the delay time to avoid unnecessary work.
The relevant code:
template <typename F, typename... Args> void TaskThread_<F, Args...>::WakeUp(Args... _args)
{
wakeups++;
EnterCriticalSection(&access);
args = CompressArguments(std::forward<Args>(_args)...);
LeaveCriticalSection(&access);
// This will fail silently if it's redundant, which is what we want.
ReleaseSemaphore(wakeupSemaphore, 1, nullptr);
}
template <typename F, typename... Args> void TaskThread_<F, Args...>::Loop()
{
std::tuple<Args...> argLatch;
while(active)
{
WaitForSingleObject(wakeupSemaphore, INFINITE);
EnterCriticalSection(&access);
argLatch = args;
ResetArgs();
LeaveCriticalSection(&access);
if(active)
{
apply_from_tuple(fn, argLatch);
std::this_thread::sleep_for(std::chrono::milliseconds(minSleepTimeMs));
execs++;
}
}
}
As an example, here is the declaration and wake of the thread that updates the call stack (an expensive operation in some cases):
static DWORD WINAPI updateCallStackThread(duint csp)
{
stackupdatecallstack(csp);
GuiUpdateCallStack();
return 0;
}
void updateCallStackAsync(duint csp)
{
static TaskThread_<decltype(&updateCallStackThread), duint> updateCallStackTask(&updateCallStackThread);
updateCallStackTask.WakeUp(csp);
}
Having a different thread handle expensive operations is critial to a responsive interface. Lots of information is rarely looked at (memory map, call stack, SEH information) and can suffer a little delay (100ms) before being updated. This is the same with the current state of the disassembly. When holding F7 to quickly step a little you don’t need perfect accuracy, as long as the disassembly lands on the correct state within reasonable time after releasing the step button.
GUI Thread
The most important (and the most annoying) thread is the Qt GUI thread. If you want to know more, check out the Qt Threading Basics for a 6 page introduction on how it works.