Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C

How to Use and Understand the Windows Console Debugger

4.80/5 (5 votes)
23 Dec 2008CPOL6 min read 48K  
An article to help the beginner get started in debugging

Introduction

In order to interpret the cdb.exe debugger prompt, we have to enter commands to figure out some details concerning the debugger output. Moreover, when we set the symbol path, we must remember that the downloaded symbols are for the Microsoft device drivers and system key components alone. This means if we are to use to the cdb.exe (or graphical WinDbg.exe) debugger on our own code, we must use a compiler switch that includes debugging information with our compiled executable. On that premise, we can write a small program in the C language in order to interpret the debugger. Below is a simple program that writes a global function that is called by the main function in order to produce a string in the standard console output:

C++
c:\Program Files\Microsoft Visual Studio 9.0\VC\bin>type con > hello.c
#include <stdio.h> <stdio.h /> 
#include <string.h><string.h />
salute(char *temp1,char *temp2){ 
char name[400]; 
strcpy(name, temp2); 
printf("Hello %s %s\n", temp1, name); 
}
main(int argc, char * argv[]){ 
salute(argv[1], argv[2]); 
printf("Bye %s %s\n", argv[1], argv[2]); 
} 

We now compile this code with Microsoft Visual Studio C++ Express Edition’s cl.exe compiler, with the /Zi switch to obtain debugging information.

C:\PROGRA~1\MICROS~1.0\VC\bin>cl /Zi hello.c
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.

hello.c
Microsoft (R) Incremental Linker Version 9.00.30729.01
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:hello.exe
/debug
hello.obj

We run the program: C:\....\bin>hello Silly Willy

c:\Program Files\Microsoft Visual Studio 9.0\VC\bin>hello Silly Willy

Hello Silly Willy

Bye Silly Willy

Having run that program, we have to find an easy way to transfer this code and its debugging information, to the C:\Program files\Debugging Tools for Windows directory. Of course it is possible to set an environmental path:

set PATH=%PATH%;.;C:\Program Files\Microsoft Visual Studio 9.0\VC\bin

Capture.JPG

Using the more basic approach, we copy and paste those files to directory containing the debuggers, where we will set the symbol path to download the symbols and cache them locally in a directory called C:\symbols.

Symbols and the Symbol Server

Symbols connect function names and arguments to offsets in a compiled executable. A method to obtain symbols is to use Microsoft’s symbol server and to fetch symbols as you need them. Windows debuggers make this easy to do by providing symsrv.dll, which you can use to set up a local cache of symbols and specify the location to get new symbols as you need them. This is done through the environment variable _NT_SYMBOL_PATH. You’ll need to set this environment variable so the debugger knows where to look for symbols. If you already have all the symbols you need locally, you can simply set the variable to that directory like this:

C:\Program Files\Debugging Tools For Windows>set _NT_SYMBOL_PATH=c:\symbols 

If you prefer to use the symbol server, the syntax goes as follows:

C:\Program Files\Debugging Tools For Windows>
set_NT_SYMBOL_PATH=symsrv*symsrv.dll*c:\symbols*
http://msdl.microsoft.com/download/symbols

User Mode Debugger Output

C:\PROGRA~1\Debugging Tools for Windows>cdb.exe Hello Silly Willy

Microsoft (R) Windows Debugger Version 6.8.0004.0 X86
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: Hello Silly Willy
Symbol search path is: srv*c:\Symbols*http://msdl.microsoft.com/download/symbols

Executable search path is:
ModLoad: 00400000 00427000   hello.exe
ModLoad: 76ea0000 76fc7000   ntdll.dll
ModLoad: 75a90000 75b6b000   C:\Windows\system32\kernel32.dll
(27c.ff4): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=0012fb08 edx=76ef9a94 esi=fffffffe edi=76efb6f8
eip=76ee7dfe esp=0012fb20 ebp=0012fb50 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!DbgBreakPoint:
76ee7dfe cc              int     3

Notice that the first line contains the process and thread identifier that generates the last debugger event. For an excellent documentation on debugging as a whole, it is strongly suggested that the reader research “Advanced Windows Debugging” by Mario Hewardt and Daniel Pravat. The last debugger event is displayed identifier (27c.ff4) along with the event description, a break instruction and exception code 80000003. The debugger handled the event on the first chance, before the normal exception handling in the user code. These next two commands are self-explanatory:

0:000> vertarget
Windows Version 6001 (Service Pack 1) MP (2 procs) Free x86 compatible
Product: WinNt, suite: SingleUserTS
kernel32.dll version: 6.0.6001.18000 (longhorn_rtm.080118-1840)
Debug session time: Tue Dec 23 16:53:26.860 2008 (GMT-5)
System Uptime: 0 days 1:48:23.592
Process Uptime: 0 days 0:00:06.037
  Kernel time: 0 days 0:00:00.000
  User time: 0 days 0:00:00.015


0:000> .lastevent
Last event: 6f8.5a0: Break instruction exception - code 80000003 (first chance)
  debugger time: Tue Dec 23 16:53:20.854 2008 (GMT-5)

As we can see, the debugger stopped at a breakpoint. A stack trace will show us the reason why:

0:000> k
ChildEBP RetAddr
0012fb1c 76f2e214 ntdll!DbgBreakPoint
0012fb50 76f12ef5 ntdll!LdrpDoDebuggerBreak+0x31
0012fc94 76ed1235 ntdll!LdrpInitializeProcess+0x1132
0012fd00 76ede2b7 ntdll!_LdrpInitialize+0xf2
0012fd10 00000000 ntdll!LdrInitializeThunk+0x10

Looking at the top instruction, we can see that Windows debugger breaks after initializing before execution begins. This is actually a convenient breakpoint, as program is loaded; now we can set any breakpoints that we’d like on our program before execution begins. Let’s set a breakpoint on main:

0:000> bm hello!main
*** WARNING: Unable to verify checksum for hello.exe
  1: 00401070 @!"hello!main"


0:000> bl
 1 e 00401070     0001 (0001)  0:**** hello!main

The thing to do now is run our program past the ntdll.dll initialization on to our main function:

0:000> g
Breakpoint 1 hit
eax=008f1d78 ebx=7ffda000 ecx=00000001 edx=76ef9a94 esi=00000000 edi=00000000
eip=00401070 esp=0012ff44 ebp=0012ff88 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
hello!main:
00401070 55              push    ebp
0:000> k
ChildEBP RetAddr
0012ff40 004014b2 hello!main
0012ff88 75ad4911 hello!__tmainCRTStartup+0xfb
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b

During any disassembly, the number 55 normally means program entry. That is, the opcode 55 indicates where the source code execution begins. If you have ever dealt with Linux or assembly, then you would recognize that the instruction push ebp is the beginning of what is called the function prologue. At this point, the Debugging Tools for Windows should be noted. If you read the section of “Debugging in Source Mode” in the debugging.chm, you will find that the .lines command will modify the stack trace to display the line that is currently being executed:

0:000> .lines
ne number information will be loaded
0:000> k
ChildEBP RetAddr
0012ff40 004014b2 hello!main [c:\program files\Microsoft visual studio 9.0\vc\bi
n\hello.c @ 8]
0012ff88 75ad4911 hello!__tmainCRTStartup+0xfb [f:\dd\vctools\crt_bld\self_x86\c
rt\src\crt0.c @ 266]
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> g

To continue past this breakpoint, our program will finish executing:

0:000> g
Hello Silly Willy
Bye Silly Willy
eax=00000000 ebx=00000001 ecx=00000000 edx=000000fe esi=008f1a34 edi=008f1a38
eip=76ef9a94 esp=0012fec0 ebp=0012fed0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!KiFastSystemCallRet:
76ef9a94 c3              ret

0:000> k
ChildEBP RetAddr
0012febc 76ef9134 ntdll!KiFastSystemCallRet
0012fec0 76eca869 ntdll!NtTerminateProcess+0xc
0012fed0 75ab3b68 ntdll!RtlExitUserProcess+0x7a
0012fee4 00402cd8 kernel32!ExitProcess+0x12
0012fee4 00402cd8 hello!__crtExitProcess+0x17 [f:\dd\vctools\crt_bld\self_x86\cr
t\src\crt0dat.c @ 731]
0012fef0 00402f3c hello!__crtExitProcess+0x17 [f:\dd\vctools\crt_bld\self_x86\cr
t\src\crt0dat.c @ 731]
0012ff34 00402f66 hello!doexit+0x113 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt
0dat.c @ 644]
0012ff48 004014c4 hello!exit+0x11 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0da
t.c @ 412]
0012ff88 75ad4911 hello!__tmainCRTStartup+0x10d [f:\dd\vctools\crt_bld\self_x86\
crt\src\crt0.c @ 272]
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> q
quit:

Examining the Debugger

A good exercise at this point is to locate the data the debugged application is using. To do this, we will launch the debugger and set breakpoints on both the main function and the salute function:

C:\PROGRA~1\Debugging Tools for Windows>cdb.exe Hello Silly Willy

Microsoft (R) Windows Debugger Version 6.8.0004.0 X86
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: Hello Silly Willy
Symbol search path is: srv*c:\Symbols*http://msdl.microsoft.com/download/symbols

Executable search path is:
ModLoad: 00400000 00427000   hello.exe
ModLoad: 76ea0000 76fc7000   ntdll.dll
ModLoad: 75a90000 75b6b000   C:\Windows\system32\kernel32.dll
(564.e0c): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=0012fb08 edx=76ef9a94 esi=fffffffe edi=76efb6f8
eip=76ee7dfe esp=0012fb20 ebp=0012fb50 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!DbgBreakPoint:
76ee7dfe cc              int     3
0:000> k
ChildEBP RetAddr
0012fb1c 76f2e214 ntdll!DbgBreakPoint
0012fb50 76f12ef5 ntdll!LdrpDoDebuggerBreak+0x31
0012fc94 76ed1235 ntdll!LdrpInitializeProcess+0x1132
0012fd00 76ede2b7 ntdll!_LdrpInitialize+0xf2
0012fd10 00000000 ntdll!LdrInitializeThunk+0x10
0:000> bm hello!main
*** WARNING: Unable to verify checksum for hello.exe
  1: 00401070 @!"hello!main"
0:000> bm hello!*salute*
  2: 00401020 @!"hello!salute"

In the C language, (int argc, char *argv[]) are the command line parameters that act as a prototype to pass to the main to launch the program. Int argc is the number of arguments, and argv[] is a vector of pointers to those arguments. That is, rather than invoking the environment to launch the program, we pass these variables to main to launch our program. Argv[0] would therefore be the name of the program, hello.exe.

0:000> g
Breakpoint 1 hit
eax=001c1d78 ebx=7ffdb000 ecx=00000001 edx=76ef9a94 esi=00000000 edi=00000000
eip=00401070 esp=0012ff44 ebp=0012ff88 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
hello!main:
00401070 55              push    ebp

Looking at the source, we can ascertain that the main should have been passed the command line used to launch the program via the argc command string counter and argv, this points to the array of strings. To verify that, we’ll use dv to list the local variables, and then poke around in memory with dt and db to find the value of those variables.

0:000> dv /V
0012ff48 @ebp+0x08            argc = 3
0012ff4c @ebp+0x0c            argv = 0x001c1d38
0:000> dt argv
Local var @ 0x12ff4c Type char**
0x001c1d38
 -> 0x001c1d48  "Hello"

From the dv output, we see that argc and argv are, indeed, local variables with argc stored 8 bytes past the local ebp, and argv stored at ebp+0xc. The dt command shows the data type of argv to be a pointer to a character pointer. The address 0x001c1d48 holds that pointer to 0x001c1d48 where the data actually lives.

0:000> db 0x001c1d48
001c1d48  48 65 6c 6c 6f 00 53 69-6c 6c 79 00 57 69 6c 6c 79 Hello.Silly.Willy

So we go some more until we ge to the salute function:

0:000> g
Breakpoint 2 hit
eax=001c1d4e ebx=7ffdb000 ecx=001c1d54 edx=001c1d38 esi=00000000 edi=00000000
eip=00401020 esp=0012ff34 ebp=0012ff40 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
hello!salute:
00401020 55              push    ebp
0:000> kp
ChildEBP RetAddr
0012ff30 00401086 hello!salute(char * temp1 = 0x001c1d4e "Silly", char * temp2 =
 0x001c1d54 "Willy")
0012ff40 004014b2 hello!main(int argc = 3, char ** argv = 0x001c1d38)+0x16
0012ff88 75ad4911 hello!__tmainCRTStartup(void)+0xfb
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b

Looking at the stack trace (or code) that the salute function is passed two arguments, Silly and Willy. So if a function is passed two arguments, then they should appear on the stack, as when is called, it must push its arguments onto the stack. This means we take a look at the local variables and map them out:

0:000> dv /V
0012ff38 @ebp+0x08           temp1 = 0x001c1d4e "Silly"
0012ff3c @ebp+0x0c           temp2 = 0x001c1d54 "Willy"
0012fd98 @ebp-0x198            name = char [400] " s"

The variable name is 0x198 above ebp. To convert this hexadecimal, use the .formats command:

.0:000> .formats 198
Evaluate expression:
  Hex:     00000198
  Decimal: 408
  Octal:   00000000630
  Binary:  00000000 00000000 00000001 10011000
  Chars:   ....
  Time:    Wed Dec 31 19:06:48 1969
  Float:   low 5.7173e-043 high 0
  Double:  2.01579e-321

0:000> pr
hello!salute+0xe:
0040102e 33c5            mov     ebp,esp
0:000> p
hello!salute+0x10:
00401030 8945fc          sub esp, 198h

0:000> pr
eax=0020201e ebx=7ffdf000 ecx=00202024 edx=00202008 esi=00000000 edi=00000000
eip=00401029 esp=0012fd98 ebp=0012ff30 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
hello!salute+0x9:
00401029 a120204200      mov     eax,dword ptr [hello!__security_cookie (0042202
0)] ds:0023:00422020=ec0c1ced

We see that at the top of the stack (esp, or address 0012fd98, as noted above where the variable “name” is in the prior ‘dv /V’ command) we find the function variable name, which goes on for the next 408 bytes. So let’s add those bytes to the stack pointer:

0:000> .formats esp+198
Evaluate expression:
  Hex:     0012ff30
  Decimal: 1244976
  Octal:   00004577460
  Binary:  00000000 00010010 11111111 00110000
  Chars:   ...0
  Time:    Thu Jan 15 04:49:36 1970
  Float:   low 1.74458e-039 high 0
  Double:  6.151e-318

Notice the value 0012ff30. This should look familiar, as it is the value of the ebp, which was saved initially by “push”ing it onto stack. We know that register ebp is a 32 bit or 4 byte pointer. So let’s examine what comes after that:

0:000> dd esp+198+4 l1
0012ff34  00401086

The command l1 stands for length of one. Now if we do a stack trace, the return address should match: The return address 00401086 matches to have rebuilt the stack.

ChildEBP RetAddr
0012ff30 00401086 hello!salute+0x13
0012ff40 004014b2 hello!main+0x16
0012ff88 75ad4911 hello!__tmainCRTStartup+0xfb
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b

0:000> dd esp+198+4+4 l1
0012ff38  001c1d4e

0:000> db 001c1d4e
001c1d4e  53 69 6c 6c 79 00 57 69-6c 6c 79 00 ab ab ab ab  Silly.Willy.....

Disassembling with Cdb.exe

To disassemble, just use the uf instruction at one of the breakpoints, like the global function:

0:000> uf hello!salute
hello!salute:
00401020 55              push    ebp
00401021 8bec            mov     ebp,esp
00401023 81ec98010000    sub     esp,198h
00401029 a120204200      mov     eax,dword ptr [hello!__security_cookie (0042202
0)]
0040102e 33c5            xor     eax,ebp
00401030 8945fc          mov     dword ptr [ebp-4],eax
00401033 8b450c          mov     eax,dword ptr [ebp+0Ch]
00401036 50              push    eax
00401037 8d8d68feffff    lea     ecx,[ebp-198h]
0040103d 51              push    ecx
0040103e e8fd010000      call    hello!strcpy (00401240)
00401043 83c408          add     esp,8
00401046 8d9568feffff    lea     edx,[ebp-198h]
0040104c 52              push    edx
0040104d 8b4508          mov     eax,dword ptr [ebp+8]
00401050 50              push    eax
00401051 6800204200      push    offset hello!__rtc_tzz <perf /> (hello+0x22000) (
00422000)
00401056 e86f000000      call    hello!printf (004010ca)
0040105b 83c40c          add     esp,0Ch
0040105e 8b4dfc          mov     ecx,dword ptr [ebp-4]
00401061 33cd            xor     ecx,ebp
00401063 e8d0020000      call    hello!__security_check_cookie (00401338)
00401068 8be5            mov     esp,ebp
0040106a 5d              pop     ebp
0040106b c3              ret

Hopefully this article will help those who struggle with debugging and the commands involved in order to obtain the data the debugged compiled source code application uses.

References

  • Advanced Windows Debugging, by Mario Hewardt and Daniel Pravat
  • Ethical Hacking by Shon Harris, Allen Harper, Chris Eagle, and Jonathan Ness

History

  • 23rd December, 2008: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)