Writing a basic Windows Debugger - Part 1

Preamble

All of us have used some kind of Debugger while programming in some language. The Debugger you used may be in C++, C#, Java or other language. It might be standalone like WinDbg, or inside an IDE like Visual Studio. But have you been inquisitive over how Debuggers work?

Well, this article presents the hidden glory on how Debuggers work. This article only covers writing Debugger on Windows. Please note that here I am concerned only on Debugger and not on: Compilers, Linkers or Debugging Extensions. Thus, we'll only debug the executables (like WinDbg).
This article assumes basic understanding of multithreading from reader (read my article on multithreading).

How to Debug a Program?

Two steps:

  1. Starting the process with DEBUG_ONLY_THIS_PROCESS or DEBUG_PROCESS flags
  2. Setting up a Debugger's loop, that will handle debugging events.
Before we move further, please remember:
  1. Debugger is the process/program which is debugging the other process (target-process).
  2. Debuggee is the process being debugged, by the Debugger.
  3. Only one Debugger can be attached to a Debuggee. However, a Debugger can debug multiple processes (in separate threads).
  4. Only the thread that created/spawned the Debuggee, can debug the target-process. Thus CreateProcess and Debugger-loop must be in same thread.
  5. When the Debugger thread terminates, the Debuggee terminates as well. The Debugger process may keep running, however.
  6. When the Debugger's debugging thread is busy processing a debug event, ALL threads in Debuggee (target-process) stand suspended. More on this later.

Starting the process with debugging flag

Use CreateProcess to start the process, specifying DEBUG_ONLY_THIS_PROCESS in its 6th parameter (dwCreationFlags). With this flag, we are asking Windows OS to communicate this thread for all debugging events, including process creation/termination, thread creation/termination, runtime exceptions and so on. Detailed explanation is given below. Please note, we'll be using DEBUG_ONLY_THIS_PROCESS in this article. It essentially means we want only to debug the process we are creating, and not any child process(es) that may be created by the process we create.
STARTUPINFO si;
PROCESS_INFORMATION pi;

ZeroMemory( &si, sizeof(si) );
si.cb = sizeof(si);
ZeroMemory( &pi, sizeof(pi) );

CreateProcess ( ProcessNameToDebug, NULL, NULL, NULL, FALSE, 
	DEBUG_ONLY_THIS_PROCESS,
	NULL,NULL, &si, &pi );
After this statement, you would see the process in Task Manager, but process hasn't started yet. The newly created process is suspended. No, we don't have to call ResumeThread, but write a Debugger-loop.

The Debugger Loop

The Debugger-loop is the central area for the Debuggers! The loop runs around WaitForDebugEvent API. This API takes 2 parameters: A pointer to DEBUG_EVENT structure and DWORD timeout parameter. For timeout, we would simply specify INFINITE. This API exists in kernel32.dll, thus we need not to link to any library.
BOOL WaitForDebugEvent(DEBUG_EVENT* lpDebugEvent, DWORD dwMilliseconds);
The DEBUG_EVENT structure contains the debugging event information. It has 4 members: Debug event code, process-id, thread-id and the event information.
As soon as WaitForDebugEvent returns, we process the received debugging event, and then eventually call ContinueDebugEvent. Here is minimal Debugger-loop:
DEBUG_EVENT debug_event = {0};
for(;;) 
{
	if (!WaitForDebugEvent(&debug_event, INFINITE))
		return;
	ProcessDebugEvent(&debug_event);  // User-defined function, not API
	ContinueDebugEvent(debug_event.dwProcessId, 
                      debug_event.dwThreadId, 
                      DBG_CONTINUE);
}
Using ContinueDebugEvent API, we are asking OS to continue executing the Debuggee. The dwProcessId and dwThreadId specify the process and thread. These values are same that we received form WaitForDebugEvent.
The last parameter specifies if execution should continue or not. This parameter is relevant only if exception-event is received. We will cover it later. Until then we'll utilize only DBG_CONTINUE (other possible value is DBG_EXCEPTION_NOT_HANDLED ).

Handling Debugging Events

There are 9 different major debugging events, and 20 different sub-events under exception-event category. I will discuss starting from the simplest. Here is DEBUG_EVENT structure:
struct DEBUG_EVENT
{  
	DWORD dwDebugEventCode;  
	DWORD dwProcessId;
	DWORD dwThreadId;  
	union {    
		EXCEPTION_DEBUG_INFO Exception;
    		CREATE_THREAD_DEBUG_INFO CreateThread;
		CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
		EXIT_THREAD_DEBUG_INFO ExitThread;
		EXIT_PROCESS_DEBUG_INFO ExitProcess;
		LOAD_DLL_DEBUG_INFO LoadDll;
		UNLOAD_DLL_DEBUG_INFO UnloadDll;
		OUTPUT_DEBUG_STRING_INFO DebugString;
		RIP_INFO RipInfo;  
	} u;
};
WaitForDebugEvent, on successful return, fills-in the values in this structure. dwDebugEventCode specifies which debugging-event has occurred. Depending on event-code received, one of the member of union u contains event information, and we should only use respective union-member. For example if debug event code is OUTPUT_DEBUG_STRING_EVENT, the member OUTPUT_DEBUG_STRING_INFO would be valid.

Processing OUTPUT_DEBUG_STRING_EVENT

Programmers generally use OutputDebugString to generate debugging-text that would be displayed on Debugger's 'Output' window. Depending on language/framework you use, you might be familiar with TRACE, ATLTRACE macros. .NET programmers may use System.Diagnostics.Debug.Print/System.Diagnostics.Trace.WriteLine methods (or other methods). But with all these methods, OutputDebugString API is called, and the Debugger would receive this event (unless it is buried with DEBUG symbol undefined!).

When this event is received, we work on DebugString member variable. The structure OUTPUT_DEBUG_STRING_INFO is defined as:

struct OUTPUT_DEBUG_STRING_INFO 
{  
   LPSTR lpDebugStringData;  // char*
   WORD fUnicode;  
   WORD nDebugStringLength;
};
The member-variable 'nDebugStringLength' specifies the length of string, including terminating null, in characters (not bytes). Variable 'fUnicode' specifies if string is Unicode (non-zero), or ANSI (zero). That means. we read 'nDebugStringLength' bytes from 'lpDebugStringData' if string is ANSI, otherwise we read (nDebugStringLength x 2) bytes. But remember the address pointed by 'lpDebugStringData' is not from the address-space of the Debugger's memory. The address is relevant to Debuggee memory. Thus we need to read the contents from the Debuggee's process memory.

To read data from other process' memory we use ReadProcessMemory function. It requires that the calling process should have appropriate permission. Since the Debugger only created the process, we do have the rights. Here is the code to process this debugging event:
case OUTPUT_DEBUG_STRING_EVENT: 
{
   CStringW strEventMessage;  // Force Unicode
   OUTPUT_DEBUG_STRING_INFO & DebugString = debug_event.u.DebugString;
			
   WCHAR *msg=new WCHAR[DebugString.nDebugStringLength]; // Don't care if string is ANSI, and we allocate double...
   
   ReadProcessMemory(pi.hProcess,       // HANDLE to Debuggee
         DebugString.lpDebugStringData, // Target process' valid pointer
         msg,                           // Copy to this address space   
         DebugString.nDebugStringLength, NULL);

   if ( DebugString.fUnicode )
      strEventMessage = msg;
   else
      strEventMessage = (char*)msg; // char* to CStringW (Unicode) conversion.
		
   delete []msg;
   // Utilize strEventMessage
}
What if Debuggee terminates before Debugger copies the memory contents?

Well... In that case I would like to remind you: When the Debugger is processing a debugging event, ALL threads in Debuggee are suspended. The process cannot kill itself in anyway at this moment. Also, no other method can terminate the process (Task Manager, Process Explorer, kill utility...). Attempting to kill process from these utilities will, however, schedule the terminating the process. Thus, the Debugger would receive EXIT_PROCESS_DEBUG_EVENT as the next event!

Processing CREATE_PROCESS_DEBUG_EVENT

This event is raised when the process (Debuggee) is being spawned. This would be the first event the Debugger receives. For this event, the relevant member of DEBUG_EVENT would be CreateProcessInfo. Here is structure definition of CREATE_PROCESS_DEBUG_INFO:

struct CREATE_PROCESS_DEBUG_INFO 
{
    HANDLE hFile;   // The handle to the physical file (.EXE)
    HANDLE hProcess; // Handle to the process
    HANDLE hThread;  // Handle to the main/initial thread of process
    LPVOID lpBaseOfImage; // base address of the executable image
    DWORD dwDebugInfoFileOffset;
    DWORD nDebugInfoSize;
    LPVOID lpThreadLocalBase;
    LPTHREAD_START_ROUTINE lpStartAddress;
    LPVOID lpImageName;  // Pointer to first byte of image name (in Debuggee)
    WORD fUnicode; // If image name is Unicode.
};
Please note that hProcess and hThread may not have the same handle values we have received in pi (PROCESS_INFORMATION). The process-id and thread-id would, however, be same. Each handle you get by Windows (for same resource) is different than other handles, and has different purpose. So, the Debugger may choose to display either handles or the Ids.

The hFile as well as lpImageName can both be used to get the file-name of the process being debugged. Although we already know what is the name of process, since we only created the debuggee. But locating the module name of EXE or DLL is important, since we would anyway need to find the name of DLL while processing LOAD_DLL_DEBUG_EVENT message.

As you can read in MSDN, lpImageName will never return the filename directly, and the name would be in target-process. Furthermore, it may not have a filename in target-process too (i.e. via ReadProcessMemory). Also, the filename may not be fully qualified (as I've tested). Thus we will not use this method. We'll retrieve filename from hFile member.

How to get the name of file by HANDLE?

Unfortunately, we need to use the method described in MSDN, that uses around 10 API calls to get the filename from handle. I have slightly modified the function GetFileNameFromHandle. The code is not shown here for brevity, it is available in source file attached with this article.
Anyway, here is basic code to process this event:

case CREATE_PROCESS_DEBUG_EVENT:
{
   CString strEventMessage = GetFileNameFromHandle(debug_event.u.CreateProcessInfo.hFile);
   // Use strEventMessage, and other members of CreateProcessInfo to intimate the user of this event.
}
You may have noticed that I did not cover few members of this structure. I would probably cover all of them in the next part of this article.

Processing LOAD_DLL_DEBUG_EVENT

This event is similar to CREATE_PROCESS_DEBUG_EVENT, and as you might have guessed, it is raised when a DLL is loaded by OS. This event is raised whenever DLL is loaded, either implicitly or explicitly (when debuggee calls LoadLibrary). This debugging event only occurs the first time the system attaches a DLL to the virtual address space of a process. For this event processing we use 'LoadDll' member of the union. It is of type LOAD_DLL_DEBUG_INFO:

struct LOAD_DLL_DEBUG_INFO 
{
   HANDLE hFile;         // Handle to the DLL physical file.
   LPVOID lpBaseOfDll;   // The DLL Actual load address in process.
   DWORD dwDebugInfoFileOffset;
   DWORD nDebugInfoSize;
   LPVOID lpImageName;   // These two member are same as CREATE_PROCESS_DEBUG_INFO
   WORD fUnicode;
};
For retrieving the filename we would use the same function, GetFileNameFromHandle, as we have used in CREATE_PROCESS_DEBUG_EVENT.
I will list out the code for processing this event, when I would describe UNLOAD_DLL_DEBUG_EVENT, since the UNLOAD_DLL_DEBUG_EVENT does not have any direct information available to find out the name of DLL file.

Processing CREATE_THREAD_DEBUG_EVENT

This debug event is generated whenever a new thread is created in the debuggee. Like CREATE_PROCESS_DEBUG_EVENT, this event is raised before the thread actually gets to run. To get information about this event, we use 'CreateThread' union member. This variable is of type CREATE_THREAD_DEBUG_INFO:

struct CREATE_THREAD_DEBUG_INFO 
{  
  HANDLE hThread;      // Handle to the newly created thread in debuggee
  LPVOID lpThreadLocalBase;  
  LPTHREAD_START_ROUTINE lpStartAddress; // pointer to the starting address of the thread
};
The thread-id for newly arrived thread is available in DEBUG_EVENT::dwThreadId.
Using this member to intimate user is straightforward:
case CREATE_THREAD_DEBUG_EVENT:
{
   CString strEventMessage;
   strEventMessage.Format(L"Thread 0x%x (Id: %d) created at: 0x%x",  
			debug_event.u.CreateThread.hThread,
			debug_event.dwThreadId,
			debug_event.u.CreateThread.lpStartAddress); // Thread 0xc (Id: 7920) created at: 0x77b15e58
}
The 'lpStartAddress' is relevant to Debuggee and not the Debugger, we are just displaying it for completeness. Remember this event is not received for the primary/initial thread of the process. It is received only for subsequent thread creations in the debuggee.

Writing a basic Windows Debugger - Part 1

Processing EXIT_THREAD_DEBUG_EVENT

This event is raised as soon as thread returns, and return code is available to the system. The 'dwThreadId' member of DEBUG_EVENT specifies which thread exited. To get the thread handle, and other information that we received in CREATE_THREAD_DEBUG_EVENT, we need to store the information in some map. This event has relevant member named 'ExitThread', which is of type EXIT_THREAD_DEBUG_INFO:

struct EXIT_THREAD_DEBUG_INFO 
{  
   DWORD dwExitCode; // The thread exit code of DEBUG_EVENT::dwThreadId
}; 
Here is the event handler code:
case EXIT_THREAD_DEBUG_EVENT:
{
   CString strEventMessage;
   strEventMessage.Format( _T("The thread %d exited with code: %d"),
      debug_event.dwThreadId,
      debug_event.u.ExitThread.dwExitCode);	// The thread 2760 exited with code: 0
}

Processing UNLOAD_DLL_DEBUG_EVENT

Of course, this event occurs when DLL is unloaded from Debuggee's memory. But wait! It is ONLY generated against FreeLibrary calls, and not when system unloads the DLLs. The Debuggee may call LoadLibrary multiple times, and thus only the last call to FreeLibrary would raise this even. It means, the implicitly loaded DLLs will not receive this event when they are unloaded, when the process exits. (You can verify this assertion in your favorite Debugger!).

For this event, you use 'UnloadDll' member of union, which is of type UNLOAD_DLL_DEBUG_INFO:

struct UNLOAD_DLL_DEBUG_INFO 
{
    LPVOID lpBaseOfDll;
};
As you can see, only the base-address of DLL (a simple pointer), is available for us to process this event. This is the reason I had delayed giving the code for LOAD_DLL_DEBUG_EVENT. In DLL Loading event, we get the 'lpBaseOfDll' also. We can use map (or other data structure you like), to store the name of DLL against the base-address of DLL. The same base-address would arrive while processing UNLOAD_DLL_DEBUG_EVENT.

It should be noted that not all DLL-load event would get DLL-unload event, still we have to store all DLL names into map, since LOAD_DLL_DEBUG_EVENT doesn't provide us how the DLL was loaded.

Here is code how we can process these two events:

std::map < LPVOID, CString > DllNameMap;
...
case LOAD_DLL_DEBUG_EVENT:
{
   strEventMessage = GetFileNameFromHandle(debug_event.u.LoadDll.hFile);	


   // Storing the DLL name into map. Map's key is the Base-address
   DllNameMap.insert( 
      std::make_pair( debug_event.u.LoadDll.lpBaseOfDll, strEventMessage) );

   strEventMessage.AppendFormat(L" - Loaded at %x", debug_event.u.LoadDll.lpBaseOfDll);
}		
break;
...
case UNLOAD_DLL_DEBUG_EVENT:
{
   strEventMessage.Format(L"DLL '%s' unloaded.",  
      DllNameMap[debug_event.u.UnloadDll.lpBaseOfDll] ); // Get DLL name from map.
}
break;

Processing EXIT_PROCESS_DEBUG_EVENT

This is one of the simplest debugging event, and as you can assess, would arrive when process exists. This event would arrive irrespective of how process exits - normally, terminated externally (Task Manager etc), or the application's (Debuggee) fault leading it to crash.

We use 'ExitProcess' member, which is of type EXIT_PROCESS_DEBUG_INFO:

struct EXIT_PROCESS_DEBUG_INFO 
{
    DWORD dwExitCode;
};
As soon as this event occurs, we also end the Debugger-loop and terminate the debugging thread. For this we can use a variable to control the loop (the 'for' loop shown in first page), and set its value to indicate the loop termination. Please download the attached files, to see the entire code.
bool bContinueDebugging=true;
...
case EXIT_PROCESS_DEBUG_EVENT:
{
   strEventMessage.Format(L"Process exited with code:  0x%x", debug_event.u.ExitProcess.dwExitCode);
   bContinueDebugging=false;
}
break;

Processing EXCEPTION_DEBUG_EVENT

This is the prodigious event amongst all debugging events! From MSDN:

Generated whenever an exception occurs in the process being debugged. Possible exceptions include attempting to access inaccessible memory, executing breakpoint instructions, attempting to divide by zero, or any other exception noted in Structured Exception Handling. The DEBUG_EVENT structure contains an EXCEPTION_DEBUG_INFO structure. This structure describes the exception that caused the debugging event.

This debugging event needs separate article to complete it fully (or partially!). Thus I would discuss only one type of exception events, along with giving introduction to this event itself.

The member variable 'Exception' holds the information regarding the exception just occurred. It is of type EXCEPTION_DEBUG_INFO:

struct EXCEPTION_DEBUG_INFO
{
    EXCEPTION_RECORD ExceptionRecord;
    DWORD dwFirstChance;
};
'ExceptionRecord' member of this structure contains detailed information regarding the exception. It is of type EXCEPTION_RECORD:
struct EXCEPTION_RECORD 
{
    DWORD     ExceptionCode;
    DWORD     ExceptionFlags;
    struct _EXCEPTION_RECORD *ExceptionRecord;
    PVOID     ExceptionAddress;
    DWORD     NumberParameters;
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];  // 15
};
The detailed information is put into this sub-structure, because exceptions may appear nested, and would be linked to each other in linked-list manner. It is out of topic for now to discuss nested exceptions.

Before we delve into EXCEPTION_RECORD, it is important to discuss EXCEPTION_DEBUG_INFO::dwFirstChance.

Are Exceptions giving chances?

Not exactly! When a process is being debugged, the Debugger always receives the exception before the Debuggee gets it. You must have seen "First-chance exception at 0x00412882 in SomeModule:..." while debugging your Visual C++ module. This is referred as First Chance Exception. The same exception may or may not follow with a second chance exception.

When the Debuggee gets the exception, it is termed as Second Chance Exception. The Debuggee may handle the exception, or may simply crash down. These types of exceptions are not C++ exceptions, but Windows' SEH (structure exception handling) mechanism. I would cover more about it in next part of this article.

The Debugger gets exceptions first (First-chance exception), so that it can handle it before giving it to Debuggee. The break-point exception is kind of exception, which is relevant to Debugger, not Debuggee. Some libraries also generate First Chance exceptions to aid Debugger, and the debugging process.

A word for ContinueDebugEvent

The third parameter (dwContinueStatus) of this function is relevant only after exception event is received. For non-exception events that we discussed, system ignores the value passed to this function.

After exception event processing, ContinueDebugEvent should be called with:

  • DBG_CONTINUE if exception event successfully handled by Debugger. No action is required by Debuggee, and Debuggee can run normally.
  • DBG_EXCEPTION_NOT_HANDLED if this event not handled/resolved by Debuggee. The Debugger might just record this event, notify the Debugger-user, or do something else.

    Please note that returning DBG_CONTINUE for improper debugging event would raise the same event in the Debugger, and same event would arrive indefinitely. Since we are early stage of writing Debuggers, we should play safe, and return EXCEPTION_NOT_HANDLED (give up flag!). The exclusion, for this article, is Break-point event, which I am discussing next.

    Exceptions Codes

    The EXCEPTION_RECORD::ExceptionCode variable holds the arrived exception code, can have one of these codes (ignore nested exceptions!):

    1. EXCEPTION_ACCESS_VIOLATION
    2. EXCEPTION_ARRAY_BOUNDS_EXCEEDED
    3. EXCEPTION_BREAKPOINT
    4. EXCEPTION_DATATYPE_MISALIGNMENT
    5. EXCEPTION_FLT_DENORMAL_OPERAND
    6. EXCEPTION_FLT_DIVIDE_BY_ZERO
    7. EXCEPTION_FLT_INEXACT_RESULT
    8. EXCEPTION_FLT_INVALID_OPERATION
    9. EXCEPTION_FLT_OVERFLOW
    10. EXCEPTION_FLT_STACK_CHECK
    11. EXCEPTION_FLT_UNDERFLOW
    12. EXCEPTION_ILLEGAL_INSTRUCTION
    13. EXCEPTION_IN_PAGE_ERROR
    14. EXCEPTION_INT_DIVIDE_BY_ZERO
    15. EXCEPTION_INT_OVERFLOW
    16. EXCEPTION_INVALID_DISPOSITION
    17. EXCEPTION_NONCONTINUABLE_EXCEPTION
    18. EXCEPTION_PRIV_INSTRUCTION
    19. EXCEPTION_SINGLE_STEP
    20. EXCEPTION_STACK_OVERFLOW
    Relax! I am not discussing all of them, but one: EXCEPTION_BREAKPOINT. Okay, here is the code:
    case EXCEPTION_DEBUG_EVENT:
    {
       EXCEPTION_DEBUG_INFO& exception = debug_event.u.Exception;
       switch( exception.ExceptionRecord.ExceptionCode)
       {
          case STATUS_BREAKPOINT:  // Same value as EXCEPTION_BREAKPOINT
    
             strEventMessage= "Break point";
             break;
    
          default:
             if(exception.dwFirstChance == 1)
             {				
                strEventMessage.Format(L"First chance exception at %x, exception-code: 0x%08x", 
                            exception.ExceptionRecord.ExceptionAddress,
                            exception.ExceptionRecord.ExceptionCode);
             }
             // else
             // { Let the OS handle }
    
    			
             // There are cases where OS ignores the dwContinueStatus, 
             // and executes the process in its own way.
             // For first chance exceptions, this parameter is not-important
             // but still we are saying that we have NOT handled this event.
    
             // Changing this to DBG_CONTINUE (for 1st chance exception also), 
             // may cause same debugging event to occur continously.
             // In short, this Debugger does not handle debug exception events
             // efficiently, and let's keep it simple for a while!
             
             dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;			
             }
    
             break;
    }
    You might be aware what breakpoint is. Out of the standard Debugger perspective, the break-pointing can happen with DebugBreak API, or {int 3} assembly instruction, or System.Diagnostics.Debugger.Break in .NET Framework. The Debugger would receive the Debug-exception code STATUS_BREAKPOINT (same as EXCEPTION_BREAKPOINT) when any of these occur in the running process. The Debuggers generally use this event to break the running process, and may display the source code where the event occurred. But in our basic Debugger, we would just display this event to the user. No source code or the instruction location is shown. We'll cover displaying the source code in next part of this article.

    Raising breakpoint from a process which is not being debugged would crash the application, or may display JIT dialog box. The is the reason I used IsDebuggerPresent in Debuggee project attached:

    if ( !IsDebuggerPresent() )
       AfxMessageBox(L"No debugger is attached currently.");
    else
       DebugBreak();

    As a final note to this simplest debug-exception event: EXCEPTION_DEBUG_EVENT would be raised first time by the kernel itself, and would always arrive. Debuggers like Visual Studio ignores this very first breakpoint exception, but Debuggers like WinDbg would always show you this event too.

    Winding up...

    Use any process to debug or use the attached Debuggee named DebugMe:

    [DebuggeeUI.jpg]

    The binaries (EXEs) attached here are compiled with Visual Studio 2005 Service Pack 1. You may not have VC++ runtime libraries for same version. You can download it from Microsoft.com or rebuild the projects from your IDE.

    What's up for next part(s)?

    I would cover few more of exception debugging events, but unable to list them now. I would also cover displaying the source code too! I might also cover displaying the memory contents, registers, stacks and so on...

    Stay tuned!



  • Downloads

    Comments

    • There are no comments yet. Be the first to comment!

    Leave a Comment
    • Your email address will not be published. All fields are required.

    Top White Papers and Webcasts

    • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

    • Live Event Date: October 29, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT It's well understood how critical version control is for code. However, its importance to DevOps isn't always recognized. The 2014 DevOps Survey of Practice shows that one of the key predictors of DevOps success is putting all production environment artifacts into version control. In this eSeminar, Gene Kim will discuss these survey findings and will share woeful tales of artifact management gone wrong! Gene will also share examples of how …

    Most Popular Programming Stories

    More for Developers

    Latest Developer Headlines

    RSS Feeds