Since we have started writing code and it is going to be more and more complex, I think it is right time to understand a bit more about the process anatomy. We should learn to debug our own code so we can at least find and fix bugs.
We will use code published here to learn native code debugging.
Build code with /Zi option to get the symbols (*.pdb).
cl /Zi Tables.c 1,287 Tables.c 543,232 Tables.exe 2,755,296 Tables.ilk 5,641 Tables.obj 7,081,984 Tables.pdb
Get Windows Debugger from Microsoft Store. This one is an app that works on Windows 10. There are other ways to get a debugger (WinDbg and KD), you can install Debugging Tools for Windows in Windows SDK/WDK.
In WinDbg, Go to File > Launch Executable (Advanced),
select Tables.exe and in argument pass a value, 5.
This will launch the application in the debugger.
This is an early breakpoint in the loader which is hit if a debugger is attached. At this point, exe is loaded in memory and ready to run.
0:000> kc # Call Site 00 ntdll!LdrpDoDebuggerBreak 01 ntdll!LdrpInitializeProcess 02 ntdll!_LdrpInitialize 03 ntdll!LdrpInitialize 04 ntdll!LdrInitializeThunk
‘k‘ command print stack, there are several variations. The one I used is ‘kc‘ which prints just the function names.
0:000> lm start end module name 00007ff6`a6230000 00007ff6`a62ba000 Tables (deferred) 00007fff`09eb0000 00007fff`0a123000 KERNELBASE (deferred) 00007fff`0b730000 00007fff`0b7e2000 KERNEL32 (deferred) 00007fff`0d7c0000 00007fff`0d9a1000 ntdll (pdb symbols)
‘lm‘ lists the modules that are loaded in process memory. Modules, here, means dlls and exe. This command also gives the start and end address of each module. Now, if you remember the PE header of the executable, it has a DOS header and a PE Signature (PE00).
We can dump each module to find this. I used “db” to dump each byte from the given address.
0:000> db 00007ff6`a6230000 L100 00007ff6`a6230000 4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00 MZ.............. 00007ff6`a6230010 b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00 ........@....... 00007ff6`a6230020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00007ff6`a6230030 00 00 00 00 00 00 00 00-00 00 00 00 f0 00 00 00 ................ 00007ff6`a6230040 0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68 ........!..L.!Th 00007ff6`a6230050 69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f is program canno 00007ff6`a6230060 74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20 t be run in DOS 00007ff6`a6230070 6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00 mode....$....... 00007ff6`a6230080 fc fb af 9a b8 9a c1 c9-b8 9a c1 c9 b8 9a c1 c9 ................ 00007ff6`a6230090 d7 fe c5 c8 b2 9a c1 c9-d7 fe c2 c8 bd 9a c1 c9 ................ 00007ff6`a62300a0 d7 fe c4 c8 34 9a c1 c9-d7 fe c0 c8 bb 9a c1 c9 ....4........... 00007ff6`a62300b0 b8 9a c0 c9 e9 9a c1 c9-ea f2 c2 c8 b0 9a c1 c9 ................ 00007ff6`a62300c0 ea f2 c4 c8 9e 9a c1 c9-ea f2 c5 c8 a8 9a c1 c9 ................ 00007ff6`a62300d0 d1 f2 c5 c8 b9 9a c1 c9-d1 f2 c3 c8 b9 9a c1 c9 ................ 00007ff6`a62300e0 52 69 63 68 b8 9a c1 c9-00 00 00 00 00 00 00 00 Rich............ 00007ff6`a62300f0 50 45 00 00 64 86 07 00-2f 0a 9e 5b 00 00 00 00 PE..d.../..[....
You can use “!dh” to dump header of any loaded module from memory.
0:000> !dh 00007ff6`a6230000 File Type: EXECUTABLE IMAGE FILE HEADER VALUES 8664 machine (X64) 7 number of sections 5B9E0A2F time date stamp Sun Sep 16 13:15:51 2018 [...] OPTIONAL HEADER VALUES 20B magic # 14.15 linker version 68600 size of code 1E200 size of initialized data 0 size of uninitialized data 120D address of entry point <<---This is the address to main() 1000 base of code ----- new ----- [...] 0 [ 0] address [size] of Export Directory 87360 [ 28] address [size] of Import Directory 0 [ 0] address [size] of Resource Directory 82000 [ 45C0] address [size] of Exception Directory 0 [ 0] address [size] of Security Directory 89000 [ 7B8] address [size] of Base Relocation Directory 76820 [ 38] address [size] of Debug Directory 0 [ 0] address [size] of Description Directory 0 [ 0] address [size] of Special Directory 0 [ 0] address [size] of Thread Storage Directory 76860 [ 100] address [size] of Load Configuration Directory 0 [ 0] address [size] of Bound Import Directory 87000 [ 360] address [size] of Import Address Table Directory 0 [ 0] address [size] of Delay Import Directory 0 [ 0] address [size] of COR20 Header Directory 0 [ 0] address [size] of Reserved Directory SECTION HEADER #1 .text name 684A6 virtual size 1000 virtual address 68600 size of raw data 400 file pointer to raw data 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 60000020 flags Code (no align specified) Execute Read SECTION HEADER #2 [...]
Now, there is an import table. This import table has information of all the external functions that will be called from the code in this binary and which dll they come from.
I’ll use “dps” to dump memory, iterating by pointer size (s to resolve symbols). I’ll add the offset of import table to base address and use that to start dumping memory.
0:000> dps 00007ff6`a6230000+87000 00007ff6`a62b7000 00007fff`0b743c80 KERNEL32!QueryPerformanceCounterStub 00007ff6`a62b7008 00007fff`0b750680 KERNEL32!GetCurrentProcessId 00007ff6`a62b7010 00007fff`0b743c50 KERNEL32!GetCurrentThreadId 00007ff6`a62b7018 00007fff`0b747a00 KERNEL32!GetSystemTimeAsFileTimeStub 00007ff6`a62b7020 00007fff`0d835b80 ntdll!RtlInitializeSListHead 00007ff6`a62b7028 00007fff`0b750570 KERNEL32!RtlCaptureContext 00007ff6`a62b7030 00007fff`0b74aa60 KERNEL32!RtlLookupFunctionEntryStub 00007ff6`a62b7038 00007fff`0b731010 KERNEL32!RtlVirtualUnwindStub 00007ff6`a62b7040 00007fff`0b74dc30 KERNEL32!IsDebuggerPresentStub 00007ff6`a62b7048 00007fff`0b765e70 KERNEL32!UnhandledExceptionFilterStub 00007ff6`a62b7050 00007fff`0b74d530 KERNEL32!SetUnhandledExceptionFilterStub 00007ff6`a62b7058 00007fff`0b74aad0 KERNEL32!GetStartupInfoWStub 00007ff6`a62b7060 00007fff`0b74a300 KERNEL32!IsProcessorFeaturePresentStub 00007ff6`a62b7068 00007fff`0b74a1c0 KERNEL32!GetModuleHandleWStub 00007ff6`a62b7070 00007fff`0b751170 KERNEL32!WriteConsoleW 00007ff6`a62b7078 00007fff`0b74d880 KERNEL32!RtlUnwindExStub
If you examine output of “!dh”, you will find a lot on interesting information embedded in the executable that helps Windows setup process address space for your application. Go on, explore it… Ask, what doesn’t make sense.
Only if you have symbols configured, i.e. debugger knows where to load symbols from, more information will be available. Like, source path and line numbers.
You can use “.sympath” to list what symbol path is configured. Default path is to download and cache symbols from microsoft.com.
0:000> .sympath Symbol search path is: cache*;SRV*https://msdl.microsoft.com/download/symbols Expanded Symbol search path is: cache*;srv*https://msdl.microsoft.com/download/symbols
If your symbol path is empty, you can restore default path with “.symfix” command.
To add your symbol path to default path list, use “.sympath+ <path_to_symbol>“.
0:000> .sympath+ c:\dev\source\mcu-india\cpl\basics\achindra_mca_2005\debugging101\ Symbol search path is: cache*;SRV*https://msdl.microsoft.com/download/symbols;c:\dev\source\mcu-india\cpl\basics\achindra_mca_2005\debugging101\ Expanded Symbol search path is: cache*;srv*https://msdl.microsoft.com/download/symbols;c:\dev\source\mcu-india\cpl\basics\achindra_mca_2005\debugging101\ ************* Path validation summary ************** Response Time (ms) Location Deferred cache* Deferred SRV*https://msdl.microsoft.com/download/symbols OK c:\dev\source\mcu-india\cpl\basics\achindra_mca_2005\debugging101\
“.reload” will reload all symbols.
Let’s put a breakpoint on main(). Breakpoint is an instruction that will stop execution on a specific address.
Below, I used ‘x‘ command to examine a given symbol. It tells me about the address and any information it has about it. In this case, it is a function, hence I am getting its prototype.
The other command is “bp” which sets a breakpoint on a given address. I used “bl” to list all breakpoints that I have set. “bd” will disable, “be” will enable and “bc” will clear a given breakpoint.
0:000> x Tables!main 00007ff6`a6236c40 Tables!main (int, char **) 0:000> bp Tables!main 0:000> bl 0 e Disable Clear 00007ff6`a6236c40 [c:\dev\source\mcu-india\cpl\basics\achindra_mca_2005\debugging101\tables.c @ 21] 0001 (0001) 0:**** Tables!main
TIP: You can put breakpoint on an address (00007ff6`a6236c40) or a symbol (Tables!main).
Command “g“, will let debugger go and it will break-in when breakpoint is hit.
0:000> g Breakpoint 0 hit Tables!main: 00007ff6`a6236c40 4889542410 mov qword ptr [rsp+10h],rdx ss:00000078`24affe78=0000000000000000
On the right, you can see the stack (or use command “k” to print the current stack). On the left, you can see the source code. In the center I have my debugger.
“F9” will toggle break point (on/off) at a given line of code that you have selected in the code window. Try it, click on atoi() in the source page and press F9. A break point is set. You can use command “bl” to see it. Now, disable it with “bd 1” (1 or number corresponding to the breakpoint you want to disable. “bc 1” to clear it.
“F10” or “p” to execute one line at a time. With functions, this steps over the function call. i.e. if you press “F10”, it will break after the function has completed execution.
“F11” or “t” to execute one instruction at a time. With function, this let’s you step into a function.
“g” anytime will make debugger continue until it hits a breakpoint or end of program.
If accidentally, you hit “g” and you program terminates, you can “.restart” to restart debugging.
0:000> p Tables!main+0xd: 00007ff6`a6236c4d c744242000000000 mov dword ptr [rsp+20h],0 ss:00000078`24affe50=00000000 0:000> dv argc = 0n1 argv = 0x000001d1`99d13bd0 retValue = 0n0
I used a new command above, “dv“, it gets you all variables in scope.
In the above output argc is 1, i.e. I forgot input parameter. So I restarted and provided an input argument.
Checkout stack that leads to program’s main function!
0:000> kc # Call Site 00 Tables!main 01 Tables!invoke_main 02 Tables!__scrt_common_main_seh 03 KERNEL32!BaseThreadInitThunk 04 ntdll!RtlUserThreadStart
A thread starts in ntdll.dll, it calls into Kernel32.dll which then calls a function __scrt_common_main_seh in your program. You have not written this code but while compiling and linking, this wrapper function is put in, if your code generates an exception, this function handles that (SEH == Structured Exception Handler). This function calls an inline function – invoke_main() which then calls your main().
F10, till you reach PrintTableFor(). Now, press F11 or ‘t’ to enter the function. You can trace through this function using F10 and inspect local variables with dv.
When you trace over the for-loop, notice when the value of “i” is updated (i++). Rewrite this code with ++i and trace to find how that is different.
If your loop is too big to trace, and you want to jump ahead, click on a line of code where you want to jump to, press F9 to put a breakpoint there and press ‘g’.
If you want to jump to exit of function, i.e. from somewhere in PrintTableFor() to main function where it was called from, you press Shift+F11 or “g @$ra”. Try it from inside PrintTableFor().
Give it a try and we shall go deeper with more commands and more deeper dissecting the process memory.