mirror of
https://git.sr.ht/~crc_/retroforth
synced 2024-11-16 19:48:56 +01:00
6e7c73f93b
FossilOrigin-Name: 1a4cb2c13230dafba7a0e14e512fe3f97aeb4b3a7b28cac5db5ec404b2acd5a2
385 lines
9.9 KiB
Text
385 lines
9.9 KiB
Text
_
|
|
__ _ _ _| |_ ___ _ __ ___ _ _
|
|
/ _` | | | | __/ _ \| '_ \/ __| | | |
|
|
| (_| | |_| | || (_) | |_) \__ | |_| |
|
|
\__,_|\__,_|\__\___/| .__/|___/\__, |
|
|
|_| |___/
|
|
|
|
Autopsy is a set of debugging tools for RETRO.
|
|
|
|
# Background
|
|
|
|
RETRO runs on a virtual machine called Nga. The instruction set
|
|
is MISC inspired, consisting of just 30 instructions:
|
|
|
|
0 .. 5 pu 10 re 15 fe 20 di 25 zr
|
|
1 li 6 po 11 eq 16 st 21 an 26 en
|
|
2 du 7 ju 12 ne 17 ad 22 or 27 ie
|
|
3 dr 8 ca 13 lt 18 su 23 xo 28 iq
|
|
4 sw 9 cc 14 gt 19 mu 24 sh 29 ii
|
|
|
|
The first two characters of each instruction name are sufficient
|
|
to identify the instruction.
|
|
|
|
Nga exposes memory as an array of 32-bit signed integers. Each
|
|
memory location can store four instructions. The assembler
|
|
expects the instructions to be named using their two character
|
|
identifiers. E.g.,
|
|
|
|
'lica.... i
|
|
#100 d
|
|
|
|
# Disassembly
|
|
|
|
I use '..' for 'no(p)' and then construct a string with all of
|
|
these. This will be used to resolve names. The ?? at the end
|
|
will be used for unidentified instructions.
|
|
|
|
~~~
|
|
'..lidudrswpupojucaccreeqneltgtfestadsumudianorxoshzrenieiqii??
|
|
'INST s:const
|
|
~~~
|
|
|
|
Since instructions are packed, I need to unpack them before I
|
|
can run or display the individual instructions. I implement
|
|
`unpack` for this.
|
|
|
|
~~~
|
|
{{
|
|
:mask #255 and ;
|
|
:next #8 shift ;
|
|
---reveal---
|
|
:unpack (n-dcba)
|
|
dup mask swap next
|
|
dup mask swap next
|
|
dup mask swap next
|
|
'abcd 'dcba reorder ;
|
|
}}
|
|
~~~
|
|
|
|
Now it's possible to write words to display instruction
|
|
bundles. The formats are kept simple. For a bundle with
|
|
`lit / lit / add / lit`, this will display either the
|
|
opcodes (`1,1,17,1`) or a string with the abbreviations
|
|
(`liliadli`).
|
|
|
|
~~~
|
|
:name-for (n-cc)
|
|
#30 n:min #2 * &INST + fetch-next swap fetch swap ;
|
|
|
|
:display:bundle-raw (n-)
|
|
unpack '%n,%n,%n,%n s:format s:put ;
|
|
|
|
:display:bundle-named (n-)
|
|
unpack #4 [ name-for c:put c:put ] times ;
|
|
~~~
|
|
|
|
So now I'm ready to write an actual disassembler. I'll
|
|
provide an output formatted like this:
|
|
|
|
(address) 'instructionbundle i
|
|
(address) #value d (possibly_`reference`)
|
|
|
|
If the value corresponds to a word in the `Dictionary`, the
|
|
disassembler will display a message indicating the possible
|
|
name that corresponds to the value.
|
|
|
|
To begin, I'll add a variable to track the number of `li`
|
|
instructions. (These require special handling as they push a
|
|
value in the following cells to the stack).
|
|
|
|
~~~
|
|
'LitCount var
|
|
~~~
|
|
|
|
I then wrap `name-for` with a simple check that increments
|
|
`LitCount` as needed.
|
|
|
|
~~~
|
|
:name-for(counting-li) (n-cc)
|
|
dup #1 eq? [ &LitCount v:inc ] if name-for ;
|
|
~~~
|
|
|
|
To actually display a bundle, I need to decide on what it is. So
|
|
I have a `validate` word to look at each instruction and make
|
|
sure all are actual instructions.
|
|
|
|
~~~
|
|
:valid? (n-f)
|
|
unpack
|
|
[ #0 #29 n:between? ] bi@ and
|
|
[ [ #0 #29 n:between? ] bi@ and ] dip and ;
|
|
~~~
|
|
|
|
With this and the `LitCount`, I can determine how to render
|
|
a bundle.
|
|
|
|
I split out each type (instruction, reference/raw, and data) into
|
|
a separate handler.
|
|
|
|
~~~
|
|
:render-inst (n-)
|
|
$' c:put unpack #4 [ name-for(counting-li) c:put c:put ] times sp $i c:put ;
|
|
|
|
:render-data (n-)
|
|
$# c:put n:to-string s:put sp $d c:put ;
|
|
|
|
:render-ref (n-)
|
|
dup d:lookup-xt n:-zero?
|
|
[ dup render-data
|
|
d:lookup-xt d:name '\t\t(possibly\_`%s`) s:format s:put ]
|
|
[ render-data ] choose ;
|
|
~~~
|
|
|
|
Then I use these and my `valid?` checker to implement a single
|
|
word to render the packed cell in a meaningful manner.
|
|
|
|
~~~
|
|
:render-packed (n-)
|
|
@LitCount n:zero?
|
|
[ dup valid?
|
|
[ render-inst ]
|
|
[ render-ref ] choose ]
|
|
[ render-ref &LitCount v:dec ] choose ;
|
|
~~~
|
|
|
|
And now to tie it all together:
|
|
|
|
~~~
|
|
'TryToIdentifyWords var
|
|
|
|
:disassemble (an-)
|
|
#0 !LitCount
|
|
[
|
|
@TryToIdentifyWords
|
|
[ dup d:lookup-xt n:-zero?
|
|
[ dup d:lookup-xt d:name nl s:put nl ] if ] if
|
|
fetch-next
|
|
over $( c:put n:dec n:put $) c:put sp (address)
|
|
render-packed nl (inst_or_data)
|
|
] times drop ;
|
|
~~~
|
|
|
|
# Execution Trace and Single Stepper
|
|
|
|
Ok, now on to the fun bit: execution trace and single stepping
|
|
through a word.
|
|
|
|
This entails writing an implementation of Nga in RETRO. So to
|
|
start, setup space for the data and address ("return") stacks,
|
|
as well as variables for the stack pointers and instruction
|
|
pointer.
|
|
|
|
~~~
|
|
'DataStack d:create #128 allot
|
|
'ReturnStack d:create #768 allot
|
|
'SP var
|
|
'RP var
|
|
'IP var
|
|
~~~
|
|
|
|
I also set up space for the actual RAM. When single stepping
|
|
or tracing, Autopsy will copy the actual RAM to this before
|
|
proceeding. Note that this is limited to 128K cells.
|
|
|
|
~~~
|
|
FREE #3 / dup 'IMAGE const
|
|
FREE swap - 'IMAGE-SIZE const
|
|
~~~
|
|
|
|
Next, helpers to push values from the real stacks to the
|
|
simulated ones. The stack pointer will point to the next
|
|
available cell, not the actual top element.
|
|
|
|
~~~
|
|
:>s (n-) &DataStack @SP + store &SP v:inc ;
|
|
:s> (-n) &SP v:dec &DataStack @SP + fetch ;
|
|
:>r (n-) &ReturnStack @RP + store &RP v:inc ;
|
|
:r> (-n) &RP v:dec &ReturnStack @RP + fetch ;
|
|
~~~
|
|
|
|
One more helper, `[IP]` will return the value in memory at the
|
|
location `IP` points to.
|
|
|
|
~~~
|
|
:[IP] @IP fetch ;
|
|
~~~
|
|
|
|
Now for the instructions. I have a separate word for each
|
|
instruction and then a jump table of addresses that point to
|
|
these.
|
|
|
|
~~~
|
|
:i:no ;
|
|
:i:li &IP v:inc [IP] >s ;
|
|
:i:du s> dup >s >s ;
|
|
:i:dr s> drop ;
|
|
:i:sw s> s> swap >s >s ;
|
|
:i:pu s> >r ;
|
|
:i:po r> >s ;
|
|
:i:ju s> n:dec !IP ;
|
|
:i:ca @IP >r i:ju ;
|
|
:i:cc s> s> [ >s i:ca ] [ drop ] choose ;
|
|
:i:re r> !IP ;
|
|
:i:eq s> s> eq? >s ;
|
|
:i:ne s> s> -eq? >s ;
|
|
:i:lt s> s> swap lt? >s ;
|
|
:i:gt s> s> swap gt? >s ;
|
|
:i:fe s> #-1 [ @SP >s ] case
|
|
#-2 [ @RP >s ] case
|
|
#-3 [ IMAGE-SIZE >s ] case
|
|
IMAGE + fetch >s ;
|
|
:i:st s> s> swap IMAGE + store ;
|
|
:i:ad s> s> + >s ;
|
|
:i:su s> s> swap - >s ;
|
|
:i:mu s> s> * >s ;
|
|
:i:di s> s> swap /mod swap >s >s ;
|
|
:i:an s> s> and >s ;
|
|
:i:or s> s> or >s ;
|
|
:i:xo s> s> xor >s ;
|
|
:i:sh s> s> swap shift >s ;
|
|
:i:zr s> dup n:zero? [ drop i:re ] [ >s ] choose ;
|
|
:i:en #0 !RP ;
|
|
:i:ie #1 >s ;
|
|
:i:iq #0 dup >s >s ;
|
|
:i:ii s> s> nip c:put ;
|
|
~~~
|
|
|
|
With the instructions defined, populate the jump table. The
|
|
order is crucial as the opcode number will be the index into
|
|
this table.
|
|
|
|
~~~
|
|
'Instructions d:create
|
|
&i:no , &i:li , &i:du , &i:dr , &i:sw , &i:pu ,
|
|
&i:po , &i:ju , &i:ca , &i:cc , &i:re , &i:eq ,
|
|
&i:ne , &i:lt , &i:gt , &i:fe , &i:st , &i:ad ,
|
|
&i:su , &i:mu , &i:di , &i:an , &i:or , &i:xo ,
|
|
&i:sh , &i:zr , &i:en , &i:ie , &i:iq , &i:ii ,
|
|
~~~
|
|
|
|
With the populated table of instructions, implementing a
|
|
`process-single-opcode` is easy. This will check the
|
|
instruction to make sure it's valid, then call the
|
|
corresponding handler in the instruction table. If not
|
|
valid, this will report an error.
|
|
|
|
~~~
|
|
:process-single-opcode (n-)
|
|
dup #0 #29 n:between?
|
|
[ &Instructions + fetch call ]
|
|
[ 'Invalid_Instruction:_%n_! s:format s:put nl ] choose ;
|
|
~~~
|
|
|
|
Next is to unpack an instruction bundle and process each
|
|
instruction.
|
|
|
|
~~~
|
|
:process-packed-opcode (n-)
|
|
unpack
|
|
process-single-opcode
|
|
process-single-opcode
|
|
process-single-opcode
|
|
process-single-opcode ;
|
|
~~~
|
|
|
|
So the guts of the Nga-in-RETRO are done at this point. Next
|
|
is a method of stepping through execution of a word.
|
|
|
|
This will display output indicating state. It'll provide:
|
|
|
|
- current memory location
|
|
- values in instruction bundle
|
|
- stack depths
|
|
- data stack before execution
|
|
- data stack after exection
|
|
|
|
E.g.,
|
|
|
|
IP:13966 SP:3 RP:1
|
|
[19,0,0,0] - mu......
|
|
Stack: 9 3 3 -> 9 9
|
|
|
|
So helpers for displaying things:
|
|
|
|
~~~
|
|
:display-data-stack
|
|
'DS:_ s:put
|
|
#0 @SP [ &DataStack over + fetch n:put sp n:inc ] times drop ;
|
|
|
|
:display-return-stack
|
|
'RS:_ s:put
|
|
#0 @RP [ &ReturnStack over + fetch n:put sp n:inc ] times drop ;
|
|
~~~
|
|
|
|
And then using the display helpers and instruction processor, a
|
|
single stepper. (This also updates a `Steps` counter)
|
|
|
|
~~~
|
|
'Steps var
|
|
|
|
:take-step
|
|
[IP] process-packed-opcode &IP v:inc
|
|
&Steps v:inc ;
|
|
|
|
{{
|
|
:sep #63 [ $- c:put ] times nl ;
|
|
:named-word? @IP d:lookup-xt n:-zero? ;
|
|
:show-name 0; drop @IP d:lookup-xt d:name s:put nl ;
|
|
:instruction [IP] [ unpack ] sip ;
|
|
:names #4 [ name-for(counting-li) c:put c:put ] times ;
|
|
:format '%n,%n,%n,%n s:format ;
|
|
:pad s:length #16 swap - #0 n:max [ sp ] times ;
|
|
:opcodes format dup s:put pad ;
|
|
:pad dup n:to-string s:length #6 swap - #0 n:max [ sp ] times ;
|
|
:header 'IP: s:put @IP pad n:put
|
|
@RP @SP '\tSP:%n_RP:%n\t s:format s:put ;
|
|
---reveal---
|
|
:details instruction drop names tab instruction [ opcodes ] dip sp n:put nl ;
|
|
:stacks display-data-stack nl display-return-stack nl ;
|
|
:step sep named-word? show-name header details take-step stacks ;
|
|
}}
|
|
~~~
|
|
|
|
And then wrap it with `times` to run multiple steps.
|
|
|
|
~~~
|
|
:steps (n-)
|
|
&step times ;
|
|
~~~
|
|
|
|
Then on to the tracer. This will `step` through execution until
|
|
the word returns. I use a similar approach to how I handle this
|
|
in the interface layers for RETRO (word execution ends when the
|
|
address stack depth reaches zero).
|
|
|
|
The `trace` will empty the step counter and display the number
|
|
of steps used.
|
|
|
|
~~~
|
|
:copy-image
|
|
#0 IMAGE IMAGE-SIZE here n:min copy ;
|
|
~~~
|
|
|
|
~~~
|
|
:trace (a-)
|
|
copy-image
|
|
#0 !Steps #0 !SP
|
|
!IP #0 >r
|
|
[ step @RP n:zero? @IP n:negative? or ] until
|
|
nl @Steps '%n_steps_taken\n s:format s:put ;
|
|
~~~
|
|
|
|
# Tests
|
|
|
|
```
|
|
:test
|
|
as{ 'liliaddu i #22 d #33 d }as
|
|
#3 #4 gt? [ #1 ] if ;
|
|
|
|
#0 #100 disassemble
|
|
nl '-------------------------- s:put nl
|
|
&TryToIdentifyWords v:on
|
|
#0 #100 disassemble
|
|
```
|
|
|