ND-500 Buddy System Memory Management¶
Overview¶
The ND-500 CPU implements a Buddy System for dynamic memory allocation, providing an efficient mechanism for allocating and freeing variable-sized memory blocks from a pool called the heap. This system is fundamentally different from the traditional stack-based memory allocation and is designed to handle:
- Co-routines - Multiple concurrent execution paths that may not return in LIFO order
- Dynamic data structures - Trees, lists, and networks that grow and shrink during execution
- Non-LIFO deallocation - Situations where the first allocated block isn't the last to be freed
The name "Buddy System" comes from the way memory blocks are split and paired - when a larger block is split in half, the two resulting blocks are "buddies" that can potentially be recombined later.
DATA STRUCTURE DEFINITIONS¶
Data Structure 1: Local Data Area (at B register)¶
The Local Data Area is the fundamental stack frame structure. The B register always points to the base of the current local data area.
Memory Layout Table¶
| Byte Offset | Word Offset | Symbol | Size | Description |
|---|---|---|---|---|
| 0 | 0 | PREVB | 4 bytes (1 word) | Previous B register value (return chain) |
| 4 | 1 | RETA | 4 bytes (1 word) | Return address |
| 8 | 2 | SP | 4 bytes (1 word) | Stack pointer (first free location) |
| 12 | 3 | AUX/LOG | 4 bytes (1 word) | Auxiliary data OR log₂(block size) for buddy |
| 16 | 4 | N | 4 bytes (1 word) | Number of arguments |
| 20 | 5 | arg1 | 4 bytes (1 word) | Address of first argument |
| 24 | 6 | arg2 | 4 bytes (1 word) | Address of second argument |
| ... | ... | argN | 4 bytes each | Addresses of remaining arguments |
| 20+N×4 | 5+N | variable | Local variables (uninitialized) |
Predefined Symbol Values (from manual)¶
| Symbol | Value (bytes) | Description |
|---|---|---|
| PREVB | 0 | Offset to previous B |
| RETA | 4 | Offset to return address |
| SP | 8 | Offset to stack pointer |
| AUX | 12 | Offset to auxiliary location |
| N | 16 | Offset to argument count |
Visual Memory Layout¶
block-beta
columns 1
block:HEADER["LOCAL DATA AREA HEADER (B register points here)"]
columns 4
A["Offset 0"] B["PREVB"] C["4 bytes"] D["Previous B value"]
E["Offset 4"] F["RETA"] G["4 bytes"] H["Return address"]
I["Offset 8"] J["SP"] K["4 bytes"] L["Stack pointer value"]
M["Offset 12"] N2["AUX/LOG"] O["4 bytes"] P["Aux data or log size"]
Q["Offset 16"] R["N"] S["4 bytes"] T["Argument count"]
end
block:ARGS["ARGUMENT ADDRESSES"]
columns 4
U["Offset 20"] V["arg1"] W["4 bytes"] X["→ first argument"]
Y["Offset 24"] Z["arg2"] AA["4 bytes"] AB["→ second argument"]
AC["..."] AD["..."] AE["..."] AF["..."]
end
block:LOCAL["LOCAL VARIABLES"]
columns 1
AG["Uninitialized local variable space"]
AH["(extends up to B.SP value)"]
end
How B Register References This Structure¶
flowchart LR
subgraph "CPU Register"
B["B Register<br/>= 0x1000"]
end
subgraph "Memory"
M0["0x1000: PREVB"]
M4["0x1004: RETA"]
M8["0x1008: SP"]
M12["0x100C: AUX/LOG"]
M16["0x1010: N"]
M20["0x1014: arg1"]
end
B -->|"B + 0"| M0
B -->|"B + 4"| M4
B -->|"B + 8"| M8
B -->|"B + 12"| M12
B -->|"B + 16"| M16
B -->|"B + 20"| M20
Data Structure 2: Heap Variables (at TOS register)¶
The Heap Variables structure describes the buddy system's free memory pool. The TOS register always points to this structure.
Memory Layout Table¶
| Byte Offset | Word Offset | Symbol | Size | Description |
|---|---|---|---|---|
| 0 | 0 | MAXL | 4 bytes (1 word) | Maximum log₂ size of allocatable blocks |
| 4 | 1 | STAH | 4 bytes (1 word) | Start address of heap pool (informational) |
| 8 | 2 | ENDH | 4 bytes (1 word) | End address of heap pool (informational) |
| 12 | 3 | FLOG0 | 4 bytes (1 word) | Freelist head for 2⁰ = 1 word blocks |
| 16 | 4 | FLOG1 | 4 bytes (1 word) | Freelist head for 2¹ = 2 word blocks |
| 20 | 5 | FLOG2 | 4 bytes (1 word) | Freelist head for 2² = 4 word blocks |
| ... | ... | FLOGn | 4 bytes each | Freelist heads continue... |
| 12+MAXL×4 | 3+MAXL | FLOG\<MAXL> | 4 bytes (1 word) | Freelist head for 2^MAXL word blocks |
FLOG Array Size¶
The FLOG array has (MAXL + 1) entries, from FLOG0 to FLOG\<MAXL>.
| If MAXL = | FLOG entries | Total heap variables size |
|---|---|---|
| 5 | FLOG0-FLOG5 (6 entries) | 12 + 6×4 = 36 bytes |
| 7 | FLOG0-FLOG7 (8 entries) | 12 + 8×4 = 44 bytes |
| 10 | FLOG0-FLOG10 (11 entries) | 12 + 11×4 = 56 bytes |
Visual Memory Layout¶
block-beta
columns 1
block:HDR["HEAP VARIABLES (TOS register points here)"]
columns 4
A["Offset 0"] B["MAXL"] C["4 bytes"] D["e.g., 7 (max 128 words)"]
E["Offset 4"] F["STAH"] G["4 bytes"] H["Start of heap pool"]
I["Offset 8"] J["ENDH"] K["4 bytes"] L["End of heap pool"]
end
block:FLOGS["FREELIST HEAD POINTERS"]
columns 4
M["Offset 12"] N["FLOG0"] O["4 bytes"] P["→ 1-word free blocks"]
Q["Offset 16"] R["FLOG1"] S["4 bytes"] T["→ 2-word free blocks"]
U["Offset 20"] V["FLOG2"] W["4 bytes"] X["→ 4-word free blocks"]
Y["Offset 24"] Z["FLOG3"] AA["4 bytes"] AB["→ 8-word free blocks"]
AC["..."] AD["..."] AE["..."] AF["..."]
AG["Offset 12+MAXL×4"] AH["FLOG<MAXL>"] AI["4 bytes"] AJ["→ largest blocks"]
end
How TOS Register References This Structure¶
flowchart LR
subgraph "CPU Register"
TOS["TOS Register<br/>= 0x5000"]
end
subgraph "Memory (Heap Variables)"
M0["0x5000: MAXL = 7"]
M4["0x5004: STAH"]
M8["0x5008: ENDH"]
M12["0x500C: FLOG0"]
M16["0x5010: FLOG1"]
M20["0x5014: FLOG2"]
M24["0x5018: FLOG3"]
M28["0x501C: FLOG4"]
M32["0x5020: FLOG5"]
M36["0x5024: FLOG6"]
M40["0x5028: FLOG7"]
end
TOS -->|"TOS + 0"| M0
TOS -->|"TOS + 4"| M4
TOS -->|"TOS + 8"| M8
TOS -->|"TOS + 12"| M12
TOS -->|"TOS + 12 + n×4"| M16
Block Sizes by Log Value¶
| Log Size (n) | Block Size (words) | Block Size (bytes) | FLOG offset from TOS |
|---|---|---|---|
| 0 | 2⁰ = 1 | 4 | TOS + 12 |
| 1 | 2¹ = 2 | 8 | TOS + 16 |
| 2 | 2² = 4 | 16 | TOS + 20 |
| 3 | 2³ = 8 | 32 | TOS + 24 |
| 4 | 2⁴ = 16 | 64 | TOS + 28 |
| 5 | 2⁵ = 32 | 128 | TOS + 32 |
| 6 | 2⁶ = 64 | 256 | TOS + 36 |
| 7 | 2⁷ = 128 | 512 | TOS + 40 |
Formula: FLOG[n] address = TOS + 12 + (n × 4)
FLOG Initialization and Lifecycle¶
Initial Setup (User Responsibility)¶
"The heap variables must be initialized by the user program and the user is responsible for building the lists." — ND-05.009.4 EN, Section 3.3
The CPU does NOT initialize the heap. After INIT sets TOS, the user program must:
- Write MAXL value at TOS+0
- Write STAH (start of heap) at TOS+4 (informational only)
- Write ENDH (end of heap) at TOS+8 (informational only)
- Build the freelists by writing FLOG entries and linking free blocks
Example: Initial Heap Setup¶
Assume after INIT, TOS = 0x15000, and we want to set up a heap pool from 0x20000 to 0x30000 with max block size 2^7 = 128 words.
Step 1: Initialize Heap Variables
| Address | Field | Value Written | Meaning |
|---|---|---|---|
| 0x15000 | MAXL | 7 | Max block size = 2^7 = 128 words |
| 0x15004 | STAH | 0x20000 | Heap pool starts here |
| 0x15008 | ENDH | 0x30000 | Heap pool ends here |
| 0x1500C | FLOG0 | 0 | No 1-word blocks |
| 0x15010 | FLOG1 | 0 | No 2-word blocks |
| 0x15014 | FLOG2 | 0 | No 4-word blocks |
| 0x15018 | FLOG3 | 0 | No 8-word blocks |
| 0x1501C | FLOG4 | 0 | No 16-word blocks |
| 0x15020 | FLOG5 | 0 | No 32-word blocks |
| 0x15024 | FLOG6 | 0 | No 64-word blocks |
| 0x15028 | FLOG7 | 0x20000 | One 128-word block available |
Step 2: Initialize Free Block at 0x20000
| Address | Value Written | Meaning |
|---|---|---|
| 0x20000 | 0x20200 | Next 128-word block at 0x20200 |
Step 3: Initialize Second Free Block at 0x20200
| Address | Value Written | Meaning |
|---|---|---|
| 0x20200 | 0 | End of FLOG7 list |
flowchart LR
subgraph "After Initial Setup"
FLOG7["FLOG7 at 0x15028<br/>value = 0x20000"]
BLK1["Block at 0x20000<br/>word 0 = 0x20200<br/>(128 words)"]
BLK2["Block at 0x20200<br/>word 0 = 0<br/>(128 words)"]
NULL["NULL (end)"]
end
FLOG7 --> BLK1
BLK1 -->|"next"| BLK2
BLK2 -->|"next = 0"| NULL
How GETB Modifies FLOG (Step by Step)¶
Scenario: W3 GETB 5 (request 32-word block)¶
Initial State:
- FLOG5 (32-word) = 0 (empty)
- FLOG6 (64-word) = 0 (empty)
- FLOG7 (128-word) = 0x20000 (one block available)
- Block at 0x20000: word 0 = 0 (end of list)
Step-by-Step Execution:
| Step | Action | Memory Read | Memory Write | Result |
|---|---|---|---|---|
| 1 | Check FLOG5 | Read 0x15020 → 0 | - | Empty, try larger |
| 2 | Check FLOG6 | Read 0x15024 → 0 | - | Empty, try larger |
| 3 | Check FLOG7 | Read 0x15028 → 0x20000 | - | Found block! |
| 4 | Unlink from FLOG7 | Read 0x20000 → 0 | Write 0x15028 ← 0 | FLOG7 now empty |
| 5 | Split 128→64+64 | - | Write 0x20100 ← 0 | Buddy at 0x20100 |
| 6 | Add buddy to FLOG6 | Read 0x15024 → 0 | Write 0x15024 ← 0x20100 | FLOG6 = 0x20100 |
| 7 | Split 64→32+32 | - | Write 0x20080 ← 0 | Buddy at 0x20080 |
| 8 | Add buddy to FLOG5 | Read 0x15020 → 0 | Write 0x15020 ← 0x20080 | FLOG5 = 0x20080 |
| 9 | Return block | - | - | W3 = 0x20000 |
Final State:
| Field | Before | After |
|---|---|---|
| FLOG5 | 0 | 0x20080 (32-word block) |
| FLOG6 | 0 | 0x20100 (64-word block) |
| FLOG7 | 0x20000 | 0 (empty) |
| W3 | - | 0x20000 |
flowchart TB
subgraph "Before GETB 5"
B_FLOG5["FLOG5 = 0"]
B_FLOG6["FLOG6 = 0"]
B_FLOG7["FLOG7 = 0x20000"]
B_BLK["0x20000: 128 words<br/>next = 0"]
end
subgraph "After GETB 5"
A_FLOG5["FLOG5 = 0x20080"]
A_FLOG6["FLOG6 = 0x20100"]
A_FLOG7["FLOG7 = 0"]
A_BLK1["0x20000: ALLOCATED<br/>(returned in W3)"]
A_BLK2["0x20080: 32 words<br/>next = 0"]
A_BLK3["0x20100: 64 words<br/>next = 0"]
end
B_FLOG7 --> B_BLK
A_FLOG5 --> A_BLK2
A_FLOG6 --> A_BLK3
How FREEB Modifies FLOG (Step by Step)¶
Scenario: FREEB 5, 0x20000 (free 32-word block)¶
Initial State: - FLOG5 = 0x20080 (one block in list) - Block at 0x20080: word 0 = 0 (end of list)
Step-by-Step Execution:
| Step | Action | Memory Read | Memory Write | Result |
|---|---|---|---|---|
| 1 | Calculate FLOG5 address | TOS + 32 = 0x15020 | - | - |
| 2 | Read old FLOG5 head | Read 0x15020 → 0x20080 | - | old_head = 0x20080 |
| 3 | Write next ptr in freed block | - | Write 0x20000 ← 0x20080 | Block points to old head |
| 4 | Update FLOG5 | - | Write 0x15020 ← 0x20000 | FLOG5 = freed block |
Final State:
| Field | Before | After |
|---|---|---|
| FLOG5 | 0x20080 | 0x20000 |
| 0x20000 (word 0) | (was allocated data) | 0x20080 (next pointer) |
flowchart LR
subgraph "Before FREEB"
B_FLOG5["FLOG5 = 0x20080"]
B_BLK["0x20080: next = 0"]
end
B_FLOG5 --> B_BLK
subgraph "After FREEB 5, 0x20000"
A_FLOG5["FLOG5 = 0x20000"]
A_BLK1["0x20000: next = 0x20080"]
A_BLK2["0x20080: next = 0"]
end
A_FLOG5 --> A_BLK1
A_BLK1 -->|"next"| A_BLK2
Key Point: FREEB prepends (adds to front), it does NOT append to end. This is O(1) operation.
How RETB Works Internally (Step by Step)¶
Scenario: RETB from subroutine entered via ENTB 5¶
Initial State: - B = 0x20000 (local data area from heap) - Memory at B: - B+0 (PREVB) = 0x10000 - B+4 (RETA) = 0x8100 - B+8 (SP) = 0x11000 - B+12 (LOG) = 5 ← This is the block size! - TOS = 0x15000 - FLOG5 (at TOS+32) = 0x20080
Step-by-Step Execution:
| Step | Action | Memory Read | Memory Write | Register Write |
|---|---|---|---|---|
| 1 | Read LOG (block size) | Read 0x2000C → 5 | - | - |
| 2 | Read PREVB | Read 0x20000 → 0x10000 | - | - |
| 3 | Read RETA | Read 0x20004 → 0x8100 | - | - |
| 4 | FREEB(5, B) - Read old FLOG5 | Read 0x15020 → 0x20080 | - | - |
| 5 | FREEB - Write next ptr | - | Write 0x20000 ← 0x20080 | - |
| 6 | FREEB - Update FLOG5 | - | Write 0x15020 ← 0x20000 | - |
| 7 | Update B register | - | - | B = 0x10000 |
| 8 | Update P (jump) | - | - | P = 0x8100 |
| 9 | Update L | - | - | L = 0x8100 |
| 10 | Clear K flag (RETB) | - | - | STATUS.K = 0 |
Final State:
| Item | Before | After |
|---|---|---|
| B | 0x20000 | 0x10000 (restored) |
| P | (subroutine) | 0x8100 (return address) |
| L | (subroutine) | 0x8100 |
| STATUS.K | ? | 0 |
| FLOG5 | 0x20080 | 0x20000 (block returned!) |
| 0x20000 word 0 | 0x10000 (was PREVB) | 0x20080 (now next ptr) |
flowchart TB
subgraph "RETB Internal Flow"
direction TB
START["RETB"]
READ_LOG["1. Read B.LOG (offset 12)<br/>log_size = 5"]
READ_PREVB["2. Read B.PREVB (offset 0)<br/>saved_B = 0x10000"]
READ_RETA["3. Read B.RETA (offset 4)<br/>return_addr = 0x8100"]
subgraph FREEB_INTERNAL["4-6. Internal FREEB(5, B)"]
FREEB1["Read FLOG5 → old_head"]
FREEB2["Write B.word0 = old_head"]
FREEB3["Write FLOG5 = B"]
end
UPDATE_REGS["7-10. Update Registers<br/>B = saved_B<br/>P = return_addr<br/>L = return_addr<br/>K = 0"]
DONE["Continue at return address"]
end
START --> READ_LOG
READ_LOG --> READ_PREVB
READ_PREVB --> READ_RETA
READ_RETA --> FREEB_INTERNAL
FREEB_INTERNAL --> UPDATE_REGS
UPDATE_REGS --> DONE
Critical: RETB Overwrites B.PREVB with Next Pointer!¶
When RETB executes FREEB internally: - The freed block's word 0 becomes the next pointer for the freelist - This overwrites whatever was in B.PREVB - This is fine because we already read PREVB before calling FREEB
Data Structure 3: Free Block (in Heap Pool)¶
Each free block in the heap pool has a simple structure - only the first word is used by the buddy system.
Memory Layout Table¶
| Byte Offset | Word Offset | Description |
|---|---|---|
| 0 | 0 | Next pointer (address of next free block, or 0 if last) |
| 4+ | 1+ | Unused (available for allocation) |
Freelist Linked Structure¶
flowchart LR
subgraph "Heap Variables"
FLOG3["FLOG3<br/>(8-word blocks)<br/>= 0x6000"]
end
subgraph "Heap Pool - Free 8-word Blocks"
BLOCK_A["Block at 0x6000<br/>Word 0: 0x6100<br/>(next ptr)"]
BLOCK_B["Block at 0x6100<br/>Word 0: 0x6200<br/>(next ptr)"]
BLOCK_C["Block at 0x6200<br/>Word 0: 0x0000<br/>(end of list)"]
end
FLOG3 --> BLOCK_A
BLOCK_A -->|"next"| BLOCK_B
BLOCK_B -->|"next"| BLOCK_C
BLOCK_C -->|"next = 0"| NULL["NULL (end)"]
Single Free Block Detail¶
block-beta
columns 1
block:BLK["FREE BLOCK (e.g., 8 words = 32 bytes)"]
columns 3
A["Word 0"] B["Next Pointer"] C["→ next free block or 0"]
D["Word 1"] E["(unused)"] F["Available when allocated"]
G["Word 2"] H["(unused)"] I["Available when allocated"]
J["Word 3"] K["(unused)"] L["Available when allocated"]
M["Word 4"] N["(unused)"] O["Available when allocated"]
P["Word 5"] Q["(unused)"] R["Available when allocated"]
S["Word 6"] T["(unused)"] U["Available when allocated"]
V["Word 7"] W["(unused)"] X["Available when allocated"]
end
INSTRUCTION OPERATIONS¶
Which Instructions to Use for the Buddy System¶
The ND-500 manual describes the following instruction relationships:
| Instruction | Purpose | Changes TOS? | Used for Buddy System? |
|---|---|---|---|
| INIT | Initialize main program stack | YES | Yes - sets up TOS to point to heap variables |
| ENTM | Enter module (cross-segment/domain) | YES | Warning: changes TOS to new heap variables |
| ENTS | Enter stack subroutine | No | No - uses stack, not heap |
| ENTB | Enter buddy subroutine | No (reads TOS) | YES - allocates from heap |
| GETB | Get standalone heap element | No (reads TOS) | YES - allocates from heap |
| FREEB | Free heap element | No (reads TOS) | YES - returns to heap |
| RETB/RETBK | Return from buddy subroutine | No (reads TOS) | YES - frees block to heap |
Typical Buddy System Setup¶
- INIT - Initialize stack and set TOS to point to heap variables location
- User code must initialize heap variables at TOS (MAXL, freelists, etc.)
- ENTB - Enter subroutines that need heap-allocated local data
- GETB - Allocate standalone elements from heap
- FREEB - Return standalone elements to heap
- RETB/RETBK - Return from buddy subroutines (frees local data area)
Entry/Return Instruction Pairings (from Manual)¶
"The programmer must ensure that the appropriate return instruction is executed." — ND-05.009.4 EN, Section 13.11
| Entry Instruction | Must Return Via | Notes |
|---|---|---|
| ENTS, ENTSN, ENTF, ENTFN | RET, RETK, or IF K RET | Stack-based local data |
| ENTD | RETD | Direct entry (no local data init) |
| ENTT | RETT | Trap handler |
| ENTB | RETB or RETBK | Buddy/heap-based local data |
Critical: Using the wrong return instruction will cause incorrect behavior: - If you use RET after ENTB: The heap block is NOT freed (memory leak) - If you use RETB after ENTS: The stack frame is incorrectly added to heap freelist (corruption)
RETB/RETBK specifically:
1. Reads B.LOG (offset 12) to get the block size
2. Calls FREEB internally to return the block to the heap
3. Then performs the normal return (restore B, jump to RETA)
Warning About ENTM (from Manual)¶
"Be aware that initializing a new stack by INIT or ENTM will change TOS, thus another set of heap variables will be used by the buddy instructions. The new heap variables may be initialized to the values of the old ones or to new values." — ND-05.009.4 EN, Section 3.3
This means: - If you use ENTM to enter a module, TOS will point to a different heap variables location - The buddy instructions (GETB, FREEB, ENTB, RETB) will use the heap described by the current TOS - You must ensure heap variables are properly initialized at the new TOS location
INIT Instruction: Initialize Stack¶
Source: ND-05.009.4 EN, Section 13.9 (Page 229)
Assembly Format¶
INIT <<bottom of stack>>, <stack demand of main program>, <total system stack demand>
| Mnemonic | Hex code | Octal code |
|---|---|---|
| INIT | 0DCH | 334B |
Operation Sequence (Exact from Manual)¶
| Step | Operation | Description |
|---|---|---|
| 1 | <<bottom of stack>> → B |
Load B register with stack base address |
| 2 | <<bottom of stack>> + <total system stack demand> → TOS |
Set TOS to end of stack area |
| 3 | <<bottom of stack>> + <stack demand of main program> → B.SP |
Set stack pointer |
| 4 | 0 → B.PREVB |
Clear previous B (no caller) |
| 5 | 0 → B.RETA → L |
Clear return address and L register |
Memory Writes¶
| Address | Value Written | Description |
|---|---|---|
<<bottom of stack>> + 0 |
0x00000000 |
PREVB = 0 |
<<bottom of stack>> + 4 |
0x00000000 |
RETA = 0 |
<<bottom of stack>> + 8 |
<<bottom of stack>> + <stack demand of main program> |
SP value |
Register Writes¶
| Register | Value Written |
|---|---|
| B | <<bottom of stack>> |
| TOS | <<bottom of stack>> + <total system stack demand> |
| L | 0x00000000 |
Concrete Example: INIT 0x10000, 0x1000, 0x5000¶
flowchart TB
subgraph "BEFORE INIT"
direction TB
PRE_B["B = (undefined)"]
PRE_TOS["TOS = (undefined)"]
PRE_L["L = (undefined)"]
end
subgraph "INIT 0x10000, 0x1000, 0x5000"
direction TB
CALC1["bottom = 0x10000"]
CALC2["main stack = 0x1000"]
CALC3["total stack = 0x5000"]
end
subgraph "AFTER INIT"
direction LR
subgraph REGS["Registers"]
POST_B["B = 0x10000"]
POST_TOS["TOS = 0x15000"]
POST_L["L = 0x00000000"]
end
subgraph MEM["Memory"]
M_PREVB["0x10000: PREVB = 0"]
M_RETA["0x10004: RETA = 0"]
M_SP["0x10008: SP = 0x11000"]
end
end
PRE_B --> CALC1
PRE_TOS --> CALC1
CALC1 --> POST_B
CALC3 --> POST_TOS
Memory Map After INIT¶
flowchart TB
subgraph "Memory Layout After INIT 0x10000, 0x1000, 0x5000"
direction TB
ADDR1["0x10000 ← B register points here"]
subgraph LDA["Local Data Area (main program)"]
L1["0x10000: PREVB = 0"]
L2["0x10004: RETA = 0"]
L3["0x10008: SP = 0x11000"]
L4["0x1000C: AUX = (uninitialized)"]
L5["0x10010: N = (uninitialized)"]
end
ADDR2["0x11000 ← B.SP value (stack grows from here)"]
subgraph FREE["Free Stack Space"]
F1["0x11000 to 0x14FFF"]
F2["Available for subroutine calls"]
end
ADDR3["0x15000 ← TOS register points here"]
subgraph HEAP["Heap Variables (must be user-initialized!)"]
H1["0x15000: MAXL = ?"]
H2["0x15004: STAH = ?"]
H3["0x15008: ENDH = ?"]
H4["0x1500C: FLOG0 = ?"]
H5["... etc"]
end
end
style ADDR1 fill:#90EE90
style ADDR2 fill:#87CEEB
style ADDR3 fill:#FFD700
ENTS Instruction: Enter Stack Subroutine¶
Source: ND-05.009.4 EN, Section 13.10 (Page 233)
Assembly Format¶
ENTS <stack demand>
| Mnemonic | Hex code | Octal code |
|---|---|---|
| ENTS | 0B8H | 270B |
Stack Demand Note (from Manual)¶
"The
<stack demand>is the number of bytes needed for the local data field of the subroutine, including the predefined locations PREVB, RETA, SP, AUX and N (a total of 20 bytes)."
Operation Sequence (Exact from Manual)¶
| Step | Operation | Description |
|---|---|---|
| 1 | B.SP → B |
New B = old stack pointer |
| 2 | oldB → B.PREVB |
Save old B for return |
| 3 | return address → B.RETA → L |
Save return address |
| 4 | newB + <stack demand> → B.SP |
Advance stack pointer |
| 5 | number of arguments → B.N |
Store argument count |
| 6 | addresses of arguments → B.ARG |
Copy argument addresses |
Memory Writes¶
| Address | Value Written | Description |
|---|---|---|
newB + 0 |
oldB |
PREVB = previous B |
newB + 4 |
return address |
RETA |
newB + 8 |
newB + <stack demand> |
SP = new stack pointer |
newB + 16 |
N |
Argument count |
newB + 20 |
arg1 address |
First argument |
newB + 24 |
arg2 address |
Second argument |
| ... | ... | Additional arguments |
Register Writes¶
| Register | Value Written | Notes |
|---|---|---|
| B | oldB.SP |
New base = where old stack pointer was |
| L | return address |
For RETD compatibility |
| TOS | (unchanged) | TOS is NOT modified by ENTS |
Concrete Example: ENTS 0x80 (128 bytes)¶
Before state: - B = 0x10000 - B.SP (memory at 0x10008) = 0x11000 - TOS = 0x15000 - Calling with 2 arguments at 0x20000 and 0x20004
flowchart TB
subgraph "BEFORE CALL + ENTS 0x80"
direction LR
subgraph REGS_BEFORE["Registers"]
B_OLD["B = 0x10000"]
TOS_OLD["TOS = 0x15000"]
end
subgraph MEM_BEFORE["Memory at 0x10000"]
M1["0x10000: PREVB = 0"]
M2["0x10004: RETA = 0"]
M3["0x10008: SP = 0x11000"]
end
end
subgraph "AFTER ENTS 0x80"
direction LR
subgraph REGS_AFTER["Registers"]
B_NEW["B = 0x11000 (was old B.SP)"]
TOS_NEW["TOS = 0x15000 (UNCHANGED)"]
L_NEW["L = return_addr"]
end
subgraph MEM_AFTER["Memory at 0x11000 (new frame)"]
N1["0x11000: PREVB = 0x10000"]
N2["0x11004: RETA = return_addr"]
N3["0x11008: SP = 0x11080"]
N4["0x1100C: AUX = (uninit)"]
N5["0x11010: N = 2"]
N6["0x11014: arg1 = 0x20000"]
N7["0x11018: arg2 = 0x20004"]
end
end
B_OLD --> B_NEW
TOS_OLD --> TOS_NEW
Stack Growth Visualization¶
flowchart TB
subgraph "Stack Growth with ENTS"
direction TB
BASE["0x10000: Main program frame<br/>B (before) points here"]
subgraph MAIN["Main Frame"]
M1["PREVB = 0"]
M2["RETA = 0"]
M3["SP = 0x11000 → (grows to next frame)"]
end
SP1["0x11000: Subroutine frame<br/>B (after) points here"]
subgraph SUB["Subroutine Frame (ENTS 0x80)"]
S1["PREVB = 0x10000 ← (return chain)"]
S2["RETA = return address"]
S3["SP = 0x11080 → (next free)"]
S4["N = argument count"]
S5["args..."]
S6["local variables..."]
end
SP2["0x11080: Next free location<br/>B.SP (after) points here"]
FREE["... free stack space ..."]
TOS_LOC["0x15000: TOS<br/>(stack overflow boundary)"]
end
BASE --> MAIN
MAIN --> SP1
SP1 --> SUB
SUB --> SP2
SP2 --> FREE
FREE --> TOS_LOC
style BASE fill:#90EE90
style SP1 fill:#87CEEB
style SP2 fill:#FFB6C1
style TOS_LOC fill:#FFD700
ENTB Instruction: Enter Buddy Subroutine¶
Source: ND-05.009.4 EN, Section 13.10 (Page 237)
Assembly Format¶
ENTB <log size>
| Mnemonic | Hex code | Octal code |
|---|---|---|
| ENTB | 0BDH | 275B |
Description (from Manual)¶
"A local data area of size 2^
<log size>words is allocated from the heap and the subroutine is entered."
Operation Sequence (Exact from Manual)¶
| Step | Operation | Description |
|---|---|---|
| 1 | Allocate 2^<log size> words from heap |
Uses TOS to find heap variables |
| 2 | address of heap element → B |
New B = allocated block |
| 3 | oldB → B.PREVB |
Save old B for return |
| 4 | oldB.SP → B.SP |
Inherit caller's stack pointer! |
| 5 | return address → B.RETA → L |
Save return address |
| 6 | log size → B.LOG |
Store block size for RETB! |
| 7 | number of arguments → B.N |
Store argument count |
| 8 | addresses of arguments → B.ARG |
Copy argument addresses |
Memory Writes¶
| Address | Value Written | Description |
|---|---|---|
heapBlock + 0 |
oldB |
PREVB = previous B |
heapBlock + 4 |
return address |
RETA |
heapBlock + 8 |
oldB.SP |
SP = caller's stack pointer (inherited!) |
heapBlock + 12 |
<log size> |
LOG = block size (crucial for RETB!) |
heapBlock + 16 |
N |
Argument count |
heapBlock + 20+ |
argument addresses | Arguments |
Register Writes¶
| Register | Value Written | Notes |
|---|---|---|
| B | address of heap element |
From heap allocation |
| L | return address |
|
| TOS | (unchanged) | But TOS is read to find heap variables |
Key Difference: ENTB vs ENTS¶
| Aspect | ENTS | ENTB |
|---|---|---|
| New B comes from | oldB.SP (stack) |
Heap allocation |
| New B.SP value | newB + <stack demand> |
oldB.SP (inherited!) |
| AUX/LOG contains | Uninitialized | <log size> |
| TOS usage | Only for overflow check | Read to find heap |
| Return via | RET/RETK | RETB/RETBK |
Heap Allocation Process¶
flowchart TB
subgraph "ENTB 5 (allocate 2^5 = 32 words)"
direction TB
START["1. Read TOS register"]
READ_MAXL["2. Read MAXL at TOS+0"]
CHECK["3. Check if 5 ≤ MAXL"]
READ_FLOG["4. Read FLOG5 at TOS+32"]
CHECK_EMPTY{"FLOG5 = 0?"}
UNLINK["5a. Unlink block from FLOG5<br/>FLOG5 = block.next"]
CHECK_LARGER["5b. Check FLOG6, FLOG7, etc."]
SPLIT["6. Split larger block"]
INIT_FRAME["7. Initialize local data area<br/>at allocated block"]
UPDATE_B["8. B = block address"]
end
START --> READ_MAXL
READ_MAXL --> CHECK
CHECK --> READ_FLOG
READ_FLOG --> CHECK_EMPTY
CHECK_EMPTY -->|"No"| UNLINK
CHECK_EMPTY -->|"Yes"| CHECK_LARGER
CHECK_LARGER --> SPLIT
SPLIT --> UNLINK
UNLINK --> INIT_FRAME
INIT_FRAME --> UPDATE_B
Concrete Example: ENTB 5¶
Before state: - B = 0x10000, B.SP = 0x11000 - TOS = 0x15000 - FLOG5 (at TOS+32 = 0x15020) = 0x20000 (free 32-word block) - Block at 0x20000, word 0 = 0x20080 (next free block)
flowchart TB
subgraph "BEFORE ENTB 5"
direction LR
subgraph REGS1["Registers"]
B1["B = 0x10000"]
TOS1["TOS = 0x15000"]
end
subgraph HEAP1["Heap Variables at 0x15000"]
HV1["0x15020: FLOG5 = 0x20000"]
end
subgraph POOL1["Heap Pool"]
BLK1["0x20000: next = 0x20080"]
BLK2["0x20080: next = 0"]
end
end
subgraph "AFTER ENTB 5"
direction LR
subgraph REGS2["Registers"]
B2["B = 0x20000 (allocated block)"]
TOS2["TOS = 0x15000 (unchanged)"]
L2["L = return_addr"]
end
subgraph HEAP2["Heap Variables at 0x15000"]
HV2["0x15020: FLOG5 = 0x20080 (updated!)"]
end
subgraph FRAME2["New Frame at 0x20000"]
F1["0x20000: PREVB = 0x10000"]
F2["0x20004: RETA = return_addr"]
F3["0x20008: SP = 0x11000 (inherited!)"]
F4["0x2000C: LOG = 5"]
F5["0x20010: N = arg count"]
end
end
GETB Instruction: Get Buddy Element¶
Source: ND-05.009.4 EN, Section 15.13 (Page 280)
Assembly Format¶
Wn GETB <log size>
| Mnemonic | Hex code | Octal code |
|---|---|---|
| W1 GETB | 0FE4CH | 177114B |
| W2 GETB | 0FE4DH | 177115B |
| W3 GETB | 0FE4EH | 177116B |
| W4 GETB | 0FE4FH | 177117B |
Operation (from Manual)¶
"Allocate an element of size 2^
<log size>words from the heap." "If an element of the given size is available, it is removed from the freelist and its address is returned to the specified register. Otherwise the list is examined for larger elements. If none are available, a stack overflow trap condition occurs. If a larger element is found, it is removed from its freelist and chopped into halves until an element of the desired size can be allocated. The other half of the chopped element(s) will be added to the appropriate freelists."
TOS Requirement (from Manual)¶
"When executing the GETB instruction, the TOS register must point to the variables describing the heap."
GETB Algorithm Flowchart¶
flowchart TB
START["GETB <log size>"]
READ_TOS["Read TOS register"]
READ_MAXL["Read MAXL at TOS+0"]
CHECK_VALID{"log size ≤ MAXL?"}
TRAP1["Stack Overflow Trap!"]
INIT_SEARCH["current_log = log size"]
CHECK_FLOG{"FLOG[current_log] ≠ 0?"}
UNLINK["block = FLOG[current_log]<br/>FLOG[current_log] = block.next"]
NEED_SPLIT{"current_log > log size?"}
SPLIT["Split block in half<br/>buddy = block + 2^(current_log-1)<br/>buddy.next = FLOG[current_log-1]<br/>FLOG[current_log-1] = buddy<br/>current_log = current_log - 1"]
INCREMENT["current_log = current_log + 1"]
CHECK_MAX{"current_log ≤ MAXL?"}
DONE["Wn = block address<br/>Done!"]
START --> READ_TOS
READ_TOS --> READ_MAXL
READ_MAXL --> CHECK_VALID
CHECK_VALID -->|"No"| TRAP1
CHECK_VALID -->|"Yes"| INIT_SEARCH
INIT_SEARCH --> CHECK_FLOG
CHECK_FLOG -->|"Yes"| UNLINK
CHECK_FLOG -->|"No"| INCREMENT
INCREMENT --> CHECK_MAX
CHECK_MAX -->|"Yes"| CHECK_FLOG
CHECK_MAX -->|"No"| TRAP1
UNLINK --> NEED_SPLIT
NEED_SPLIT -->|"Yes"| SPLIT
SPLIT --> NEED_SPLIT
NEED_SPLIT -->|"No"| DONE
Splitting Example¶
Request: W3 GETB 4 (16 words), but only 64-word block available at FLOG6
flowchart TB
subgraph "Step 1: Initial State"
S1_FLOG4["FLOG4 = 0 (empty)"]
S1_FLOG5["FLOG5 = 0 (empty)"]
S1_FLOG6["FLOG6 = 0x30000"]
S1_BLK["Block 0x30000 (64 words)<br/>next = 0"]
end
subgraph "Step 2: Remove from FLOG6"
S2_FLOG6["FLOG6 = 0 (now empty)"]
S2_BLK["block = 0x30000 (64 words)"]
end
subgraph "Step 3: Split 64→32+32"
S3_BLK1["block = 0x30000 (32 words)"]
S3_BLK2["buddy = 0x30080 (32 words)"]
S3_FLOG5["FLOG5 = 0x30080"]
end
subgraph "Step 4: Split 32→16+16"
S4_BLK1["block = 0x30000 (16 words)"]
S4_BLK2["buddy = 0x30040 (16 words)"]
S4_FLOG4["FLOG4 = 0x30040"]
end
subgraph "Step 5: Return block"
S5_W3["W3 = 0x30000"]
S5_FLOG4["FLOG4 = 0x30040"]
S5_FLOG5["FLOG5 = 0x30080"]
S5_FLOG6["FLOG6 = 0"]
end
S1_FLOG6 --> S2_FLOG6
S2_BLK --> S3_BLK1
S3_BLK1 --> S4_BLK1
S4_BLK1 --> S5_W3
FREEB Instruction: Free Buddy Element¶
Source: ND-05.009.4 EN, Section 15.14 (Page 281)
Assembly Format¶
FREEB <log size>, <element>
| Mnemonic | Hex code | Octal code |
|---|---|---|
| FREEB | 0FDB6H | 176666B |
Operation (from Manual)¶
"The specified
<element>is appended to the appropriate freelist of the heap. Elements are not combined; this may be done by a trap handler for the stack overflow condition."
TOS Requirement (from Manual)¶
"When executing the FREEB instruction, the TOS register must point to the variables describing the heap."
FREEB Algorithm¶
flowchart TB
START["FREEB <log size>, <element>"]
READ_TOS["Read TOS register"]
READ_FLOG["old_head = FLOG[log size] at TOS+12+log_size×4"]
WRITE_NEXT["element.word0 = old_head"]
UPDATE_FLOG["FLOG[log size] = element"]
DONE["Done (block prepended to freelist)"]
START --> READ_TOS
READ_TOS --> READ_FLOG
READ_FLOG --> WRITE_NEXT
WRITE_NEXT --> UPDATE_FLOG
UPDATE_FLOG --> DONE
Memory Operations¶
| Step | Memory Address | Operation | Value |
|---|---|---|---|
| 1 | TOS + 12 + log_size × 4 |
Read | old FLOG head |
| 2 | <element> + 0 |
Write | old FLOG head (next pointer) |
| 3 | TOS + 12 + log_size × 4 |
Write | <element> address |
Important: No Automatic Coalescing!¶
"Elements are not combined; this may be done by a trap handler for the stack overflow condition."
The hardware does NOT merge buddies automatically. This is by design.
RETB/RETBK Instructions: Return from Buddy Subroutine¶
Source: ND-05.009.4 EN, Section 13.11 (Page 238-239)
Assembly Format¶
RETB ; Clear K flag on return
RETBK ; Set K flag on return (error return)
| Mnemonic | Hex code | Octal code | Description |
|---|---|---|---|
| RETB | 0FE1CH | 177034B | Buddy subroutine return |
| RETBK | 0FE1DH | 177035B | Buddy subroutine error return |
Description (from Manual)¶
"Return from subroutine using a heap element as local data area. The local data area is released to the heap described by the variables pointed at by the TOS register."
Trap Conditions (from Manual)¶
- Addressing traps
- Stack Underflow (STU)
- Branch Trap (BT)
Data Status Bits¶
Unaffected
Operation (Exact from Manual)¶
RETB:
Local data area released to heap
0 → STATUS.K
B.RETA → P → L
B.PREVB → B
RETBK:
Local data area released to heap
1 → STATUS.K
B.RETA → P → L
B.PREVB → B
Operation Sequence¶
| Step | Operation | Description |
|---|---|---|
| 1 | Read B.LOG at B + 12 |
Get block size for FREEB |
| 2 | Read B.PREVB at B + 0 |
Get return B value |
| 3 | Read B.RETA at B + 4 |
Get return address |
| 4 | FREEB(B.LOG, B) | Release block to heap |
| 5 | B.RETA → P |
Jump to return address |
| 6 | B.RETA → L |
Update L register |
| 7 | B.PREVB → B |
Restore previous B |
| 8 | 0/1 → STATUS.K |
Set/clear K flag |
RETB Visualization¶
flowchart TB
subgraph "BEFORE RETB"
direction LR
subgraph REGS1["Registers"]
B1["B = 0x20000 (heap block)"]
TOS1["TOS = 0x15000"]
end
subgraph FRAME1["Frame at 0x20000"]
F1["0x20000: PREVB = 0x10000"]
F2["0x20004: RETA = 0x8100"]
F3["0x20008: SP = 0x11000"]
F4["0x2000C: LOG = 5"]
end
subgraph HEAP1["FLOG5 at 0x15020"]
H1["FLOG5 = 0x20080"]
end
end
subgraph "AFTER RETB"
direction LR
subgraph REGS2["Registers"]
B2["B = 0x10000 (restored)"]
P2["P = 0x8100 (return addr)"]
L2["L = 0x8100"]
K2["STATUS.K = 0"]
TOS2["TOS = 0x15000 (unchanged)"]
end
subgraph HEAP2["FLOG5 at 0x15020"]
H2["FLOG5 = 0x20000 (block returned!)"]
end
subgraph FREED["Freed Block at 0x20000"]
FB1["0x20000: next = 0x20080"]
end
end
COMPLETE SYSTEM DIAGRAM¶
All Data Structures and Their Relationships¶
flowchart TB
subgraph CPU["CPU Registers"]
B["B Register"]
TOS["TOS Register"]
P["P (Program Counter)"]
L["L (Link Register)"]
end
subgraph MEM["Memory"]
subgraph STACK["Stack Area"]
FRAME0["Main Frame<br/>(from INIT)"]
FRAME1["Subroutine Frame 1<br/>(from ENTS)"]
FRAME2["Subroutine Frame 2<br/>(from ENTS)"]
end
subgraph HEAPVARS["Heap Variables"]
MAXL["MAXL"]
STAH["STAH"]
ENDH["ENDH"]
FLOG0["FLOG0"]
FLOG1["FLOG1"]
FLOGN["FLOG..."]
end
subgraph HEAPPOOL["Heap Pool"]
BUDDY1["Buddy Frame<br/>(from ENTB)"]
FREE1["Free Block"]
FREE2["Free Block"]
end
end
B -->|"points to current frame"| FRAME2
TOS -->|"points to"| MAXL
FRAME2 -->|"PREVB"| FRAME1
FRAME1 -->|"PREVB"| FRAME0
FLOG1 -->|"head"| FREE1
FREE1 -->|"next"| FREE2
BUDDY1 -->|"PREVB"| FRAME1
style TOS fill:#FFD700
style B fill:#90EE90
style HEAPVARS fill:#FFE4B5
style HEAPPOOL fill:#E0FFE0
Memory Address Flow Example¶
flowchart TB
subgraph "Complete Memory Map Example"
direction TB
A1["0x10000: Main Frame (INIT)"]
A2["├─ PREVB = 0"]
A3["├─ RETA = 0"]
A4["└─ SP = 0x11000"]
B1["0x11000: Sub1 Frame (ENTS 0x200)"]
B2["├─ PREVB = 0x10000"]
B3["├─ RETA = addr1"]
B4["└─ SP = 0x11200"]
C1["0x11200: Sub2 Frame (ENTS 0x100)"]
C2["├─ PREVB = 0x11000"]
C3["├─ RETA = addr2"]
C4["└─ SP = 0x11300 ← Current B.SP"]
D1["0x11300 to 0x14FFF: Free Stack"]
E1["0x15000: Heap Variables ← TOS"]
E2["├─ MAXL = 7"]
E3["├─ STAH = 0x20000"]
E4["├─ ENDH = 0x40000"]
E5["├─ FLOG0 = 0"]
E6["├─ FLOG5 = 0x20000"]
E7["└─ FLOG6 = 0x25000"]
F1["0x20000: Free 32-word block"]
F2["└─ next = 0x20080"]
G1["0x20080: Free 32-word block"]
G2["└─ next = 0"]
H1["0x25000: Free 64-word block"]
H2["└─ next = 0"]
I1["0x30000: Buddy Frame (ENTB 5)"]
I2["├─ PREVB = 0x11200"]
I3["├─ RETA = addr3"]
I4["├─ SP = 0x11300 (inherited!)"]
I5["└─ LOG = 5 ← B points here"]
end
A1 --> B1
B1 --> C1
C1 --> D1
D1 --> E1
E1 --> F1
F1 --> G1
G1 --> H1
H1 --> I1
style E1 fill:#FFD700
style I1 fill:#90EE90
EMULATOR IMPLEMENTATION¶
Data Structure Definitions (TypeScript)¶
// Constants from ND-500 manual
const OFFSET_PREVB = 0; // bytes
const OFFSET_RETA = 4; // bytes
const OFFSET_SP = 8; // bytes
const OFFSET_AUX_LOG = 12; // bytes
const OFFSET_N = 16; // bytes
const OFFSET_ARGS = 20; // bytes
// Heap variable offsets from TOS
const HEAP_OFFSET_MAXL = 0; // bytes
const HEAP_OFFSET_STAH = 4; // bytes
const HEAP_OFFSET_ENDH = 8; // bytes
const HEAP_OFFSET_FLOG0 = 12; // bytes
// Calculate FLOG offset: TOS + 12 + (logSize * 4)
function getFlogOffset(logSize: number): number {
return HEAP_OFFSET_FLOG0 + (logSize * 4);
}
Memory Access Helpers¶
class ND500Memory {
private memory: DataView;
// Read 32-bit word at byte address
readWord(address: number): number {
return this.memory.getUint32(address, false); // big-endian
}
// Write 32-bit word at byte address
writeWord(address: number, value: number): void {
this.memory.setUint32(address, value, false); // big-endian
}
// Read local data area field
readLocalField(B: number, offset: number): number {
return this.readWord(B + offset);
}
// Write local data area field
writeLocalField(B: number, offset: number, value: number): void {
this.writeWord(B + offset, value);
}
// Read heap variable
readHeapVar(TOS: number, offset: number): number {
return this.readWord(TOS + offset);
}
// Write heap variable
writeHeapVar(TOS: number, offset: number, value: number): void {
this.writeWord(TOS + offset, value);
}
// Read FLOG[n]
readFLOG(TOS: number, logSize: number): number {
return this.readHeapVar(TOS, getFlogOffset(logSize));
}
// Write FLOG[n]
writeFLOG(TOS: number, logSize: number, value: number): void {
this.writeHeapVar(TOS, getFlogOffset(logSize), value);
}
}
INIT Implementation¶
executeINIT(bottomOfStack: number, mainStackDemand: number,
totalStackDemand: number): void {
// Trap check: main demand must be less than total
if (mainStackDemand >= totalStackDemand) {
this.triggerTrap(TRAP_STACK_OVERFLOW);
return;
}
// Operation sequence from manual:
// <<bottom of stack>> -> B
this.registers.B = bottomOfStack;
// <<bottom of stack>> + <total system stack demand> -> TOS
this.registers.TOS = bottomOfStack + totalStackDemand;
// <<bottom of stack>> + <stack demand of main program> -> B.SP
this.memory.writeLocalField(this.registers.B, OFFSET_SP,
bottomOfStack + mainStackDemand);
// 0 -> B.PREVB
this.memory.writeLocalField(this.registers.B, OFFSET_PREVB, 0);
// 0 -> B.RETA -> L
this.memory.writeLocalField(this.registers.B, OFFSET_RETA, 0);
this.registers.L = 0;
}
ENTS Implementation¶
executeENTS(stackDemand: number, returnAddress: number,
argCount: number, argAddresses: number[]): void {
// Save old values
const oldB = this.registers.B;
const oldSP = this.memory.readLocalField(oldB, OFFSET_SP);
// Trap check: new SP must be less than TOS
const newSP = oldSP + stackDemand;
if (newSP >= this.registers.TOS) {
this.triggerTrap(TRAP_STACK_OVERFLOW);
return;
}
// Operation sequence from manual:
// B.SP -> B (new B = old stack pointer)
const newB = oldSP;
this.registers.B = newB;
// oldB -> B.PREVB
this.memory.writeLocalField(newB, OFFSET_PREVB, oldB);
// return address -> B.RETA -> L
this.memory.writeLocalField(newB, OFFSET_RETA, returnAddress);
this.registers.L = returnAddress;
// newB + <stack demand> -> B.SP
this.memory.writeLocalField(newB, OFFSET_SP, newSP);
// number of arguments -> B.N
this.memory.writeLocalField(newB, OFFSET_N, argCount);
// addresses of arguments -> B.ARG
for (let i = 0; i < argCount; i++) {
this.memory.writeWord(newB + OFFSET_ARGS + (i * 4), argAddresses[i]);
}
// Note: TOS is NOT modified
}
ENTB Implementation¶
executeENTB(logSize: number, returnAddress: number,
argCount: number, argAddresses: number[]): void {
// Allocate block from heap (this uses TOS internally)
const blockAddress = this.allocateFromHeap(logSize);
if (blockAddress === 0) {
// Allocation failed - trap already triggered
return;
}
// Save old values
const oldB = this.registers.B;
const oldSP = this.memory.readLocalField(oldB, OFFSET_SP);
// Operation sequence from manual:
// address of heap element -> B
this.registers.B = blockAddress;
// oldB -> B.PREVB
this.memory.writeLocalField(blockAddress, OFFSET_PREVB, oldB);
// oldB.SP -> B.SP (INHERITED - key difference from ENTS!)
this.memory.writeLocalField(blockAddress, OFFSET_SP, oldSP);
// return address -> B.RETA -> L
this.memory.writeLocalField(blockAddress, OFFSET_RETA, returnAddress);
this.registers.L = returnAddress;
// log size -> B.LOG (crucial for RETB!)
this.memory.writeLocalField(blockAddress, OFFSET_AUX_LOG, logSize);
// number of arguments -> B.N
this.memory.writeLocalField(blockAddress, OFFSET_N, argCount);
// addresses of arguments -> B.ARG
for (let i = 0; i < argCount; i++) {
this.memory.writeWord(blockAddress + OFFSET_ARGS + (i * 4), argAddresses[i]);
}
}
// Helper: Allocate from heap using buddy system
private allocateFromHeap(logSize: number): number {
const TOS = this.registers.TOS;
const MAXL = this.memory.readHeapVar(TOS, HEAP_OFFSET_MAXL);
// Check valid size
if (logSize > MAXL) {
this.triggerTrap(TRAP_STACK_OVERFLOW);
return 0;
}
// Search for available block
let currentLog = logSize;
while (currentLog <= MAXL) {
const head = this.memory.readFLOG(TOS, currentLog);
if (head !== 0) {
// Found a block - unlink from freelist
const nextBlock = this.memory.readWord(head);
this.memory.writeFLOG(TOS, currentLog, nextBlock);
// Split if we got a larger block
while (currentLog > logSize) {
currentLog--;
const blockSizeWords = Math.pow(2, currentLog);
const blockSizeBytes = blockSizeWords * 4;
const buddyAddress = head + blockSizeBytes;
// Add buddy to freelist
const oldHead = this.memory.readFLOG(TOS, currentLog);
this.memory.writeWord(buddyAddress, oldHead);
this.memory.writeFLOG(TOS, currentLog, buddyAddress);
}
return head;
}
currentLog++;
}
// No block found
this.triggerTrap(TRAP_STACK_OVERFLOW);
return 0;
}
GETB Implementation¶
executeGETB(logSize: number, destRegister: number): void {
const blockAddress = this.allocateFromHeap(logSize);
if (blockAddress !== 0) {
// Store address in destination register (W1-W4)
this.registers.W[destRegister] = blockAddress;
}
// If allocation failed, trap was already triggered
}
FREEB Implementation¶
executeFREEB(logSize: number, elementAddress: number): void {
const TOS = this.registers.TOS;
// Get current freelist head
const oldHead = this.memory.readFLOG(TOS, logSize);
// Prepend element to freelist
// element.word0 = old head (next pointer)
this.memory.writeWord(elementAddress, oldHead);
// FLOG[logSize] = element
this.memory.writeFLOG(TOS, logSize, elementAddress);
// Note: NO automatic coalescing per manual
}
RETB/RETBK Implementation¶
executeRETB(setKFlag: boolean): void {
const B = this.registers.B;
// Read local data area fields
const prevB = this.memory.readLocalField(B, OFFSET_PREVB);
const retAddr = this.memory.readLocalField(B, OFFSET_RETA);
const logSize = this.memory.readLocalField(B, OFFSET_AUX_LOG);
// Check for stack underflow (return past bottom)
if (prevB === 0 && retAddr === 0) {
this.handleStackUnderflow();
return;
}
// Release block to heap
this.executeFREEB(logSize, B);
// Operation sequence from manual:
// B.RETA -> P -> L
this.registers.P = retAddr;
this.registers.L = retAddr;
// B.PREVB -> B
this.registers.B = prevB;
// Set/clear K flag
this.registers.STATUS_K = setKFlag ? 1 : 0;
}
REFERENCE TABLES¶
Instruction Summary¶
| Instruction | Opcode (Hex) | Opcode (Octal) | TOS Read | TOS Write | Heap Access |
|---|---|---|---|---|---|
| INIT | 0DCH | 334B | No | Yes | No |
| ENTS | 0B8H | 270B | Yes (check) | No | No |
| ENTB | 0BDH | 275B | Yes | No | Read+Write |
| RETB | 0FE1CH | 177034B | Yes | No | Write |
| RETBK | 0FE1DH | 177035B | Yes | No | Write |
| Wn GETB | 0FE4C+(n-1) | 177114B+(n-1) | Yes | No | Read+Write |
| FREEB | 0FDB6H | 176666B | Yes | No | Write |
Memory Offset Summary¶
Local Data Area (B register base)¶
| Offset (bytes) | Symbol | Written by INIT | Written by ENTS | Written by ENTB |
|---|---|---|---|---|
| 0 | PREVB | 0 | oldB | oldB |
| 4 | RETA | 0 | return addr | return addr |
| 8 | SP | bottom + main_demand | newB + stack_demand | oldB.SP |
| 12 | AUX/LOG | - | - | log size |
| 16 | N | - | arg count | arg count |
| 20+ | args | - | arg addresses | arg addresses |
Heap Variables (TOS register base)¶
| Offset (bytes) | Symbol | Description |
|---|---|---|
| 0 | MAXL | Maximum log size |
| 4 | STAH | Start of heap (informational) |
| 8 | ENDH | End of heap (informational) |
| 12 | FLOG0 | Freelist for 1-word blocks |
| 16 | FLOG1 | Freelist for 2-word blocks |
| 12 + n×4 | FLOGn | Freelist for 2^n-word blocks |
References¶
- ND-05.009.4 EN: ND-500 Reference Manual
- Section 3.3: Heap Allocation (Pages 34-35)
- Section 13.9: Initialize Stack - INIT (Page 229)
- Section 13.10: Subroutine Entry Points - ENTS, ENTB (Pages 233, 237)
- Section 13.11: Subroutine Return - RETB, RETBK (Pages 238-239)
- Section 15.13: Get Buddy Element - GETB (Page 280)
- Section 15.14: Free Buddy Element - FREEB (Page 281)
- ND-60.113.02 EN: ND-500 Assembler Reference Manual
- Buddy instruction opcodes (Page 84)
CRITICAL: MEMORY ALLOCATION AND INITIALIZATION¶
Who Allocates the Stack Memory?¶
The CPU does NOT allocate stack memory. The stack lives in a memory segment that must be set up BEFORE INIT is called.
SINTRAN Monitor Call: GSWSP (MON 422B / 274 decimal)¶
The operating system (SINTRAN) allocates memory segments via monitor calls. The key call for stack/heap allocation is:
| Field | Value |
|---|---|
| MON Number | 422B (octal) / 274 (decimal) |
| Name | GSWSP |
| Long Name | GetScratchSegment |
| Description | Connects an empty data segment to the user's domain and reserves space for it on the swap file |
Parameters:
| Direction | Name | Type | Description |
|---|---|---|---|
| [I] | SizeInBytes | INTEGER | Segment size in bytes |
| [I] | LogSegmentNo | INTEGER | Logical segment number to use (0 = system selects) |
| [O] | RetLogSegmentNo | INTEGER | Returns the logical segment number actually selected |
Complete Stack/Heap Setup Sequence¶
sequenceDiagram
participant Program as User Program
participant OS as SINTRAN (OS)
participant MMU as Memory Management
participant CPU as CPU
participant Memory as Physical Memory
Note over Program,Memory: PHASE 1: Segment Allocation (OS Level)
Program->>OS: MON 422B (GSWSP)<br/>SizeInBytes = 0x50000<br/>LogSegmentNo = 0
OS->>MMU: Allocate segment entry in PST
OS->>MMU: Set up capability in domain table
OS->>Memory: Reserve swap space
OS->>Program: RetLogSegmentNo = 5 (example)
Note over Program,Memory: Segment 5 now exists, mapped to logical address 0x0A000000
Note over Program,Memory: PHASE 2: Stack Initialization (CPU Level)
Program->>CPU: INIT 0x0A010000, 0x1000, 0x5000
CPU->>Memory: Write PREVB=0, RETA=0, SP
CPU->>CPU: B = 0x0A010000
CPU->>CPU: TOS = 0x0A015000
CPU->>CPU: L = 0
Note over Program,Memory: PHASE 3: Heap Variable Setup (User Code)
Program->>Memory: Write MAXL at TOS+0
Program->>Memory: Write STAH, ENDH
Program->>Memory: Write FLOG0..FLOGn
Program->>Memory: Link free blocks
Note over Program,Memory: NOW ready for ENTS, ENTB, GETB, FREEB!
Address Calculation Example¶
If GSWSP returns segment 5, and ND-500 uses 5-bit segment numbers in the high bits:
| Component | Bits | Value |
|---|---|---|
| Segment number | 31:27 | 5 (binary: 00101) |
| Segment offset | 26:0 | 0x0000000 (start of segment) |
| Logical address | 31:0 | 0x0A000000 |
Then for INIT:
- <<bottom of stack>> = 0x0A010000 (offset 0x10000 into segment)
- TOS will be at 0x0A015000
- Heap pool might start at 0x0A020000
Memory Hierarchy¶
flowchart TB
subgraph "Memory Allocation Responsibility"
direction TB
OS["Operating System (SINTRAN)"]
SEG["Allocates SEGMENTS to processes"]
DOM["Sets up DOMAINS with capability tables"]
PROG["User Program / Compiler"]
INIT_CALL["Calls INIT with addresses within allocated segment"]
HEAP_INIT["Initializes heap variables at TOS"]
CPU["CPU Hardware"]
REGS["Sets B, TOS, B.SP registers"]
WRITES["Writes PREVB=0, RETA=0 to memory"]
OS --> SEG
SEG --> DOM
DOM --> PROG
PROG --> INIT_CALL
INIT_CALL --> CPU
CPU --> REGS
REGS --> WRITES
WRITES --> PROG
PROG --> HEAP_INIT
end
The Address Translation System¶
The <<bottom of stack>> address in INIT is a logical address (32-bit). The memory management system translates it:
flowchart LR
subgraph "Logical Address (32 bits)"
SEG_NUM["Segment<br/>5 bits"]
SEG_ADDR["Segment-relative address<br/>27 bits"]
end
subgraph "Translation"
CAP["Capability Table<br/>(in Domain Info Table)"]
PST["Physical Segment Table"]
IDX["Index Tables A/B"]
end
subgraph "Physical Address"
PHYS["Physical page + offset"]
RAM["Actual RAM location"]
end
SEG_NUM --> CAP
CAP --> PST
SEG_ADDR --> IDX
PST --> IDX
IDX --> PHYS
PHYS --> RAM
What Happens at Each Level¶
| Level | Responsibility | When |
|---|---|---|
| Operating System | Allocates physical memory, creates segments, sets up PST entries, creates domain with capability tables | Before program starts |
| Loader/Runtime | Knows segment addresses, calculates stack bottom address | Program load time |
| INIT instruction | Sets B, TOS, B.SP registers; writes PREVB=0, RETA=0 to memory | Runtime |
| User code | Initializes heap variables at TOS | After INIT, before buddy instructions |
What Initializes Memory at TOS? (Heap Variables)¶
Critical: The CPU Does NOT Initialize Heap Variables!¶
"The heap variables must be initialized by the user program and the user is responsible for building the lists." — ND-05.009.4 EN, Section 3.3
INIT only does these things:
1. Sets B = <<bottom of stack>>
2. Sets TOS = <<bottom of stack>> + <total system stack demand>
3. Sets B.SP = <<bottom of stack>> + <stack demand of main program>
4. Writes 0 to B.PREVB (memory at B+0)
5. Writes 0 to B.RETA (memory at B+4)
6. Sets L = 0
INIT does NOT: - Initialize memory at TOS - Set up MAXL - Set up STAH or ENDH - Create any freelists (FLOGs) - Allocate any heap blocks
Heap Variable Initialization Sequence¶
sequenceDiagram
participant OS as Operating System
participant Loader as Program Loader
participant Program as User Program
participant CPU as CPU
participant Memory as Memory
OS->>Memory: Allocate segment for stack/heap
OS->>Loader: Load program into segment
Loader->>Program: Start execution
Program->>CPU: INIT bottom, main_demand, total_demand
CPU->>Memory: Write PREVB=0, RETA=0, SP
CPU->>CPU: Set B, TOS, L registers
Note over Program,Memory: INIT complete - TOS points to uninitialized memory!
Program->>Memory: Write MAXL at TOS+0
Program->>Memory: Write STAH at TOS+4
Program->>Memory: Write ENDH at TOS+8
Program->>Memory: Write FLOG0 at TOS+12
Program->>Memory: Write FLOG1 at TOS+16
Program->>Memory: ... (initialize all FLOGs)
Program->>Memory: Build initial freelist (link free blocks)
Note over Program,Memory: NOW heap is ready for GETB/ENTB!
Example: Complete Stack and Heap Setup¶
; Assume OS allocated segment 5 for our data, starting at logical address 0x00000000
; We want:
; - Stack from 0x10000 to 0x14FFF (main program uses 0x1000 bytes)
; - Heap variables at 0x15000
; - Heap pool from 0x20000 to 0x40000
; Step 1: Initialize stack (CPU does this)
INIT 0x10000, 0x1000, 0x5000 ; B=0x10000, TOS=0x15000, B.SP=0x11000
; Step 2: Initialize heap variables (USER CODE must do this!)
W1 := 7 ; MAXL = 7 (max 128 words)
W1 =: IND(TOS) ; Write to TOS+0
W1 := 0x20000 ; STAH = heap pool start
W1 =: IND(TOS+4) ; Write to TOS+4
W1 := 0x40000 ; ENDH = heap pool end
W1 =: IND(TOS+8) ; Write to TOS+8
; Initialize all FLOGs to 0 (empty)
W1 := 0
W1 =: IND(TOS+12) ; FLOG0 = 0
W1 =: IND(TOS+16) ; FLOG1 = 0
W1 =: IND(TOS+20) ; FLOG2 = 0
W1 =: IND(TOS+24) ; FLOG3 = 0
W1 =: IND(TOS+28) ; FLOG4 = 0
W1 =: IND(TOS+32) ; FLOG5 = 0
W1 =: IND(TOS+36) ; FLOG6 = 0
; Initialize FLOG7 with first free block
W1 := 0x20000 ; Address of first 128-word block
W1 =: IND(TOS+40) ; FLOG7 = 0x20000
; Initialize free block linked list
W1 := 0x20200 ; Address of second 128-word block
W1 =: IND(0x20000) ; First block points to second
W1 := 0 ; End of list
W1 =: IND(0x20200) ; Second block points to NULL
; NOW the buddy system is ready to use!
Stack Allocation and Freeing¶
How Stack Frames Are Allocated (ENTS)¶
Stack frames are allocated by advancing B.SP:
flowchart TB
subgraph "ENTS Stack Allocation"
direction TB
BEFORE["BEFORE: B.SP = 0x11000"]
ENTS["ENTS 0x100 (256 bytes)"]
AFTER["AFTER:<br/>new B = old B.SP = 0x11000<br/>new B.SP = 0x11100"]
CHECK{"new B.SP < TOS?"}
OK["Allocation succeeds"]
TRAP["STO Trap!"]
end
BEFORE --> ENTS
ENTS --> CHECK
CHECK -->|"Yes"| OK
CHECK -->|"No"| TRAP
Key insight: Stack allocation is just pointer arithmetic! The memory already exists (allocated by OS).
How Stack Frames Are Freed (RET)¶
Stack frames are freed by restoring B from PREVB:
flowchart TB
subgraph "RET Stack Deallocation"
direction TB
BEFORE["BEFORE:<br/>B = 0x11000<br/>B.PREVB = 0x10000"]
RET["RET"]
AFTER["AFTER:<br/>B = 0x10000 (from PREVB)<br/>(old frame is now 'free')"]
end
BEFORE --> RET --> AFTER
Key insight: Stack "freeing" is just restoring the B register! The memory isn't zeroed or returned to OS - it's just available for the next ENTS to overwrite.
Stack vs Heap: Memory Lifecycle Comparison¶
| Aspect | Stack (ENTS/RET) | Heap (ENTB/RETB, GETB/FREEB) |
|---|---|---|
| Allocation | Advance B.SP pointer | Unlink from freelist |
| Deallocation | Restore B from PREVB | Prepend to freelist |
| Order constraint | LIFO (last in, first out) | Any order |
| Memory source | Contiguous area below TOS | Blocks from heap pool |
| Overflow check | B.SP ≥ TOS | Freelist empty |
| Memory reuse | Automatic (LIFO) | Requires FREEB |
Memory Layout: The Complete Picture¶
flowchart TB
subgraph "Complete Memory Layout"
direction TB
subgraph SEG["Data Segment (allocated by OS)"]
direction TB
LOW["Low addresses"]
subgraph STATIC["Static Data Area"]
SD["Global variables<br/>Constants<br/>Static arrays"]
end
subgraph STACK["Stack Area"]
MAIN_FRAME["Main frame (B after INIT)"]
SUB_FRAMES["Subroutine frames<br/>(grow upward via ENTS)"]
FREE_STACK["Free stack space"]
end
TOS_LINE["═══ TOS ═══"]
subgraph HEAP_VARS["Heap Variables"]
HV["MAXL, STAH, ENDH<br/>FLOG0..FLOG<MAXL>"]
end
subgraph HEAP_POOL["Heap Pool"]
BUDDY_FRAMES["Buddy frames (ENTB)"]
FREE_BLOCKS["Free blocks (linked lists)"]
GETB_BLOCKS["GETB allocated blocks"]
end
HIGH["High addresses"]
end
end
LOW --> STATIC
STATIC --> STACK
STACK --> TOS_LINE
TOS_LINE --> HEAP_VARS
HEAP_VARS --> HEAP_POOL
HEAP_POOL --> HIGH
style TOS_LINE fill:#FFD700,stroke:#000,stroke-width:3px
For Emulator Developers: Address Space Handling¶
Option 1: Flat Memory Array (Simple Emulator)¶
class SimpleND500Memory {
// Single flat array - ignore segments/domains
private memory: Uint8Array;
constructor(size: number) {
this.memory = new Uint8Array(size);
}
// Direct address access
readWord(address: number): number {
return (this.memory[address] << 24) |
(this.memory[address + 1] << 16) |
(this.memory[address + 2] << 8) |
this.memory[address + 3];
}
}
Option 2: Segmented Memory (Accurate Emulator)¶
class SegmentedND500Memory {
private segments: Map<number, Uint8Array>;
private capabilityTable: Map<number, number>; // segment -> PST entry
// Logical address translation
readWord(logicalAddress: number): number {
const segmentNum = (logicalAddress >> 27) & 0x1F;
const offset = logicalAddress & 0x07FFFFFF;
const segment = this.segments.get(segmentNum);
if (!segment) {
this.triggerTrap(TRAP_ADDRESS_FETCH);
return 0;
}
// Check bounds, permissions, etc.
return this.readWordFromSegment(segment, offset);
}
}
What the Emulator Must Track¶
| Component | Description |
|---|---|
| B register | Current local data area base |
| TOS register | Points to heap variables; stack overflow boundary |
| Memory array | The actual memory contents |
| Segment tables (optional) | For accurate memory protection emulation |
DEEP ANALYSIS AND INSIGHTS¶
Status Bits Affected by Buddy Instructions¶
The ND-500 status register has 64 bits. The buddy instructions affect specific status bits:
STO - Stack Overflow Status Bit¶
"The STO status bit is set/reset for each ENTS, ENTSN, ENTB, INIT, ENTM and GETB instruction." — ND-05.009.4 EN, Section 6.5.3.1
| Instruction | STO is SET when... |
|---|---|
| INIT | <stack demand of main program> ≥ <total system stack demand> |
| ENTM | <stack demand of main program> ≥ <total system stack demand> |
| ENTS/ENTSN | B.SP (new value) ≥ TOS |
| ENTB | No free block of requested size or larger exists |
| GETB | No free block of requested size or larger exists, OR <log size> > MAXL |
STU - Stack Underflow Status Bit¶
"Performing a subroutine return instruction with RETA, PREVB or both equal to zero leads to a STack Underflow trap condition if there is no alternative domain (CAD zero or equal to CED)." — ND-05.009.4 EN, Section 6.5.3.1
| Instruction | STU is SET when... |
|---|---|
| RET/RETK | B.RETA = 0 OR B.PREVB = 0, AND no alternative domain |
| RETB/RETBK | B.RETA = 0 OR B.PREVB = 0, AND no alternative domain |
BT - Branch Trap Status Bit¶
RETB and RETBK can trigger BT (Branch Trap) because they change the program counter to a non-sequential address.
Complete Status Bits Summary¶
| Instruction | Trap Conditions | Status Bits Affected |
|---|---|---|
| INIT | STO, Addressing | STO set/reset |
| ENTS | STO, ISE, Addressing | STO set/reset |
| ENTB | STO, ISE, Addressing | STO set/reset |
| GETB | STO, Addressing | STO set/reset |
| FREEB | Addressing | None (data status unaffected) |
| RETB/RETBK | STU, BT, Addressing | K flag set/cleared |
Stack Overflow Trap Handler Use Case¶
The manual explicitly describes using the stack overflow trap handler for heap management:
"The STAH and ENDH variables are not used by the heap instructions, but are available for a heap administration routine implemented as a trap handler for the stack overflow trap." — ND-05.009.4 EN, Section 3.3
"The stack overflow trap is used to signal that all lists containing blocks of wanted size or larger are empty." — ND-05.009.4 EN, Section 3.3
Trap Handler Strategy¶
flowchart TB
subgraph "Stack Overflow Trap Handler for Buddy System"
TRAP["STO Trap Triggered"]
CHECK["Check: Was it GETB/ENTB?"]
CHECK -->|"Yes"| ANALYZE["Analyze heap state"]
CHECK -->|"No (ENTS)"| STACK_FULL["Stack is full - error"]
ANALYZE --> OPTION1["Option 1: Coalesce buddies"]
ANALYZE --> OPTION2["Option 2: Allocate more from STAH-ENDH"]
ANALYZE --> OPTION3["Option 3: Return error to caller"]
OPTION1 --> RETRY["Retry allocation"]
OPTION2 --> RETRY
OPTION3 --> FAIL["Signal failure"]
RETRY --> SUCCESS{"Success?"}
SUCCESS -->|"Yes"| RETURN["Return to instruction"]
SUCCESS -->|"No"| FAIL
end
Coalescing Buddies in Trap Handler¶
Since FREEB does NOT automatically combine buddies, a trap handler can implement coalescing:
flowchart TB
subgraph "Buddy Coalescing Algorithm"
START["For each FLOG[n] from 0 to MAXL-1"]
SCAN["Scan freelist for adjacent buddies"]
FOUND{"Found two<br/>buddies?"}
REMOVE["Remove both from FLOG[n]"]
COMBINE["Combine into one block<br/>size = 2^(n+1)"]
ADD["Add to FLOG[n+1]"]
NEXT["Next FLOG level"]
DONE["Done coalescing"]
end
START --> SCAN
SCAN --> FOUND
FOUND -->|"Yes"| REMOVE
REMOVE --> COMBINE
COMBINE --> ADD
ADD --> SCAN
FOUND -->|"No"| NEXT
NEXT --> DONE
Buddy Address Calculation¶
Two blocks are buddies if: 1. They are the same size (2^n words) 2. They were originally split from the same parent block
Buddy address formula:
buddy_address = block_address XOR (block_size_in_bytes)
Example: Block at 0x20000, size 32 words (128 bytes = 0x80):
buddy = 0x20000 XOR 0x80 = 0x20080
Co-routine Warning: Deep Analysis¶
"If ENTB is used to allocate space for co-routines, care should be exercised if the called routines make further calls to stack routines. When co-routines use a common stack and a second co-routine is activated before the return, the stack areas will overlap because B.SP is the same in both routines." — ND-05.009.4 EN, Section 3.3
The Problem Visualized¶
flowchart TB
subgraph "The Co-routine Stack Overlap Problem"
direction TB
MAIN["Main Program<br/>B = 0x10000<br/>B.SP = 0x11000"]
CO1["Co-routine 1 (ENTB 5)<br/>B = 0x20000 (heap)<br/>B.SP = 0x11000 (inherited!)"]
CO2["Co-routine 2 (ENTB 5)<br/>B = 0x20080 (heap)<br/>B.SP = 0x11000 (SAME!)"]
STACK1["Co-routine 1 calls ENTS<br/>New frame at 0x11000"]
STACK2["Co-routine 2 calls ENTS<br/>New frame at 0x11000<br/>OVERWRITES Co-routine 1's frame!"]
CRASH["DATA CORRUPTION!"]
end
MAIN --> CO1
MAIN --> CO2
CO1 --> STACK1
CO2 --> STACK2
STACK1 --> CRASH
STACK2 --> CRASH
style CRASH fill:#ff0000,color:#ffffff
Why This Happens¶
- ENTB inherits
oldB.SPinstead of advancing it - Both co-routines have the same B.SP value
- If either calls an ENTS routine, it will allocate stack at B.SP
- The second co-routine's ENTS call overwrites the first's stack frame
Solutions (from Manual)¶
"No problems will occur if all routines in the system are entered through ENTB or if the stack routine is certain to terminate before another co-routine is activated. (Standard library routines may be used freely; they will not cause activation of other co-routines.)"
| Solution | Description |
|---|---|
| All ENTB | Use only buddy allocation for all routines |
| Sequential execution | Ensure stack routines complete before switching co-routines |
| Separate stacks | Give each co-routine its own stack (different TOS) |
Design Decisions and Rationale¶
Why No Automatic Coalescing?¶
The buddy system deliberately does NOT coalesce blocks in hardware:
"A released element will be linked to the appropriate freelist according to the size of the element. Elements are not combined; this may be done by the trap handler for the stack overflow trap condition."
Rationale: 1. Performance: FREEB is O(1) - just prepend to list 2. Flexibility: Trap handler can implement custom coalescing policy 3. Simplicity: Hardware implementation is simpler 4. Lazy coalescing: Only coalesce when actually needed (out of memory)
Why Power-of-2 Block Sizes?¶
All buddy block sizes are 2^n:
Rationale: 1. Fast splitting: Divide by 2 is a right-shift 2. Easy buddy calculation: XOR with size gives buddy address 3. Alignment: Natural word/cache line alignment 4. Simple freelists: Fixed number of lists (MAXL + 1)
Why Inherit B.SP in ENTB?¶
ENTB sets newB.SP = oldB.SP instead of advancing:
Rationale: 1. Heap blocks aren't on the stack: No need to reserve stack space 2. Stack can be shared: Multiple buddy routines can share common stack 3. Independence: Buddy routines don't consume stack space
Edge Cases and Error Handling¶
Edge Case 1: MAXL = 0¶
If MAXL = 0, only 1-word blocks can be allocated. - GETB 0 works (if FLOG0 has blocks) - GETB 1 triggers STO trap (log size > MAXL)
Edge Case 2: Empty Heap¶
If all FLOGs are 0: - GETB/ENTB immediately triggers STO trap - Trap handler must add blocks or fail
Edge Case 3: Fragmentation¶
After many allocations/frees without coalescing: - Many small blocks, no large blocks - GETB for large size fails even though total free memory is sufficient - Trap handler should coalesce
Edge Case 4: Corrupted LOG Value¶
If B.LOG (offset 12) is corrupted before RETB: - RETB will FREEB with wrong size - Block added to wrong freelist - Memory corruption!
Emulator recommendation: Validate LOG value is ≤ MAXL before FREEB.
Edge Case 5: RETB After RET¶
If programmer mistakenly uses RETB after ENTS: - Stack frame address (not heap) added to freelist - Future GETB returns stack address as "free block" - Severe memory corruption!
Emulator recommendation: Track which frames are heap vs stack.
Performance Characteristics¶
Time Complexity¶
| Operation | Best Case | Worst Case | Notes |
|---|---|---|---|
| GETB (exact size available) | O(1) | O(1) | Just unlink from freelist |
| GETB (need splitting) | O(1) | O(MAXL) | May split up to MAXL times |
| FREEB | O(1) | O(1) | Just prepend to freelist |
| ENTB | Same as GETB | Same as GETB | Allocates then initializes |
| RETB | O(1) | O(1) | FREEB then return |
Memory Overhead¶
| Overhead Type | Amount | Notes |
|---|---|---|
| Per heap | 12 + (MAXL+1)×4 bytes | Heap variables at TOS |
| Per free block | 0 extra | First word reused as next pointer |
| Per allocated block | 0 extra | Size stored in B.LOG, not block |
| Internal fragmentation | Up to 50% | Power-of-2 rounding |
Memory Utilization¶
Worst case internal fragmentation: Request 2^n + 1 words → Allocate 2^(n+1) words → ~50% waste
Emulator Implementation Checklist¶
Required State¶
- [ ] TOS register (points to heap variables)
- [ ] B register (points to current local data area)
- [ ] Memory array (for heap pool and stack)
- [ ] STATUS.STO bit
- [ ] STATUS.STU bit
- [ ] STATUS.K flag
INIT Implementation Checklist¶
- [ ] Check: main_demand < total_demand (else STO trap)
- [ ] Write: B = bottom_of_stack
- [ ] Write: TOS = bottom_of_stack + total_demand
- [ ] Write: Memory[B+0] = 0 (PREVB)
- [ ] Write: Memory[B+4] = 0 (RETA)
- [ ] Write: Memory[B+8] = bottom_of_stack + main_demand (SP)
- [ ] Write: L = 0
- [ ] Clear: STO status bit (if successful)
GETB Implementation Checklist¶
- [ ] Read: MAXL from TOS+0
- [ ] Check: log_size ≤ MAXL (else STO trap)
- [ ] Search: FLOG[log_size] through FLOG[MAXL]
- [ ] If found: Unlink block from freelist
- [ ] If larger: Split repeatedly, add buddies to freelists
- [ ] If not found: Set STO, trigger trap
- [ ] Write: Wn = block address
- [ ] Set/Clear: STO status bit
FREEB Implementation Checklist¶
- [ ] Read: old_head from FLOG[log_size]
- [ ] Write: block.word0 = old_head
- [ ] Write: FLOG[log_size] = block
- [ ] Note: NO coalescing!
ENTB Implementation Checklist¶
- [ ] Allocate: block via GETB logic (may trap)
- [ ] Save: oldB, oldB.SP
- [ ] Write: Memory[block+0] = oldB (PREVB)
- [ ] Write: Memory[block+4] = return_address (RETA)
- [ ] Write: Memory[block+8] = oldB.SP (SP - inherited!)
- [ ] Write: Memory[block+12] = log_size (LOG - critical!)
- [ ] Write: Memory[block+16] = arg_count (N)
- [ ] Write: Memory[block+20+] = arg_addresses
- [ ] Write: B = block
- [ ] Write: L = return_address
RETB Implementation Checklist¶
- [ ] Read: log_size from B+12 (LOG)
- [ ] Read: prevB from B+0 (PREVB)
- [ ] Read: retAddr from B+4 (RETA)
- [ ] Check: (prevB=0 AND retAddr=0) → STU trap
- [ ] FREEB: Release B to FLOG[log_size]
- [ ] Write: B = prevB
- [ ] Write: P = retAddr
- [ ] Write: L = retAddr
- [ ] Write: STATUS.K = 0 (RETB) or 1 (RETBK)
Complete Instruction Lifecycle Diagram¶
stateDiagram-v2
[*] --> Uninitialized: Power On
Uninitialized --> MainInitialized: INIT
note right of MainInitialized
B points to main frame
TOS points to heap vars location
User must init heap vars!
end note
MainInitialized --> HeapReady: User initializes MAXL, FLOGs
HeapReady --> StackRoutine: ENTS (stack)
HeapReady --> BuddyRoutine: ENTB (heap)
HeapReady --> ElementAllocated: GETB
StackRoutine --> HeapReady: RET/RETK
StackRoutine --> StackRoutine: Nested ENTS
StackRoutine --> BuddyRoutine: ENTB from stack routine
BuddyRoutine --> HeapReady: RETB/RETBK
BuddyRoutine --> BuddyRoutine: Nested ENTB
BuddyRoutine --> StackRoutine: ENTS from buddy routine ⚠️
BuddyRoutine --> ElementAllocated: GETB
ElementAllocated --> HeapReady: FREEB
ElementAllocated --> ElementAllocated: More GETB
HeapReady --> TrapHandler: STO (no memory)
StackRoutine --> TrapHandler: STO (stack full)
BuddyRoutine --> TrapHandler: STO (heap full)
ElementAllocated --> TrapHandler: STO (heap full)
TrapHandler --> HeapReady: Add memory / Coalesce
TrapHandler --> [*]: Unrecoverable error
note right of StackRoutine
⚠️ ENTS from buddy routine:
Uses inherited B.SP
May overlap with other co-routines!
end note
Glossary¶
| Term | Definition |
|---|---|
| Buddy | Two blocks of equal size that were split from the same parent block |
| Coalescing | Combining two buddy blocks back into their parent |
| FLOG | Freelist head pointer for blocks of a specific log size |
| Freelist | Linked list of free blocks (same size) |
| Heap | Memory pool managed by buddy system |
| Log size | log₂ of block size in words |
| MAXL | Maximum log size allowed (limits largest allocatable block) |
| STO | Stack Overflow trap/status bit |
| STU | Stack Underflow trap/status bit |
| TOS | Top of Stack register (points to heap variables) |