Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ fastlane/screenshots
fastlane/test_output

e2e_out/
tests/aarch64/hello
tests/aarch64/exit42
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</a>
</p>

A project to get a Linux shell running on iOS, using usermode x86 emulation and syscall translation.
A project to get a Linux shell running on iOS, using usermode AArch64 (default) or x86 emulation and syscall translation.

For the current status of the project, check the issues tab, and the commit logs.

Expand Down Expand Up @@ -42,6 +42,20 @@ Open the project in Xcode, open iSH.xcconfig, and change `ROOT_BUNDLE_IDENTIFIER

To set up your environment, cd to the project and run `meson build` to create a build directory in `build`. Then cd to the build directory and run `ninja`.

By default the guest architecture is AArch64. Build in Xcode with `ISH_GUEST_ARCH = aarch64` (set in `app/iSH.xcconfig`).

For a runnable shell you need an **Alpine aarch64** fakefs root (the bundled App Store rootfs is i386):

```bash
curl -fL -o alpine-aarch64.tar.gz \
https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/aarch64/alpine-minirootfs-3.21.7-aarch64.tar.gz
./build/tools/fakefsify alpine-aarch64.tar.gz alpine-aarch64
```

Then import `alpine-aarch64` in the iSH app or run `./build/ish -f alpine-aarch64 /bin/sh` from the CLI build.

Legacy i386 guest: `-Dguest_arch=i386`.

To set up a self-contained Alpine linux filesystem, download the Alpine minirootfs tarball for i386 from the [Alpine website](https://alpinelinux.org/downloads/) and run `./tools/fakefsify`, with the minirootfs tarball as the first argument and the name of the output directory as the second argument. Then you can run things inside the Alpine filesystem with `./ish -f alpine /bin/sh`, assuming the output directory is called `alpine`. If `tools/fakefsify` doesn't exist for you in your build directory, that might be because it couldn't find libarchive on your system (see above for ways to install it.)

You can replace `ish` with `tools/ptraceomatic` to run the program in a real process and single step and compare the registers at each step. I use it for debugging. Requires 64-bit Linux 4.11 or later.
Expand All @@ -64,6 +78,8 @@ Available channels:

Possibly the most interesting thing I wrote as part of iSH is the interpreter. It's not quite a JIT since it doesn't target machine code. Instead it generates an array of pointers to functions called gadgets, and each gadget ends with a tailcall to the next function; like the threaded code technique used by some Forth interpreters. The result is a speedup of roughly 3-5x compared to emulation using a simpler switch dispatch.

The default guest architecture is now AArch64. Guest gadgets live under `asbestos/gadgets-guest-aarch64/` (assembly on AArch64 hosts, C dispatch on x86_64 hosts for CI). Legacy i386 guests still use `asbestos/gadgets-x86_64/` or `asbestos/gadgets-aarch64/` depending on host CPU.

Unfortunately, I made the decision to write nearly all of the gadgets in assembly language. This was probably a good decision with regards to performance (though I'll never know for sure), but a horrible decision with regards to readability, maintainability, and my sanity. The amount of bullshit I've had to put up with from the compiler/assembler/linker is insane. It's like there's a demon in there that makes sure my code is sufficiently deformed, and if not, makes up stupid reasons why it shouldn't compile. In order to stay sane while writing this code, I've had to ignore best practices in code structure and naming. You'll find macros and variables with such descriptive names as `ss` and `s` and `a`. Assembler macros nested beyond belief. And to top it off, there are almost no comments.

So a warning: Long-term exposure to this code may cause loss of sanity, nightmares about GAS macros and linker errors, or any number of other debilitating side effects. This code is known to the State of California to cause cancer, birth defects, and reproductive harm.
5 changes: 5 additions & 0 deletions app/iSH.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ ROOT_BUNDLE_IDENTIFIER = app.ish.iSH
// It's easiest to specify your development team ID in the project build settings, but you can alternatively put it here to reduce merge conflicts
DEVELOPMENT_TEAM =

// Guest CPU architecture for meson (-Dguest_arch). Default: aarch64.
ISH_GUEST_ARCH = aarch64

// Choose logging channels to enable. Separate by spaces. Try "verbose strace".
ISH_LOG =
ISH_LOGGER = $(ISH_LOGGER_$(PLATFORM_NAME))
ISH_LOGGER_iphoneos = nslog
ISH_LOGGER_iphonesimulator = nslog
ISH_LOGGER_macosx = dprintf

// AArch64 rootfs: use Alpine aarch64 minirootfs via fakefsify (see README).
// The bundled appstore APK below is i386-only; replace after converting rootfs.
ROOTFS_URL = github.com/ish-app/roots/releases/download/g00712ff0a54b2839c5aa1a8ed758003ca65357dc/appstore-apk.tar.gz
3 changes: 2 additions & 1 deletion app/xcode-meson.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ if [[ -n "$ISH_KERNEL" ]]; then
kernel=$ISH_KERNEL
fi
kconfig=""
for var in buildtype log b_ndebug b_sanitize log_handler kernel kconfig; do
guest_arch=${ISH_GUEST_ARCH:-aarch64}
for var in buildtype log b_ndebug b_sanitize log_handler kernel kconfig guest_arch; do
old_value=$(python3 -c "import sys, json; v = next(x['value'] for x in json.load(sys.stdin) if x['name'] == '$var'); print(str(v).lower() if isinstance(v, bool) else ','.join(v) if isinstance(v, list) else v)" <<< $config)
new_value=${!var}
if [[ $old_value != $new_value ]]; then
Expand Down
23 changes: 19 additions & 4 deletions asbestos/asbestos.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,20 @@
#include "asbestos/gen.h"
#include "asbestos/frame.h"
#include "emu/cpu.h"
#include "emu/interrupt.h"
#include "guest/interrupt.h"
#include "guest/guest-config.h"
#include "util/list.h"

#if GUEST_AARCH64
#define guest_ip(cpu) ((cpu)->pc)
#define set_guest_ip(cpu, val) ((cpu)->pc = (val))
#define GUEST_BLOCK_LIMIT (PAGE_SIZE - 4)
#else
#define guest_ip(cpu) ((cpu)->eip)
#define set_guest_ip(cpu, val) ((cpu)->eip = (val))
#define GUEST_BLOCK_LIMIT (PAGE_SIZE - 15)
#endif

extern int current_pid(void);

static void fiber_block_disconnect(struct asbestos *asbestos, struct fiber_block *block);
Expand Down Expand Up @@ -124,7 +135,7 @@ static struct fiber_block *fiber_block_compile(addr_t ip, struct tlb *tlb) {
// guarantee that by stopping as soon as there's less space left than
// the maximum length of an x86 instruction
// TODO refuse to decode instructions longer than 15 bytes
if (state.ip - ip >= PAGE_SIZE - 15) {
if (state.ip - ip >= GUEST_BLOCK_LIMIT) {
gen_exit(&state);
break;
}
Expand Down Expand Up @@ -187,7 +198,7 @@ static int cpu_step_to_interrupt(struct cpu_state *cpu, struct tlb *tlb) {

int interrupt = INT_NONE;
while (interrupt == INT_NONE) {
addr_t ip = frame->cpu.eip;
addr_t ip = guest_ip(&frame->cpu);
size_t cache_index = fiber_cache_hash(ip);
struct fiber_block *block = cache[cache_index];
if (block == NULL || block->addr != ip) {
Expand Down Expand Up @@ -244,7 +255,7 @@ static int cpu_step_to_interrupt(struct cpu_state *cpu, struct tlb *tlb) {

static int cpu_single_step(struct cpu_state *cpu, struct tlb *tlb) {
struct gen_state state;
gen_start(cpu->eip, &state);
gen_start(guest_ip(cpu), &state);
gen_step(&state, tlb);
gen_exit(&state);
gen_end(&state);
Expand All @@ -263,7 +274,11 @@ int cpu_run_to_interrupt(struct cpu_state *cpu, struct tlb *tlb) {
if (cpu->poked_ptr == NULL)
cpu->poked_ptr = &cpu->_poked;
tlb_refresh(tlb, cpu->mmu);
#if GUEST_AARCH64
int interrupt = cpu_step_to_interrupt(cpu, tlb);
#else
int interrupt = (cpu->tf ? cpu_single_step : cpu_step_to_interrupt)(cpu, tlb);
#endif
cpu->trapno = interrupt;

struct asbestos *asbestos = cpu->mmu->asbestos;
Expand Down
4 changes: 4 additions & 0 deletions asbestos/frame.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#ifndef ASBESTOS_FRAME_H
#define ASBESTOS_FRAME_H
#include <stdatomic.h>
#include "emu/cpu.h"

Expand All @@ -13,3 +15,5 @@ struct fiber_frame {
struct fiber_block *last_block;
long ret_cache[FIBER_RETURN_CACHE_SIZE]; // a map of ip to pointer-to-call-gadget-arguments
};

#endif
5 changes: 5 additions & 0 deletions asbestos/gadgets-generic.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#ifndef GADGETS_GENERIC_H
#define GADGETS_GENERIC_H

#include "cpu-offsets.h"

#define ifin(thing, ...) _ifin(thing, __COUNTER__, __VA_ARGS__)
Expand Down Expand Up @@ -90,4 +93,6 @@
#define N ;
#endif

#endif

# vim: ft=gas
183 changes: 183 additions & 0 deletions asbestos/gadgets-guest-aarch64/control.S
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#include "gadgets-asm.h"
#include "guest/interrupt.h"

.gadget exit
ldr x10, [_ip], #8
str x10, [_cpu, CPU_pc]
b fiber_ret

.gadget interrupt
ldr w8, [_ip], #8
ldr x10, [_ip], #8
str x10, [_cpu, CPU_pc]
ldr x11, [_ip], #8
str x11, [_cpu, CPU_segfault_addr]
strb wzr, [_cpu, CPU_segfault_was_write]
mov _tmp, w8
b fiber_exit

.gadget nop
ldr x10, [_cpu, CPU_pc]
add x10, x10, #4
str x10, [_cpu, CPU_pc]
gret

.gadget svc
ldr x10, [_cpu, CPU_pc]
add x10, x10, #4
str x10, [_cpu, CPU_pc]
mov _tmp, #INT_SYSCALL
b fiber_exit

.gadget brk
ldr x10, [_cpu, CPU_pc]
add x10, x10, #4
str x10, [_cpu, CPU_pc]
mov _tmp, #INT_BREAKPOINT
b fiber_exit

.gadget ret
ldr w10, [_ip], #8
read_xn x11, w10
str x11, [_cpu, CPU_pc]
b fiber_ret

.gadget branch
ldr x10, [_ip], #8
ldr w11, [_ip], #8
ldr x12, [_cpu, CPU_pc]
add x12, x12, x10
cbnz w11, 1f
b 2f
1:
add x13, x12, #4
str x13, [_cpu, CPU_x + 30*8]
2:
str x12, [_cpu, CPU_pc]
b fiber_ret

.gadget mov_wide
ldr w8, [_ip], #8
ldr w9, [_ip], #8
ldr w10, [_ip], #8
ldr w11, [_ip], #8
lsl w9, w9, #4
lsl w12, w11, w9
uxtw x12, w12
cmp w8, #0
b.eq 1f
cmp w8, #2
b.eq 2f
cmp w8, #3
b.ne 3f
read_xn x13, w10
mov x14, #0xffff
uxtw x9, w9
lsl x14, x14, x9
mvn x14, x14
and x13, x13, x14
orr x12, x13, x12
b 3f
1:
mvn x12, x12
b 3f
2:
nop
3:
write_xn w10, x12
ldr x10, [_cpu, CPU_pc]
add x10, x10, #4
str x10, [_cpu, CPU_pc]
gret

.gadget branch_cond
ldr w8, [_ip], #8
ldr x10, [_ip], #8
stp x29, x30, [sp, -0x10]!
mov x0, _cpu
mov w1, w8
bl helper_a64_cond_true
ldp x29, x30, [sp], 0x10
cbz w0, cond_false
ldr x14, [_cpu, CPU_pc]
add x14, x14, x10
str x14, [_cpu, CPU_pc]
b fiber_ret
cond_false:
ldr x14, [_cpu, CPU_pc]
add x14, x14, #4
str x14, [_cpu, CPU_pc]
gret

.gadget cbz
// args: sf, op (0=cbz/1=cbnz), rt, imm
ldr w8, [_ip], #8
ldr w9, [_ip], #8
ldr w10, [_ip], #8
ldr x11, [_ip], #8
read_xn x12, w10
cbnz w8, 1f
// sf=0: compare as 32-bit (W-form CBZ/CBNZ)
uxtw x12, w12
1:
// take branch when (val==0) == (op==0), i.e. is_zero != op
cmp x12, #0
cset w12, eq
cmp w12, w9
b.eq 2f
// take branch
ldr x13, [_cpu, CPU_pc]
add x13, x13, x11
str x13, [_cpu, CPU_pc]
b fiber_ret
2:
ldr x13, [_cpu, CPU_pc]
add x13, x13, #4
str x13, [_cpu, CPU_pc]
gret

.gadget tbz
ldr w8, [_ip], #8
ldr w9, [_ip], #8
ldr w10, [_ip], #8
ldr x11, [_ip], #8
read_xn x12, w9
uxtw x10, w10
lsr x12, x12, x10
and x12, x12, #1
cmp w12, w8
b.ne 1f
ldr x13, [_cpu, CPU_pc]
add x13, x13, x11
str x13, [_cpu, CPU_pc]
b fiber_ret
1:
ldr x13, [_cpu, CPU_pc]
add x13, x13, #4
str x13, [_cpu, CPU_pc]
gret

.gadget adr
ldr w9, [_ip], #8
ldr x10, [_ip], #8
ldr x11, [_cpu, CPU_pc]
add x11, x11, x10
write_xn w9, x11
ldr x10, [_cpu, CPU_pc]
add x10, x10, #4
str x10, [_cpu, CPU_pc]
gret

.gadget adrp
ldr w9, [_ip], #8
ldr x10, [_ip], #8
ldr x11, [_cpu, CPU_pc]
bic x11, x11, #0xfff
add x11, x11, x10, lsl #12
write_xn w9, x11
ldr x10, [_cpu, CPU_pc]
add x10, x10, #4
str x10, [_cpu, CPU_pc]
gret

# vim: ft=gas
16 changes: 16 additions & 0 deletions asbestos/gadgets-guest-aarch64/entry-host.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#include "asbestos/asbestos.h"
#include "asbestos/gadgets-guest-aarch64/gadgets.h"
#include "guest/interrupt.h"

int fiber_enter(struct fiber_block *block, struct fiber_frame *frame, struct tlb *tlb) {
unsigned long *ip = block->code;
while (true) {
a64_gadget_fn gadget = (a64_gadget_fn) *ip++;
int result = gadget(frame, tlb, &ip);
int kind = result & 0xff;
if (kind == A64_GADGET_INTERRUPT)
return result >> 8;
if (kind == A64_GADGET_END_BLOCK)
return INT_NONE;
}
}
Loading