Menu

#4052 Area ZP Length Error

closed-invalid
None
MOS6502
5
6 days ago
2026-08-06
Under4Mhz
No

When I build the below code, I get the following error.

While this code is a obviously a construed example, I have games where this error is actually occurring due to the build up of the memory use in the zp over a number of various valid functions.

sdcc -mmos6502 --opt-code-speed --max-allocs-per-node 10000 --data-loc 0x0001 --code-loc 0x4000 zptest.c -o zptest.ihx
?ASlink-Warning-Paged Area ZP Length Error
#include <stdint.h>

uint8_t buffer[64];

///< Hold three pointers and a 16 bit total live across a call to the next one
///< down, so the spill slots of the two cannot be overlaid
#define ZP_SPILL_FUNCTION( name, next )                                     \
    uint8_t name( uint8_t *a, uint8_t *b, uint8_t *c, uint8_t i ) {         \
                                                                            \
        uint8_t *p = a + i;                                                 \
        uint8_t *q = b + i;                                                 \
        uint8_t *r = c + i;                                                 \
        uint16_t total = 0;                                                 \
                                                                            \
        while ( i-- ) {                                                     \
                                                                            \
            total += *p++;                                                  \
            *q++ = total >> 8;                                              \
            *r++ = total;                                                   \
        }                                                                   \
                                                                            \
        return total + *p + *q + *r + next( a, b, c, 3 );                   \
    }

///< Ten of them at a time, named zp_spill_<ten>0 to zp_spill_<ten>9 and each
///< calling the one before it. "below" is what the first of the ten calls, so
///< passing the last of the previous ten carries the chain on across the groups
#define ZP_SPILL_DECADE( ten, below )                                       \
    ZP_SPILL_FUNCTION( zp_spill_##ten##0, below )                           \
    ZP_SPILL_FUNCTION( zp_spill_##ten##1, zp_spill_##ten##0 )               \
    ZP_SPILL_FUNCTION( zp_spill_##ten##2, zp_spill_##ten##1 )               \
    ZP_SPILL_FUNCTION( zp_spill_##ten##3, zp_spill_##ten##2 )               \
    ZP_SPILL_FUNCTION( zp_spill_##ten##4, zp_spill_##ten##3 )               \
    ZP_SPILL_FUNCTION( zp_spill_##ten##5, zp_spill_##ten##4 )               \
    ZP_SPILL_FUNCTION( zp_spill_##ten##6, zp_spill_##ten##5 )               \
    ZP_SPILL_FUNCTION( zp_spill_##ten##7, zp_spill_##ten##6 )               \
    ZP_SPILL_FUNCTION( zp_spill_##ten##8, zp_spill_##ten##7 )               \
    ZP_SPILL_FUNCTION( zp_spill_##ten##9, zp_spill_##ten##8 )

///< Bottom of the chain, keeps nothing live
uint8_t zp_spill_leaf( uint8_t *a, uint8_t *b, uint8_t *c, uint8_t i ) {

    (void)b;
    (void)c;

    return a[i];
}

ZP_SPILL_DECADE( 0, zp_spill_leaf )
ZP_SPILL_DECADE( 1, zp_spill_09 )
ZP_SPILL_DECADE( 2, zp_spill_19 )
ZP_SPILL_DECADE( 3, zp_spill_29 )

///< Enter at the top, so the whole chain is reachable and nothing is dropped
int main( void ) {

    return zp_spill_39( buffer, buffer + 8, buffer + 16, 4 );
}

Looking at the map, REGTEMP starts at 169h, then fills from there. I'd like to request adding the variables filling the ZP before REGTEMP to the map file so it's possible to see what's causing the problem.

ZP                                  00000001    00000170 =         368. bytes (REL,CON,PAG)

         Value  Global                             Global Defined In Module
     00000169  REGTEMP                            __sdcc_regs
     00000169  ___SDCC_m6502_ret0                 __sdcc_regs
....
$ sdcc -v
SDCC : z80/sm83/ez80/z80n/mos6502/mos65c02 4.6.2 #16701 (Linux)
3 Attachments

Discussion

  • Gabriele Gorla

    Gabriele Gorla - 2026-08-15

    I have no idea how why the spill locations do not show up in the linker map.

    However, the asm file has the complete list of register spill locations in ZP (look for slocX_Y_Z).

    ;--------------------------------------------------------
    ; ZP ram data
    ;--------------------------------------------------------
        .area ZP      (PAG)
    _zp_spill_00_sloc0_1_0:
        .ds 2
    _zp_spill_00_sloc1_1_0:
        .ds 2
    _zp_spill_00_sloc2_1_0:
        .ds 2
    _zp_spill_00_sloc3_1_0:
        .ds 1
    _zp_spill_00_sloc4_1_0:
        .ds 2
    

    You can also see which one each function uses in in the function header comments.

    ;------------------------------------------------------------
    ;Allocation info for local variables in function 'zp_spill_00'
    ;------------------------------------------------------------
    ;sloc0         Allocated with name '_zp_spill_00_sloc0_1_0'
    ;sloc1         Allocated with name '_zp_spill_00_sloc1_1_0'
    ;sloc2         Allocated with name '_zp_spill_00_sloc2_1_0'
    ;sloc3         Allocated with name '_zp_spill_00_sloc3_1_0'
    ;sloc4         Allocated with name '_zp_spill_00_sloc4_1_0'
    

    If you can't reduce the ZP spills, you can use --no-zp-spill option and the register spills will be placed at an absolute address instead of ZP.

     
    • Under4Mhz

      Under4Mhz - 2026-08-17

      The --no-zp-spill flag fixes the problem.

      I'd be inclined to suggest to enable no-zp-spill as the default. I approve of speed optimisations, but in this case I would personally prefer that all my code compiles successfully without setting flags, even if it means it runs slightly slower. The code size is 16K, which is not huge. I'll enable this flag in my build system for everything from now on so it won't be an issue for me anymore.

      It would be great if we could simply move some variables out of the spill area at link time, but I assume this isn't be possible since sdcc will need to make this decision at compile time.

       
  • Maarten Brock

    Maarten Brock - 2026-08-17

    The object files in the libraries do not hold the "debug" information the linker needs to show the spill locations and other local variables by name in the linker map file. It would be a nice extension if it could. I think it would require the .lst files alongside the .rel files in the libs.

    I would vote against making --no-zp-spill the default. It generates less efficient code by default and when anyone bounces into this problem the solution to enable this option seems simple enough. Maybe it could use a better explanation in the manual then.

    It could also be interesting to investigate if there really only is stuff in the zero page that belongs there and nothing else is eating into it.

     
    • Under4Mhz

      Under4Mhz - 2026-08-18

      My concern is that users won't think to go looking at compiler switches when they get an internal compiler error. It looks like a compiler bug. I think users will give up and go look for another compiler or wait for a future fix.

      It never once occured to me there might be a compiler switch to fix this. I assumed it was a compiler bug.

      I think it's better to be conservative and make sure the compiler always works by default. Make potentially breaking optimisation switches optional.

       
    • Gabriele Gorla

      Gabriele Gorla - 2026-08-22

      The object files in the libraries do not hold the "debug" information the linker needs to show the spill locations and other local variables by name in the linker map file. It would be a nice extension if it could. I think it would require the .lst files alongside the .rel files in the libs.

      the spills are from the compiled code, not the library. It seems the reason why they don't show up in the map is because they are not declared as global.

      It could also be interesting to investigate if there really only is stuff in the zero page that belongs there and nothing else is eating into it.

      There are 2 more things that end up in ZP:
      1- 8 bytes used by the compiler for code generation (REGTEMP stack, DPTR, big return)
      2- Overlay. Usually very small as it's the largest parameter size out of all leaf functions. There is some dead code to move this also out of ZP but the usefulness is very limited.

      I would vote against making --no-zp-spill the default. It generates less efficient code by default and when anyone bounces into this problem the solution to enable this option seems simple enough.

      I agree, enabling --no-zp-spill by default does not seem a good idea.

      Maybe it could use a better explanation in the manual then.

      The manual says:

      --no-zp-spill Force the compiler to spill registers to 16-bit addressable memory (xdata) instead of Page Zero. When running out of Page Zero space, this option will allow to free many Page Zero locations at the expense of slightly larger and slower code

      Any suggestions for improvement?

       

      Last edit: Gabriele Gorla 2026-08-22
  • Gabriele Gorla

    Gabriele Gorla - 2026-08-22

    It would be great if we could simply move some variables out of the spill area at link time, but I assume this isn't be possible since sdcc will need to make this decision at compile time.

    That is correct.
    It is possible to implement enabling/disabling --no-zp-spill using #pragma to give the user finer control of how the spills are allocated.

    The longer term fix is to implement fixed virtual registers in ZP to reduce the register pressure and limit the spills. This will also improve performance as the virtual registers will get register tracking (not possible on the spills)

     
  • Gabriele Gorla

    Gabriele Gorla - 2026-08-27
    • status: open --> closed-invalid
     
  • Maarten Brock

    Maarten Brock - 6 days ago
    • assigned_to: Gabriele Gorla
     

Log in to post a comment.