I’m finally getting breakpoints to start working!

RST 20H (0xe7) is what I use to generate breakpoints. Since the code copies itself to RAM banks and executes from RAM, the breakpoint code can swap in e7 for whatever instruction is at a certain point. This causes the execution to run off to 0x0020.

Once there, it pushes all registers. It also pushes sp (i.e. ld hl, 0; add hl, sp; push hl) and then calls a function that loads hl with a pointer to a function and jumps to it. The default function just returns. But if the pointer is modified, execution can go someplace else.

And that place is bpBreak.

The bpBreak function handles loading a pointer to the registers contents struct over the stack where the the registers are stored. That struct pointer can be used to display and change register contents. It then it replaces all the RST 20H opcodes with the original contents. While doing that, it updates the breakpoint ignore counter, deletes delete-on-break breakpoints, and deletes break-on-hit breakpoints if the pc is where the break occurred. Finally, if any breakpoint had a count of 0, it will start the break prompt, otherwise skip straight to continuing.

The break prompt is where user can interactively set, delete, and change breakpoints, view and change register contents, and view and change memory.

Step is done with temporary breakpoints that delete on break. A breakpoint usually goes at the beginning of the next instruction. The exceptions are ret, jp, and jr instructions since execution never gets beyond. Except, of course, when they are conditional. In that case, the breakpoint must be set at the destination of the jump. There are 6 possibilities of which I deal with 4; call, jp: place breakpoint at (uint16_t)(bpRegs->pc+1) jr: place breakpoint at pc+(uint8_t)(bpRegs->pc+1) ret: place breakpoint at (uint16_t)(bpRegs->sp) jp (hl): place breakpoint at bpRegs->hl rst: this is not done but one could place breakpoints at 00, 08, 10, 18, 20, 28, 30, or 38 halt: this is not done but one could place a breakpoint at 66

Next is like step except if there is a call (i.e. breakpoint uses absolute addressing, may reach next instruction, and is not conditional), then the breakpoint is not placed at the destination. This fails to take into account that calls can be conditional, but the SDCC compiler is not generating conditional calls, at least at the optimization level I’m using.

Continue is a little tricky. It is assumed that the current instruction is a breakpoint. If it isn’t this is overkill, but if it is, we can’t just load the breakpoints and run–we’d break at the same point again. Rather, the breakpoint has be temporarily disabled, a step has to take place, and the breakpoint must be restored once past it. This is done by never loading any breakpoint at the current pc, and having the code that unloads the breakpoint determine if a break is necessary. If a deletion-break breakpoint is hit and has an ignore count greater than 0, it will not generate a break.

Here are two breakpoint files. The first is the basic breakpoint function:

  1 // __asm__("rst 20h") == 0xe7
  2 #define RST_20H ((uint8_t)(0xe7))
  3 
  4 struct Regs* bpRegs;
  5 struct BP bpInfo[BREAKPOINT_COUNT];
  6 
  7 
  8 // declare fnRST_20H for telling bank 0 how to get to bpBreak
  9 extern void(*fnRST_20H)();
 10 
 11 // Initialize breakpoint system
 12 void bpInit()
 13 {
 14     memset(bpInfo, 0, sizeof(bpInfo));
 15     fnRST_20H = bpBreak;
 16 }
 17 
 18 // crt1 calls this instead of main. This calls main
 19 void bpStart(void(*initBP)())
 20 {
 21     bpSet((uint8_t*)initBP, (1 << BP_FLAG_ENABLED) | (1 << BP_FLAG_DELETE_ON_HIT), 0);
 22     bpLoadBreakPoints(NULL);
 23 }
 24 
 25 // Sets a breakpoint at location with behavior flags and an ignore count
 26 uint8_t bpSet(uint8_t* location, uint8_t flags, uint8_t count)
 27 {
 28     uint8_t bp;
 29 
 30     // get unusued and look for same
 31     for (bp = 0; bp < BREAKPOINT_COUNT; bp++)
 32     {
 33         struct BP* b = &bpInfo[bp];
 34         if ((b->flags & (1 << BP_FLAG_ASSIGNED)) == 0)
 35         {
 36             b->flags |= (1 << BP_FLAG_ASSIGNED);
 37             break;
 38         }
 39         else if (b->addr == location)
 40         {
 41             if (((b->flags ^ flags) & ~0x03) == 0)
 42             {
 43                 // such a breakpoint already exists
 44                 return bp;
 45             }
 46         }
 47     }
 48 
 49     // Set the breakpoint
 50     if (bp < BREAKPOINT_COUNT)
 51     {
 52         struct BP* b = &bpInfo[bp];
 53         b->flags = flags | (1 << BP_FLAG_ASSIGNED);
 54         b->addr = location;
 55         b->count = count;
 56         printf("Set BP%x @ %p (%x-%d)\r\n", bp, location, flags, count);
 57     }
 58     return bp;
 59 }
 60 
 61 // Delete a breakpoint
 62 void bpUnset(uint8_t bp)
 63 {
 64     struct BP* b = &bpInfo[bp];
 65     b->flags = 0x00;
 66     printf("Unset BP%x @ %p (%x-%d)\r\n", bp, b->addr, b->flags, b->count);
 67 }
 68 
 69 // Enable or disable a breakpoint
 70 void bpEnable(uint8_t bp, bool enable)
 71 {
 72     if (enable)
 73     {
 74         bpInfo[bp].flags |= (1 << BP_FLAG_ENABLED);
 75         bpInfo[bp].count = 0;
 76     }
 77     else
 78     {
 79         bpInfo[bp].flags &= ~(1 << BP_FLAG_ENABLED);
 80     }
 81 }
 82 
 83 // Find if a breakpoint is enabled
 84 bool bpIsEnabled(uint8_t bp)
 85 {
 86     return (bpInfo[bp].flags & (1 << BP_FLAG_ENABLED)) != 0;
 87 }
 88 
 89 // Gets the first breakpoint at addr of 0xff
 90 uint8_t bpAt(uint8_t* addr)
 91 {
 92     uint8_t bp;
 93     for (bp = 0; bp < BREAKPOINT_COUNT; bp++)
 94     {
 95         if (bpInfo[bp].addr == addr)
 96         {
 97             return bp;
 98         }
 99     }
100     return 0xff;
101 }
102 
103 // Swaps in the RST 20H into the locations with breakpoints
104 // Breakpoints are not swapped at 'pc'
105 void bpLoadBreakPoints(uint8_t* pc)
106 {
107     uint8_t bp = 0;
108     for (bp = 0; bp < BREAKPOINT_COUNT; bp++)
109     {
110         struct BP* b = &bpInfo[bp];
111         if ((b->flags & 0x03) == 0x03 && b->addr != pc) // assigned and enabled and not at PC
112         {
113             uint8_t swap = *(b->addr);
114             if (swap == RST_20H)
115             {
116                 // already a breakpoint there
117             }
118             else
119             {
120                 b->swap = swap;
121                 *(b->addr) = RST_20H;
122                 printf("RST 20 to %02x @ %p\r\n", b->swap, b->addr);
123             }
124         }
125     }
126 }
127 
128 // Restores RST 20H with the original opcodes
129 // decrements ignore count for breakpoints who addr is 'pc'
130 // Returns whether any breakpoints at 'pc' were hit and had an ignore count of 0
131 bool bpUnloadBreakPoints(uint8_t* pc)
132 {
133     uint8_t bp = 0;
134     bool doBreak = false;
135     for (bp = 0; bp < BREAKPOINT_COUNT; bp++)
136     {
137         struct BP* b = &bpInfo[bp];
138         if ((b->flags & (1 << BP_FLAG_ASSIGNED)) != 0)
139         {
140             if ((b->flags & (1 << BP_FLAG_ENABLED)) != 0)
141             {
142                 uint8_t swap = *(b->addr);
143                 if (swap == RST_20H)
144                 {
145                     printf("RST 20 from %02x @ %p\r\n", b->swap, b->addr);
146                     *(b->addr) = b->swap;
147                 }
148                 if (b->addr == pc)
149                 {
150                     if ((b->count > 0))
151                     {
152                         b->count--;
153                     }
154                     else
155                     {
156                         doBreak = true;
157                     }
158 
159                     // delete on hit
160                     if ((b->flags & (1 << BP_FLAG_DELETE_ON_HIT)) != 0)
161                     {
162                         b->flags = 0;
163                     }
164                 }
165             }
166             // delete on break BP
167             if ((b->flags & (1 << BP_FLAG_DELETE_ON_BREAK)) != 0)
168             {
169                 b->flags = 0;
170             }
171         }
172     }
173     return doBreak;
174 }
175 
176 // This set breakpoints for certain types: 0-step 1-next 2-continue
177 // Step/Next: if !BRANCH_NO_STEP set bp @ pc+il
178 // Step: if BRANCH_ABS set @ (pc+1)
179 // Step: if BRANCH_REL set @ pc + (pc+1)
180 // Step: if BRANCH_RET set @ (sp-2)
181 // Step: if BRANCH_HL set @ hl
182 void bpStep(uint8_t type)
183 {
184     uint8_t* pc;
185     uint8_t branchInfo;
186     uint8_t instrLen;
187     bool breakOnNextInstr;
188     bool breakOnDest;
189     uint8_t count;
190     uint8_t nextFlags;
191 
192     // get PC
193     pc = (uint8_t*)bpRegs->pc;
194 
195     // get next instruction address
196     instrLen = z80InstructionLength(pc);
197     branchInfo = instrLen & 0xf8;
198     instrLen = instrLen & 0x07;
199 
200     printf("branch %02x\r\n", branchInfo);
201 
202     // figure out what to do
203     // if not branching away permanently, set a breakpoint there
204     breakOnNextInstr = (branchInfo & BRANCH_ALWAYS) == 0;
205 
206     // count is 0 unless type is continue
207     // If count is 1, will not return to break point prompt--will just continue
208     count = (type == 2) ? 1 : 0;
209 
210     // How flags work
211     // step: delete on hit for next and delete on break dest
212     // next: delete on hit for next
213     // continue: always delete on any break--restart immediately
214     nextFlags = (type == 2) ? (1 << BP_FLAG_DELETE_ON_BREAK) : (1 << BP_FLAG_DELETE_ON_HIT);
215     nextFlags |= (1 << BP_FLAG_ENABLED);
216     //nextFlags = (1 << BP_FLAG_DELETE_ON_BREAK) | (1 << BP_FLAG_ENABLED);
217 
218     // place breakpoint if not NO_STEP branch (jp **, jp (hl), ret/i/n, jr)
219     if (breakOnNextInstr)
220     {
221         bpSet(pc + instrLen, nextFlags, count);
222     }
223 
224     // BP on branch destination
225     breakOnDest = ((branchInfo & 0xf8) != 0);
226     if ((type == 1) && ((branchInfo & BRANCH_CALL) != 0))
227     {
228         breakOnDest = false; // if user is using 'next' on a call
229     }
230 
231     if (breakOnDest)
232     {
233         uint8_t branchType = branchInfo & 0x30;
234         uint16_t target = 0;
235         // get branch addr (rel, abs, (hl), (sp-2)
236         if (branchType == BRANCH_ABS)
237         {
238             target = *(uint16_t*)(pc + 1);
239         }
240         else if (branchType == BRANCH_REL)
241         {
242             target = (uint16_t)pc + (int8_t)(pc[1]);
243         }
244         else if (branchType == BRANCH_RET)
245         {
246             target = *(uint16_t*)(bpRegs->sp);
247         }
248         else// if (branchType == BRANCH_HL)
249         {
250             target = bpRegs->hl;
251         }
252 
253         // place breakpoint
254         bpSet((uint8_t*)target, (1 << BP_FLAG_DELETE_ON_BREAK) | (1 << BP_FLAG_ENABLED), count);
255     }
256 }
257 
258 // RST 20H calls this after 
259 void bpBreak() __naked
260 {
261     //RST_20H all registers pushed
262     __asm__("di");
263     __asm__("ld hl, #2");
264     __asm__("add hl, sp");
265     __asm__("push hl");
266     __asm__("ld (_bpRegs), hl");
267     __asm__("call _bpPrompt");
268     __asm__("pop hl ; toss");
269     __asm__("ret");
270 }

This file snippet is for breakpoint prompt features:

  1 struct BpCmd {
  2     const char* cmd;
  3     const char* desc;
  4     uint8_t(*fn)(const char* line);
  5 };
  6 
  7 void showMem(uint16_t address, uint16_t length)
  8 {
  9     uint8_t i;
 10     char* ph;
 11     char* pa;
 12     char line[75];
 13     w_bb shift;
 14     address = address & 0xfff0;
 15     length = (length + 15) & 0xfff0;
 16     memset(line, ' ', sizeof(line) - 1);
 17     line[sizeof(line) - 1] = 0;
 18     line[4] = ':';
 19     while (length)
 20     {
 21         shift.uw = address;
 22         ph = &line[0];
 23         pa = &line[4 + 1 + 1 + 1 + 16 * 3 + 1];
 24         *ph++ = __ascii[shift.ubb.h >> 4];
 25         *ph++ = __ascii[shift.ubb.h & 0x0f];
 26         *ph++ = __ascii[shift.ubb.l >> 4];
 27         *ph++ = __ascii[shift.ubb.l & 0x0f];
 28         ph++;
 29         ph++;
 30         // 16 bytes
 31         for (i = 0; i < 16; i++)
 32         {
 33             uint8_t b;
 34             b = *(char*)(address + i);
 35             *ph++ = __ascii[b >> 4];
 36             *ph++ = __ascii[b & 0x0f];
 37             ph++;
 38 
 39             // unprintables to .
 40             if (b < ' ' || b > 0x7e)
 41             {
 42                 b = '.';
 43             }
 44             *pa++ = b;
 45 
 46             // gap
 47             if (i == 7)
 48             {
 49                 ph++;
 50                 pa++;
 51             }
 52 
 53         }
 54         puts(line);
 55         address += 16;
 56         length -= 16;
 57     }
 58 }
 59 
 60 static void showRegs()
 61 {
 62     printf("af: %04x  bc: %04x  de: %04x  hl=%04x\r\n", bpRegs->af, bpRegs->bc, bpRegs->de, bpRegs->hl);
 63     printf("pc: %04x  sp: %04x  ix: %04x  iy=%04x\r\n", bpRegs->pc, bpRegs->sp, bpRegs->ix, bpRegs->iy);
 64 }
 65 
 66 static void showBreakpoint(uint8_t bp)
 67 {
 68     struct BP* b = &bpInfo[bp];
 69     printf("%x: addr=%04x en=%x count=%02x\r\n", bp, b->addr, (b->flags & (1 << BP_FLAG_ENABLED)) != 0, b->count);
 70 }
 71 static void showBreakpoints()
 72 {
 73     uint8_t i;
 74     for (i = 0; i < BREAKPOINT_COUNT; i++)
 75     {
 76         struct BP* b = &bpInfo[i];
 77         if ((b->flags & (1 << BP_FLAG_ASSIGNED)) != 0)
 78         {
 79             showBreakpoint(i);
 80         }
 81     }
 82 }
 83 
 84 static const struct BpCmd commands[] =
 85 {
 86     {
 87         "?",
 88         "list",
 89         bpIntHelp
 90     },
 91     {
 92         "l",
 93         "list bp's",
 94         bpIntList
 95     },
 96     {
 97         "s",
 98         "step",
 99         bpIntStep
100     },
101     {
102         "n",
103         "next",
104         bpIntNext
105     },
106     {
107         "c",
108         "continue",
109         bpIntContinue
110     },
111     {
112         "b",
113         "set bp @ addr",
114         bpIntCreate
115     },
116     {
117         "d",
118         "delete bp # or all",
119         bpIntDelete
120     },
121     {
122         "t",
123         "toggle bp",
124         bpIntToggle
125     },
126     {
127         "r",
128         "see regs or set rr v",
129         bpIntRegs
130     },
131     {
132         "m",
133         "see RAM",
134         bpIntMem
135     },
136 };
137 
138 const static uint8_t commandLength = sizeof(commands) / sizeof(commands[0]);
139 
140 
141 static uint8_t bpIntList(const char* line)
142 {
143     line; // suppress warning
144     showBreakpoints();
145     return 0;
146 }
147 
148 static uint8_t bpIntHelp(const char* line)
149 {
150     uint8_t i;
151     line; // suppress warning
152     for (i = 0; i < commandLength; i++)
153     {
154         printf("%s - %s\r\n", commands[i].cmd, commands[i].desc);
155     }
156     return 0;
157 }
158 
159 static uint8_t bpIntContinue(const char* line)
160 {
161     line; // suppress warning
162     bpStep(2);
163     return 0xff; // continue
164 }
165 
166 static uint8_t bpIntStep(const char* line)
167 {
168     line; // suppress warning
169     bpStep(0);
170     return 0xff; // continue
171 }
172 
173 static uint8_t bpIntNext(const char* line)
174 {
175     line; // suppress warning
176     bpStep(1);
177     return 0xff; // continue
178 }
179 
180 static uint8_t bpIntCreate(const char* line)
181 {
182     if (*line++ == ' ')
183     {
184         uint16_t addr = extractHex(&line);
185         uint8_t count = 0;
186         if (*line == ' ')
187         {
188             line++;
189             count = extractHex(&line);
190         }
191         if (*line == 0)
192         {
193             uint8_t i = bpSet((uint8_t*)addr, (1<<BP_FLAG_ENABLED), count);
194             if (i == 0xff)
195             {
196                 puts("BP full");
197                 return 1;
198             }
199             else
200             {
201                 showBreakpoint(i);
202             }
203         }
204         else
205         {
206             return 2;
207         }
208     }
209     else
210     {
211         return 3;
212     }
213     return 0;
214 }
215 
216 static uint8_t bpIntDelete(const char* line)
217 {
218     uint8_t status = 0;
219     if (*line == ' ')
220     {
221         uint8_t i;
222         line++;
223         i = extractHex(&line);
224         if (*line == 0 && i < BREAKPOINT_COUNT)
225         {
226             bpUnset(i);
227         }
228     }
229     return 0;
230 }
231 
232 static uint8_t bpIntToggle(const char* line)
233 {
234     //uint8_t status = 0;
235     uint8_t bp;
236     if (*line == ' ')
237     {
238         bp = extractHex(&line);
239     }
240     else if (*line == 0)
241     {
242         bp = bpAt((uint8_t*)bpRegs->pc);
243     }
244     else
245     {
246         return 1;
247     }
248     if (bp >= BREAKPOINT_COUNT)
249     {
250         return 2;
251     }
252     bpEnable(bp, !bpIsEnabled(bp));
253     showBreakpoint(bp);
254     return 0;
255 }
256 
257 static const char* const regs = "SPiyixhldebcafpc";
258 
259 static uint8_t bpIntRegs(const char* line)
260 {
261     //uint8_t status = 0;
262     if (*line == 0)
263     {
264         showRegs();
265         return 0;
266     }
267     else if (*line == ' ')
268     {
269         uint8_t i;
270         uint16_t search;
271         const uint16_t* reg = (const uint16_t*)regs;
272 
273         line++;
274         search = *(uint16_t*)line;
275 
276         for (i = 0; i < 7; i++)
277         {
278             if (search == *reg)
279             {
280                 uint16_t val;
281                 line += 3;
282                 val = extractHex(&line);
283                 if (*line == 0)
284                 {
285                     ((uint16_t*)bpRegs)[i] = val;
286                     showRegs();
287                 }
288                 else
289                 {
290                     puts("?");
291                 }
292             }
293             reg++;
294         }
295     }
296     return 0;
297 }
298 
299 static uint8_t bpIntMem(const char* line)
300 {
301     uint16_t addr = 0;
302     uint16_t len = 16;
303     if (*line++ == ' ')
304     {
305         addr = extractHex(&line);
306         if (*line++ == ' ')
307         {
308             len = extractHex(&line);
309         }
310         if (*line == 0)
311         {
312             showMem(addr, len);
313             return 0;
314         }
315     }
316     return 1;
317 }
318 
319 // do prompt for breaks
320 //  ?           list commands
321 //  l           list breakpoints
322 //  c           continue
323 //  s           step
324 //  n           next
325 //  b xxxx (cc) break @ xxxx (optional countdown)
326 //  d ##        delete ##
327 //  t ##        toggle ## enable
328 //  r           show regs
329 //  r rr xxxx   set reg (except sp)
330 //  m xxxx xxxx show mem
331 void bpPrompt()
332 {
333     char buffer[32];
334     char* p;
335     uint8_t i;
336     uint16_t pc = bpRegs->pc - 1; // source of RST_20
337     uint8_t interruptSave;
338     bool doBreak;
339 
340     bpRegs->pc = pc; // restore to the instruction that was supposed to be there
341     doBreak = bpUnloadBreakPoints((uint8_t*)pc);
342     interruptSave = ei_save();
343 
344     while (doBreak)
345     {
346         uint8_t result = 0;
347         char c;
348         printf("(brk @ %p)> ", pc);
349         gets(buffer);
350         p = &buffer[0];
351         c = *p;
352         for (i = 0; i < commandLength; i++)
353         {
354             const char* cmd = commands[i].cmd;
355             uint8_t len = strlen(cmd);
356             if (strncmp(commands[i].cmd, p, len) == 0)
357             {
358                 result = commands[i].fn(&p[len]);
359             }
360         }
361 
362         if (result == 0xff)
363         {
364             break;
365         }
366         else if (result != 0)
367         {
368             printf("error %d\r\n", result);
369         }
370     }
371     bpLoadBreakPoints((uint8_t*)pc);
372     di_restore(interruptSave);
373 }

This is the part of interrupt.c that is relevant:

 1 static void ret() __naked
 2 {
 3     __asm__("ret");
 4 }
 5 // This is preset to 'ret' If the code runs frojmj flash, this is necessary.
 6 // If the code runs from RAM, this can be overridden.
 7 void(*fnRST_20H)() = ret;
 8 static void call_hl() __naked
 9 {
10     __asm__("jp (hl)");
11 }
12 void do_RST_20H() __naked
13 {
14     __asm__("push af");
15     __asm__("push bc");
16     __asm__("push de");
17     __asm__("push hl");
18     __asm__("push ix");
19     __asm__("push iy");
20     // push sp
21     __asm__("ld hl, #14");
22     __asm__("add hl, sp");
23     __asm__("push hl");
24 
25     __asm__("ld hl, (_fnRST_20H)");
26     __asm__("call _call_hl");
27 
28     __asm__("pop iy"); //  toss  SP
29     __asm__("pop iy");
30     __asm__("pop ix");
31     __asm__("pop hl");
32     __asm__("pop de");
33     __asm__("pop bc");
34     __asm__("pop af");
35     __asm__("ret");
36 }