Position Independent Code (6502) on the Commodore VIC-20

If we are writing 6502 machine code and want to to create a routine or program that can be placed in any location then we have to create Position Independent Code (PIC) or make the code relocatable. Here I'm going to show how to make our code Position Independent. There are three full examples near the end of the article which show how to put this altogether.

Branches Instead of Jumps

One of the first things we can do is use branches instead of JMP to an absolute address where possible. This is because branches jump to a relative address that will still be the same if the code moves. A branch allows a relative jump to an address -127/+128 bytes from the current address.

To do something similar to a BRA (BRanch Always) we just need to force the condition before using one of the Branch instructions. An easy one to use is BVC (Branch on oVerflow Clear) as the oVerflow flag is only altered by the ADC, BIT, CLV and SBC instructions which makes it easy to keep track of.

The following absolute jump:

            jmp label          ; 3 bytes, 3 cycles
            ...
            ...
label       nop

can easily be turned into a relative jump:

            clv                ; 1 byte,  2 cycles
            bvc label          ; 2 bytes, 3-4 cycles (depending on page cross)
            ...
            ...
label       nop

Both jumps take 3 bytes of code, but we can see that the relative jump takes 2-3 more cycles to complete and therefore this is something to keep an eye on. If you are sure that the oVerflow flag is clear then you can omit the CLV and actually save 1 byte and only take 0-1 more cycles to complete.

Indirect Jumps for Long Jumps

If the location we want to jump to is going to be further than -127/+128 bytes away then we can put the location we want to jump to in fixed memory address and then jump to that location indirectly. We may want to do this using a data table as described further down the article.

To compare, here is an absolute JMP

            jmp labelA                ; 3 bytes, 3 cycles

Here is an indirect JMP using a data table with ILABELA as an index. We can see that there is a 2 cycle overhead per JMP.

            jmp (DATA_TABLE+ILABELA)  ; 3 bytes, 5 cycles

A Data Table to Access Memory Indirectly

To access memory locations that would move with the code we can create a data table in a static location that contains pointers to each location in the code. This would be accessed via an index using indirect addressing.

Here is a 'Hello, World!' routine that uses a static location for helloMsg which we will convert in the second piece of code to become Position Independent by using a data table.

CCHROUT    = $FFD2             ; Output character to current output device

helloMsg   .asc "HELLO, WORLD!" : .byt CHR_RTN : .byt 0

            ; Non position independent
SayHello    .(
            ; Output message
            ldy #$00
loop        lda helloMsg, y               ; 3 bytes, 4-5 cycles (depending on page cross)
            beq finished
            jsr CCHROUT                   ; Output char to screen
            iny
            bne loop
finished    rts
.)

The following Hello World routine adapts the one above and makes it Position Independent by looking up the address of helloMsg in DATA_TABLE using IHELLOMSG as an index and storing it in TMP_DPTR. TMP_DPTR is then accessed using indirect addressing by LDA. DATA_TABLE is located in the cassette buffer at $0334.

CHR_RTN    = $0D               ; Return character
CCHROUT    = $FFD2             ; Output character to current output device
TMP_DPTR   = $FB               ; Temporary data pointer
DATA_TABLE = $0334             ; The location of the data table
IHELLOMSG  = 0                 ; Index to 'HELLO, WORLD!' message

helloMsg   .asc "HELLO, WORLD!" : .byt CHR_RTN : .byt 0

            ; Position Independent
SayHello    .(
            ; Point TMP_DPTR to helloMsg
            lda DATA_TABLE+IHELLOMSG      ; 3 bytes, 4 cycles
            sta TMP_DPTR                  ; 2 bytes, 3 cycles
            lda DATA_TABLE+IHELLOMSG+1    ; 3 bytes, 4 cycles
            sta TMP_DPTR+1                ; 2 bytes, 3 cycles

            ; Output message
            ldy #$00
loop        lda (TMP_DPTR), y             ; 2 bytes, 5-6 cycles (depending on page cross)
            beq finished
            jsr CCHROUT                   ; Output char to screen
            iny
            bne loop
finished    rts
.)

If we compare the two routines we can see that there is an 8 byte overhead for the PIC using a data table and a 15 cycle overhead. This is in addition to the overhead of creating the data table in the first place.

A Jump Table for Subroutine Calls

The JSR (Jump to Subroutine) instruction can only jump to absolute addresses. One easy way to overcome this is to use a jump table. This will contain a series of jumps to the correct location for each subroutine.

The jump table would contain code such as the following:

CPRINTSCORE   jmp  $1420
CPRINTLIVES   jmp  $1430
CSHIPLEFT     jmp  $1440
CSHIPRIGHT    jmp  $1460

Then all the PIC would do is JSR to one of the locations in the jump table which would then jump to the location of the subroutine. E.g. if a game contained PIC that wanted to move the ship left it would run the following:

              jsr CSHIPLEFT

The jump table would have to be placed in a static location unless self-modifying code is used. One possible location for the jump table could be at $02A1-02FF which is reserved for program indirects. This would be enough room for a jump table containing 31 jumps.

The extra JMP instruction adds a 3 cycle overhead to every JSR instruction. This is in addition to the overhead of creating the jump table in the first place.

Getting the Program Counter

There are few situations where we would need to get the Program Counter because we could just get the address of the start of the code from the loader. However, in case a situation arises or just for curiosity we can get the address of the Program Counter with the code that follows. This code should be copied to a static address, such as the cassette buffer, and JSR should call it to put the address of the calling JSR in PC.

PC        = $09                ; Location to store PC in

            ; Put address of calling JSR into PC
GetPC       .(
            pla
            sta PC             ; Store the 16-bit program counter at PC
            pla
            sta PC+1
            pha                ; Restore the return address to the stack
            lda PC
            pha
            bne decL1          ; Decrement PC by 2 to point to calling instruction
            dec PC+1
decL1       dec PC
            bne decL2
            dec PC+1
decL2       dec PC
            rts
.)

This works because JSR puts the PC+2 onto the stack and RTS takes two bytes from the stack, increments them and jumps to this address.

Calculating Absolute Addresses

To access data and run subroutines we need to calculate absolute addresses for them. This can be done by using offsets from a certain point in the code and adding them to that point once its absolute addresses has been determined. The absolute address of our reference point could be supplied by the machine code loader or sought using the GetPC routine above.

The following code shows how the absolute address used in a data table could be calculated to refer to a region of memory containing a Hello, World message.

CHR_RTN    = $0D               ; Return character

PC         = $09               ; Location to store PC in
DATA_TABLE = $0334             ; The location of the data table
IHELLOMSG  = 0                 ; Index to 'HELLO, WORLD!' message

            * = $1001

start       jsr GetPC          ; Put absolute address of start in PC

            ; BRanch Always
            clv
            bvc makeDTable

helloMsg   .asc "HELLO, WORLD!" : .byt CHR_RTN : .byt 0

            ; Make data table
makeDTable  clc
            lda PC
            adc #<(helloMsg-start)      ; LSB of offset from start
            sta DATA_TABLE+IHELLOMSG
            lda PC+1
            adc #>(helloMsg-start)      ; MSB of offset from start
            sta DATA_TABLE+IHELLOMSG+1

Self-Modifying Code

We can avoid the use of jump tables and data tables by using self-modifying code. This can create quicker, smaller and more readable code, but it isn't possible to do this if the code is located in ROM.

To create self-modifying code we can create a table which contains address offsets to change. The address offsets will be offset from a location in the code and at run-time the values at these addresses will be changed to absolute addresses.

TXTTAB     = $2B               ; Pointer to start of tokenized Basic
SMADDR     = $09               ; Address to modify
PSMTABLE   = $FB               ; Pointer to self-modification table
CHR_RTN    = $0D               ; Return character


            ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
            ; Code has been removed here to create a
            ; Basic stub with first byte of stub
            ; labeled 'start'.  Where the stub is
            ; loaded can be found by looking at
            ; TXTTAB.
            ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

            ;=========================================
            ; Start of machine language
            ;=========================================
mLang
            * = mLang-start

            ; BRanch Always
            clv
            bvc setupSM

            ;-----------------------------------------
            ; Message strings
            ;-----------------------------------------
helloMsg   .asc "HELLO, WORLD!" : .byt CHR_RTN : .byt 0
byeMsg     .asc "GOODBYE, WORLD!" : .byt CHR_RTN : .byt 0

            ;=========================================
            ; Self-modification
            ;=========================================

            ;-----------------------------------------
            ; Self-modification table
            ; offsetAddr
            ;-----------------------------------------
smTable     .word main+1       ; Address of SayHello routine
            .word main+4       ; Address of SayGoodbye routine
            .word SayHello+3   ; Entry within SayHello
                               ; routine pointing to helloMsg
            .word SayGoodbye+3 ; Entry within SayGoodbye
                               ; routine pointing to byeMsg
            .word 0            ; End of table

            ;-----------------------------------------
            ; Self-modify code to point to correct
            ; locations
            ;-----------------------------------------

setupSM     .(
            ; Create pointer to smTable
            clc
            lda TXTTAB         ; Get start of tokenized Basic
            adc #<smTable
            sta PSMTABLE
            lda TXTTAB+1
            adc #>smTable
            sta PSMTABLE+1

            ; Point offsets in smTable to absolute addresses
            ldy #00
            lda (PSMTABLE), y
loop       ; Calculate address to change
            clc
            adc TXTTAB
            sta SMADDR
            iny
            lda TXTTAB+1
            adc (PSMTABLE), y
            iny
            sta SMADDR+1
            ; Move value at address
            tya
            tax
            ldy #00
            clc
            lda TXTTAB
            adc (SMADDR), y
            sta (SMADDR), y
            iny
            lda TXTTAB+1
            adc (SMADDR), y
            sta (SMADDR), y
            txa
            tay
            lda (PSMTABLE), y
            bne loop           ; If not end of table
.)

            ;=========================================
            ; MAIN program
            ;=========================================
main        jsr SayHello
            jsr SayGoodbye
            rts

Video Demonstrating Position Independent Code

You can see Position Independent Code explained and run in the following video:

Full Examples

The examples have been written for the XA assembler and are hosted on GitHub: position_independent_code_vic20.

Position Independent Code Without Self-Modification

The code below demonstrates creating a jump and data table to point to the subroutines and their associated message data. The code contains a Basic stub to make the code easier to load and test. It relies on location $2B/$2C (TXTAB) pointing to the start of tokenized Basic so that it can use this as a reference point to calculate the absolute addresses in the rest of the code. The start of tokenized Basic will change depending on the memory configuration of the Vic, but the machine language code will work regardless because it is Position Independent.

; Basic Stub
OPEN_PR    = $28               ; ( character
CLOSE_PR   = $29               ; ) character
PLUS       = $AA               ; + character
TIMES      = $AC               ; * character
TOK_PEEK   = $C2               ; PEEK token
TOK_SYS    = $9E               ; SYS token

CHR_RTN    = $0D               ; Return character
OP_JMPA    = $4C               ; JMP absolute opcode
TXTTAB     = $2B               ; Pointer to start of tokenized Basic
CCHROUT    = $FFD2             ; Output character to current output device

TMP_DPTR   = $FB               ; Temporary data pointer
JMP_TABLE  = $02A1             ; The location of the jump table
DATA_TABLE = $0334             ; The location of the data table
IHELLOMSG  = 0                 ; Index to 'HELLO, WORLD!' message
IBYEMSG    = IHELLOMSG+2       ; Index to 'GOODBYE, WORLD!' message
CSAYHELLO  = JMP_TABLE         ; Vector to SayHello
CSAYBYE    = JMP_TABLE+3       ; Vector to SayGoodbye


            .byt $01, $80      ; Load Address.  Using character ROM because
                               ; correct RAM location memory configuration
                               ; dependent


            ;=========================================
            ;  Basic Stub
            ;=========================================

            ; 2020 SYS PEEK(43)+PEEK(44)*256+27
            * = $1001
start       .word basicEnd     ; Next Line link, here end of Basic program
            .word 2020         ; The line number for the SYS statement
            .byt  TOK_SYS      ; SYS token
            .asc  " "
            .byt  TOK_PEEK     ; PEEK token
            .byt  OPEN_PR      ; (
            .asc  "43"         ; 43
            .byt  CLOSE_PR     ; )
            .byt  PLUS         ; +
            .byt  TOK_PEEK     ; PEEK token
            .byt  OPEN_PR      ; (
            .asc  "44"         ; 44
            .byt  CLOSE_PR     ; )
            .byt  TIMES        ; *
            .asc  "256"        ; 256
            .byt  PLUS         ; +
            .asc  "27"         ; 27 (The size of the stub)
            .byt  0            ; End of Basic line
basicEnd    .word 0            ; End of Basic program


            ;=========================================
            ; Start of machine language
            ;=========================================

            ; BRanch Always
            clv
            bvc makeJTable

            ;-----------------------------------------
            ; Message strings
            ;-----------------------------------------
helloMsg   .asc "HELLO, WORLD!" : .byt CHR_RTN : .byt 0
byeMsg     .asc "GOODBYE, WORLD!" : .byt CHR_RTN : .byt 0


            ;=========================================
            ; Create jump and data tables
            ;=========================================

            ;-----------------------------------------
            ; Create jump table
            ;-----------------------------------------

            ; Store JMP absolute opcodes
makeJTable  lda #OP_JMPA       ; JMP absolute opcode
            sta CSAYHELLO
            sta CSAYBYE

            ; Jump table address for SayHello
            clc
            lda TXTTAB         ; Get start of tokenized Basic
            adc #<(SayHello-start)
            sta CSAYHELLO+1
            lda TXTTAB+1
            adc #>(SayHello-start)
            sta CSAYHELLO+2

            ; Jump table address for SayGoodbye
            clc
            lda TXTTAB         ; Get start of tokenized Basic
            adc #<(SayGoodbye-start)
            sta CSAYBYE+1
            lda TXTTAB+1
            adc #>(SayGoodbye-start)
            sta CSAYBYE+2

            ;-----------------------------------------
            ; Create data table
            ;-----------------------------------------

            ; Point entry at index IHELLOMSG to helloMsg
            clc
            lda TXTTAB         ; Get start of tokenized Basic
            adc #<(helloMsg-start)
            sta DATA_TABLE+IHELLOMSG
            lda TXTTAB+1
            adc #>(helloMsg-start)
            sta DATA_TABLE+IHELLOMSG+1

            ; Point entry at index IBYEMSG to byeMsg
            clc
            lda TXTTAB         ; Get start of tokenized Basic
            adc #<(byeMsg-start)
            sta DATA_TABLE+IBYEMSG
            lda TXTTAB+1
            adc #>(byeMsg-start)
            sta DATA_TABLE+IBYEMSG+1


            ;=========================================
            ; MAIN program
            ;=========================================
            jsr CSAYHELLO
            jsr CSAYBYE
            rts


            ;=========================================
            ; Subroutines
            ;=========================================

            ;-----------------------------------------
            ; SayHello
            ; Displays 'HELLO, WORLD!' message
            ;-----------------------------------------
SayHello    .(
            ; Point TMP_DPTR to helloMsg
            lda DATA_TABLE+IHELLOMSG
            sta TMP_DPTR
            lda DATA_TABLE+IHELLOMSG+1
            sta TMP_DPTR+1

            ; Output message
            ldy #$00
loop        lda (TMP_DPTR), y
            beq finished
            jsr CCHROUT        ; Output char to screen
            iny
            bne loop
finished    rts
.)

            ;-----------------------------------------
            ; SayGoodbye
            ; Displays 'GOODBYE, WORLD!' message
            ;-----------------------------------------
SayGoodbye  .(
            ; Point TMP_DPTR to byeMsg
            lda DATA_TABLE+IBYEMSG
            sta TMP_DPTR
            lda DATA_TABLE+IBYEMSG+1
            sta TMP_DPTR+1

            ; Output message
            ldy #$00
loop        lda (TMP_DPTR), y
            beq finished
            jsr CCHROUT        ; Output char to screen
            iny
            bne loop
finished    rts
.)

Position Independent Code Using Self-Modification

This example also uses a Basic stub and the TXTTAB location mentioned in the previous example. We can see that the code below is shorter, easier to read and would run faster once the self-modification had been completed. However, self modifying code can't be placed in ROM.

; Basic Stub
OPEN_PR    = $28               ; ( character
CLOSE_PR   = $29               ; ) character
PLUS       = $AA               ; + character
TIMES      = $AC               ; * character
TOK_PEEK   = $C2               ; PEEK token
TOK_SYS    = $9E               ; SYS token

; Self Modification
TXTTAB     = $2B               ; Pointer to start of tokenized Basic
SMADDR     = $09               ; Address to modify
PSMTABLE   = $FB               ; Pointer to self-modification table

; Character output
CHR_RTN    = $0D               ; Return character
CCHROUT    = $FFD2             ; Output character to current output device


            .byt $01, $80      ; Load Address.  Using character ROM because
                               ; correct RAM location memory configuration
                               ; dependent


            ;=========================================
            ;  Basic Stub
            ;=========================================
            * = $1001

            ; 2020 SYS PEEK(43)+PEEK(44)*256+27
start       .word basicEnd     ; Next Line link, here end of Basic program
            .word 2020         ; The line number for the SYS statement
            .byt  TOK_SYS      ; SYS token
            .asc  " "
            .byt  TOK_PEEK     ; PEEK token
            .byt  OPEN_PR      ; (
            .asc  "43"         ; 43
            .byt  CLOSE_PR     ; )
            .byt  PLUS         ; +
            .byt  TOK_PEEK     ; PEEK token
            .byt  OPEN_PR      ; (
            .asc  "44"         ; 44
            .byt  CLOSE_PR     ; )
            .byt  TIMES        ; *
            .asc  "256"        ; 256
            .byt  PLUS         ; +
            .asc  "27"         ; 27 (The size of the stub)
            .byt  0            ; End of Basic line
basicEnd    .word 0            ; End of Basic program


            ;=========================================
            ; Start of machine language
            ;=========================================
mLang
            * = mLang-start

            ; BRanch Always
            clv
            bvc setupSM

            ;-----------------------------------------
            ; Message strings
            ;-----------------------------------------
helloMsg   .asc "HELLO, WORLD!" : .byt CHR_RTN : .byt 0
byeMsg     .asc "GOODBYE, WORLD!" : .byt CHR_RTN : .byt 0


            ;=========================================
            ; Self-modification
            ;=========================================

            ;-----------------------------------------
            ; Self-modification table
            ; offsetAddr
            ;-----------------------------------------
smTable     .word main+1
            .word main+4
            .word SayHello+3
            .word SayGoodbye+3
            .word 0            ; End of table

            ;-----------------------------------------
            ; Self-modify code to point to correct
            ; locations
            ;-----------------------------------------

setupSM     .(
            ; Create pointer to smTable
            clc
            lda TXTTAB         ; Get start of tokenized Basic
            adc #<smTable
            sta PSMTABLE
            lda TXTTAB+1
            adc #>smTable
            sta PSMTABLE+1

            ; Skip self-modication if has already been run
            ; This isn't needed if sure code will only be run once
            ldy #01
            lda (PSMTABLE), y
            cmp #$FF           ; Page $FF indicates SM already run
            beq main

            ; Point offsets in smTable to absolute addresses
            ldy #00
            lda (PSMTABLE), y
loop       ; Calculate address to change
            clc
            adc TXTTAB
            sta SMADDR
            iny
            lda TXTTAB+1
            adc (PSMTABLE), y
            iny
            sta SMADDR+1
            ; Move value at address
            tya
            tax
            ldy #00
            clc
            lda TXTTAB
            adc (SMADDR), y
            sta (SMADDR), y
            iny
            lda TXTTAB+1
            adc (SMADDR), y
            sta (SMADDR), y
            txa
            tay
            lda (PSMTABLE), y
            bne loop           ; If not end of table

            ; Record that self-modification has been run
            ; by setting page of first address to $FF
            ldy #01
            lda #$FF
            sta (PSMTABLE), y
.)

            ;=========================================
            ; MAIN program
            ;=========================================
main        jsr SayHello
            jsr SayGoodbye
            rts


            ;=========================================
            ; Subroutines
            ;=========================================

            ;-----------------------------------------
            ; SayHello
            ; Displays 'HELLO, WORLD!' message
            ;-----------------------------------------
SayHello    .(
            ; Output message
            ldy #$00
loop        lda helloMsg, y
            beq finished
            jsr CCHROUT        ; Output char to screen
            iny
            bne loop
finished    rts
.)

            ;-----------------------------------------
            ; SayGoodbye
            ; Displays 'GOODBYE, WORLD!' message
            ;-----------------------------------------
SayGoodbye  .(
            ; Output message
            ldy #$00
loop        lda byeMsg, y
            beq finished
            jsr CCHROUT        ; Output char to screen
            iny
            bne loop
finished    rts
.)

Position Independent Code Getting Absolute Address of Point in Code

The last example shows how we can determine the absolute address of a point in the code from which we can calculate offsets. The code works by creating a routine at $02A1 to get the Program Counter (GetPC). Once an absolute address has been determined all the other addresses can be calculated in relation to that address.

This example doesn't use a Basic stub, but does specify address $1203 as its load address. This address should work for all memory configurations. To load and run it we could use:

LOAD "*",8,1
SYS 4611

We can change the load address and as long as it's a valid area for our Vic and we SYS to the correct address, the code will work fine.

; Opcodes
OP_BNE     = $D0               ; BNE
OP_DECA    = $CE               ; DEC absolute
OP_JMPA    = $4C               ; JMP absolute
OP_PHA     = $48               ; PHA
OP_PLA     = $68               ; PLA
OP_RTS     = $60               ; RTS
OP_LDAA    = $AD               ; LDA absolute
OP_STAA    = $8D               ; STA absolute

CHR_RTN    = $0D               ; Return character
CCHROUT    = $FFD2             ; Output character to current output device

PC         = $09               ; Location to store PC in
TMP_DPTR   = $FB               ; Temporary data pointer
JMP_TABLE  = $02A1             ; The location of the jump table
DATA_TABLE = $0334             ; The location of the data table
IHELLOMSG  = 0                 ; Index to 'HELLO, WORLD!' message
IBYEMSG    = IHELLOMSG+2       ; Index to 'GOODBYE, WORLD!' message
CSAYHELLO  = JMP_TABLE         ; Vector to SayHello
CSAYBYE    = JMP_TABLE+3       ; Vector to SayGoodbye

GetPC      = $02A1             ; Location of GetPC routine

            .byt $03, $12      ; Load Address.
                               ; $1203 should work for any memory configuration


            ;=========================================
            ; Static data
            ;=========================================
            * = $1203

            ; BRanch Always
            clv
            bvc start

            ;-----------------------------------------
            ; Message strings
            ;-----------------------------------------
helloMsg   .asc "HELLO, WORLD!" : .byt CHR_RTN : .byt 0
byeMsg     .asc "GOODBYE, WORLD!" : .byt CHR_RTN : .byt 0


            ;=========================================
            ; Determine Absolute address for start of
            ; code
            ;=========================================

            ;-----------------------------------------
            ;  Create GetPC at $02A1
            ;-----------------------------------------
start       lda #OP_PLA        ; PLA
            sta GetPC
            lda #OP_STAA       ; STA PC
            sta GetPC+1        ; - Store the 16-bit program counter at PC
            lda #<PC           ;
            sta GetPC+2
            lda #>PC
            sta GetPC+3
            lda #OP_PLA        ; PLA
            sta GetPC+4
            lda #OP_STAA       ; STA PC+1
            sta GetPC+5
            lda #<PC+1
            sta GetPC+6
            lda #>PC+1         ; Using MSB of PC in case moved out of zero page
            sta GetPC+7
            lda #OP_PHA        ; PHA
            sta GetPC+8        ; - Restore the return address to the stack
            lda #OP_LDAA       ; LDA PC
            sta GetPC+9
            lda #<PC
            sta GetPC+10
            lda #>PC
            sta GetPC+11
            lda #OP_PHA        ; PHA
            sta GetPC+12
            lda #OP_BNE        ; BNE decL1
            sta GetPC+13       ; - PC=PC-2 to point to calling instruction
            lda #$03
            sta GetPC+14
            lda #OP_DECA      ; DEC PC+1
            sta GetPC+15       ; - MSB
            lda #<PC+1
            sta GetPC+16
            lda #>PC+1
            sta GetPC+17
            lda #OP_DECA      ; decL1:  DEC PC
            sta GetPC+18       ; - LSB
            lda #<PC
            sta GetPC+19
            lda #>PC
            sta GetPC+20
            lda #OP_BNE        ; BNE decL2
            sta GetPC+21
            lda #$03
            sta GetPC+22
            lda #OP_DECA      ; DEC PC+1
            sta GetPC+23
            lda #<PC+1
            sta GetPC+24
            lda #>PC+1
            sta GetPC+25
            lda #OP_DECA      ; decL2:  DEC PC
            sta GetPC+26
            lda #<PC
            sta GetPC+27
            lda #>PC
            sta GetPC+28
            lda #OP_RTS        ; RTS
            sta GetPC+29

callGetPC   jsr GetPC          ; Get absolute address of callGetPC


            ;=========================================
            ; Create jump and data tables
            ;=========================================

            ;-----------------------------------------
            ; Create jump table
            ;-----------------------------------------

            ; Store JMP absolute opcodes
            lda #OP_JMPA       ; JMP absolute opcode
            sta CSAYHELLO
            sta CSAYBYE

            ; Jump table address for SayHello
            clc
            lda PC
            adc #<(SayHello-callGetPC)
            sta CSAYHELLO+1
            lda PC+1
            adc #>(SayHello-callGetPC)
            sta CSAYHELLO+2

            ; Jump table address for SayGoodbye
            clc
            lda PC
            adc #<(SayGoodbye-callGetPC)
            sta CSAYBYE+1
            lda PC+1
            adc #>(SayGoodbye-callGetPC)
            sta CSAYBYE+2

            ;-----------------------------------------
            ; Create data table
            ;-----------------------------------------

            ; Point entry at index IHELLOMSG to helloMsg
            clc
            lda PC
            adc #<(helloMsg-callGetPC)
            sta DATA_TABLE+IHELLOMSG
            lda PC+1
            adc #>(helloMsg-callGetPC)
            sta DATA_TABLE+IHELLOMSG+1

            ; Point entry at index IBYEMSG to byeMsg
            clc
            lda PC
            adc #<(byeMsg-callGetPC)
            sta DATA_TABLE+IBYEMSG
            lda PC+1
            adc #>(byeMsg-callGetPC)
            sta DATA_TABLE+IBYEMSG+1


            ;=========================================
            ; MAIN program
            ;=========================================
            jsr CSAYHELLO
            jsr CSAYBYE
            rts


            ;=========================================
            ; Subroutines
            ;=========================================

            ;-----------------------------------------
            ; SayHello
            ; Displays 'HELLO, WORLD!' message
            ;-----------------------------------------
SayHello    .(
            ; Point TMP_DPTR to helloMsg
            lda DATA_TABLE+IHELLOMSG
            sta TMP_DPTR
            lda DATA_TABLE+IHELLOMSG+1
            sta TMP_DPTR+1

            ; Output message
            ldy #$00
loop        lda (TMP_DPTR), y
            beq finished
            jsr CCHROUT        ; Output char to screen
            iny
            bne loop
finished    rts
.)

            ;-----------------------------------------
            ; SayGoodbye
            ; Displays 'GOODBYE, WORLD!' message
            ;-----------------------------------------
SayGoodbye  .(
            ; Point TMP_DPTR to byeMsg
            lda DATA_TABLE+IBYEMSG
            sta TMP_DPTR
            lda DATA_TABLE+IBYEMSG+1
            sta TMP_DPTR+1

            ; Output message
            ldy #$00
loop        lda (TMP_DPTR), y
            beq finished
            jsr CCHROUT        ; Output char to screen
            iny
            bne loop
finished    rts
.)
Creative Commons License
Position Independent Code (6502) on the Commodore VIC-20 by Lawrence Woodman is licensed under a Creative Commons Attribution 4.0 International License.

Share This Post

Feedback/Discuss

Related Articles

Getting the Address of BASIC Variables on the VIC-20

Getting the address of a BASIC variable can be useful if you want to pass data to a machine code routine or want to access the bytes of a variable directly to improve speed and reduce garbage collectio...   Read More

Saving and Loading Memory on the VIC-20

Saving and loading memory is quite easy on the VIC-20 once you know how. However, it isn't obvious how to do this and therefore this article will present a few simple ways of doing it from BASIC and A...   Read More

Programming in Assembly with VICMON on the VIC-20

VICMON is a machine language monitor released by Commodore in 1982 and is great for programming the VIC-20. Its interactive nature means that it can often be quicker to develop via this rather than us...   Read More

Storing Machine Code in REM Statements on the VIC-20

BASIC programs often contain machine code routines but they take up quite a lot of space in BASIC. An interesting way to reduce the amount of space that they take is to store the machine code in REM s...   Read More

Code and Data in Display Memory on the VIC-20

The unexpanded Commodore VIC-20 only had 5K of RAM and therefore creative ways had to be found to maximize the available RAM. The display memory would use some of this memory and therefore one option ...   Read More