DuckStack is a simple stack-based bytecode VM for executing compiled duckyScript binaries.
duckyPad uses it for HID macro scripting.
This VM is currently under public beta test
duckStack uses 32-bit variables, arithmetics, and stack width.
Addressing is 16-bit, executable 64KB max.
| Address | Purpose | Size | Comment | PEEK andPOKE-able |
|---|---|---|---|---|
0000EFFF |
Shared Executable and Stack |
61440 Bytes | See Notes Below | ✅ |
F000F3FF |
User-defined Global Variables |
1024 Bytes 4 Bytes/Entry 256 Entries |
ZI Data | ✅ |
F400F7FF |
Scratch Memory |
1024 Bytes | General-purpose | ✅ |
F800FBFF |
Reserved | 1024 Bytes | ❌ | |
FC00FDFF |
Persistent Global Variables |
512 Bytes 4 Bytes/Entry 128 Entries |
Non-volatile Data Saved on SD card |
✅ |
FE00FEFF |
VM Internal Variables |
256 Bytes 4 Bytes/Entry 64 Entries |
Read/Adjust VM Settings |
❌ |
FF00FFFF |
Memory- Mapped IO |
256 Bytes | ✅ |
0x00xEFFF towards smaller address
| Address | Purpose | Size | Comment | PEEK andPOKE-able |
|---|---|---|---|---|
0000DFFF |
Binary Executable | 57344 Bytes | ❌ | |
E000EFFF |
Data Stack | 4096 Bytes | Grows towards smaller address |
❌ |
F000F0FF |
User-defined Global Variables |
256 Bytes 4 Bytes/Entry 64 Entries |
ZI Data | ✅ |
.... |
Unused | ❌ | ||
F400F4FF |
Scratch Memory |
256 Bytes | General-purpose | ✅ |
.... |
Unused | ❌ | ||
FC00FC7F |
Persistent Global Variables |
128 Bytes 4 Bytes/Entry 32 Entries |
Non-volatile Data Saved on SD card |
✅ |
.... |
Unused | ❌ | ||
FE00FE7F |
VM Internal Variables |
128 Bytes 4 Bytes/Entry 32 Entries |
Read/Adjust VM Settings |
❌ |
.... |
Unused | ❌ |
Variable-length between 1 to 5 bytes.
PUSHR / POPR Offset is a byte-addressed signed 16-bit integer
| Name | Inst. Size |
Opcode Byte 0 |
Comment | Payload Byte 1-4 |
|---|---|---|---|---|
NOP |
1 | 0/0x0 |
Do nothing | None |
PUSHC16 |
3 | 1/0x1 |
Push unsigned 16-bit (0-65535) constant on stack For negative numbers, push abs then use USUB. |
2 Bytes:CONST_LSBCONST_MSB |
PUSHI |
3 | 2/0x2 |
Read 4 Bytes at ADDRPush to stack as one 32-bit number |
2 Bytes:ADDR_LSBADDR_MSB |
PUSHR |
3 | 3/0x3 |
Read 4 Bytes at offset from FP Push to stack as one 32-bit number |
2 Bytes:OFFSET_LSBOFFSET_MSB |
POPI |
3 | 4/0x4 |
Pop one item off TOS Write 4 bytes to ADDR |
2 Bytes:ADDR_LSBADDR_MSB |
POPR |
3 | 5/0x5 |
Pop one item off TOS Write as 4 Bytes at offset from FP |
2 Bytes:OFFSET_LSBOFFSET_MSB |
BRZ |
3 | 6/0x6 |
Pop one item off TOS If value is zero, jump to ADDR |
2 Bytes:ADDR_LSBADDR_MSB |
JMP |
3 | 7/0x7 |
Unconditional Jump | 2 Bytes:ADDR_LSBADDR_MSB |
ALLOC |
3 | 8/0x8 |
Push n blank entries to stackUsed to allocate local variables on function entry |
2 Bytes:n_LSBn_MSB |
CALL |
3 | 9/0x9 |
Construct 32b value frame_info:Top 16b current_FP,Bottom 16b return_addr (PC+3).Push frame_info to TOSSet FP to TOS Jump to ADDR |
2 Bytes:ADDR_LSBADDR_MSB |
RET |
3 | 10/0xa |
return_value on TOSPop return_value into temp locationPop items until TOS is FPPop frame_info, restore FP and PC.Pop off ARG_COUNT itemsPush return_value back on TOSResumes execution at PC |
2 Bytes:ARG_COUNTReserved |
HALT |
1 | 11/0xb |
Stop execution | None |
PUSH0 |
1 | 12/0xc |
Push 0 to TOS |
None |
PUSH1 |
1 | 13/0xd |
Push 1 to TOS |
None |
DROP |
1 | 14/0xe |
Discard ONE item off TOS | None |
DUP |
1 | 15/0xf |
Duplicate the item on TOS | None |
RANDINT |
1 | 16/0x10 |
Pop TWO item off TOS First Upper, then Lower.Push a SIGNED random number inbetween (inclusive) on TOS |
None |
RANDUINT |
1 | 17/0x10 |
Pop TWO item off TOS First Upper, then Lower.Push an UNSIGNED random number inbetween (inclusive) on TOS |
None |
PUSHC32 |
5 | 18/0x11 |
Push 32-bit constant on stack | 4 BytesCONST_LSBCONST_B1CONST_B2CONST_MSB |
PUSHC8 |
2 | 19/0x12 |
Push unsigned 8-bit (0-255) constant on stack For negative numbers, push abs then use USUB. |
1 Byte |
VMVER |
3 | 255/0xff |
VM Version Check Abort if mismatch |
2 Bytes:VM_VERReserved |
ADDR
| Name | Opcode Byte 0 |
Comment |
|---|---|---|
PEEK8 |
24/0x18 |
Read ONE byte at ADDRPush on stack SIGN-extended |
PEEKU8 |
25/0x19 |
Read ONE byte at ADDRPush on stack ZERO-extended |
PEEK16 |
26/0x1a |
Read TWO bytes at ADDRPush on stack SIGN-extended |
PEEKU16 |
27/0x1b |
Read TWO bytes at ADDRPush on stack ZERO-extended |
PEEK32 |
28/0x1c |
Read FOUR bytes at ADDRPush on stack AS-IS |
ADDR, then VAL| Name | Opcode Byte 0 |
Comment |
|---|---|---|
POKE8 |
29/0x1d |
Write low 8 bits of VAL to ADDR |
POKE16 |
30/0x1e |
Write low 16 bits of VAL to ADDR |
POKE32 |
31/0x1f |
Write VAL to ADDR as-is |
Binary as in involving two operands.
| Name | Opcode Byte 0 |
Comment |
|---|---|---|
EQ |
32/0x20 |
Equal |
NOTEQ |
33/0x21 |
Not Equal |
LT |
34/0x22 |
SIGNED Less Than |
LTE |
35/0x23 |
SIGNED Less Than or Equal |
GT |
36/0x24 |
SIGNED Greater Than |
GTE |
37/0x25 |
SIGNED Greater Than or Equal |
ADD |
38/0x26 |
Add |
SUB |
39/0x27 |
Subtract |
MULT |
40/0x28 |
Multiply |
DIV |
41/0x29 |
SIGNED Integer Division |
MOD |
42/0x2a |
SIGNED Modulus |
POW |
43/0x2b |
Power of |
LSL |
44/0x2c |
Logical Shift Left |
ASR |
45/0x2d |
Arithmetic Shift Right (Sign-extend) |
BITOR |
46/0x2e |
Bitwise OR |
BITXOR |
47/0x2f |
Bitwise XOR |
BITAND |
48/0x30 |
Bitwise AND |
LOGIAND |
49/0x31 |
Logical AND |
LOGIOR |
50/0x32 |
Logical OR |
ULT |
51/0x33 |
UNSIGNED Less Than |
ULTE |
52/0x34 |
UNSIGNED Less Than or Equal |
UGT |
53/0x35 |
UNSIGNED Greater Than |
UGTE |
54/0x36 |
UNSIGNED Greater Than or Equal |
UDIV |
55/0x37 |
UNSIGNED Integer Division |
UMOD |
56/0x38 |
UNSIGNED Modulus |
LSR |
57/0x39 |
Logical Shift Right (Zero-extend) |
| Name | Opcode Byte 0 |
Comment |
|---|---|---|
BITINV |
60/0x3c |
Bitwise Invert |
LOGINOT |
61/0x3d |
Logical NOT |
USUB |
62/0x3e |
Unary Minus |
| Name | Opcode Byte 0 |
Comment | ||
|---|---|---|---|---|
DELAY |
64/0x40 |
Delay Pop ONE item Delay amount in milliseconds |
||
KDOWN |
65/0x41 |
Press Key Pop ONE item \|MSB\|B2\|B1\|LSB\|Unused\|Unused\|KeyType\|KeyCode\| |
||
KUP |
66/0x42 |
Release Key Pop ONE item \|MSB\|B2\|B1\|LSB\|Unused\|Unused\|KeyType\|KeyCode\| |
||
MSCL |
67/0x43 |
Mouse Scroll Pop TWO items First hline, then vlineScroll hline horizontally(Positive: RIGHT, Negative: LEFT)Scroll vline vertically(Positive: UP, Negative: DOWN) |
||
MMOV |
68/0x44 |
Mouse Move Pop TWO items: x then yx: Positive RIGHT, Negative LEFT.y: Positive UP, Negative DOWN. |
||
SWCF |
69/0x45 |
Switch Color Fill Pop THREE items Red, Green, BlueSet ALL LED color to the RGB value |
||
SWCC |
70/0x46 |
Switch Color Change Pop FOUR item N, Red, Green, BlueSet N-th switch to the RGB value If N is 0, set current switch. |
||
SWCR |
71/0x47 |
Switch Color Reset Pop ONE item If value is 0, reset color of current key If value is between 1 and 20, reset color of that key If value is 99, reset color of all keys. |
||
STR |
72/0x48 |
Type String Pop ONE item as ADDRPrint zero-terminated string at ADDR |
None | |
STRLN |
73/0x49 |
Type Line Pop ONE item as ADDRPrint zero-terminated string at ADDRPress ENTER at end |
||
OLED_CUSR |
74/0x4a |
OLED Set Cursor Pop TWO items: x then y |
||
OLED_PRNT |
75/0x4b |
OLED Print Pop TWO items: OPTIONS then ADDRPrint zero-terminated string at ADDR to OLEDOPTIONS Bit 0: If set, print center-aligned. |
None | |
OLED_UPDE |
76/0x4c |
OLED Update | ||
OLED_CLR |
77/0x4d |
OLED Clear | ||
OLED_REST |
78/0x4e |
OLED Restore | ||
OLED_LINE |
79/0x4f |
OLED Draw Line Pop FOUR items x1, y1, x2, y2Draw single-pixel line in-between |
||
OLED_RECT |
80/0x50 |
OLED Draw Rectangle Pop FIVE items opt, x1, y1, x2, y2Draw rectangle between two points optBit 0: Fill, Bit 1: Color |
||
OLED_CIRC |
81/0x51 |
OLED Draw Circle Pop FOUR items opt, radius, x, yDraw circle with radius at (x,y)optBit 0: Fill, Bit 1: Color |
||
BCLR |
82/0x52 |
Clear switch event queue | ||
SKIPP |
83/0x53 |
Skip Profile Pop ONE item as nIf n is positive, go to next profileIf n is negative, go to prev profile |
||
GOTOP |
84/0x54 |
Goto Profile Pop ONE item as ADDRRetrieve zero-terminated string at ADDRIf resolves into an integer nGo to nth profile.Otherwise jump to profile name |
||
SLEEP |
85/0x55 |
Sleep Put duckyPad to sleep Terminates execution |
||
RANDCHR |
86/0x56 |
Random Character Pop ONE item as bitmask. Bit 0: Letter Lowercase Bit 1: Letter Uppercase Bit 2: Digits Bit 3: Symbols Bit 8: Type via Keyboard Bit 9: OLED Print-at-cursor |
||
PUTS |
87/0x57 |
Print String Pop ONE item off TOS —— Bit 0-15: ADDRBit 16-23: nBit 29: OLED Print-at-cursor Bit 30: OLED Print-Center-Aligned Bit 31: Type via Keyboard Print string starting from ADDR—— If n=0, print until zero-termination.Else, print max n chars (or until \0). |
None | |
HIDTX |
88/0x59 |
Pop ONE item off TOS as ADDRRead 9 bytes from ADDRConstruct & send raw HID message See HIDTX() in duckyScript doc |
The following commands involves user-provided strings:
STRING/ STRINGLNOLED_PRINT / OLED_CPRINTGOTO_PROFILEPUTS()Strings are zero-terminated and appended at the end of the binary executable.
The starting address of a string is pushed onto stack before calling one of those commands, who pops off the address and fetch the string there.
Identical strings are deduplicated and share the same address.
STRING Hello World!
STRINGLN Hello World!
OLED_PRINT Hi there!
3 PUSHC16 16 0x10 ;STRING Hello World!
6 STR ;STRING Hello World!
7 PUSHC16 16 0x10 ;STRINGLN Hello World!
10 STRLN ;STRINGLN Hello World!
11 PUSHC16 29 0x1d ;OLED_PRINT Hi there!
14 OLED_PRNT ;OLED_PRINT Hi there!
15 HALT
16 DATA: b'Hello World!\x00'
29 DATA: b'Hi there!\x00'
When printing a variable, its info is embedded into the string between two separator bytes.
0x1f for Global Variables
[0x1f][ADDR_LSB][ADDR_MSB][Format Specifiers][0x1f]0x1e for Local variables & arguments inside functions
[0x1e][OFFSET_LSB][OFFSET_MSB][Format Specifiers][0x1e]VAR foo = 255
STRING Count is: $foo%02x
3 PUSHC16 255 0xff ;VAR foo = 255
6 POPI 63488 0xf800 ;VAR foo = 255
9 PUSHC16 14 0xe ;STRING Count is: $foo%02x
12 STR ;STRING Count is: $foo%02x
13 HALT
14 DATA: b'Count is: \x1f\x00\xf8%02x\x1f\x00'
Exceptions such as Division-by-Zero, Stack Over/Underflow, etc, result in immediate termination of the VM execution.
Outside function calls, FP points to base of stack.
| … | |
|---|---|
| … | |
FP -> |
Base (EFFF) |
When calling a function: foo(a, b, c)
a |
|
b |
|
c |
|
| … | |
FP -> |
Base (EFFF) |
Caller then executes CALL instruction, which:
frame_info
current_FPreturn_addressframe_info to TOSFP -> |
Prev_FP \| Return_addr |
a |
|
b |
|
c |
|
| … | |
Base (EFFF) |
Once in function, callee uses ALLOC n to make space for local variables.
To reference arguments and locals, FP + Byte_Offset is used.
FP - 4 points to first local, etcFP + 4 points to leftmost argument, etcPUSHR + Offset and POPR + Offset to read/write to args and locals.| … | |
FP - 8 |
localvar_2 |
FP - 4 |
localvar_1 |
FP -> |
Prev_FP \| Return_addr |
FP + 4 |
a |
FP + 8 |
b |
FP + 12 |
c |
| … | |
Base (EFFF) |
At end of a function, return_value is on TOS.
RETURN statement, 0 is returned.return_value |
|
temp data |
|
FP - 8 |
localvar_2 |
FP - 4 |
localvar_1 |
FP -> |
Prev_FP \| Return_addr |
FP + 4 |
a |
FP + 8 |
b |
FP + 12 |
c |
| … | |
Base (EFFF) |
Callee executes RET n instruction, which:
return_value into temp locationframe_info is on TOS
SP + 4 == FPframe_info
previous FP into FPreturn address into PCn argumentsreturn_value back on TOSreturn_val |
|
| … | |
FP -> |
Base (EFFF) |
Normally, duckyScript compilation is taken care of in the configurator.
But of course you can also try a standalone version below.
test.txt
ds_compiler directory, run:python3 ./dsvm_make_bytecode.py test.txt test.dsb
A minimal C-based VM is provided. Based on real duckyPad firmware, but uses placeholders for hardware commands.
In ds_c_vm folder, run python3 ./compile.py to compile the source. (Or write your own Makefile)
Run the VM: ./main test.dsb
Set PRINT_DEBUG to 1 in main.h for execution and stack trace.
Please feel free to open an issue, ask in the official duckyPad discord, or email dekuNukem@gmail.com!