TCL API Reference
While the EMU Engine handles the heavy lifting of system call interception, the TCL scripting layer acts as the “brain” of your virtual hardware. Standard TCL language features are fully supported, supplemented by the injected certo:: namespace.
Hardware State Management
Because EMU intercepts individual system calls, your scripts need a way to remember what happened previously. Use standard TCL global variables by prefixing variables with :: to create a state that persists across different system calls.
# In your 'sendto' script (Setting the state)
set ::virtual_radio_freq 100000000
# In your 'recvfrom' script (Reading the state)
if {$::virtual_radio_freq > 90000000} {
# Generate high-frequency tone...
}
Accessing Syscall Parameters
EMU provides the raw arguments passed by the application as a standard TCL list via certo::arguments.
# Example: recvfrom(int fd, void *restrict buf, size_t len, ...)
set fd_num [lindex $certo::arguments 0]
set buf_addr [lindex $certo::arguments 1]
set requested_size [lindex $certo::arguments 2]
Memory Manipulation
For direct access to the application’s memory pointers:
::certo::read_memory addr ?len?: Reads raw bytes from the target process’s memory.::certo::write_memory addr payload: Writes a raw byte array back into the target process’s memory.
# Example: Modifying a write() buffer
set buf_addr [lindex $certo::arguments 1]
set write_size [lindex $certo::arguments 2]
set raw_bytes [::certo::read_memory $buf_addr $write_size]
set modified_bytes [string map {"OFF" "ON "} $raw_bytes]
::certo::write_memory $buf_addr $modified_bytes
Schema Parsing
Translate between raw C memory bytes and TCL Dictionaries using registered schemas.
::certo::bytes_to_dict schema_name bytes: Decodes raw bytes into a dictionary.::certo::dict_to_bytes schema_name dict: Encodes a dictionary back into raw C-struct bytes.::certo::create_struct schema_name: Generates a zero-initialized dictionary for a schema.
System Control & Logging
::certo::logger ?message...?: Sends a formatted log message to the UI. Standardputsis automatically intercepted and funneled here.certo_set_return_value value: Spoofs the return value of the intercepted system call.
Asynchronous Readiness (Completion Waker)
Some transports split one logical operation across multiple syscalls: a request
is submitted by one call and collected by a later one (USB URB
submit/reap, netlink request/response, HCI command/event). When the target
application blocks in poll/epoll_wait between those calls, the engine needs
to know when the pending result becomes ready so it can wake the wait.
certo_set_ready_in ?fd? ms: Declares that the intercepted file descriptor becomes readablemsmilliseconds from now (0= immediately). A target blocked inpoll/epollon that fd wakes when the deadline passes. Readiness is level-triggered — it persists across polls until cleared.certo_clear_ready ?fd?: Drops the readiness state for the fd, e.g. once the collect-side stub has delivered the last queued result.$certo::fd: The file descriptor of the syscall currently under evaluation. The readiness commands default to this fd when one is not given, so a stub can simply callcerto_set_ready_in 0.
# Submit side (e.g. USBDEVFS_SUBMITURB): queue the completion and announce it.
lappend ::pending_urbs $urb_addr
certo_set_ready_in 5 ;# this fd becomes readable in 5ms
# Collect side (e.g. USBDEVFS_REAPURB): hand back the result, clear when drained.
set urb [lindex $::pending_urbs 0]
set ::pending_urbs [lrange $::pending_urbs 1 end]
if {[llength $::pending_urbs] == 0} { certo_clear_ready }
Lazy / polling targets work without any waker — the collect-side stub simply returns the queued result.
certo_set_ready_inis only needed to wake targets that block inpoll/epoll/select-style waits.
Lifecycle & Context
Emulation Callbacks
proc certo::emulation_enabled {} {
set ::virtual_radio_freq 0
}
proc certo::emulation_disabled {} {
unset -nocomplain ::virtual_radio_freq
}
Execution Direction
The variable $certo::direction indicates if the script is currently being evaluated before the real system call (“Inbound”) or after (“Outbound”).
if {$certo::direction eq "Inbound"} {
::certo::logger "PRE phase logic"
} else {
::certo::logger "POST phase logic"
}