SAVE attribute scope

To me at least, static variables made much more sense after I wrote my little CHIP-8 emulator.

Like everything in a Von Neumann architecture, a static variable is just a chunk of memory. Nowadays it is more complicated, because of the mapping into the address space, loading at run-time, etc.

Counter with static variable in CHIP-8 assembly
; CHIP-8 uses 16 registers, named V0 - VF (hex)
; The value I is a 16-bit address pointer

main:
    LD VA, 0        ; VA = 0
    CALL count
    CALL count
    CALL count
    LD VA, 1        ; VA = 1, retrieve count,
    CALL count      ; result is found in VB 
    LD V3, 2
    LD V4, 2
    LD V5, VB       
    call write      ; Write count to screen
.fin:
    JP .fin

; Count number of function calls using a 
; static variable
;
; Arguments:
;   VA ... if 1, write output value
;   VB ... output value
;
count:
    JP .body        ; Jump to executable part 
.counter:           ; Counter variable, reserve
    #d8 0x0         ;    1 byte of memory              <-- static variable
.body:
    LD I, .counter  ; Load address of counter
    LD V0, [I]      ; Load counter into register
    ADD V0, 1       ; Increment
    LD I, .counter  ; Re-load address
    LD [I], V0      ; Store counter
    SE VA, 1        ; Skip if VA == 1
    JP .end
    LD VB, V0       ; VB = V0
.end:
    RET

write:
   ; ... omitted ...

It’s kind of wonky because the instruction set is very primitive, I’m using a ad-hoc calling convention, and instructions and data share the same address space without any form of protection.

After compilation it becomes a binary blob (shown here as a hex dump); the static (or saved in Fortran parlance) variable is the one in column 9, row 2, that is 00, just below 6a.

                                vv
 00 | 6a 00 22 16  22 16 22 16  6a 01 22 16  63 02 64 02 | j.".".".j.".c.d. |
 10 | 85 b0 22 2b  12 14 12 19  00 a2 18 f0  65 70 01 a2 | .."+........ep.. | <--
 20 | 18 f0 55 3a  01 12 29 8b  00 00 ee a2  6a d3 45 73 | ..U:..).....j.Es |
 30 | 06 a2 6f d3  45 73 06 a2  74 d3 45 73  06 a2 79 d3 | ..o.Es..t.Es..y. |
 40 | 45 73 06 a2  7e d3 45 73  06 a2 83 d3  45 73 06 a2 | Es..~.Es....Es.. |
 50 | 67 f5 33 f2  65 f0 29 d3  45 73 05 f1  29 d3 45 73 | g.3.e.).Es..).Es |
 60 | 05 f2 29 d3  45 00 ee 00  00 00 f8 80  80 80 f8 f8 | ..).E........... |
 70 | 88 88 88 f8  88 88 88 88  f8 88 c8 a8  98 88 f8 20 | ...............  |
 80 | 20 20 20 00  f8 00 f8 00  .. .. .. ..  .. .. .. .. |    ............. |

After running in the emulator, it works like intended:

image

The blog posts from Raymond Chen from Microsoft are also a gold mine of knowledge on memory organization and argument passing conventions:

As Raymond nicely illustrates in the second link, in the past everything including local variables would have static storage duration. There are still ways to force this behavior, e.g. using -fno-automatic or certain DEC extensions.

Edit: after sleeping over this, I began to ponder if any compilers store static variables within the instructions themselves making use of self-modifying code:

; Increment counter (uses self-modifying code) 
; Set VA = 1 beforehand to retrieve it. Output in VB.
; The counter is stored as an immediate value in the 
; instructions that loads it
count:
    LD I, .load ; Load address
    LD V1, [I]  ; Load V0 and V1 with contents at I and I+1
    ADD V1, 1   ; Add 1 to counter
    LD I, .load ; Re-load address
    LD [I], V1  ; Store V0 and V1 at I and I+1
    SNE VA, 1   ; Skip if VA != 1
.load:
    LD VB, 0x0  ; Retrieve counter (stored in this immediate value)
    RET