DuckStack VM

DuckStack Bytecode Virtual Machine

DuckStack is a simple stack-based bytecode VM for executing compiled duckyScript binaries.

duckyPad uses it for HID macro scripting.

⚠️ UNDER BETA TEST ⚠️

This VM is currently under public beta test

Features

Table of Contents

Architecture Overview

duckStack uses 32-bit variables, arithmetics, and stack width.

Addressing is 16-bit, executable 64KB max.

Memory Map: duckyPad Pro (2024)

Address Purpose Size Comment PEEK and
POKE-able
0000
EFFF
Shared
Executable
and Stack
61440 Bytes See Notes Below
F000
F3FF
User-defined
Global
Variables
1024 Bytes
4 Bytes/Entry
256 Entries
ZI Data
F400
F7FF
Scratch
Memory
1024 Bytes General-purpose
F800
FBFF
Reserved 1024 Bytes  
FC00
FDFF
Persistent
Global
Variables
512 Bytes
4 Bytes/Entry
128 Entries
Non-volatile Data
Saved on SD card
FE00
FEFF
VM
Internal
Variables
256 Bytes
4 Bytes/Entry
64 Entries
Read/Adjust
VM Settings
FF00
FFFF
Memory-
Mapped IO
256 Bytes  

Memory Map: duckyPad (2020)

Address Purpose Size Comment PEEK and
POKE-able
0000
DFFF
Binary Executable 57344 Bytes  
E000
EFFF
Data Stack 4096 Bytes Grows towards
smaller address
F000
F0FF
User-defined
Global
Variables
256 Bytes
4 Bytes/Entry
64 Entries
ZI Data
.... Unused    
F400
F4FF
Scratch
Memory
256 Bytes General-purpose
.... Unused    
FC00
FC7F
Persistent
Global
Variables
128 Bytes
4 Bytes/Entry
32 Entries
Non-volatile Data
Saved on SD card
.... Unused    
FE00
FE7F
VM
Internal
Variables
128 Bytes
4 Bytes/Entry
32 Entries
Read/Adjust
VM Settings
.... Unused    

Instruction Set

Variable-length between 1 to 5 bytes.

CPU Instructions

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_LSB
CONST_MSB
PUSHI 3 2/0x2 Read 4 Bytes at ADDR
Push to stack as one 32-bit number
2 Bytes:
ADDR_LSB
ADDR_MSB
PUSHR 3 3/0x3 Read 4 Bytes at offset from FP
Push to stack as one 32-bit number
2 Bytes:
OFFSET_LSB
OFFSET_MSB
POPI 3 4/0x4 Pop one item off TOS
Write 4 bytes to ADDR
2 Bytes:
ADDR_LSB
ADDR_MSB
POPR 3 5/0x5 Pop one item off TOS
Write as 4 Bytes at offset from FP
2 Bytes:
OFFSET_LSB
OFFSET_MSB
BRZ 3 6/0x6 Pop one item off TOS
If value is zero, jump to ADDR
2 Bytes:
ADDR_LSB
ADDR_MSB
JMP 3 7/0x7 Unconditional Jump 2 Bytes:
ADDR_LSB
ADDR_MSB
ALLOC 3 8/0x8 Push n blank entries to stack
Used to allocate local variables
on function entry
2 Bytes:
n_LSB
n_MSB
CALL 3 9/0x9 Construct 32b value frame_info:
Top 16b current_FP,
Bottom 16b return_addr (PC+3).
Push frame_info to TOS
Set FP to TOS
Jump to ADDR
2 Bytes:
ADDR_LSB
ADDR_MSB
RET 3 10/0xa return_value on TOS
Pop return_value into temp location
Pop items until TOS is FP
Pop frame_info, restore FP and PC.
Pop off ARG_COUNT items
Push return_value back on TOS
Resumes execution at PC
2 Bytes:
ARG_COUNT
Reserved
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 Bytes
CONST_LSB
CONST_B1
CONST_B2
CONST_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_VER
Reserved

Memory Access

PEEK Instructions

Name Opcode
Byte 0
Comment
PEEK8 24/0x18 Read ONE byte at ADDR
Push on stack SIGN-extended
PEEKU8 25/0x19 Read ONE byte at ADDR
Push on stack ZERO-extended
PEEK16 26/0x1a Read TWO bytes at ADDR
Push on stack SIGN-extended
PEEKU16 27/0x1b Read TWO bytes at ADDR
Push on stack ZERO-extended
PEEK32 28/0x1c Read FOUR bytes at ADDR
Push on stack AS-IS

POKE Instructions

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 Operators

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)

Unary Operators

Name Opcode
Byte 0
Comment
BITINV 60/0x3c Bitwise Invert
LOGINOT 61/0x3d Logical NOT
USUB 62/0x3e Unary Minus

duckyScript Commands

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 vline
Scroll hline horizontally
(Positive: RIGHT, Negative: LEFT)
Scroll vline vertically
(Positive: UP, Negative: DOWN)
   
MMOV 68/0x44 Mouse Move
Pop TWO items: x then y
x: Positive RIGHT, Negative LEFT.
y: Positive UP, Negative DOWN.
   
SWCF 69/0x45 Switch Color Fill
Pop THREE items
Red, Green, Blue
Set ALL LED color to the RGB value
   
SWCC 70/0x46 Switch Color Change
Pop FOUR item
N, Red, Green, Blue
Set 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 ADDR
Print zero-terminated
string at ADDR
None  
STRLN 73/0x49 Type Line
Pop ONE item as ADDR
Print zero-terminated
string at ADDR
Press 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 ADDR
Print zero-terminated
string at ADDR to OLED
OPTIONS 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, y2
Draw single-pixel line in-between
   
OLED_RECT 80/0x50 OLED Draw Rectangle
Pop FIVE items
opt, x1, y1, x2, y2
Draw rectangle between two points
optBit 0: Fill, Bit 1: Color
   
OLED_CIRC 81/0x51 OLED Draw Circle
Pop FOUR items
opt, radius, x, y
Draw 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 n
If n is positive, go to next profile
If n is negative, go to prev profile
   
GOTOP 84/0x54 Goto Profile
Pop ONE item as ADDR
Retrieve zero-terminated string at ADDR
If resolves into an integer n
Go 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: ADDR
Bit 16-23: n
Bit 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 ADDR
Read 9 bytes from ADDR
Construct & send raw HID message
See HIDTX() in duckyScript doc
   

String Encoding

The following commands involves user-provided strings:

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'

Printing Variables

When printing a variable, its info is embedded into the string between two separator bytes.

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'

Run-time Exceptions

Exceptions such as Division-by-Zero, Stack Over/Underflow, etc, result in immediate termination of the VM execution.

Calling Convention

Stack Set-up

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:

   
FP -> Prev_FP \| Return_addr
  a
  b
  c
 
  Base (EFFF)

Arguments / Locals

Once in function, callee uses ALLOC n to make space for local variables.

To reference arguments and locals, FP + Byte_Offset is used.

   
 
FP - 8 localvar_2
FP - 4 localvar_1
FP -> Prev_FP \| Return_addr
FP + 4 a
FP + 8 b
FP + 12 c
 
  Base (EFFF)

Stack Unwinding

At end of a function, return_value is on TOS.

   
  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_val
 
FP -> Base (EFFF)

Standalone Compiler

Normally, duckyScript compilation is taken care of in the configurator.

But of course you can also try a standalone version below.

Compile

python3 ./dsvm_make_bytecode.py test.txt test.dsb

Execute

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.

Questions or Comments?

Please feel free to open an issue, ask in the official duckyPad discord, or email dekuNukem@gmail.com!