A toy Linux x86_64 ELF binary emulator using Unicorn Engine.
Celebi loads statically-linked x86_64 ELF binaries and executes them by emulating the CPU and intercepting syscalls. It provides minimal Linux syscall emulation sufficient to run simple programs compiled with musl libc. This was written as part of a personal learning exercise around early Linux binary startup procedures and emulation requirements.
- ELF binary loading (PT_LOAD segments)
- x86_64 CPU emulation via Unicorn Engine
- Linux syscall emulation:
- Process control:
exit,exit_group - Memory:
brk,mmap,mprotect - I/O:
write,writev,ioctl,readlink - Identity:
getuid,geteuid,getgid,getegid,getpid,gettid - System:
uname,getrandom,arch_prctl - Signals:
rt_sigprocmask
- Process control:
- Thread Local Storage (TLS) setup via
arch_prctl(ARCH_SET_FS) - Proper stack setup with auxiliary vectors (auxv)
- Only supports statically-linked ELF binaries (no dynamic linking)
- Single-threaded only
- No file I/O beyond stdout/stderr
- Limited syscall support (just enough for basic programs)
- No signal delivery
cargo build --release# Run a binary
./target/release/celebi <path-to-elf-binary>
# Or use cargo
cargo run --release -- <path-to-elf-binary>The examples/ directory contains simple C programs for testing. Build them with Docker:
# Build all examples (requires Docker)
./scripts/build-examples.sh
# Build a specific example
./scripts/build-examples.sh hello.c
# Clean compiled binaries
./scripts/build-examples.sh cleanRun examples:
# Using the helper script
./scripts/run-example.sh hello
# Or directly
cargo run --release -- examples/bin/hello| Example | Description | Exit Code |
|---|---|---|
hello |
printf test (writev, stdio, TLS) | 42 |
loops |
Control flow and function calls | 44 |
minimal |
Minimal program (just exit) | 0 |
$ ./scripts/run-example.sh hello
[*] Running: hello
========================================
[*] Loading: "examples/bin/hello"
[*] Loading segment: vaddr=0x400000, filesz=0x28c, memsz=0x28c
[*] Loading segment: vaddr=0x401000, filesz=0x39e7, memsz=0x39e7
[*] Loading segment: vaddr=0x405000, filesz=0xcdc, memsz=0xcdc
[*] Loading segment: vaddr=0x406fb8, filesz=0x158, memsz=0x838
[*] Entry point: 0x40105b
[*] Setting up stack at 0x7f0000000000
[*] Auxiliary vector written, rsp=0x7f00001fff90
[*] Stack setup complete, rsp=0x7f00001fff78
[*] Setting up heap at 0x600000
[*] Starting emulation...
----------------------------------------
arch_prctl(SetFs, 0x407698)
set_tid_address(0x4077d0)
ioctl(1, 0x5413)
Hello from celebi emulator!
Testing: 2 + 3 = 5
exit_group(42)
celebi/
├── src/
│ ├── main.rs # Emulator core
│ └── syscall_linux.rs # Syscall definitions
├── examples/
│ ├── hello.c # printf test
│ ├── loops.c # Control flow test
│ ├── minimal.c # Minimal program
│ └── bin/ # Compiled binaries (gitignored)
├── scripts/
│ ├── build-examples.sh # Docker-based compiler
│ └── run-example.sh # Example runner
├── Cargo.toml
└── README.md
-
ELF Loading: Parses the ELF header and loads PT_LOAD segments into emulated memory at their specified virtual addresses.
-
Stack Setup: Creates a proper stack with argc, argv (empty), envp (empty), and auxiliary vectors. The auxv includes AT_RANDOM (for stack canaries), AT_PAGESZ, AT_ENTRY, and program header info.
-
Heap Setup: Initializes the program break (brk) for heap allocation. The
brk()syscall expands this region as needed. -
TLS Setup: When the C library calls
arch_prctl(ARCH_SET_FS, addr), we write to the x86_64 FS_BASE register. This enables thread-local storage, which is required for errno and other libc internals. -
Syscall Interception: Hooks the SYSCALL instruction and dispatches to handlers based on the syscall number in RAX. Each handler reads arguments from registers and writes the return value to RAX.
-
Execution: Starts emulation at the ELF entry point and runs until
exit()orexit_group()is called.
MIT