Linux Extensions
This document describes the Linux-extension opcodes that ZBC adds to the ARM-semihosting syscall set. These opcodes let a Linux VFS driver expose host files directly to a guest Linux kernel, support block device access via disk image files, and back a TTY driver with non-blocking console input.
The opcodes are defined in Protocol API (header
zbc_protocol.h), dispatched in the C host
(Host API), and implemented in the ANSI backends
(Backend API).
Background
The ARM-compatible syscalls (opcodes 0x01-0x32) provide basic file
I/O, console, and timekeeping services – sufficient for bare-metal
applications and simple embedded systems. To support a Linux
semihostfs VFS driver, additional operations are required.
The extension opcodes start at 0x80 to avoid collision with the ARM range. Each one wraps a POSIX function directly on the host side.
Networking is explicitly out of scope for ZBC semihosting.
Syscalls
Directory Operations
SYS_OPENDIR (0x80)
Open a directory for enumeration.
Arguments:
Slot |
Type |
Description |
|---|---|---|
[0] |
pointer |
Path to directory (DATA chunk) |
[1] |
integer |
Path length |
Returns:
>= 0: Directory handle-1: Error (errno set)
Host implementation: Wraps POSIX opendir().
SYS_READDIR (0x81)
Read one directory entry.
Arguments:
Slot |
Type |
Description |
|---|---|---|
[0] |
integer |
Directory handle (from SYS_OPENDIR) |
[1] |
pointer |
Output buffer (DATA chunk destination) |
[2] |
integer |
Buffer size |
Returns:
> 0: Bytes written to buffer (one entry)0: End of directory-1: Error (errno set)
Output buffer format:
d_ino[8] - Inode number (little-endian)
d_type[1] - File type (DT_REG, DT_DIR, DT_LNK, etc.)
d_namlen[1] - Name length (not including null terminator)
d_name[...] - Null-terminated filename
Host implementation: Wraps POSIX readdir(). One entry per call;
guest loops until return value is 0.
SYS_CLOSEDIR (0x82)
Close a directory handle.
Arguments:
Slot |
Type |
Description |
|---|---|---|
[0] |
integer |
Directory handle |
Returns:
0: Success-1: Error (errno set)
Host implementation: Wraps POSIX closedir().
File Metadata
SYS_STAT (0x83)
Get file metadata by path.
Arguments:
Slot |
Type |
Description |
|---|---|---|
[0] |
pointer |
Path to file (DATA chunk) |
[1] |
integer |
Path length |
[2] |
pointer |
Output buffer (DATA chunk destination, 48 bytes) |
[3] |
integer |
Buffer size (must be 48) |
Returns:
0: Success-1: Error (ENOENT, EACCES, etc.)
Output buffer format (48 bytes, all little-endian):
ino[8] - Inode number
mode[4] - File type and permissions (S_IFREG, S_IFDIR, etc.)
nlink[4] - Number of hard links
size[8] - File size in bytes
mtime[8] - Modification time (seconds since epoch)
atime[8] - Access time (seconds since epoch)
ctime[8] - Change time (seconds since epoch)
Host implementation: Wraps POSIX stat(). Returns real host
permissions (the guest needs accurate permission info for the VFS
driver).
Why 48 bytes: The buffer contains all fields required by Linux VFS
struct kstat (inode, nlink, mode, size, timestamps). Per the
Linux VFS documentation,
getattr must populate these fields.
SYS_FSTAT (0x84)
Get file metadata by descriptor.
Arguments:
Slot |
Type |
Description |
|---|---|---|
[0] |
integer |
File descriptor |
[1] |
pointer |
Output buffer (DATA chunk destination, 48 bytes) |
[2] |
integer |
Buffer size (must be 48) |
Returns:
0: Success-1: Error
Host implementation: Wraps POSIX fstat(). Same output format
as SYS_STAT.
SYS_LSTAT (0x8D)
stat() that does not follow symbolic links. Same arguments and
output format as SYS_STAT. Wraps POSIX lstat().
File Operations
SYS_MKDIR (0x85)
Create a directory.
Arguments:
Slot |
Type |
Description |
|---|---|---|
[0] |
pointer |
Path (DATA chunk) |
[1] |
integer |
Path length |
[2] |
integer |
Mode (passed to host mkdir, may be masked by umask) |
Returns:
0: Success-1: Error (EEXIST, ENOENT for missing parent, etc.)
Host implementation: Wraps POSIX mkdir().
SYS_RMDIR (0x86)
Remove an empty directory.
Arguments:
Slot |
Type |
Description |
|---|---|---|
[0] |
pointer |
Path (DATA chunk) |
[1] |
integer |
Path length |
Returns:
0: Success-1: Error (ENOTEMPTY, ENOENT, etc.)
Host implementation: Wraps POSIX rmdir().
SYS_FTRUNCATE (0x87)
Truncate an open file to a specified length.
Arguments:
Slot |
Type |
Description |
|---|---|---|
[0] |
integer |
File descriptor |
[1] |
pointer |
Pointer to 8-byte length value (DATA chunk, little-endian) |
[2] |
integer |
Length size (always 8) |
Returns:
0: Success-1: Error
Host implementation: Wraps POSIX ftruncate(). Length is sent
as an 8-byte little-endian value in a DATA chunk to handle 64-bit
file sizes on all guest architectures.
SYS_FSYNC (0x88)
Flush file data to storage.
Arguments:
Slot |
Type |
Description |
|---|---|---|
[0] |
integer |
File descriptor |
Returns:
0: Success-1: Error
Host implementation: Wraps POSIX fsync().
Console Extension
SYS_READC_POLL (0x89)
Non-blocking console character read.
Problem: The existing SYS_READC (0x07) blocks forever waiting for input. A Linux TTY driver cannot block the kernel; it needs to poll for input availability.
Arguments: None.
Returns:
0-255: Character read-1: No character available (not an error, just empty)
Host implementation: Uses select() or poll() on stdin with
zero timeout, then read() if data is available.
Symlink Operations
These are lower priority than the core directory and metadata operations but are required for complete filesystem support.
Opcode |
Syscall |
POSIX Wrapper |
Description |
|---|---|---|---|
0x8A |
SYS_LINK |
|
Create hard link |
0x8B |
SYS_SYMLINK |
|
Create symbolic link |
0x8C |
SYS_READLINK |
|
Read symbolic link target |
Opcode Summary
Directory Operations (3):
Opcode |
Syscall |
POSIX Wrapper |
|---|---|---|
0x80 |
SYS_OPENDIR |
|
0x81 |
SYS_READDIR |
|
0x82 |
SYS_CLOSEDIR |
|
File Metadata (3):
Opcode |
Syscall |
POSIX Wrapper |
|---|---|---|
0x83 |
SYS_STAT |
|
0x84 |
SYS_FSTAT |
|
0x8D |
SYS_LSTAT |
|
File Operations (4):
Opcode |
Syscall |
POSIX Wrapper |
|---|---|---|
0x85 |
SYS_MKDIR |
|
0x86 |
SYS_RMDIR |
|
0x87 |
SYS_FTRUNCATE |
|
0x88 |
SYS_FSYNC |
|
Console (1):
Opcode |
Syscall |
Implementation |
|---|---|---|
0x89 |
SYS_READC_POLL |
|
Symlinks (3):
Opcode |
Syscall |
POSIX Wrapper |
|---|---|---|
0x8A |
SYS_LINK |
|
0x8B |
SYS_SYMLINK |
|
0x8C |
SYS_READLINK |
|
Total: 14 opcodes in the 0x80-0x8D range.
Linux Driver Architecture
Filesystem Driver (semihostfs)
A Linux VFS driver implements these operations against the extension opcodes:
static struct file_system_type semihostfs_type = {
.name = "semihostfs",
.mount = semihostfs_mount,
.kill_sb = kill_litter_super,
};
static const struct inode_operations semihostfs_dir_iops = {
.lookup = semihostfs_lookup, /* SYS_STAT */
.mkdir = semihostfs_mkdir, /* SYS_MKDIR */
.rmdir = semihostfs_rmdir, /* SYS_RMDIR */
.create = semihostfs_create, /* SYS_OPEN with O_CREAT */
.unlink = semihostfs_unlink, /* SYS_REMOVE */
.rename = semihostfs_rename, /* SYS_RENAME */
};
static const struct file_operations semihostfs_dir_fops = {
.iterate_shared = semihostfs_readdir, /* SYS_READDIR */
};
static const struct file_operations semihostfs_file_fops = {
.read = semihostfs_read, /* SYS_READ */
.write = semihostfs_write, /* SYS_WRITE */
.llseek = semihostfs_llseek, /* SYS_SEEK */
.fsync = semihostfs_fsync, /* SYS_FSYNC */
};
static const struct inode_operations semihostfs_file_iops = {
.getattr = semihostfs_getattr, /* SYS_FSTAT */
.setattr = semihostfs_setattr, /* SYS_FTRUNCATE for size */
};
Mount usage:
mount -t semihostfs none /mnt/host
ls /mnt/host # Lists host's share directory
cat /mnt/host/foo.txt # Reads host file
echo "hi" > /mnt/host/x # Creates/writes host file
Block Device Access
A Linux block driver can use disk image files on the host via the existing ARM-compatible syscalls. No block-specific opcodes are needed:
SYS_OPEN: Open disk image file
SYS_READ / SYS_WRITE: Read/write sectors
SYS_SEEK: Seek to sector offset
SYS_FSYNC: Flush writes to storage
SYS_FSTAT: Get image size
Synchronous operation is acceptable for an initial implementation.
TTY Driver
A Linux TTY driver uses the existing console syscalls plus SYS_READC_POLL:
Opcode |
Syscall |
Linux Use |
|---|---|---|
0x03 |
SYS_WRITEC |
Write single character to console |
0x04 |
SYS_WRITE0 |
Write null-terminated string |
0x07 |
SYS_READC |
Read character (blocking) |
0x89 |
SYS_READC_POLL |
Read character (non-blocking, for poll/select) |
0x09 |
SYS_ISTTY |
Check if fd is a TTY |
The blocking SYS_READC is usable for simple console input;
SYS_READC_POLL enables proper TTY driver poll() implementation.
Other Existing Syscalls Useful from Linux
Heap / Memory Info (SYS_HEAPINFO, 0x16):
Early boot: kernel can discover available RAM
Platform driver: export memory layout to
/sys/firmware/Stack guard: inform kernel of stack boundaries
Time Services:
SYS_CLOCK (0x10): monotonic clock (centiseconds since start)
SYS_TIME (0x11): RTC / wall clock (seconds since epoch)
SYS_ELAPSED (0x30): high-resolution timer (64-bit tick count)
SYS_TICKFREQ (0x31): timer frequency (ticks per second)
System Services:
SYS_GET_CMDLINE (0x15): pass kernel boot parameters from host
SYS_EXIT (0x18) / SYS_EXIT_EXTENDED (0x20): implement
reboot()/halt()
Implementation Notes
Guest-Side Allocation Model
The ZBC protocol requires the guest to allocate everything; the host only fills in pre-allocated space. This applies to all the extension opcodes:
Guest builds the complete RIFF buffer with:
RIFF header (
RIFF+ size +SEMI)CNFG chunk (on first request only)
CALL chunk with sub-chunks (opcode, PARM, DATA)
Pre-allocated RETN chunk (sized for the expected response)
Pre-allocated ERRO chunk (typically 64 bytes)
Guest writes buffer address to RIFF_PTR register.
Host reads buffer, executes syscall, writes response into RETN.
Guest reads response from same buffer.
For opcodes returning variable-length data (SYS_READDIR, SYS_STAT), the guest must pre-allocate sufficient RETN space based on the maximum expected response size.
Opcode Table Entries
Each extension opcode has an entry in the opcode table. Example for SYS_STAT:
{SH_SYS_STAT, 4,
{{ZBC_CHUNK_DATA_PTR, 0, 1}, /* path from args[0], len from args[1] */
{ZBC_CHUNK_PARM_UINT, 1, 0}, /* path_len */
{ZBC_CHUNK_NONE, 0, 0},
{ZBC_CHUNK_NONE, 0, 0}},
ZBC_RESP_DATA, 2, 3} /* dest=args[2], max_len=args[3]=48 */
/* Guest call: args = {path_ptr, path_len, stat_buf_ptr, 48} */
Backend Vtable
The host zbc_backend_t vtable carries one function pointer per
extension opcode:
/* Directory operations - wrap POSIX opendir/readdir/closedir */
int (*opendir)(void *ctx, const char *path, size_t path_len);
int (*readdir)(void *ctx, int dirfd, void *buf, size_t buf_size);
int (*closedir)(void *ctx, int dirfd);
/* File metadata - wrap POSIX stat/fstat/lstat */
int (*stat)(void *ctx, const char *path, size_t path_len, void *stat_buf);
int (*fstat)(void *ctx, int fd, void *stat_buf);
int (*lstat)(void *ctx, const char *path, size_t path_len, void *stat_buf);
/* File operations - wrap POSIX directly */
int (*mkdir)(void *ctx, const char *path, size_t path_len, int mode);
int (*rmdir)(void *ctx, const char *path, size_t path_len);
int (*ftruncate)(void *ctx, int fd, uint64_t length);
int (*fsync)(void *ctx, int fd);
/* Console */
int (*readc_poll)(void *ctx);
/* Symlinks */
int (*link)(void *ctx, const char *src, size_t src_len,
const char *dst, size_t dst_len);
int (*symlink)(void *ctx, const char *target, size_t target_len,
const char *linkpath, size_t link_len);
int (*readlink)(void *ctx, const char *path, size_t path_len,
void *buf, size_t buf_size);
Client Code Size Impact
For constrained platforms like the 6502, adding the 14 extension opcodes to the client library costs approximately 200-600 bytes of code, depending on which subset is actually called. Unused opcodes are dead-code eliminated by the linker.