Files
lopez-repo/calling-convention/Output-explanation.md
2026-03-09 13:27:35 -06:00

3.8 KiB

Calling Convention — Output Explanation

This document explains the output of the calling convention algorithm implemented in calling-convention.ipynb, tested across 3 scenarios.


TEST 1: A single 1-byte argument

Function signature: result = func(x) where x is 8 bits (1 byte)

Output:

===== TEST 1: a 1 byte argument =====
=== State after do_function_call ===
Registers used: ['RA']
Stack: [{'type': 'caller_saved', 'reg': 'R0', 'value': 0}, {'type': 'caller_saved', 'reg': 'R1', 'value': 0}, {'type': 'caller_saved', 'reg': 'R2', 'value': 0}, {'type': 'caller_saved', 'reg': 'R3', 'value': 0}]
RS points to: 4
=== State after undo_function_call ===
Clean stack: []
RS: 0
Collected result: {'result': {'type': 'result', 'value': 42}}

Explanation:

Since there is only one argument and it fits in a single register, it is placed directly in RA (the first available argument register). Before the call, the 4 caller-saved registers (R0-R3) are pushed onto the stack to preserve their values. RS points to position 4, reflecting those 4 entries. After undo_function_call, the stack is empty, RS returns to 0, and the result value 42 is successfully collected from RA.


TEST 2: 8 arguments, some overflow to the stack

Function signature: result = func(a, b, c, d, e, f, g, h)

Output

===== TEST 2: 8 arguments, some on the stack =====
=== State after do_function_call ===
Registers used: ['RA', 'RB', 'RC', 'RD', 'R8', 'R9']
Stack: [{'type': 'caller_saved', 'reg': 'R0', 'value': 0}, {'type': 'caller_saved', 'reg': 'R1', 'value': 0}, {'type': 'caller_saved', 'reg': 'R2', 'value': 0}, {'type': 'caller_saved', 'reg': 'R3', 'value': 0}, {'type': 'param', 'name': 'g', 'size': 64, 'value': 0}, {'type': 'param', 'name': 'h', 'size': 64, 'value': 0}]
RS points to: 6
=== State after undo_function_call ===
Clean stack: []
RS: 0
Collected result: {'result': {'type': 'result', 'value': 99}}

Explanation: Spider provides 6 registers for passing arguments: RA, RB, RC, RD, R8, R9. The first 6 arguments (a through f) fill all available registers. Arguments g and h, which are 64-bit values, have no registers left and are pushed onto the stack instead. RS points to 6, reflecting the 4 caller-saved entries plus 2 stack parameters. After undo_function_call, all stack entries are cleaned and the result 99 is collected from RA.


TEST 3: Boolean arguments

Function signature: ok = func(flag1, flag2, flag3, x)

Output:


===== TEST 3: Boolean arguments =====
=== State after do_function_call ===
Registers used: ['RA', 'RB']
Stack: [{'type': 'caller_saved', 'reg': 'R0', 'value': 0}, {'type': 'caller_saved', 'reg': 'R1', 'value': 0}, {'type': 'caller_saved', 'reg': 'R2', 'value': 0}, {'type': 'caller_saved', 'reg': 'R3', 'value': 0}]
...
=== State after undo_function_call ===
Clean stack: []
RS: 0
Collected result: {'ok': {'type': 'result', 'value': True}}

Explanation: Booleans (1-bit values) are not placed individually into registers. Instead they are accumulated into a queue and packed together before occupying a single register. The 3 boolean flags (flag1, flag2, flag3) are packed together and placed in RB as a bool_pack_padded entry. The regular argument x (8 bits) is placed in RA as normal. No arguments overflow to the stack. After undo_function_call, the result True is successfully collected from RA.


General observations

  • The stack always starts and ends with the same state, proving the algorithm correctly preserves the machine's previous context.
  • Caller-saved registers (R0-R3) are always pushed before the call and restored after, regardless of the number of arguments.
  • RS accurately tracks the stack size at every step.