Disassembler-friendly constant parameter to routine

Programming the 6502 microprocessor and its relatives in assembly and other languages.
Post Reply
blargg
Posts: 42
Joined: 30 Dec 2003
Contact:

Disassembler-friendly constant parameter to routine

Post by blargg »

I write lots of test programs that are run on hardware and emulators to be sure they both have the same behavior. People often step through them with a debugger in emulators, so I'd like them to be relatively easy to disassemble. I have a few routines that take constant parameters and preserve all registers (so they can be inserted anywhere without worrying about stomping on anything). Putting the value/address into registers means I have to save and restore them, which becomes too bloated, especially since this is for the NMOS 6502 which lacks PHX, PHY, etc. As an example, I'll use a print_string routine that takes a zero-terminates ASCII string. The following are the various approaches I've come up with. I'm only focusing on the caller, since that's what matters most.

Code: Select all

.zeropage
addr: .res 2
.rodata
test: .byte "Test",0
.code

; The most straight-forward way, but it's bloated
pha
tya
pha
lda #<test
ldy #>test
jsr print_string
pla
tay
pla

; More compact and still very clean. I might go with this.
pha
lda #<test
sta addr
lda #>test
jsr print_string
pla

; Will confuse a disassembler since it will interpret the string
; as instructions and could easily "munch" the next instruction.
; The return addr is used to find the string data, then execution
; resumes just after the zero terminator.
jsr print_string
.byte "Test",0

; Also involves return address examination, but doesn't confuse
; disassembler/debugger. Still more complex to implement.
; The return addr is used to find the address inside the BIT
; instruction. The point of using BIT is that it only modifies
; flags, and we don't have to adjust the return address either,
; just read it.
jsr print_string
bit test

; Simpler to implement, since nested return addr would be popped off
; and never returned to. Main problem is then that each string
; printed turns into a JSR to a different address in the main code.
jsr print_test
...

print_test:
jsr print_string
.byte "Test",0
The details are of course encapsulated in a macro, so the user code just contains the following:

print_string "Test"

Any other ideas for making it play well with a disassembler?
kc5tja
Posts: 1706
Joined: 04 Jan 2003

Post by kc5tja »

Quote:
I write lots of test programs that are run on hardware and emulators to be sure they both have the same behavior. People often step through them with a debugger in emulators, so I'd like them to be relatively easy to disassemble.
This is why I always choose to rely upon automated testing, instead of manual testing, and in particular using test-driven development practices. Manual testing is laborious, error-prone, and a gigantic time-sink in terms of productivity.
Quote:
Any other ideas for making it play well with a disassembler?
Why not load the X register with an index into a table of message pointers, then have your print_string routine index the table to grab the appropriate address on its own? That permits up to 256 messages to be stored per print_string entry-point.

Code: Select all

.proc print_string_proc
  pha
  tya
  pha

  lda stringTableL,x
  sta ptrL
  lda stringTableH,x
  sta ptrH

  ...etc...

  pla
  tay
  pla
  rts
.endproc
blargg
Posts: 42
Joined: 30 Dec 2003
Contact:

Post by blargg »

Quote:
This is why I always choose to rely upon automated testing, instead of manual testing, and in particular using test-driven development practices. Manual testing is laborious, error-prone, and a gigantic time-sink in terms of productivity.
Maybe I was unclear. OK, we have a piece of hardware that uses a 6502 processor. Someone wants to duplicate its behavior in a simulator. I write a test program that runs on the hardware and exercises some aspect of it, and verifies that it responds appropriately. This test program passes on hardware. Then the author runs it on his simulator. If it passes, great, but if it fails, the author might want to step through the simulated 6502 to find where his simulator is messing up. I'm not seeing how this could be automated.
Quote:
Why not load the X register with an index into a table of message pointers, then have your print_string routine index the table to grab the appropriate address on its own? That permits up to 256 messages to be stored per print_string entry-point.
Sure; got a macro to automatically assign message indicies and generate the message table? It's got to be really convenient to use.
kc5tja
Posts: 1706
Joined: 04 Jan 2003

Post by kc5tja »

blargg wrote:
Maybe I was unclear.
On the contrary -- you were crystal.
Quote:
If it passes, great, but if it fails, the author might want to step through the simulated 6502 to find where his simulator is messing up. I'm not seeing how this could be automated.
I'm not trying to be offensive when I say this, but rather honest. Not seeing how it could be automated is not the same as affirming that it cannot be automated. To believe so is just inexperience with automated testing procedures. There are two methods of testing at your disposal: internal and external. Internal testing would rely on the emulated 65816/6502 itself (assuming it is "correct enough" to do so), while External testing relies on instrumenting the simulator itself.

Internal testing depends on some amount of sweat-equity. Basically, you need a certain minimum set of opcodes to implement a unit test framework -- those opcodes need to be verified manually. However, such a framework will positively not rely on ALL of the CPU's instructions and addressing modes. Therefore, you can still rely on the unit testing framework you've written to exercise those instructions.

Once the CPU is verified, then you can use that to do some tests on well-known I/O peripherals too. Indeed, this is what most BIOS ROMs do during their "self-test." RAM size determination, keyboard I/O check, etc. All that isn't really for the user's benefit. The user couldn't really care less about the adequacy of RAM from boot to boot -- statistics says if it booted before, it'll boot again. Those tests are really for the BIOS and manufacturer's benefit -- if a peripheral fails to respond, it'll spew an error message, or beep a certain number of times, etc. This allows a computer technician, not a user!, to determine the fault, and replace motherboards appropriately.

External testing relies on instrumenting the simulator or emulator itself. This works by thorough modularization of the simulator's code. For example, my Kestrel emulator, written as a spike and not even fully tested in an automated fashion, is built modularly to facilitate easy testing. Address decoding, MGIA, keyboard/mouse, SerBus, and even the CPU core itself are all separate modules, all fully re-usable in other projects, with well-defined interfaces that are easy to test. Indeed, the SerBus implementation and the disk storage system were the first two components that I rigorously unit-tested. You can find the source to these in the Kestrel emulator's source package on my website. The unit tests exercise SerBus down to individual register-level transfers, completely without the aid of the emulated CPU.

I don't want to make myself out like I'm beating my chest. That's not my intent. I'm just saying, "yes," it is possible to automate the testing and verification of hardware against a software model (heck, that's what I did at Hifn as post-silicon verification technician!). For those who are interested in techniques on how to do this, there are volumes of websites on test-driven development, behavior-driven development, mock-object testing, etc. I'm not going to repeat them here.
Quote:
Sure; got a macro to automatically assign message indicies and generate the message table? It's got to be really convenient to use.
Me? No. Again, failure for you to think of how this is done does not mean it cannot be done.

Here's an idea, using a completely hypothetical assembly language syntax, based on my recollection of a 68000 assembler I used on the Amiga called A68K all the time.

Code: Select all

Print   MACRO   Msg
        SECTION RODATA
L_Msg:  DC.B    Msg, 0

        SECTION MsgTableLoSection
idx     SET     (*-MsgTableLoOrigin)
        DC.B    <L_Msg

        SECTION MsgTableHiSection
        DC.B    >L_Msg

        SECTION CODE
        LDX     #idx
        JSR     printIndexedMessage
        ENDM
If your assembler provides a feature-set analogous to the above (the critical point here is that SET functions like EQU, but allows symbol re-definition), then it be structurally similar to the above. This even works across multiple compilation/assembly units -- the linker would coalesce all like-named sections appropriately, and take care of any load-time fixups.

At least, if it were a good system. :) I'm pretty sure ca65 offers a macro processor powerful enough for this, but I've never used it, so my memory could be incorrect. Fachat's assembler (xa65 IIRC) might also be powerful enough to support this kind of construct.
blargg
Posts: 42
Joined: 30 Dec 2003
Contact:

Post by blargg »

Quote:
I'm just saying, "yes," it is possible to automate the testing and verification of hardware against a software model.
That's exactly what I'm doing already. User runs automated test. User's simulator fails the test with the result "Reading from $2002 should suppress NMI if read just when it's set". User wants to figure out exactly why it's failing, so he examines his code but doesn't find an obvious cause. He starts integrated 6502 debugger and steps through the test, to see if he can get some idea of what's happening. He comes to a JSR PRINT_STR followed by an ASCII string, which looks like junk instructions, which makes the task more difficult for him. But I give up in trying to get this point across, since it's just not going to make it past the tangential issue you've decided to use this thread for. In that spirit, I'll use it as the context for why people like you drive people like me away:

This topic was about disassembler-friendly coding, not about automated testing of programs. I mentioned the context only so it would make a bit more sense. It wasn't an invitation for advice on the subject of testing. If it were, I would have given more information so that useful advice could be given, and titled the thread accordingly. As it is, you made unwarranted assumptions and refused to correct them. I know much more about the project, yet you ignore my attempts to say "please stick to the topic and stop offering unwanted, inapplicable advice". You continued to hammer this tangential issue in a condescending way. Somehow my feedback isn't able to alter your view of my situation; your paradigm seems to convert it into more confirmation that your view is correct and that I am in need of enlightenment from you. You may consider me to be too sensitive, but sensitivity and being mentally open are essential to finding novel solutions to problems, and this being a technical forum for solving problems, I come here with an open mind. There is just no excuse for the mean approach you take, and I'm not going to try to accommodate it. I don't know why it's tolerated. People like you make me be on constant guard for having my words twisted to fit your view, and I absolutely hate it. I doubt I'm alone. You turn things into battles, respond to people in a predatory way, and in general kill the atmosphere of exploration and open sharing. And that's all.
User avatar
dclxvi
Posts: 362
Joined: 11 Mar 2004

Post by dclxvi »

A couple of variations on the theme...

Code: Select all

; Just like PRIMM, except (a) it uses return address + 4 instead of
; return address + 1, and (b) it doesn't (need to) modify the return
; address on the stack.  Since the data is preceded by a JMP it doesn't
; look like its falling through to garbage.
;
; Since neither JSR or JMP affects any flags or registers, preservation of
; registers and/or flags can be done inside print_string rather than by
; the caller.
;
   JSR print_string
   JMP L1
   .byte "string",0
L1
Variation #2. I would recommend using INX(s) with $100,X rather than $100+n,X (depending on when you do a TSX) inside print_string so that it works no matter what the value of the stack pointer is.

Code: Select all

; Uses PHA and PLA instead of STA data.  print_string reads the lo byte
; from the stack, but doesn't alter the stack.
;
; The main advantage is that there is no need to worry about any memory
; conflicts with the "data" variable, or the fact that its address may
; different from program to program or even from build to build (easier
; to follow when looking at a disassembly without labels).
;
PHP              ; save flags
PHA              ; save A
LDA #<string
PHA              ; pass the lo byte on the stack
LDA #>string     ; pass the hi byte in A
JSR print_string
PLA              ; discard the lo byte
PLA              ; restore A
PLP              ; restore flags
User avatar
GARTHWILSON
Forum Moderator
Posts: 8773
Joined: 30 Aug 2002
Location: Southern California
Contact:

Post by GARTHWILSON »

Quote:

Code: Select all

   JSR   print_string
   JMP   L1
   .byte "string",0
L1 
If you replace JMP L1 with BRA L1, the parameter becomes the string length (assuming you don't need more than 127) and you can save two bytes.
User avatar
8BIT
Posts: 1787
Joined: 30 Aug 2002
Location: Sacramento, CA
Contact:

Post by 8BIT »

Here's a thought....

Place 3 NOP instructions after your string text.

The worst case is the last byte of text is interpreted as an instruction that uses the $00 and possibly one NOP as its operands. The Next two NOP's ensure that the disassembler can lock onto the code after the NOP's. It would not "Munch" any valid instructions. The PRINT_IMM would not need modification as it would just execute the NOP's and continue.

Its not space saving, efficient, or brilliant, but it would work, wouldn't it?

Daryl
User avatar
dclxvi
Posts: 362
Joined: 11 Mar 2004

Post by dclxvi »

GARTHWILSON wrote:
If you replace JMP L1 with BRA L1, the parameter becomes the string length (assuming you don't need more than 127) and you can save two bytes.
According to the original post, it's using an NMOS 6502. Overwriting V usually won't cause any issues, so if print_string used CLV before its RTS, BVC could be used. The one thing that's nice about JMP is that it represents a consistent format for any inline data, regardless of length (the original post alluded to other routines).
kc5tja
Posts: 1706
Joined: 04 Jan 2003

Post by kc5tja »

Response to blargg's rude and presumptuous response to me removed, and taken off-line.
Post Reply