------------------------------------- Z80 Return-oriented Virtual Machine (c) 2016 - 2017 kio@little-bat.de ------------------------------------- Z80 Registers: AF = BC = VM vstack (top down) DE = TOP: top vstack value on vstack HL = AUX: top.hi or 2nd vstack value if possible IX = IY = AF' = BC' = VM rstack (bottom up) DE' = HL' = Storage types handled by the VM: immediate value local variable on vstack global variable in array in struct Data types as seen by the VM: VOID void value in register DE (TOP) may be void BYTE 8 when writing: byte u8 when reading: unsigned byte s8 when reading: signed byte when reading, bytes are sign-extended to word-size local byte variables on vstack actually have word-size but the high byte of s8 variables may be void! when writing to local s8 variables the high byte IS NOT UPDATED! WORD standard data type, min. stack size reading or writing: word u unsigned word: functions may distinguish between signed and unsigned s signed word: functions may distinguish between signed and unsigned pointers: var¢ pointer to handle to struct data b[]¢ pointer to handle to byte array data w[]¢ word array l[]¢ long array p8 pointer to raw byte p16 pointer to raw word p32 pointer to raw long LONG (4 bytes) 32 reading or writing u32 unsigned: functions may distinguish between signed and unsigned s32 signed: functions may distinguish between signed and unsigned f32 float Stacks: Values on stacks are always 2 bytes. vstack: This is a top-down stack. This allows use for the real machine return stack in mcode. All data on the variables stack is 2*N bytes in size. Pushing and popping is expensive! TOP: DE may contain the top value of the vstack or may be void to reduce pushing and popping on the vstack. AUX: HL may contain the 2nd value or the high word of a long value of the vstack. rstack: This is a bottom-up stack. B'C' points behind the last entry. The vstack and rstack typically share a common memory block. All data on the return stack is 2*N bytes in size. Pushing and popping is expensive! Opcodes: All opcodes are addresses (2 bytes). If they have immediate arguments, these may be bytes (smaller) or words (faster). Fetching the next opcode (with 'ret') and fetching immediate data (with 'pop hl') is fast. To reduce expensive work with the vstack, typical opcode sequences with a balanced vstack should be combined into combi opcodes. Interrupts: Interrupts are permanently disabled. Interrupts are polled (K1-Bus makes polling easy) in flow control opcodes. The machine stack cannot be used to push temp values – this would overwrite program code. Machine code can be executed if the SP is saved and loaded with the vstack pointer. There are macros to save the SP (the VM's PC) and to switch between SP and BC (the VM's SP). Byte Variables: For simplicity, bytes are immediately extended to word size (signed or unsigned) when read. -> local byte variables (on the vstack) are actually 2 bytes. -> the high byte of a signed byte local variable is invalid. (be careful with DUP or OVER) Procedures: Are called like VM opcodes: the procedure is entered in native Z80 code! if the proc has arguments then DE = TOP else DE = void. macro p_enter pushes the VM PC on the VM rstack and switches to VM code. the procedure must return with RETURN (or variant). foo ( … n -- n ) in: DE = TOP, out: DE = TOP foo ( -- n ) in: DE = void, out: DE = TOP foo ( … n -- ) in: DE = TOP, out: DE = void foo ( -- ) in: DE = void, out: DE = void Runtime optimization: opcode ( -- n ) Opcodes ohne Argumente sollte es in einer Version mit und ohne vorangehendem PUSH_DE geben. idR. kann man dem Opcode 'ohne' einfach den Code für PUSH_DE voranstellen. (6 Bytes) Außerdem in einer Version, die HL als SOT (2nd on top) benutzt. Betroffene Opcodes: LVAR, GVAR, IVAL und Varianten PUSH_LVAR ( -- n ) ; push DE, then get LVAR in DE LVAR ( void -- n ) ; just overwrite DE with LVAR LVAR_hlde ( de:n1 -- hl:n1 de:n2 ) ; move TOP into SOT and load LVAR into TOP opcode ( n -- n ) sind super. Kein Stackzugriff nötig. Keine Varianten nötig. Sie sind das Ziel der Stackoptimierung! opcode ( n1 n2 -- n ) sollten in einer Version mit n1 auf dem Stack und einer mit n1 in HL vorliegen. soweit sinnvoll mit n2 als inline-Argument oder mit implizitem n2. Betroffene Opcodes: Operatoren etc. ADD ( n1 n2 -- n ) ; n1 auf dem Stack ADD_hlde ( hl:n1 de:n2 -- n ) ; n1 in HL ADDI ( n1 $w -- n ) ; n2 = inline argument ADD2 ( n1 -- n ) ; n2 = implizit opcode ( * -- void ) Funktionen ohne rval hinterlassen DE void. Betroffene Opcodes: Assignment operators call procedure: muss konsistent sein für den Aufruf von Prozeduren (und Opcodes!) über ProcPointer. Prozeduren und Opcodes sollen sich aufruftechnisch nicht unterscheiden. CALLPROCPTR unterscheidet bisher nicht nach Anzahl Argumente. Man sollte aber ein weiteres CALLPROCPTR, für Prozeduren ohne Argumente einführen. Prozeduren mit Argumenten werden mit DE = TOP aufgerufen, weil die Opcodes mit der Signatur "( n -- n )" so aufgerufen werden. Prozeduren ohne Argumente werden mit TOP = void aufgerufen.