Skip to content

Add MIPS64 (n64 ABI) Linux support#1631

Draft
retrocpugeek wants to merge 18 commits into
qilingframework:devfrom
retrocpugeek:feature/mips64be
Draft

Add MIPS64 (n64 ABI) Linux support#1631
retrocpugeek wants to merge 18 commits into
qilingframework:devfrom
retrocpugeek:feature/mips64be

Conversation

@retrocpugeek

@retrocpugeek retrocpugeek commented Jun 21, 2026

Copy link
Copy Markdown

Summary

Adds QL_ARCH.MIPS64 so Qiling can emulate MIPS64 Linux user-space in both big-
and little-endian, built out from raw shellcode up to full dynamically-linked
glibc ELF binaries. The native backends already support MIPS64 (Unicorn
UC_MODE_MIPS64, Capstone/Keystone MIPS64); this wires it through the framework
and fixes the big-endian / n64 correctness issues that only surface once real
glibc binaries run.

What's included

  • QlArchMIPS64 (qiling/arch/mips64.py) — 64-bit arch class reusing the
    shared MIPS register map; defaults to a MIPS64R2-class CPU model.
  • n64 calling convention & syscall ABI — first eight args in a0a7
    (the tail four are the physical registers $8$11) with no o32 shadow space;
    n64 syscall table (base 5000) from asm/unistd_n64.h; MIPS64 routed through the
    MIPS arch-specific syscall constants.
  • ELF loadingEM_MIPS + ELFCLASS64 detection and n64 doubleword stack
    alignment; static, PIE, and non-PIE (ET_EXEC) dynamically-linked big-endian
    MIPS64 binaries.
  • Big-endian / n64 syscall correctness fixes — statx and stat64 struct byte
    order on BE guests; legacy getdents and getdents64 record alignment for the
    n64 unaligned walk; MIPS SO_* socket option values.
  • clone3 + multithreading support, with an example and test.
  • gdb-stub support — MIPS64 register description (debugger/gdb/xml/mips64/)
    and QL_ARCH.MIPS64 wiring in the gdb debugger, so ql.debugger = "gdb:..."
    works for MIPS64 targets (verified with gdb-multiarch: registers, memory,
    disassembly, single-step, breakpoints, continue, backtraces). Registers a
    target description declares but the backend can't supply are reported as zero
    rather than <unavailable>, so stricter clients (e.g. Ghidra's ghidragdb)
    can attach. The stub also honours an async interrupt (ctrl-c / \x03): the
    run hook polls the client socket during emu_start, so a free-running guest
    (an idle/event loop) can be paused from the client rather than only stopping
    at a breakpoint or exit.
  • Rootfs & tests — static and dynamic MIPS64 hello binaries, a Buildroot glibc
    tree split into its own rootfs, and shellcode / ELF / statx / multithread
    regression tests.

Commits

Core arch & n64 ABI

  • Add MIPS64 (n64 ABI) Linux support — QlArchMIPS64, n64 calling convention +
    syscall table, ELF machine detection, doubleword stack alignment
  • Route MIPS64 through the MIPS arch-specific syscall constants
  • Support loading MIPS64 ELF binaries
  • Fix MIPS socket option (SO_*) values

Examples & features

  • Add examples/shellcode_mips64_run.py
  • Add MIPS64 customapi examples and function-hook support
  • Add clone3 syscall and MIPS64 multithreading example/test

Big-endian / n64 correctness

  • Fix statx byte order on big-endian guests
  • Wire up stat64 for MIPS64
  • Align getdents64 records to fix unaligned access on MIPS
  • Align legacy getdents records (fixes n64 unaligned walk)
  • Add MIPS64 EB statx regression test

Rootfs & test binaries

  • Bump rootfs submodule to MIPS64 static glibc hello binaries
  • Bump rootfs submodule for MIPS64 dynamic hello binaries
  • tests: add MIPS64 BE Buildroot dynamic-load regression + rootfs split

Debugging

  • Add MIPS64 gdb-stub support (register XML + QL_ARCH.MIPS64 in the gdb debugger)
  • gdb: report 0 for unavailable registers so strict clients can attach
  • gdb: support async interrupt (ctrl-c) to break into a running guest

Testing

The MIPS64 paths are covered by the existing suites: test_shellcode.py
(test_linux_mips64eb / test_linux_mips64el), test_elf.py (MIPS64 EB static,
Buildroot dynamic, statx, stat64, getdents byte-order/alignment),
test_elf_multithread.py (clone3 multithreading), and test_debugger.py
(test_gdbdebug_mips64).

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Introduce QL_ARCH.MIPS64 so Qiling can emulate MIPS64 Linux user-space
shellcode in both big- and little-endian. The native backends already
support it (Unicorn UC_MODE_MIPS64, Capstone/Keystone CS/KS_MODE_MIPS64).

  - QlArchMIPS64: 64-bit arch class reusing the shared MIPS register map
  - n64 calling convention and syscall ABI: the first eight arguments are
    passed in registers (a0-a3 and a4-a7, the physical registers $8-$11)
    with no on-stack shadow space, unlike o32
  - n64 syscall table (base 5000) generated from the Linux uapi header
    asm/unistd_n64.h
  - wire MIPS64 through the Linux/blob OS layers, ELF machine detection
    (EM_MIPS + ELFCLASS64) and n64 doubleword stack alignment
  - add big- and little-endian MIPS64 shellcode tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@retrocpugeek

Copy link
Copy Markdown
Author

Follow-up note: reconcile with #1630

This PR and #1630 both modify the MIPS branch in qiling/os/linux/linux.py, so whichever merges second will need a small reconciliation.

#1630 introduces a shared hook_cpu_exception handler (faithful SIGILL on illegal instructions) and a MIPS_EXCP enum, and registers them for MIPS32. To keep this PR self-contained against dev, MIPS64 here mirrors dev's current MIPS32 convention — it hooks the syscall exception with the literal number 17 and does not register the RI illegal-instruction handler.

Once both land, MIPS64 should also be wired to hook_cpu_exception on MIPS_EXCP.RI (and switch 17MIPS_EXCP.SYSCALL), so it terminates cleanly on illegal instructions the same way MIPS32 will. The MIPS64 shellcode test here (write + exit_group) doesn't exercise that path, which is why it's safe to defer.

Demonstrate MIPS64 (n64 ABI) Linux shellcode emulation in both big- and
little-endian, using a write/exit_group shellcode that prints to stdout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@retrocpugeek retrocpugeek marked this pull request as draft June 21, 2026 11:00
The Linux/POSIX syscall layer has several arch-specific code paths keyed on
QL_ARCH.MIPS (open flags, socket type/domain/level/option/ip-option tables,
mmap flags, SHMLBA, the sigset/sigaction layout, SIGSTOP=23, the MIPS
sigprocmask variant, the MIPS stat struct, the pipe()-returns-in-registers
convention and set_thread_area). These values are MIPS-arch-specific and
shared by the n64 ABI, so MIPS64 must take the same paths.

Notably the MIPS64 stat struct already existed but was reached via
QL_ARCH.MIPS + bits==64; with a distinct QL_ARCH.MIPS64 type that branch was
missed and stat fell back to the x86 layout. The struct definitions are
parameterised on arch.bits/native_type, so the n64 layouts fall out correctly
once the branch fires.

pread64's o32 stack-offset hack is intentionally left MIPS-only: under n64 the
64-bit offset arrives in a register, so the generic argument handling is
already correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
retrocpugeek and others added 12 commits June 22, 2026 00:05
Two unicorn-level obstacles prevented MIPS64 ELFs from running; both are
handled in QlArchMIPS64 so the rest of Qiling is unaffected:

  - Address space: unicorn's MIPS64 CPU-MMU only executes within the low 2GB
    (useg); MIPS64 ELFs link at 0x120000000 and Qiling lays the stack/mmap out
    above 4GB. Switch the engine to unicorn's virtual-TLB mode and install an
    identity TLB-fill hook (paddr == vaddr). This bypasses the CPU-MMU segment
    checks while keeping virtual == physical, so the flat memory model (mem
    map/read/write, loader writes, syscall pointer reads) keeps working as-is.

  - Instruction set: unicorn's default MIPS64 core is an old MIPS III (R4000)
    that lacks MIPS IV / MIPS64 instructions such as movn/movz, which compilers
    emit freely; they trap as reserved instructions. Default MIPS64 to a generic
    MIPS64 Release 2 core (add MIPS64_CPU_MODEL) so the full ISA decodes.

Also give FunctionHook a MIPS64 branch (shares the MIPS R_MIPS_* relocation
type numbers) so it initializes for MIPS64 ELFs.

Add static-ELF tests (big- and little-endian) that load at 0x120000000 and
execute a movz, exercising both fixes. They reference the mips64 rootfs
binaries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Points examples/rootfs at the rootfs commit that replaces the freestanding
MIPS64 BE/EL hello binaries with real static-glibc ones, so the
test_elf_linux_mips64{eb,el}_static tests exercise full libc startup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
function_hook.py's get_ret_pc/context_fixup/set_ret handled MIPS (o32)
but not MIPS64 (n64), so set_api/function interception raised on MIPS64.
n64 returns via $ra like o32, so fold MIPS64 into the MIPS branch.

Add hello_mips64_linux_customapi.py and hello_mips64el_linux_customapi.py
(mirroring hello_mips32_linux_customapi.py) that run the dynamic n64 hello
binaries and intercept puts().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Picks up the rootfs commit adding the dynamic n64 hello binaries + loader
and libc used by the hello_mips64*_linux_customapi.py examples.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Statx32/Statx64 (and their StatxTimestamp structs) were defined only as
little-endian ctypes.Structure, and ql_syscall_statx used them regardless
of guest endianness. Every other stat-family struct already has an explicit
big-endian variant selected by ql.arch.endian; statx was the lone gap.

On a big-endian guest (e.g. MIPS/MIPS64 EB) this byte-swaps every statx
field. In particular stx_mode loses its S_IFDIR bit, so a directory looks
like a plain file and `busybox ls /` just echoes the argument instead of
listing the directory.

Add Statx{32,64}EB / StatxTimestamp{32,64}EB (ctypes.BigEndianStructure,
reusing the little-endian field layouts) and pick them when the guest is
big-endian. The little-endian path is byte-for-byte unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getdents64 wrote linux_dirent64 records with d_reclen = header + name,
without rounding up to the alignment of the leading u64 d_ino. The next
record's d_ino then lands unaligned, and a strict-alignment guest (e.g.
MIPS/MIPS64) faults with UC_ERR_READ_UNALIGNED when walking the buffer.
x86 tolerates the unaligned load, which is why this was never caught.

Round d_reclen up to the d_ino alignment, matching the kernel. The rounding
is scoped to the getdents64 (is_64) branch only: getdents64 places d_type
before d_name so the trailing pad bytes are inert, whereas the legacy
getdents layout stores d_type at offset d_reclen-1 and would break if padded
the same way -- that is what got the earlier blanket fix (PR qilingframework#1419) reverted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
statx() filled a little-endian statx struct regardless of guest endianness,
so on a big-endian guest stx_mode was byte-swapped and lost its S_IFDIR bit
(fixed in 7550983). Add examples/src/linux/statx.c and a static MIPS64 EB
test binary, plus test_elf_linux_mips64eb_statx asserting statx reports "/"
as a directory. Without the fix the test sees a byte-swapped mode (NOTDIR).

Bumps the rootfs submodule pin to include the mips64_statx binary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement ql_syscall_clone3 (sched.py) by translating struct clone_args
into the legacy clone() argument set and delegating. Modern glibc issues
clone3 from pthread_create, so without this thread creation fails on any
arch built against a recent toolchain (Qiling does not return -ENOSYS for
unimplemented syscalls, so glibc's clone3->clone fallback never fires).

Translations: child_stack = stack + stack_size (clone3 gives base+size,
legacy clone wants the top), exit_signal folded into the flags' CSIGNAL
byte, and an x8664 pre-swap to cancel ql_syscall_clone's arch-specific
newtls<->child_tidptr swap.

Also fix examples/src/linux/multithreading.c: pthread_join writes a void*
(8 bytes on 64-bit), so passing an int* makes the store unaligned and
faults on strict-alignment 64-bit targets (MIPS64). Use a void* dest.

Add examples/multithreading_mips64_linux.py and a mips64eb regression
test mirroring the other arches; rootfs pin bumped for the new binary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_stat64_struct (used by the stat64/fstat64/lstat64/fstatat64 handlers,
and by the plain lstat handler which also routes through pack_stat64_struct)
had no MIPS64 branch: a 64-bit MIPS guest fell through to the little-endian
x86 stat64 struct, byte-swapping and misplacing every field. busybox 'ls -la'
on a big-endian MIPS64 (Octeon) rootfs showed garbage perms/size as a result.

Handle MIPS64 like get_stat_struct does -- return the 64-bit MIPS stat struct
(LinuxMips64Stat / LinuxMips64EBStat by endianness); n64 has no distinct
stat64, its layout is the 64-bit stat. Also stop warning 'stat64 on a 64bit
system' for MIPS64, where this path is legitimately reached.

Add test_elf.ELFTest.test_elf_linux_mips64eb_stat64: drives ql_syscall_stat64
and ql_syscall_lstat on a directory and asserts S_ISDIR reads back correctly
under the MIPS64 BE struct (fails on the x86 fallback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
__getdents_common did not round legacy linux_dirent records up to the
alignment of their word-sized d_ino, so on n64 (8-byte d_ino) the next
record's d_ino landed unaligned and a strict-alignment guest (MIPS)
faulted while walking the buffer -- e.g. older glibc busybox 'ls' (which
uses the legacy getdents syscall) crashed on directory listings.

Pad each record to the d_ino alignment like the kernel does. Legacy
linux_dirent stores d_type in the record's last byte (offset d_reclen-1),
so the padding goes between d_name and d_type to keep d_type there;
padding without relocating d_type is what got the earlier blanket fix
(PR qilingframework#1419) reverted. getdents64 is unchanged (its d_type precedes d_name).

Add test_elf.ELFTest.test_elf_linux_mips64eb_getdents: walks a real
directory's records and asserts each is 8-byte aligned, tiles the buffer,
and keeps d_type (DT_DIR for '.'/'..'). Existing x86/x8664 getdents tests
still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
linux_mips_socket_options used generic/incorrect values. MIPS has its own
SO_* numbering (arch/mips/include/uapi/asm/socket.h) with the buffer/timeout/
type options in the 0x1000 range. The table listed SO_SNDBUF=0x01,
SO_RCVBUF=0x02 (should be 0x1001/0x1002), SO_SND/RCVLOWAT and SO_SND/RCVTIMEO_OLD
all wrong, SO_RCVLOWAT=0x04 silently collided with SO_REUSEADDR (Enum alias),
and SO_OOBINLINE/SO_REUSEPORT were 0x00.

A guest setsockopt(SOL_SOCKET, SO_RCVBUF, ...) (e.g. busybox ping) therefore
raised 'Could not convert emulated socket option 4098'. Correct all values to
the MIPS uapi and add the missing SO_TYPE/SO_ERROR/SO_ACCEPTCONN/SO_PROTOCOL/
SO_DOMAIN.

Add test_elf.ELFTest.test_setsockopt_mips_so_rcvbuf: opens a socket on a MIPS
guest and asserts setsockopt(SOL_SOCKET, SO_RCVBUF) succeeds. Self-contained,
runs on stock unicorn; fails on dev, passes with the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
examples/rootfs now provides mips64_linux_buildroot, a dedicated rootfs
carrying the Buildroot toolchain's ld.so.1 + glibc, so a dynamically-linked
non-PIE MIPS64 binary can be brought up by its own dynamic loader with
libc.so.6 mapped and relocated before main(). The default mips64_linux tree
ships a different glibc build whose ld.so cannot load that binary, and both
binaries hard-code interp /lib64/ld.so.1, so the two glibc environments live
in separate trees.

- bump examples/rootfs (mips64_hello_buildroot moved into
  mips64_linux_buildroot/, with matching lib/{libc.so.6,ld.so.1} + lib64)
- test_elf.py: test_elf_linux_mips64eb_buildroot_dynamic

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@retrocpugeek retrocpugeek reopened this Jul 12, 2026
qiling has QL_ARCH.MIPS64 as a full architecture, but its gdbserver had
no MIPS64 register description, so `ql.debugger = "gdb:..."` failed for
MIPS64 targets. Add generic (endianness-agnostic) support:

- debugger/gdb/xml/mips64/{target,mips64-cpu,mips64-cp0,mips64-fpu}.xml:
  64-bit register description mirroring the 32-bit mips/ set (GPRs,
  lo/hi/pc, cp0, fpu); architecture mips:isa64r2.
- xmlregs.py: wire QL_ARCH.MIPS64 into the register map (reuses the
  shared MIPS unicorn reg_map; register values are read 64-bit wide).
- gdb.py: handle QL_ARCH.MIPS64 in the '?' stop-reply handler (it was a
  KeyError) and skip the frame-pointer reg pair as for MIPS. Also make
  qXfer:exec-file:read fall back to the host path when the executable was
  loaded from outside the rootfs (it previously raised ValueError).
- tests/test_debugger.py: add test_gdbdebug_mips64 (mips64_hello_static).

Verified with gdb-multiarch (set architecture mips:isa64r2; set endian
big): registers, memory, disassembly, single-step, software breakpoints,
continue and backtraces all work against a big-endian MIPS64 guest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
retrocpugeek and others added 2 commits July 12, 2026 17:14
Registers that a target description declares but the backend cannot supply
(e.g. MIPS cp0 badvaddr/cause and the fpu regs, which unicorn does not expose)
were sent as '<unavailable>' in the g/p replies. Plain gdb tolerates that, but
stricter clients abort: Ghidra's ghidragdb raises "value is not available" /
"Cannot convert <reg>'s value: '<unavailable>'" when a register in gdb's
'general' group is unavailable, which prevented attaching Ghidra's debugger to
a MIPS64 target. Report such registers as zero instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The stub drove continue by calling emu_start synchronously and only read
the socket again once the target stopped on its own, so the bare \x03 break
byte a client sends to pause a running target was never seen. A guest that
free-runs (e.g. an idle/event loop) could not be interrupted at all --
gdb/Ghidra reported 'Cannot execute this command while the target is running'.

Poll the client socket for the break byte from the per-instruction run hook
(dbg_hook), which does run on the emulation thread during emu_start:

- GdbSerialConn.poll_interrupt(): non-blocking select+recv, True on \x03.
- QlGdbUtils.dbg_hook: throttled (every INTR_POLL_INTERVAL insns) check of an
  installed check_interrupt callback; on a break, stop emulation and record it.
- handle_c: reply SIGINT when the stop was an interrupt rather than a
  breakpoint or normal exit.

Verified against a free-running MIPS64 BE guest: \x03 -> S02 in <1ms; the
existing gdb-stub regression test still passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant