How Windows DTrace Works Under the Hood: bcdedit → winload.efi → ntoskrnl → dtrace.sys

About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.

This research is published for educational and defensive purposes. The target sample is anonymized throughout.

DTrace is a command-line tracing utility for monitoring and debugging system behavior in real time. The Windows port — integrated since the Windows 10 19H1-era kernels — lets you dynamically instrument both kernel and user-mode code without modifying the code itself. This is a full walkthrough of how Windows DTrace actually works, traced end to end from the bcdedit switch through the EFI boot loader, kernel initialization, and finally the syscall audit path.

Architecture

Windows DTrace architecture

Enabling it

Enable DTrace with bcdedit, after making sure the DTrace install directory is on PATH:

set PATH=%PATH%;"C:\Program Files\DTrace"
bcdedit /set dtrace ON

A reboot is required. Listing available probes:

dtrace -l output

A simple aggregation example — count syscall entries per process over five seconds:

dtrace -Fn "tick-5sec {exit(0);} syscall:::entry{ @num[pid,execname] = count();} "

syscall aggregation output

How bcdedit sets it

bcdedit /set dtrace ON writes a specific registry value:

bcdedit /set dtrace ON

It sets the Element value to 1 under HKEY_LOCAL_MACHINE\BCD00000000\Objects\{5ea502ac-8b8f-11f0-9f0f-70d823d98bbc}\Elements\26000145:

Element value = 1

/set dtrace OFF writes 0 to the same key:

Element value = 0

Now let’s reverse bcdedit.exe to see how this happens.

BcdEditMain → BcdEditSetSwitch

BcdEditMain

Inside BcdEditMain, BcdEditIsSwitchPresent("Set") detects the Set switch and routes into BcdEditSetSwitch().

BcdEditSetSwitch returns the object GUID

BcdEditGetParameterSet(L"set") looks the Set parameter up in a global BcdEditParameterList:

struct _BCD_EDIT_ParamList
{
     LIST_ENTRY* pFirst;
     LIST_ENTRY *pEnd;
     WCHAR*      ParamString0;
     _int64         ParamNums;
     WCHAR*      ParamString1[3];
};

The global BcdElements array defines 284 elements total:

BcdElements (1) BcdElements (2)

BcdStringToGuid resolves the set parameter to its store GUID:

BcdStringToGuid

The Set parameter’s GUID is {0FA926493h, 6F1Ch, 4193h, <0A4h, 14h, 58h, 0F0h, 0B2h, 45h, 6Dh,1Eh>}.

Opening the store and object

BcdEditOpenStore opens the store, then BcdOpenObject opens the GUID’s object:

BcdEditOpenStore

BcdEditOpenStore parameters

BcdOpenObject

Resolving the element and writing it

BcdiStringToDataType finds the dtrace element’s value from the global BcdElements array:

BcdiStringToDataType (1) BcdiStringToDataType (2)

The _BCD_ELEMENT structure:

struct _BCD_ELEMENT
{
unsigned __int64 ElementObjectValue;
void *BcdTitle;
unsigned __int32 Flags4;
unsigned __int32 Flags5;
void *BcdCustomsParam;
void *p1;
void *p2;
void *p3;
};

With 284 elements defined, the dtrace entry is:

BcdElements array

.rdata:000000014003B1F8 dq 26000145h
.rdata:000000014003B200 dq offset aBcdeOsloaderTy_94 ; "BCDE_OSLOADER_TYPE_DTRACE_ENABLED"
.rdata:000000014003B208 dq 10000003h
.rdata:000000014003B210 dq offset aDtrace       ; "dtrace"
.rdata:000000014003B218 dq 0
.rdata:000000014003B220 dq 0
.rdata:000000014003B228 dq 0

That’s the dtrace element ID 0x26000145. Finally, BcdSetElementDataWithFlags sets the element’s ON(1)/OFF(0) switch, ultimately calling BiSetRegistryValue to write the value:

BcdSetElementDataWithFlags / BiSetRegistryValue

That’s the bcdedit side. Next: what happens at boot, after the reboot.

Boot: winload.efi

winload.efi defines a structure for the DTrace extension:

.rdata:00000001802A2D10 OslpDynamicSchemaExtensions WINLOAD_OslpDynamicSchemaExtensions <offset aExtMsWinNtosTr, \
.rdata:00000001802A2D10   ; DATA XREF: OslpApplyDynamicSchemaExtensions+1D↑o
.rdata:00000001802A2D10   ; OslpApplyDynamicSchemaExtensions+24↑r
.rdata:00000001802A2D10   offset aDtraceSys, \ ; "ext-ms-win-ntos-trace-l1-1-0"
.rdata:00000001802A2D10   offset OslpDtraceExtensionEnabled> ; "dtrace.sys"
.rdata:00000001802A2D28
struct _WINLOAD_OslpDynamicSchemaExtensions
{
  WCHAR *aExtMsWinNtosTr;
  WCHAR *aDtraceSys;
  void *OslpDtraceExtensionEnabled;
};

The third field is the OslpDtraceExtensionEnabled function:

OslpDtraceExtensionEnabled

It reads the boolean value of BCD object 26000145:

BootOptionBoolean = BlGetBootOptionBoolean(*(_QWORD *)(ApplicationEntry + 24), 0x26000145, v12);

v10 defaults to 0; if the read value is 1, v10 becomes 1, and OslpIsApiSetHosted loads dtrace.sys.

When is this called? From OslpApplyDynamicSchemaExtensions:

OslpDtraceExtensionEnabled call site (1)

OslpDtraceExtensionEnabled call site (2)

Debugging it live

Under WinDbg two-machine debugging, sxe ld winload.efi breaks into winload’s debug-service entry after the debug-mode restart:

winload debug service entry

winload!DebugService2+0x5:
00000000`00d20935 cc              int     3
kd> kb
 # RetAddr               : Args to Child                                                           : Call Site
00 00000000`00b02822     : 00000044`00003ecf 00000000`00cb0255 00000000`00000000 00000000`00000000 : winload!DebugService2+0x5
01 00000000`00cadc5c     : fffff803`5a626960 00000000`00ab60f0 00000000`00000000 00000000`00000000 : winload!DbgLoadImageSymbols+0x62
02 00000000`00cae229     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : winload!BlBdStart+0x1c8
03 00000000`00c30697     : 000007ff`ffff8000 00000000`00000000 00000000`00000000 00000000`00000000 : winload!BlBdInitialize+0x409
04 00000000`00c2ec44     : 00000000`00000000 00000000`001a7f50 00000000`00000940 00000000`001a8000 : winload!InitializeLibrary+0x47b
05 00000000`00ab71ab     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : winload!BlInitializeLibrary+0x58
06 00000000`1025e1b6     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`001a8000 : winload!OslMain+0x10b
07 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`001a8000 50504120`544f4f42 : 0x1025e1b6

Break on winload!OslpDtraceExtensionEnabled and g:

bp OslpDtraceExtensionEnabled

At this point only one winload.efi is loaded:

single winload.efi

Step through:

single-stepping

Before the call, the boolean in the memory pointed to by r8 is 0:

boolean = 0 before

After BlGetBootOptionBoolean runs, it’s 1:

boolean = 1 after

That means DTrace should be enabled — this sets the point where the apischem driver loads, and later BlLdrLoadImage loads dtrace.sys:

BlLdrLoadImage loading dtrace.sys

So the chain runs from winload.efi’s entry OslpMain all the way down to this function; once dtrace.sys is loaded at boot, control passes into ntoskrnl initialization.

Initialization: KeInitSystem

Windows boot initialization has Phase 0 and Phase 1. DTrace is set up in Phase 1’s KeInitSystem:

KeInitSystem

One step inside is KiInitDynamicTraceSupport():

KiInitDynamicTraceSupport call

KiInitDynamicTraceSupport

KiInitDynamicTraceSupport

The logic initializes KiDynamicTraceEnnable to 0; when TraceInitSystem succeeds, it becomes 1 | 2 (= 3), and when KdDebuggerNotPresent is nonzero (i.e. no kernel debugger present), it becomes 2 | 5 (= 7). The main work is in TraceInitSystem, an exported function from the external driver — if DTrace is enabled, this call succeeds.

TraceInitSystem

TraceInitSystem

The system build must be > 1900 (Windows 10). TraceInitSystem registers several callback/probe types:

TraceInitSystem callbacks

  • DtEtwpEventCallback — ETW event probes
  • StpCallbackEntry — syscall entry probes
  • StpCallbackReturn — syscall return probes
  • FbtpCallback — function boundary probes
  • FtpPidCallback — process-PID function probes

Debugging it live

Break on dtrace.sys’s TraceInitSystem and g:

breakpoint on TraceInitSystem

The breakpoint hits, and the call stack confirms the path:

call stack

The instruction is call dtrace!TraceInitSystem, confirming it’s implemented inside dtrace:

call dtrace!TraceInitSystem

TraceInitSystem detail

# RetAddr               : Args to Child                                           : Call Site
00 fffff803`cc9e8941     : 00000000`00000000 00000000`00000000  : dtrace!TraceInitSystem
01 fffff803`cca2067b     : 00000000`00000000 00000000`00000000  : nt!KiInitDynamicTraceSupport+0x51
02 fffff803`cc9cba4f     : 00000000`00000001 ffffd401`30e06b10  : nt!KeInitSystem+0x153
03 fffff803`cc4ca713     : fffff803`cc4ca6f0 fffff803`5a63e530  : nt!Phase1InitializationDiscard+0xaf7
04 fffff803`cc25904a     : ffffa585`1f702080 fffff803`cc4ca6f0  : nt!Phase1Initialization+0x23
05 fffff803`cc4741c4     : fffff803`5b87c180 ffffa585`1f702080  : nt!PspSystemThreadStartup+0x5a
06 00000000`00000000     : ffffd401`30e07000 ffffd401`30e01000  : nt!KiStartSystemThread+0x34

dtrace!TraceInitSystem’s first argument (ecx) returns the address of dtrace!TraceSystemApi, which ntoskrnl assigns to KiDynamicTraceContext — i.e. dtrace!TraceSystemApi ends up pointing at KiDynamicTraceContext:

TraceSystemApi table

KiDynamicTraceContext

The second argument (edx) passes the global KiDynamicTraceCallouts structure:

KiDynamicTraceCallouts

The third argument passes the global qword_140D1F2A8, which gets assigned to dtrace!TraceAccessMemory:

qword_140D1F2A8 → TraceAccessMemory

The return value in eax is 0, indicating success. The overall call graph:

call graph

KeSetSystemServiceCallback

Inside KiInitDynamicTraceSupport there’s a key structure:

.rdata:0000000140009994 KiDynamicTraceContext dd 48h       ; DATA XREF: KiInitDynamicTraceSupport+1A009↓o
.rdata:0000000140009994                 dd 3C0h
.rdata:0000000140009998                 dd 1
.rdata:000000014000999C                 dd 90h
.rdata:00000001400099A0                 dd 0F0h
.rdata:00000001400099A4                 dd 0
.rdata:00000001400099A8                 dq offset KeSetSystemServiceCallback
.rdata:00000001400099B0                 dq offset KeSetTracepoint
.rdata:00000001400099B8                 dq offset EtwRegisterEventCallback
.rdata:00000001400099C0                 dq offset PsGetBaseTrapFrame
.rdata:00000001400099C8                 dq offset KiGetTrapFrameRegister
.rdata:00000001400099D0                 dq offset MmEnumerateSystemImages

The notable one is KeSetSystemServiceCallback, which installs callbacks on kernel functions to monitor. It first gets the service table from KiGetSystemServiceTraceTable, then computes a hash of the function name and sets a callback in that function’s entry in the trace table:

KeSetSystemServiceCallback

Debugging it live

Break on KeSetSystemServiceCallback:

bp KeSetSystemServiceCallback

KeSetSystemServiceCallback args

It takes four arguments. rcx:

arg rcx

r8:

arg r8

Audit: KiSystemCall64

Running dtrace -Fn "tick-5sec {exit(0);} syscall:::entry{ @num[pid,execname] = count();} " triggers a syscall-entry trace:

dtrace -Fn

The command has dtrace.exe send an IoControlFile request to dtrace.sys, whose IO routine calls KeSetSystemServiceCallback to set up the audit. From then on, a process calling into the SSDT lands in the kernel’s KiSystemCall64, which checks the KiDynamicTraceMask flag to decide whether DTrace auditing is active:

KiSystemCall64

If KiDynamicTraceMask has bit 1 set, KiSystemCall64 enters KiTrackSystemCallEntry (DTrace’s callback entry point), then calls the original SSDT handler, and finally KiTrackSystemCallExit leaves DTrace. KiDynamicTraceMask gets its 1 bit from KeSetSystemServiceCallback, which does a lock or 1 on it.

Inside KiTrackSystemCallEntry, it first checks whether the current audited SystemApi is in the trace table:

KiTrackSystemCallEntry lookup

Then it checks whether the resolved structure’s flag is in the KiDynamicTraceEnabled state; if so, it calls the trace function:

KiDynamicTraceEnabled check

Debugging it live

Break at KiSystemCall64’s KiTrackSystemCallEntry:

bp KiTrackSystemCallEntry

KiTrackSystemCallEntry

After the breakpoint, a call nt!KscpCfgDispatchUserCallTargetEsSmep appears. Looking at that function shows it’s just a jmp rax:

KscpCfgDispatchUserCallTargetEsSmep — jmp rax

At that point rax = fffff805545de810, which disassembles to dtrace!StpCallbackEntry — DTrace’s syscall-audit callback:

dtrace!StpCallbackEntry

That’s the full path, from the bcdedit switch to the per-syscall audit callback.

Summary

Windows DTrace is wired in three layers: bcdedit writes a BCD element (0x26000145), winload.efi reads it during boot via BlGetBootOptionBoolean and loads dtrace.sys, and ntoskrnl (in KeInitSystem → KiInitDynamicTraceSupport) calls TraceInitSystem to hand over KiDynamicTraceContext and the callout table. From there, KeSetSystemServiceCallback installs the syscall-trace callbacks and KiSystemCall64 gates the whole thing behind KiDynamicTraceMask, dispatching through KiTrackSystemCallEntry into dtrace!StpCallbackEntry.

What’s interesting is how cleanly the layers separate: the BCD switch is just a boolean in the boot configuration store, and everything downstream reacts to that single bit. Once you’ve traced the bit from the registry through the boot loader to the kernel, the “magic” of DTrace reduces to a fairly ordinary initialization-and-callback pattern.

Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).

Get in touch