Articles "What if?" Scenario analysis in the CPU Window by Brian Long

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
"What if?" Scenario analysis in the CPU Window
Brian Long - 29/Oct/2017
[SHOWTOGROUPS=4,20]
Last Tuesday, 24th October I did some sessions at Для просмотра ссылки Войди или Зарегистрируйся, one of which was on Creative Debugging Techniques. During the session there was a section where I was trying to demonstrate an idea or technique that happened to fully involve the CPU window. Unfortunately a series of finger fumbles on my part meant I couldn’t show what I wanted to show, albeit I think the point was made.

Anyway, I mentioned that maybe I’d write up that little snippet into a blog post, just to prove that it really does work as I suggested it does, and so here it is.

Oh, apologies up front for all the animated GIFs below- it seemed the most expeditious way to make sure I could really convey some of the points.

So the context was ‘What if?’ situations and testing out such scenarios during a debug session.

Clearly the primary tool for testing out ‘What if?’ scenarios is the Для просмотра ссылки Войди или Зарегистрируйся (Ctrl+F7) dialog. This dialog’s Modify button allows you to alter the value of the expression you have evaluated to find out how code behaves when the expression has a value other than what it actually had.

That’s a good and very valuable tool. But the case in point in the EKON 21 session was a bit different.

Consider a scenario where you are in the midst of a lengthy debug session, one that you’d really rather not reset and start again. Also consider that from some observations made in the debug session you have realised that a certain function B that is called from function A ought in fact not to be called. You want to test how well things pan out with B not being called.

In an entirely fabricated ultra-academic example, let’s use this code here, where A is TMainForm.WhatIfButtonClick and B is CommonRoutine.
Код:
procedure TMainForm.WhatIfButtonClick(Sender: TObject);
{$REGION '"What if?" scenarios'}
var
S: string;
begin
S := 'Hello world';
Caption := S;
CommonRoutine;
Color := Random($1000000);
{$ENDREGION}
end;

One solution to this is to move the instruction pointer to skip the call to B just as B is about to be called. This can be done in a number of ways in Delphi. Set a breakpoint on the call to B and when it hits do one of the following four options to achieve this:

1) Set next statement menu item
Right-click on the statement that follows the call to B and select Для просмотра ссылки Войди или Зарегистрируйся, a menu item added in Delphi 2006 and described by Chris Hesik in Для просмотра ссылки Войди или Зарегистрируйся.

Для просмотра ссылки Войди или Зарегистрируйся

2) Drag the instruction pointer editor gutter icon
Drag the instruction pointer icon in the editor gutter to point at the following statement. This drag and drop support for the instruction pointer symbol was Для просмотра ссылки Войди или Зарегистрируйся.

Для просмотра ссылки Войди или Зарегистрируйся

3) Change the instruction pointer in the CPU window
Invoke the Для просмотра ссылки Войди или Зарегистрируйся (View, Debug Windows, CPU Windows, Entire CPU or Ctrl+Alt+C), or at the very least the Disassembly pane (View, Debug Windows, CPU Windows, Disassembly or Ctrl+Alt+D). Right click on the next statement and choose New EIP (or Ctrl+N).

Для просмотра ссылки Войди или Зарегистрируйся

4) Update the EIP register in the CPU window
Invoke the Для просмотра ссылки Войди или Зарегистрируйся (View, Debug Windows, CPU Windows, Entire CPU or Ctrl+Alt+C). Note the address of the instruction you want to execute next. Right-click the Для просмотра ссылки Войди или Зарегистрируйся register in the Registers pane and choose Change Register… (Ctrl+H) and enter the new value as a hexadecimal number, i.e. with a $ prefix. An alternative to Change Register… is to choose Increment Register (Ctrl+I) a sufficient number of times to get the value to match the target address.

Для просмотра ссылки Войди или Зарегистрируйся


OK, so all of those achieve the goal on that single invocation of routine A, but what about the case where A is called lots of times – lots and lots of times? This idea falls down in that situation and so we might seek out an alternative option.

Maybe we can get rid of the call to B entirely for this run of the executable. Yes, maybe we can and indeed that was just the very technique I tried to show, but made a couple of silly mistakes by not paying attention to what exactly was on the screen. Mea culpa.

There are a couple of approaches to getting rid of the call to B from the code present in A. One is to replace the first few bytes of that statement with an instruction that jumps to the next statement. The other is to replace the entire statement with opcodes corresponding to ‘no operation’, i.e. the no-op opcode NOP. Let’s look at both approaches.

Both these approaches involve changing existing machine instructions in memory. With that end goal comes a rule, and the rule is that you can’t successfully change a machine instruction that your program is currently stopped at in the debugger or that the debugger has a breakpoint on. In other words, if you want to change the call to CommonRoutine to be something else this must be done when the program is stopped at a different instruction in the debugger and there must be no breakpoint on that instruction.

This is simply a side effect of the way debuggers implement breakpoints and statement stepping - they replace the first byte of the instruction to break at with $CC, the byte value, or opcode, for the assembly instruction Для просмотра ссылки Войди или Зарегистрируйся. When execution continues the $CC is swapped back for the original value.

So if you change the instruction at the current EIP when the execution has stopped in the debugger, when you ask it to move on your first byte will get replaced, just by the mechanics of your debugger doing its day job. This will most likely cause a very much unwanted opcode combination leading quickly to an application crash. [ One of my EKON fumbles was to instantly forget this previously well known (by me) fact and promptly get a crashed Для просмотра ссылки Войди или Зарегистрируйся. ]

Your best bet is to put a breakpoint on the preceding instruction, and then modify/replace your target instruction. Make sure there is no breakpoint on the target instruction.

When you look in the CPU window you can see the assembly instructions that correspond to the Pascal statement above it.

Для просмотра ссылки Войди или Зарегистрируйся

In the case of the call to CommonRoutine the assembly code is:
Код:
mov eax,[ebp-$04]
call TMainForm.CommonRoutine

The machine code bytes (opcodes) that represent those 2 instructions are $8B, $45, $FC and $E8, $11, $F8, $FF, $FF respectively. The 3 bytes for the first instruction are stored at locations starting at $5D1287 and the 5 bytes for the second instruction start at $5D128A.

The statement following the call to CommonRoutine starts at address $5D128F, 8 bytes on from $5D1287.



[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,438
Credits
573
[SHOWTOGROUPS=4,20]
1) Overwriting an instruction with a jump instruction
The goal is to write some opcodes into memory starting at address $5D1287 that represent an assembly instruction to jump 8 bytes forward. If we look at the Для просмотра ссылки Войди или Зарегистрируйся, a small jump is 2 bytes of instructions encoded as $EB coupled with the jump distance from the end of the jump instruction. So 8 bytes minus the 2 byte instruction is 6, so $EB $06. [ One of my fumbles in the EKON session was to misread the $EB as $E8, which is a CALL opcode. ]

So, to change the current code for new instructions we have to move our attention away from the Disassembly pane to the Memory pane. You can either use the one embedded into the Entire CPU view or open up one of four standalone memory panes using an item from the submenu View, Debug Windows, CPU Windows:
  • Memory 1 (Ctrl+Alt+E)
  • Memory 2 (Ctrl+Alt+2)
  • Memory 3 (Ctrl+Alt+3)
  • Memory 4 (Ctrl+Alt+4)
By default the memory pane will be settled on address $401000, the start of the application’s Code segment (according to first piece of information in Для просмотра ссылки Войди или Зарегистрируйся, as generated by the Для просмотра ссылки Войди или Зарегистрируйся).

Для просмотра ссылки Войди или Зарегистрируйся

You should reposition to the target instruction by using Go to Address… (Ctrl+G) from the context menu and entering (in this examples case) $5D1287. You’ll see the ‘familiar’ 8 bytes we saw for the instructions right there on the first line:

Для просмотра ссылки Войди или Зарегистрируйся

To change these 6 bytes to be bytes representing our jump instruction you can select Change (Ctrl+H) from the context menu and enter the values: $EB $06.

Для просмотра ссылки Войди или Зарегистрируйся

You can also simply start typing those values directly into the Memory pane and the Enter New Value dialog will pop up.
This changes the first 2 bytes of that instruction and the Disassembly pane echoes this by showing the JMP instruction.

Для просмотра ссылки Войди или Зарегистрируйся

As you’ll note, however, there is a bit of “noise” after this for the remaining 6 opcodes: some junk that is jumped over.

[ Update 30/10/2017 – thanks to The Arioch for welcome interjections. It should be noted that in this case after trampling over the first 2 opcodes the remainder of the previous set of opcodes still “make sense” to the disassembler. So much so that the very next Delphi statement is still shown and is still translated directly into its constituent opcodes.

It is, however, often the case that having bulldozed over a couple of essentially arbitrary opcodes, what’s left is a bit of a mess, and puts things “out of kilter”, leaving subsequent Delphi statements not showing in the disassembly pane thanks to what opcodes have come before.

As a simple example, not necessarily demonstrating the ultimate confusion that can be caused, here’s some code:

Для просмотра ссылки Войди или Зарегистрируйся

If we wish to skip the ShowMessage call we need to workout the JMP opcodes.

Run a copy of Windows Calculator, go into programmer mode (Alt+3), and calculate $5D1686 – $5D167C to get the gap from ShowMessage to the following statement. Then subtract 2 to take off the size of the small JMP instruction. This gives a final result of 8, so we enter new opcodes of $EB $08 and what’s then showing in the disassembly pane is this:

Для просмотра ссылки Войди или Зарегистрируйся

The disassembly of the call to CommonRoutine has gone rather up the spout, even though the opcodes for it are actually still quite intact.

End of update ]

To clean this up we could fill in the remaining 6 bytes with opcode $90, which corresponds to NOP, the assembly no-op instruction:

Для просмотра ссылки Войди или Зарегистрируйся

This shows as:

Для просмотра ссылки Войди или Зарегистрируйся

[ Another of my EKON fumbles was to enter too many $90 bytes having miscounted the required bytes, or perhaps forgetting that I need to subtracted the size of the jump instruction. This rather messed up the following instruction, which should have been left intact. This got another crash. ]

Or you could fill with data byte $FF – just data:

Для просмотра ссылки Войди или Зарегистрируйся

Для просмотра ссылки Войди или Зарегистрируйся

2) Overwriting an instruction with NOP opcodes
This is just an extension of the last points in the option above. In a memory pane that has been located on the start of the target instruction, just change all the bytes of the instruction to the NOP opcode, $90. We have 8 bytes here, so use the Change submenu item (Ctrl+H):

Для просмотра ссылки Войди или Зарегистрируйся

Для просмотра ссылки Войди или Зарегистрируйся


There we go, that’s what I meant to show in that 5 minute section of the session – apologies for the poor demonstration but hopefully this makes up for it ¯\_(ツ)_/¯

[/SHOWTOGROUPS]