1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
;;;
;; An example that showcases how you can tell apart a hardware IRQ from a
;; software IRQ, and how to get the "break mark" from a software IRQ (see:
;; https://www.masswerk.at/6502/6502_instruction_set.html#BRK).
.segment "HEADER"
.byte 'N', 'E', 'S', $1A
.byte $02, $01
.byte $00
.byte $00
.segment "VECTORS"
.addr nmi, reset, irq
.segment "CODE"
;; asan:stack full
zp_ptr = $00 ; asan:reserve $02
zp_brk = $02
m_stack = $100
.proc reset
;; Our code will simply call 'brk' to interrupt execution and then loop
;; forever.
brk
.byte $42
here:
jmp here
.endproc
.proc nmi
rti
.endproc
.proc irq
;; Save all registers.
pha
txa
pha
tya
pha
;; On a 'brk' instruction, the status register is pushed onto the stack with
;; the 'B' flag set. Otherwise, on a hardware IRQ we will not get any such
;; thing. Hence, we first try to pull the status register.
tsx
;; Where is it located? Well, the stack pointer points to the next element,
;; and we have pushed onto the stack three times in order to save the
;; registers when we entered irq(). Hence, it's at the current stack
;; position + 4 + the current stack frame (in 'x' thanks to the previous
;; 'tsx').
lda m_stack + 4, x
;; Is the B flag set on the status register?
and #$10
;; If that's not the case, then it's a hardware IRQ.
;;
;; NOTE: this crucial part is actually prone to errors! It might just be the
;; case that the stack contained garbage, we got a hardware IRQ, and
;; 'm_stack + 4 + stack frame' just so happened to match '#$10'. In order to
;; guarantee this to work, 'pla' usage would need to be coupled with a
;; cleaning of the given stack address as well, which is just unrealistic.
beq is_hardware_irq
;; This is a software IRQ. Now, the low byte from the PC address was pushed
;; just before the status register. But the address that was pushed was
;; actually PC + 2 so to hop over the "break mark". That is, every 'brk'
;; instruction is coupled with an extra byte containing debugging
;; information for the given 'brk'. Hence, if we want to get the "break
;; mark", we have to pick up the address of PC - 1.
lda m_stack + 5, x
sec
sbc #$01
sta zp_ptr
;; The high byte from the PC address was pushed onto the stack before the
;; low one. Hence, it's the next byte. Moreover, we call 'sbc #$00' to apply
;; the borrow from the previous subtraction just in case it underflowed.
lda m_stack + 6, x
sbc #$00
sta zp_ptr + 1
;; And now 'zp_ptr' points to the actual break mark.
ldy #$00
lda (zp_ptr), y
;; NOTE: when running this example, you should see the "break mark" ($42)
;; set into memory address 'zp_brk' ($02).
sta zp_brk
is_hardware_irq:
;; Restore all registers.
pla
tay
pla
tax
pla
rti
.endproc
.segment "CHARS"
.byte $01
|