Hints for z88dk and assembly functions on CP/M

166 views
Skip to first unread message

Phillip Stevens

unread,
Oct 24, 2024, 10:57:54 PM10/24/24
to RC2014-Z80
It has been a while since I've been at my keyboard and written anything useful, so I thought it might be a good time to write some notes on a recent discussion at Z88DK for the 8085, and also to discuss using a simple example how to use the Z88DK to write mixed C and assembly programs for other CPUs in the extended 8080 family (eg z80, z180, ez80, z80n, gbz80, etc).

The exercise is to write a quick proof that the 8085 undocumented instruction LD DE,HL+n doesn't affect the carry or any other flag bits.

There would be many ways to do this. I've just dome a simple demo here.

Rather than writing raw assembly to output and format a result, the easiest method is to use CP/M as a host platform and just use the C stdio functions to produce a nice result with one line of code.

But to actually test the instruction we need to write an assembly function to exercise all the opcode options. The assembly function needs to take and return parameters to the main routine, and then can be written to the screen.

Here's the whole program in one file, I'll break down each line as we go through...

/*
   check the 8085 ld de,hl+n instruction doesn't influence carry
   clear flags, add 1 to input hl, and look at flag bits
*/

// zcc +cpm -clib=8085 -SO2 --list bug-2631.c -o bug-2631.com

#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>

#pragma printf = "%s %c %04X"     // enables  %s %c %X only

uint32_t testBug( uint16_t input) __z88dk_fastcall;

uint32_t testBug( uint16_t input)
{
    __asm__(                    \
        "xor a              \n" \
        "ld de,hl+1         \n" \
        "push af            \n" \
        "pop hl             \n" \
        );                      \
}

void main (void)
{
    for (uint16_t test = 0x0001; test != 0x0000; ++test) {
        fprintf(stdout, " Input: HL %04X  Output: LD DE,HL+1 %04X AF %04X\n", test, (uint16_t)(testBug(test)>>16), (uint16_t)testBug(test));
    }
}

First the comment line showing the zcc command to be used to initiate the compiler / assembler / linker / packager system to produce a .COM binary for CP/M.
We're picking the cpm target and specifying that this is for a 8085 CPU. For z80 the library specification can be omitted.
SO2 is for strong inline optimisation, but doesn't push for the size minimisation that SO3 would do. SO3 would use helper subroutines for all variable fetches which saves space, but slows things down considerably.
--list provides us with listings of the resulting assembled program, and the CRT0 code from which _main will be called.

// zcc +cpm -clib=8085 -SO2 --list bug-2631.c -o bug-2631.com

The next interest point is the #pragma line, which defines which printf format converters need to be available, and whether the formatting versions (larger) or simple versions can be selected. Here we're just asking to be able to print strings and characters, and to be able to format unsigned hexadecimal integers.

#pragma printf = "%s %c %04X"     // enables  %s %c %X only

Now we declare our test function inventively titled testBug, and the desired calling convention.

It is to be a fastcall function, which means that it is passed only one parameter, and that parameter is found in the DEHL registers, right justified.
It also returns one value, also in the DEHL registers, right justified. That means that a 16-bit parameter will be found in the HL registers, and an 8-bit parameter is in the L register.

The fastcall declaration avoids the compiler pushing parameters onto the stack, and then popping them off on return, saving substantial cycles in stack management.
If there are multiple parameters to pass then the _z88dk_callee decoration causes the compiler to assume that the callee function has removed its own parameters from the calling stack, which can also save some cycles.

The testBug function is going to take a 16-bit input and return two 16-bit results, cheated into one uint32_t C variable.
We'll see later how this is done.

uint32_t testBug( uint16_t input) __z88dk_fastcall;

Now, writing the testBug function means putting some assembly code within a C function. This is not the cleanest way to write assembly with z88dk, but for the purpose of our one line program it is the most straightforward way to get the result needed.

Using the __asm__() MACRO the assembly code is just written into the C function.
This doesn't prevent the compiler from doing its normal entry and function return handling, like for instance adding a ret opcode at the end of the function.

The testBug function clears the A register, and resets the Carry and Sign flags. Which flags are set by XOR A is left for further reading.

Then the opcode we're investigating is issued with the result in DE, and the resulting flag byte moved into the L register to be returned to the C program.

uint32_t testBug( uint16_t input)
{
    __asm__(                    \
        "xor a              \n" \
        "ld de,hl+1         \n" \
        "push af            \n" \
        "pop hl             \n" \
        );                      \
}

Finally the main function simply iterates over the values that need to be tested, and prints the results.

Lazily the testBug function is called twice each iteration, but with a little more complexity an intermediate variable could hold the result and return it. To get access to the DE result the return value is shifted 16 bits to the right. The compiler interprets this as an EX DE,HL instruction and it is very efficient.

To format the output so that it looks nice the %04X converter is used. This puts the resulting hexadecimal (with capital numerals) into 4 columns.

void main (void)
{
    for (uint16_t test = 0x0001; test != 0x0000; ++test) {
        fprintf(stdout, " Input: HL %04X  Output: LD DE,HL+1 %04X AF %04X\n", test, (uint16_t)(testBug(test)>>16), (uint16_t)testBug(test));
    }
}

So that's it. The resulting bug-2631.com is then sent to CP/M using XMODEM or XM or similar and, when run, the results appear on the screen.

bug-2631

 Input: HL 0001  Output: LD DE,HL+1 0002 AF 0044
 Input: HL 0002  Output: LD DE,HL+1 0003 AF 0044
 Input: HL 0003  Output: LD DE,HL+1 0004 AF 0044
 Input: HL 0004  Output: LD DE,HL+1 0005 AF 0044
 Input: HL 0005  Output: LD DE,HL+1 0006 AF 0044
 Input: HL 0006  Output: LD DE,HL+1 0007 AF 0044
 Input: HL 0007  Output: LD DE,HL+1 0008 AF 0044
 Input: HL 0008  Output: LD DE,HL+1 0009 AF 0044
 Input: HL 0009  Output: LD DE,HL+1 000A AF 0044
 Input: HL 000A  Output: LD DE,HL+1 000B AF 0044
 Input: HL 000B  Output: LD DE,HL+1 000C AF 0044
 Input: HL 000C  Output: LD DE,HL+1 000D AF 0044
 Input: HL 000D  Output: LD DE,HL+1 000E AF 0044
 Input: HL 000E  Output: LD DE,HL+1 000F AF 0044
 Input: HL 000F  Output: LD DE,HL+1 0010 AF 0044
 Input: HL 0010  Output: LD DE,HL+1 0011 AF 0044
 Input: HL 0011  Output: LD DE,HL+1 0012 AF 0044
 Input: HL 0012  Output: LD DE,HL+1 0013 AF 0044


And we see, as expected, that the LD DE,HL+n opcode doesn't affect the flags at all.

For further reading Ken Shirriff explains that the undocumented 8085 instructions are implemented in the ACT register of the ALU (bypassing the Accumulator), and therefore don't have access to the flag bit control lines.

Anyway, I hope that was useful.

Phillip


Richard Deane

unread,
Oct 25, 2024, 2:35:51 AM10/25/24
to rc201...@googlegroups.com

Very interesting read. As a retired software QA person I note that AF always has the value 44, and therefore we have no proof that the test code is returning AF, even though the code is simple and believable. I would like to see AF set to a known value, and that same code return the value and display it. 😀 Am I being too QA?


--
You received this message because you are subscribed to the Google Groups "RC2014-Z80" group.
To unsubscribe from this group and stop receiving emails from it, send an email to rc2014-z80+...@googlegroups.com.
To view this discussion, visit https://groups.google.com/d/msgid/rc2014-z80/7363a235-05f1-4c9d-aefd-866b8ba65ea0n%40googlegroups.com.

Phillip Stevens

unread,
Oct 25, 2024, 3:57:06 AM10/25/24
to RC2014-Z80
On Friday 25 October 2024 Richard Deane wrote:

Very interesting read. As a retired software QA person I note that AF always has the value 44, and therefore we have no proof that the test code is returning AF, even though the code is simple and believable. I would like to see AF set to a known value, and that same code return the value and display it. 😀 Am I being too QA?

Hi Richard,

glad you found it entertaining!

Yes, you're right that the demo here is not exhaustive at all. I just cut everything down to make a readable example.

To test exhaustively you'd need to test all the immediate n additive values across all the iteration range.
Also the companion LD DE,SP+n instruction would need to be tested too, which I've just ignored.

But in the process I was reminded that XOR A doesn't clear all the flags by the surprising (for me) result of 0044 being presented.
I had expected to see 0000, so then I went back and did some reading and noticed that both the Zero flag and Parity flag should be set on XOR A, and then tested that this was being properly returned before moving on.

Screenshot from 2024-10-25 18-23-46.png

But the fact that the Zero flag remains set even though the LD DE,HL+n instruction is executed is already a pretty good indication the flag situation is as expected.

I think we're writing code to different ends. Usually I'm writing the worst (simplest) code I can to find an elusive bug, not being exhaustive at all, just trying to trigger and instrument the issue.
I guess you're usually in correctness testing QA code mode, so writing the compete cases first is an important first step. 😀

I'm not sure if you've ever looked at the z88dk test suite code?
There is code to exercise the z80asm assembler, separate to the code for exercising the library. I don't know much about the assembler testing code. It is all written in Perl, which looks Greek to me.
The library test suite is here. You might find it interesting.
Probably needs a QA Engineer to expand on the cases, which are usually just written as a result of resolving issues.

Cheers, Phillip


On Fri, 25 Oct 2024 Phillip Stevens wrote:
It has been a while since I've been at my keyboard and written anything useful, so I thought it might be a good time to write some notes on a recent discussion at Z88DK for the 8085, and also to discuss using a simple example how to use the Z88DK to write mixed C and assembly programs for other CPUs in the extended 8080 family (eg z80, z180, ez80, z80n, gbz80, etc).

The exercise is to write a quick proof that the 8085 undocumented instruction LD DE,HL+n doesn't affect the carry or any other flag bits.

Now, writing the testBug function means putting some assembly code within a C function. This is not the cleanest way to write assembly with z88dk, but for the purpose of our one line program it is the most straightforward way to get the result needed.

Using the __asm__() MACRO the assembly code is just written into the C function.
This doesn't prevent the compiler from doing its normal entry and function return handling, like for instance adding a ret opcode at the end of the function.

The testBug function clears the A register, and resets the Carry and Sign flags. Which flags are set by XOR A is left for further reading.

Then the opcode we're investigating is issued with the result in DE, and the resulting flag byte moved into the L register to be returned to the C program.

uint32_t testBug( uint16_t input)
{
    __asm__(                    \
        "xor a              \n" \
        "ld de,hl+1         \n" \
        "push af            \n" \
        "pop hl             \n" \
        );                      \
}

Richard Deane

unread,
Oct 25, 2024, 4:11:50 AM10/25/24
to rc201...@googlegroups.com

I tried casually to use z88dk and found the documentation and complexity intimidating, which is why your approachable example is so good.


--
You received this message because you are subscribed to the Google Groups "RC2014-Z80" group.
To unsubscribe from this group and stop receiving emails from it, send an email to rc2014-z80+...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages