The core doesn't do this, the debug software does this.
When you create the breakpoint, you copy out the original instruction, and store it with the "break point meta data" that you must maintain anyway. You need to meta data to know which break point was hit when you get the BRK interrupt.
After the breakpoint is hit, when you hit "continue", it's up to the debugger to mimic the actual instruction to execute.
So, say for example, you had this:
Code:
0200 LBL: LDA #0
0202 BEQ LBL1
0204 ...
...
0220 LBL1: ...
To put in the break point, you replace the A9 with BRK (00). When its hit, the handler figures out where it came from, located the meta data, presents the code to debugger, etc.
But, when you "continue" from the debugger, you relocate the instruction to a local work area and add a JMP:
Code:
XXXX LDA #00
XXX2 JMP 0202
That will mimic the instruction being executed, and then continue on, while leaving your break point in place.
The trick here is couple of things. First, you need to know what instruction you're mimicking, so that you can pull out the necessary data for the operand. (i.e. you need to know that the LDA # here has a single byte argument, the 00, so you can copy it in to your mimic instruction buffer), also, if you happen to be stomping on a JSR or branch, you'll need to simulate those instructions. In the JSR case, you need to stuff the stack correctly, and then do the jump. With a branch, you'll need to test the proper bits for the instruction, calculate the proper destination, and then JMP there.
The branch simulation is easy. Consider the above code, with the breakpoint on the BEQ.
Code:
XXXX PLP ; Pull the processor status off the stack, after you made sure it's the proper one captured from the BRK
XXX1 BEQ XXX6
XXX3 JMP 0204
XXX6 JMP 0220 ; calculated from the branch instruction
You need to jump through these hoops. You can't simply replace the BRK with the instruction and continue, as you then lose the breakpoint and have no way of setting it back again.
For single stepping, you just create a "new" temporary break point on the next instruction, and handle it like any other break point. For branches, you'll need to predict where it will go so you can set the BRK properly (not like you don't know...).
So, there's a bit of advanced disassembly and simulation involved in a software debugger.
But this is all done in the debugger software.