Debugging – Modifying Code At Runtime

Introduction

The build and debug cycle can be tedious especially when you are unsure whether the change you have in mind solves your problem or not. Sometimes it is good to be able to tweak the logic of your code during a debugging session. This article explains how to make minor adjustments to code without having to rebuild your application.

The article requires tweaking the assembly and dealing with opcodes. If you are not very familar with debugging in assembly, you may want to read this article first. The tips shared here are also useful while debugging libraries where you either do not have the source code or the build environment is unavailable.

Debuggers work on the common principle of launching the executable as a child program and requesting special permissions from the operating system to access and alter the state of the program. This allows it to show and/or modify the memory, registers, code, etc in a debugging session. If a debugger attaches itself to another program already loaded in memory, it requests special permission from the operating system to take control of the program for the purpose of debugging. It will now seem very obvious why the same program in memory cannot be attached by two debuggers at the same time. The operating system caters to the request on a first come first basis and refuses permissions to the second debugger.

The debugger’s access to a program’s memory and registers not only allows users to view the state of the program but also set software breakpoints. Software breakpoints are set by altering and restoring the instructions in memory and has already been covered indepth in an earlier article.

How to modify code at runtime

Altering instructions without compilation of code during debugging works on a similar logic. If one uses the debugger to alter instructions of the program which it controls, then one can essentially change the logic compiled into the executable that is currently loaded in memory. It is worth noting that altering a program in memory is temporary. When the program is restarted and reloaded in memory, all changes are lost and no real harm has been done. As this requires directly tweaking the opcodes in memory, one needs to know how to deal with assembly and this cannot be accomplished with knowledge of a high level language alone.
Independent of the operating system and debugger, the steps needed to change the logic from within the debugger are as follows –

  1. Launch the program in a debugger.
  2. Set a breakpoint in the code at the location which you want to alter execution.
  3. Execute the program and drive the program so that your breakpoint is hit.
  4. Request the debugger to display the disassembly of the code.
  5. Get comfortable with the source-assembly mapping.
  6. Identify the address of the assembly line you would like to alter. Altering usually means changing an if statement to if NOT or “jump if zero” to “jump if not zero”.
  7. Take the address and dump the memory contents at that address. You will see opcodes corresponding to the assembly you just saw.
  8. Modify the memory location with new opcodes (more on this later).
  9. Cross your fingers so that your change won’t cause a crash 🙂
  10. Disable the breakpoint so that execution doesn’t stop too many times at this breakpoint as it makes debugging distracting.
  11. Continue execution and now the program will respond to the new logic in the program.
  12. To undo the effect of change, restart the program.

Example

Let us look at the following simple snippet of code below.


bool flag = true;
if( !flag )
cout << "flag is false" ;
else
cout << "flag is true" ;


The code when compiled and run normally would print “flag is true” and this is obvious from the code above. Say we at runtime would like to alter the logic by replacing if(!flag) with if(flag) so that the statement “flag is false” is printed instead.
Let us see how to do this with the two most commonly available debuggers.

GDB

The steps below are in the same order as the generic steps explained earlier.

  • gdb <myprogram>
  • break <line_number_of_if_statement>
  • run   Code will stop at the breakpoint set above.
  • disassemble. The dump on my machine is as follows –

disassemble

0x00401170 <main+32>:   call   0x40f260 <_alloca>
0x00401175 <main+37>:   call   0x410410 <__main>
0x0040117a <main+42>:   movb   $0x1,-0x1(%ebp)
0x0040117e <main+46>:   cmpb   $0x0,-0x1(%ebp)
0x00401182 <main+50>:   jne    0x40119a <main+74>
0x00401184 <main+52>:   movl   $0x443000,0x4(%esp)
0x0040118c <main+60>:   movl   $0x4463a0,(%esp)
0x00401193 <main+67>:   call   0x43e720 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
0x00401198 <main+72>:   jmp    0x4011ae <main+94>
0x0040119a <main+74>:   movl   $0x44300e,0x4(%esp)
0x004011a2 <main+82>:   movl   $0x4463a0,(%esp)
0x004011a9 <main+89>:   call   0x43e720 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
0x004011ae <main+94>:   mov    $0x0,%eax
  • Note that main+67 and main+89 are calls to cout. We have now identified the two important statements and the if check should be just above it. It usually helps to corelate the assembly with the source code in this manner.
  • The if check is on lines main+42, main+46 and main+50 (if !(flag==0) ). If we change jne to je, we can negate the if condition and thus alter the logic of the code. The address of the opcode jne above is 0x00401182.
  • x/6x 0x00401182 will dump the opcodes and operands in hexadecimal. In the dump below, 0x75 is the opcode for jne and the operand is 0x16. For details, refer to the Intel instruction set.
0x401182 <main+50>: 0x75 0x16 0xc7 0x44 0x24 0x04
  • set *(char*)0x00401182=0x74 where 0x74 is the opcode for je. Effectively “jump if not equal” has been changed to “jump if equal” thereby inverting the logic of the if statement.
  • cross your fingers.
  • disable 1. Disable breakpoint set earlier.
  • continue

You will observe that as we have modified the runtime code using gdb, the output of the program after the above steps would be “flag is false”.

Visual Studio

Assembly Screen Shot in Visual Studio
The above screen shot shows the debugger displaying the assembly after hitting the breakpoint at the if statement. Do make sure that when you right click in the assembly, “Show Code Bytes” and “Show Address” are checked.
From the screen shot above, the address of jne instruction is 0x003314B8 and its opcode is 0x75. If we modify the opcode at location 0x003314B8 and change it to 0x74 (opcode for je), we effectively negate the if statement at runtime.

To do this, open the memory window (Debug->Windows->Memory->Memory 1) and enter 0x003314B8 in the Address field. The first byte shows as 0x75. Put the cursor on 0x75 and type 74 to change the opcode. You can confirm this by seeing the effect in the disassembly window.

This modifies the code in memory and now when the program execution is continued, the output would be “flag is false”.

Conclusion

Another useful tip to keep in mind is that a lot of code can be disabled at runtime if all corresponding opcodes and operands are replaced with NOPs which has the opcode 0x90 on an Intel machine.  Similarly useful results can also be achived by altering the operands instead of the opcodes.

The ability to change code at runtime is an effective tool for quick debugging though it requires a good understanding of the code at the assembly level.   This technique is meant for small tweaks because it is faster to recompile in case complex changes in assembly are needed.   Moreover, when the program finishes execution, the modification needs to be redone as this technique does not modify the image on the disk.

9 Replies to “Debugging – Modifying Code At Runtime”

    1. @Chris,
      Thanks for the comments. Edit and Continue is a great feature in Visual Studio however if you are missing the build environment or do not have the sources (in case of a third party library) or are using a different debugger (or a different platform), then it is handy to know how to tweak logic by changing opcodes in memory in any debugger.

  1. how could we change the assembly instructon in the text segment which is read+execute ? Shouldn’t OS generate an exception ?

    1. @maverick – I have never run into such a situation when I debug however the same should hold true for a debugger when inserting software breakpoints where the debugger too needs to change instructions in memory to insert the “int 3” instruction.

  2. while( index < m_size ) {
    const auto &entry = m_heap[ index ];
    out << delimiter << entry.first << " " << entry.second;
    // Forgot to increment index
    }

    Relevant Assembly:
    0x00002aaaafe5d347 : call 0x2aaaaf87fbd0 <std::vector<std::pair, std::allocator<std::pair > >::operator[](unsigned long)@plt>
    0x00002aaaafe5d34c : mov %rax,-0x20(%rbp)
    0x00002aaaafe5d350 : mov -0x20(%rbp),%rax
    0x00002aaaafe5d354 : mov 0x8(%rax),%ebx
    0x00002aaaafe5d357 : mov -0x20(%rbp),%rax
    0x00002aaaafe5d35b : mov (%rax),%r12
    0x00002aaaafe5d35e : mov -0x30(%rbp),%rax
    0x00002aaaafe5d362 : mov -0x38(%rbp),%rdx
    0x00002aaaafe5d366 : mov %rdx,%rsi
    0x00002aaaafe5d369 : mov %rax,%rdi
    0x00002aaaafe5d36c : call 0x2aaaaf8e63a0 <std::basic_ostream<char, std::char_traits >& std::operator<< <char, std::char_traits, std::allocator >(std::basic_ostream<char, std::char_traits >&, std::basic_string<char, std::char_traits, std::allocator > const&)@plt>
    0x00002aaaafe5d371 : mov %r12,%rsi

    How to increment index?

Leave a Reply to chintu Cancel reply

Your email address will not be published. Required fields are marked *