mirror of
https://github.com/hsoft/collapseos.git
synced 2024-11-17 05:08:05 +11:00
zasm: big local symbols overhaul
This commit is contained in:
parent
e9244b80ee
commit
0ae91e55ec
@ -46,3 +46,6 @@ ioSeek:
|
|||||||
ld ix, (IO_IN_SEEK)
|
ld ix, (IO_IN_SEEK)
|
||||||
jp (ix)
|
jp (ix)
|
||||||
|
|
||||||
|
ioTell:
|
||||||
|
ld ix, (IO_IN_TELL)
|
||||||
|
jp (ix)
|
||||||
|
@ -39,7 +39,16 @@
|
|||||||
.equ ZASM_FIRST_PASS RAMSTART
|
.equ ZASM_FIRST_PASS RAMSTART
|
||||||
; The offset where we currently are with regards to outputting opcodes
|
; The offset where we currently are with regards to outputting opcodes
|
||||||
.equ ZASM_PC ZASM_FIRST_PASS+1
|
.equ ZASM_PC ZASM_FIRST_PASS+1
|
||||||
.equ ZASM_RAMEND ZASM_PC+2
|
; whether we're in "local pass", that is, in local label scanning mode. During
|
||||||
|
; this special pass, ZASM_FIRST_PASS will also be set so that the rest of the
|
||||||
|
; code behaves as is we were in the first pass.
|
||||||
|
.equ ZASM_LOCAL_PASS ZASM_PC+2
|
||||||
|
; I/O position (in terms of ioSeek/ioTell) of the current context. Used to
|
||||||
|
; rewind to it after having parsed local labels.
|
||||||
|
.equ ZASM_CTX_POS ZASM_LOCAL_PASS+1
|
||||||
|
; What ZASM_PC was when we started our context
|
||||||
|
.equ ZASM_CTX_PC ZASM_CTX_POS+2
|
||||||
|
.equ ZASM_RAMEND ZASM_CTX_PC+2
|
||||||
|
|
||||||
; *** Code ***
|
; *** Code ***
|
||||||
jp zasmMain
|
jp zasmMain
|
||||||
@ -66,7 +75,10 @@ zasmMain:
|
|||||||
ld a, l
|
ld a, l
|
||||||
ld de, IO_OUT_GETC
|
ld de, IO_OUT_GETC
|
||||||
call JUMP_BLKSEL
|
call JUMP_BLKSEL
|
||||||
|
|
||||||
; Init modules
|
; Init modules
|
||||||
|
xor a
|
||||||
|
ld (ZASM_LOCAL_PASS), a
|
||||||
call ioInit
|
call ioInit
|
||||||
call symInit
|
call symInit
|
||||||
|
|
||||||
@ -89,6 +101,12 @@ zasmIsFirstPass:
|
|||||||
cp 1
|
cp 1
|
||||||
ret
|
ret
|
||||||
|
|
||||||
|
; Sets Z according to whether we're in local pass.
|
||||||
|
zasmIsLocalPass:
|
||||||
|
ld a, (ZASM_LOCAL_PASS)
|
||||||
|
cp 1
|
||||||
|
ret
|
||||||
|
|
||||||
; Increase (ZASM_PC) by A
|
; Increase (ZASM_PC) by A
|
||||||
incOutputOffset:
|
incOutputOffset:
|
||||||
push de
|
push de
|
||||||
@ -105,13 +123,21 @@ zasmParseFile:
|
|||||||
ld de, 0
|
ld de, 0
|
||||||
ld (ZASM_PC), de
|
ld (ZASM_PC), de
|
||||||
.loop:
|
.loop:
|
||||||
inc de
|
|
||||||
call parseLine
|
call parseLine
|
||||||
ret nz ; error
|
ret nz ; error
|
||||||
ld a, b ; TOK_*
|
ld a, b ; TOK_*
|
||||||
cp TOK_EOF
|
cp TOK_EOF
|
||||||
ret z ; if EOF, return now with success
|
jr z, .eof
|
||||||
jr .loop
|
jr .loop
|
||||||
|
.eof:
|
||||||
|
call zasmIsLocalPass
|
||||||
|
jr nz, .end ; EOF and not local pass
|
||||||
|
; we're in local pass and EOF. Unwind this
|
||||||
|
call _endLocalPass
|
||||||
|
jr .loop
|
||||||
|
.end:
|
||||||
|
cp a ; ensure Z
|
||||||
|
ret
|
||||||
|
|
||||||
; Parse next token and accompanying args (when relevant) in I/O, write the
|
; Parse next token and accompanying args (when relevant) in I/O, write the
|
||||||
; resulting opcode(s) through ioPutC and increases (ZASM_PC) by the number of
|
; resulting opcode(s) through ioPutC and increases (ZASM_PC) by the number of
|
||||||
@ -178,18 +204,32 @@ _parseDirec:
|
|||||||
_parseLabel:
|
_parseLabel:
|
||||||
; The string in (scratchpad) is a label with its trailing ':' removed.
|
; The string in (scratchpad) is a label with its trailing ':' removed.
|
||||||
ld hl, scratchpad
|
ld hl, scratchpad
|
||||||
|
|
||||||
|
call zasmIsLocalPass
|
||||||
|
jr z, .processLocalPass
|
||||||
|
|
||||||
|
; Is this a local label? If yes, we don't process it in the context of
|
||||||
|
; parseLine, whether it's first or second pass. Local labels are only
|
||||||
|
; parsed during the Local Pass
|
||||||
|
call symIsLabelLocal
|
||||||
|
jr z, .success ; local? don't do anything.
|
||||||
|
|
||||||
call zasmIsFirstPass
|
call zasmIsFirstPass
|
||||||
jr z, .registerLabel ; When we encounter a label in the first
|
jr z, .registerLabel ; When we encounter a label in the first
|
||||||
; pass, we register it in the symbol
|
; pass, we register it in the symbol
|
||||||
; list
|
; list
|
||||||
; When we're not in the first pass, we set the context (if label is not
|
; At this point, we're in second pass, we've encountered a global label
|
||||||
; local) to that label.
|
; and we'll soon continue processing our file. However, before we do
|
||||||
|
; that, we should process our local labels.
|
||||||
|
call _beginLocalPass
|
||||||
|
jr .success
|
||||||
|
.processLocalPass:
|
||||||
call symIsLabelLocal
|
call symIsLabelLocal
|
||||||
jr z, .success ; local? don't set context
|
jr z, .registerLabel ; local label? all good, register it
|
||||||
call symSetContext
|
; normally
|
||||||
jr z, .success
|
; not a local label? Then we need to end local pass
|
||||||
; NZ? this means that (HL) couldn't be found in symbol list. Weird
|
call _endLocalPass
|
||||||
jr .error
|
jr .success
|
||||||
.registerLabel:
|
.registerLabel:
|
||||||
ld de, (ZASM_PC)
|
ld de, (ZASM_PC)
|
||||||
call symRegister
|
call symRegister
|
||||||
@ -201,3 +241,38 @@ _parseLabel:
|
|||||||
.error:
|
.error:
|
||||||
call JUMP_UNSETZ
|
call JUMP_UNSETZ
|
||||||
ret
|
ret
|
||||||
|
|
||||||
|
_beginLocalPass:
|
||||||
|
; remember were I/O was
|
||||||
|
call ioTell
|
||||||
|
ld (ZASM_CTX_POS), hl
|
||||||
|
; Remember where PC was
|
||||||
|
ld hl, (ZASM_PC)
|
||||||
|
ld (ZASM_CTX_PC), hl
|
||||||
|
; Fake first pass
|
||||||
|
ld a, 1
|
||||||
|
ld (ZASM_FIRST_PASS), a
|
||||||
|
; Set local pass
|
||||||
|
ld (ZASM_LOCAL_PASS), a
|
||||||
|
; Empty local label registry
|
||||||
|
xor a
|
||||||
|
ld (SYM_LOC_NAMES), a
|
||||||
|
call symSelectLocalRegistry
|
||||||
|
ret
|
||||||
|
|
||||||
|
|
||||||
|
_endLocalPass:
|
||||||
|
call symSelectGlobalRegistry
|
||||||
|
; recall I/O pos
|
||||||
|
ld hl, (ZASM_CTX_POS)
|
||||||
|
call ioSeek
|
||||||
|
; recall PC
|
||||||
|
ld hl, (ZASM_CTX_PC)
|
||||||
|
ld (ZASM_PC), hl
|
||||||
|
; unfake first pass
|
||||||
|
xor a
|
||||||
|
ld (ZASM_FIRST_PASS), a
|
||||||
|
; Unset local pass
|
||||||
|
ld (ZASM_LOCAL_PASS), a
|
||||||
|
cp a ; ensure Z
|
||||||
|
ret
|
||||||
|
@ -162,6 +162,7 @@ parseNumberOrSymbol:
|
|||||||
ret z ; first pass? we don't care about the value,
|
ret z ; first pass? we don't care about the value,
|
||||||
; return success.
|
; return success.
|
||||||
; Not a number. Try symbol
|
; Not a number. Try symbol
|
||||||
|
call symSelect
|
||||||
call symFind
|
call symFind
|
||||||
ret nz ; not found
|
ret nz ; not found
|
||||||
; Found! index in A, let's fetch value
|
; Found! index in A, let's fetch value
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
; Manages both constants and labels within a same namespace and registry.
|
; Manages both constants and labels within a same namespace and registry.
|
||||||
;
|
;
|
||||||
; About local labels: They are treated as regular labels except they start with
|
; Local Labels
|
||||||
; a dot (example: ".foo"). Because labels are registered in order and because
|
;
|
||||||
; constants are registered in the second pass, they end up at the end of the
|
; Local labels during the "official" first pass are ignored. To register them
|
||||||
; symbol list and don't mix with labels. Therefore, we easily iterate through
|
; in the global registry during that pass would be wasteful in terms of memory.
|
||||||
; local labels of a context by starting from that context's index and iterating
|
;
|
||||||
; as long as symbol name start with a '.'
|
; What we don instead is set up a separate register for them and have a "second
|
||||||
|
; first pass" whenever we encounter a new context. That is, we wipe the local
|
||||||
|
; registry, parse the code until the next global symbol (or EOF), then rewind
|
||||||
|
; and continue second pass as usual.
|
||||||
|
|
||||||
; *** Constants ***
|
; *** Constants ***
|
||||||
; Duplicate symbol in registry
|
; Duplicate symbol in registry
|
||||||
@ -20,6 +23,9 @@
|
|||||||
; length for a single symbol, just a maximum size for the whole pool.
|
; length for a single symbol, just a maximum size for the whole pool.
|
||||||
.equ SYM_BUFSIZE 0x1000
|
.equ SYM_BUFSIZE 0x1000
|
||||||
|
|
||||||
|
; Size of the names buffer for the local context registry
|
||||||
|
.equ SYM_LOC_BUFSIZE 0x200
|
||||||
|
|
||||||
; *** Variables ***
|
; *** Variables ***
|
||||||
; Each symbol is mapped to a word value saved here.
|
; Each symbol is mapped to a word value saved here.
|
||||||
.equ SYM_VALUES SYM_RAMSTART
|
.equ SYM_VALUES SYM_RAMSTART
|
||||||
@ -29,15 +35,12 @@
|
|||||||
; index of the name, then go get the value at that index in SYM_VALUES.
|
; index of the name, then go get the value at that index in SYM_VALUES.
|
||||||
.equ SYM_NAMES SYM_VALUES+(SYM_MAXCOUNT*2)
|
.equ SYM_NAMES SYM_VALUES+(SYM_MAXCOUNT*2)
|
||||||
|
|
||||||
; Index of the symbol found during the last symSetContext call
|
; Registry for local labels. Wiped out after each context change.
|
||||||
.equ SYM_CONTEXT_IDX SYM_NAMES+SYM_BUFSIZE
|
.equ SYM_LOC_VALUES SYM_NAMES+SYM_BUFSIZE
|
||||||
|
.equ SYM_LOC_NAMES SYM_LOC_VALUES+(SYM_MAXCOUNT*2)
|
||||||
; Pointer, in the SYM_NAMES buffer, of the string found during the last
|
|
||||||
; symSetContext call
|
|
||||||
.equ SYM_CONTEXT_PTR SYM_CONTEXT_IDX+1
|
|
||||||
|
|
||||||
; Pointer to the currently selected registry
|
; Pointer to the currently selected registry
|
||||||
.equ SYM_CTX_NAMES SYM_CONTEXT_PTR+2
|
.equ SYM_CTX_NAMES SYM_LOC_NAMES+SYM_LOC_BUFSIZE
|
||||||
.equ SYM_CTX_NAMESEND SYM_CTX_NAMES+2
|
.equ SYM_CTX_NAMESEND SYM_CTX_NAMES+2
|
||||||
.equ SYM_CTX_VALUES SYM_CTX_NAMESEND+2
|
.equ SYM_CTX_VALUES SYM_CTX_NAMESEND+2
|
||||||
|
|
||||||
@ -67,15 +70,35 @@ _symNext:
|
|||||||
symInit:
|
symInit:
|
||||||
xor a
|
xor a
|
||||||
ld (SYM_NAMES), a
|
ld (SYM_NAMES), a
|
||||||
ld (SYM_CONTEXT_IDX), a
|
ld (SYM_LOC_NAMES), a
|
||||||
ld hl, SYM_CONTEXT_PTR
|
; Continue to symSelectGlobalRegistry
|
||||||
ld (SYM_CONTEXT_PTR), hl
|
|
||||||
|
symSelectGlobalRegistry:
|
||||||
|
push af
|
||||||
|
push hl
|
||||||
ld hl, SYM_NAMES
|
ld hl, SYM_NAMES
|
||||||
ld (SYM_CTX_NAMES), hl
|
ld (SYM_CTX_NAMES), hl
|
||||||
ld hl, SYM_NAMES+SYM_BUFSIZE
|
ld hl, SYM_NAMES+SYM_BUFSIZE
|
||||||
ld (SYM_CTX_NAMESEND), hl
|
ld (SYM_CTX_NAMESEND), hl
|
||||||
ld hl, SYM_VALUES
|
ld hl, SYM_VALUES
|
||||||
ld (SYM_CTX_VALUES), hl
|
ld (SYM_CTX_VALUES), hl
|
||||||
|
pop hl
|
||||||
|
pop af
|
||||||
|
ret
|
||||||
|
|
||||||
|
symSelectLocalRegistry:
|
||||||
|
push af
|
||||||
|
push hl
|
||||||
|
ld hl, SYM_LOC_NAMES
|
||||||
|
ld (SYM_CTX_NAMES), hl
|
||||||
|
ld hl, SYM_LOC_NAMES+SYM_LOC_BUFSIZE
|
||||||
|
ld (SYM_CTX_NAMESEND), hl
|
||||||
|
ld hl, SYM_LOC_VALUES
|
||||||
|
ld (SYM_CTX_VALUES), hl
|
||||||
|
ld a, h
|
||||||
|
ld a, l
|
||||||
|
pop hl
|
||||||
|
pop af
|
||||||
ret
|
ret
|
||||||
|
|
||||||
; Sets Z according to whether label in (HL) is local (starts with a dot)
|
; Sets Z according to whether label in (HL) is local (starts with a dot)
|
||||||
@ -153,6 +176,11 @@ symRegister:
|
|||||||
ld b, 0
|
ld b, 0
|
||||||
ldir ; copy C chars from HL to DE
|
ldir ; copy C chars from HL to DE
|
||||||
|
|
||||||
|
; We need to add a second null char to indicate the end of the name
|
||||||
|
; list. DE is already correctly placed.
|
||||||
|
xor a
|
||||||
|
ld (de), a
|
||||||
|
|
||||||
; I'd say we're pretty good just about now. What we need to do is to
|
; I'd say we're pretty good just about now. What we need to do is to
|
||||||
; save the value in our original DE that is just on top of the stack
|
; save the value in our original DE that is just on top of the stack
|
||||||
; into the proper index in (SYM_CTX_VALUES). Our index, remember, is
|
; into the proper index in (SYM_CTX_VALUES). Our index, remember, is
|
||||||
@ -176,16 +204,16 @@ symRegister:
|
|||||||
pop hl
|
pop hl
|
||||||
ret
|
ret
|
||||||
|
|
||||||
|
; Select global or local registry according to label name in (HL)
|
||||||
|
symSelect:
|
||||||
|
call symIsLabelLocal
|
||||||
|
jp z, symSelectLocalRegistry
|
||||||
|
jp symSelectGlobalRegistry
|
||||||
|
|
||||||
; Find name (HL) in (SYM_CTX_NAMES) and returns matching index in A.
|
; Find name (HL) in (SYM_CTX_NAMES) and returns matching index in A.
|
||||||
; If we find something, Z is set, otherwise unset.
|
; If we find something, Z is set, otherwise unset.
|
||||||
symFind:
|
symFind:
|
||||||
push hl
|
push hl
|
||||||
call _symFind
|
|
||||||
pop hl
|
|
||||||
ret
|
|
||||||
|
|
||||||
; Same as symFind, but leaks HL
|
|
||||||
_symFind:
|
|
||||||
push bc
|
push bc
|
||||||
push de
|
push de
|
||||||
|
|
||||||
@ -193,21 +221,10 @@ _symFind:
|
|||||||
call strlen
|
call strlen
|
||||||
ld c, a ; let's save that
|
ld c, a ; let's save that
|
||||||
|
|
||||||
call symIsLabelLocal ; save Z for after the 3 next lines, which
|
|
||||||
; doesn't touch flags. We need to call this now
|
|
||||||
; before we lose HL.
|
|
||||||
ex hl, de ; it's easier if HL is haystack and DE is
|
ex hl, de ; it's easier if HL is haystack and DE is
|
||||||
; needle.
|
; needle.
|
||||||
ld b, 0
|
ld b, 0
|
||||||
ld hl, (SYM_CTX_NAMES)
|
ld hl, (SYM_CTX_NAMES)
|
||||||
jr nz, .loop ; not local? jump right to loop
|
|
||||||
; local? then we need to adjust B and HL
|
|
||||||
ld hl, (SYM_CONTEXT_PTR)
|
|
||||||
ld a, (SYM_CONTEXT_IDX)
|
|
||||||
ld b, a
|
|
||||||
xor a
|
|
||||||
sub b
|
|
||||||
ld b, a
|
|
||||||
.loop:
|
.loop:
|
||||||
ld a, c ; recall strlen
|
ld a, c ; recall strlen
|
||||||
call JUMP_STRNCMP
|
call JUMP_STRNCMP
|
||||||
@ -218,6 +235,7 @@ _symFind:
|
|||||||
djnz .loop
|
djnz .loop
|
||||||
; exhausted djnz? no match
|
; exhausted djnz? no match
|
||||||
.nomatch:
|
.nomatch:
|
||||||
|
out (99), a
|
||||||
call JUMP_UNSETZ
|
call JUMP_UNSETZ
|
||||||
jr .end
|
jr .end
|
||||||
.match:
|
.match:
|
||||||
@ -228,13 +246,14 @@ _symFind:
|
|||||||
.end:
|
.end:
|
||||||
pop de
|
pop de
|
||||||
pop bc
|
pop bc
|
||||||
|
pop hl
|
||||||
ret
|
ret
|
||||||
|
|
||||||
; Return value associated with symbol index A into DE
|
; Return value associated with symbol index A into DE
|
||||||
symGetVal:
|
symGetVal:
|
||||||
; our index is in A. Let's fetch the proper value
|
; our index is in A. Let's fetch the proper value
|
||||||
push hl
|
push hl
|
||||||
ld hl, SYM_VALUES
|
ld hl, (SYM_CTX_VALUES)
|
||||||
call JUMP_ADDHL
|
call JUMP_ADDHL
|
||||||
call JUMP_ADDHL ; twice because our values are words
|
call JUMP_ADDHL ; twice because our values are words
|
||||||
ld e, (hl)
|
ld e, (hl)
|
||||||
@ -242,18 +261,3 @@ symGetVal:
|
|||||||
ld d, (hl)
|
ld d, (hl)
|
||||||
pop hl
|
pop hl
|
||||||
ret
|
ret
|
||||||
|
|
||||||
; Find symbol name (HL) in the symbol list and set SYM_CONTEXT_* accordingly.
|
|
||||||
; When symFind will be called with a symbol name starting with a '.', the search
|
|
||||||
; will begin at that context instead of the beginning of the register.
|
|
||||||
; Sets Z if symbol is found, unsets it if not.
|
|
||||||
symSetContext:
|
|
||||||
push hl
|
|
||||||
call _symFind
|
|
||||||
jr nz, .end ; Z already unset
|
|
||||||
ld (SYM_CONTEXT_IDX), a
|
|
||||||
ld (SYM_CONTEXT_PTR), hl
|
|
||||||
; Z already set
|
|
||||||
.end:
|
|
||||||
pop hl
|
|
||||||
ret
|
|
||||||
|
@ -41,7 +41,7 @@ static uint8_t mem[0x10000];
|
|||||||
static uint8_t inpt[STDIN_BUFSIZE];
|
static uint8_t inpt[STDIN_BUFSIZE];
|
||||||
static int inpt_size;
|
static int inpt_size;
|
||||||
static int inpt_ptr;
|
static int inpt_ptr;
|
||||||
static uint8_t received_first_seek_byte = 0;
|
static uint8_t middle_of_seek_tell = 0;
|
||||||
|
|
||||||
static uint8_t io_read(int unused, uint16_t addr)
|
static uint8_t io_read(int unused, uint16_t addr)
|
||||||
{
|
{
|
||||||
@ -52,6 +52,17 @@ static uint8_t io_read(int unused, uint16_t addr)
|
|||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
} else if (addr == STDIN_SEEK) {
|
||||||
|
if (middle_of_seek_tell) {
|
||||||
|
middle_of_seek_tell = 0;
|
||||||
|
return inpt_ptr & 0xff;
|
||||||
|
} else {
|
||||||
|
#ifdef DEBUG
|
||||||
|
fprintf(stderr, "tell %d\n", inpt_ptr);
|
||||||
|
#endif
|
||||||
|
middle_of_seek_tell = 1;
|
||||||
|
return inpt_ptr >> 8;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "Out of bounds I/O read: %d\n", addr);
|
fprintf(stderr, "Out of bounds I/O read: %d\n", addr);
|
||||||
return 0;
|
return 0;
|
||||||
@ -67,12 +78,15 @@ static void io_write(int unused, uint16_t addr, uint8_t val)
|
|||||||
putchar(val);
|
putchar(val);
|
||||||
#endif
|
#endif
|
||||||
} else if (addr == STDIN_SEEK) {
|
} else if (addr == STDIN_SEEK) {
|
||||||
if (received_first_seek_byte) {
|
if (middle_of_seek_tell) {
|
||||||
inpt_ptr |= val;
|
inpt_ptr |= val;
|
||||||
received_first_seek_byte = 0;
|
middle_of_seek_tell = 0;
|
||||||
|
#ifdef DEBUG
|
||||||
|
fprintf(stderr, "seek %d\n", inpt_ptr);
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
inpt_ptr = (val << 8) & 0xff00;
|
inpt_ptr = (val << 8) & 0xff00;
|
||||||
received_first_seek_byte = 1;
|
middle_of_seek_tell = 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "Out of bounds I/O write: %d / %d\n", addr, val);
|
fprintf(stderr, "Out of bounds I/O write: %d / %d\n", addr, val);
|
||||||
|
@ -51,10 +51,18 @@ emulSeek:
|
|||||||
out (STDIN_SEEK), a
|
out (STDIN_SEEK), a
|
||||||
ret
|
ret
|
||||||
|
|
||||||
|
emulTell:
|
||||||
|
; same principle as STDIN_SEEK
|
||||||
|
in a, (STDIN_SEEK)
|
||||||
|
ld h, a
|
||||||
|
in a, (STDIN_SEEK)
|
||||||
|
ld l, a
|
||||||
|
ret
|
||||||
|
|
||||||
#include "core.asm"
|
#include "core.asm"
|
||||||
.equ BLOCKDEV_RAMSTART RAMSTART
|
.equ BLOCKDEV_RAMSTART RAMSTART
|
||||||
.equ BLOCKDEV_COUNT 2
|
.equ BLOCKDEV_COUNT 2
|
||||||
#include "blockdev.asm"
|
#include "blockdev.asm"
|
||||||
; List of devices
|
; List of devices
|
||||||
.dw emulGetC, 0, emulSeek, 0
|
.dw emulGetC, 0, emulSeek, emulTell
|
||||||
.dw 0, emulPutC, 0, 0
|
.dw 0, emulPutC, 0, 0
|
||||||
|
Loading…
Reference in New Issue
Block a user