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:
The blog posts from Raymond Chen from Microsoft are also a gold mine of knowledge on memory organization and argument passing conventions:
- Subroutine calls in the ancient world, before computers had stacks or heaps - The Old New Thing
- This processor has no stack (insert spooky laughter) - The Old New Thing
- Why is Identical COMDAT Folding called Identical COMDAT Folding? - The Old New Thing
- The history of calling conventions, part 1 - The Old New Thing
- The history of calling conventions, part 2 - The Old New Thing
- The history of calling conventions, part 3 - The Old New Thing
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