My penguin avatar

Kiesel Devlog #9: JavaScript on a Printer

Published on 2024-07-02.

Firstly: go and read Pwning a Brother labelmaker, for fun and interop! by Domi until the end!

Super cool, right? Getting SSH credentials in response to my shitpost was not exactly what I expected but here we are. Let's briefly dive into how I ported my toy JS engine to this Linux-based labelmaker :^)

Initial Attempt

I knew the thing is powered by a 32-bit ARM CPU, so I quickly looked for a suitable Zig target:

$ zig targets | jq .libc | grep arm
  "armeb-linux-gnueabi",
  "armeb-linux-gnueabihf",
  "armeb-linux-musleabi",
  "armeb-linux-musleabihf",
  "arm-linux-gnueabi",
  "arm-linux-gnueabihf",
  "arm-linux-musleabi",
  "arm-linux-musleabihf",
  "arm-windows-gnu",

arm-linux-musleabi seemed the most promising (statically linked libc, softfloat) so I tried that, which seemingly worked fine:

$ zig build -Dtarget=arm-linux-musleabi -Doptimize=ReleaseSafe
$ file ./zig-out/bin/kiesel
./zig-out/bin/kiesel: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, stripped

Alas, on the other side:

# wget https://f.sakamoto.pl/linus/kiesel && chmod +x kiesel
# ./kiesel
Illegal instruction
#

Luckily the crash is obvious: we need to refine the compilation target instead of letting Zig pick a baseline CPU.

What's Inside, Exactly?

Definitely not Intel:

# cat /proc/cpuinfo
Processor	: ARM926EJ-S rev 5 (v5l)
BogoMIPS	: 177.15
Features	: swp half thumb fastmult edsp java
CPU implementer	: 0x41
CPU architecture: 5TEJ
CPU variant	: 0x0
CPU part	: 0x926
CPU revision	: 5

Hardware	: Conexant DigiColor
Revision	: 0000
Serial		: 0000000000000000

Aha, it's the processor that first introduced running Java bytecode in hardware, from 2001. Off to a good start!

the first processor with Jazelle technology was the ARM926EJ-S.

Luckily Zig has a target for that:

// std/Target/arm.zig
pub const cpu = struct {
    // ...
    pub const arm926ej_s = CpuModel{
        .name = "arm926ej_s",
        .llvm_name = "arm926ej-s",
        .features = featureSet(&[_]Feature{
            .v5te,
        }),
    };
};

Which we can use like this:

// build.zig
pub fn build(b: *std.Build) void {
    const target = b.resolveTargetQuery(.{
        .cpu_arch = .arm,
        .cpu_model = .{ .explicit = &std.Target.arm.cpu.arm926ej_s },
        .os_tag = .linux,
        .abi = .musleabi,
    });
    // ...
}

The baseline ARM CPU model specifies v7a as its feature set, which explains the illegal instruction.

Atomics, Atomics Everywhere

Next I was greeted with a sea of __atomic_* linker errors. It appears this CPU doesn't have the features needed by Zig's compiler_rt atomics implementation so it doesn't end up exporting any of the symbols.

I was eagerly starting to write stubs for a bunch of functions (what could go wrong!) but I soon realised we can improve the situation somewhat:

Now I was left with this, similar to what's been reported in ziglang/zig#4959:

install
└─ install kiesel
   └─ zig build-exe kiesel ReleaseSafe arm-linux-musleabi 5 errors
error: ld.lld: undefined symbol: __sync_val_compare_and_swap_4
    note: referenced by kiesel
    note:               /home/linus/Dev/kiesel/.zig-cache/o/b4dc1cb84146a4bec8e0e93957b1843c/kiesel.o:(heap.PageAllocator.alloc)
error: ld.lld: undefined symbol: __sync_fetch_and_add_1
    note: referenced by kiesel
    note:               /home/linus/Dev/kiesel/.zig-cache/o/b4dc1cb84146a4bec8e0e93957b1843c/kiesel.o:(debug.handleSegfaultPosix)
    note: referenced by kiesel
    note:               /home/linus/Dev/kiesel/.zig-cache/o/b4dc1cb84146a4bec8e0e93957b1843c/kiesel.o:(debug.panicImpl)
error: ld.lld: undefined symbol: __sync_fetch_and_sub_1
    note: referenced by kiesel
    note:               /home/linus/Dev/kiesel/.zig-cache/o/b4dc1cb84146a4bec8e0e93957b1843c/kiesel.o:(debug.handleSegfaultPosix)
    note: referenced by kiesel
    note:               /home/linus/Dev/kiesel/.zig-cache/o/b4dc1cb84146a4bec8e0e93957b1843c/kiesel.o:(debug.panicImpl)
error: ld.lld: undefined symbol: __sync_val_compare_and_swap_1
    note: referenced by kiesel
    note:               /home/linus/Dev/kiesel/.zig-cache/o/b4dc1cb84146a4bec8e0e93957b1843c/kiesel.o:(crypto.tlcsprng.tlsCsprngFill)
error: ld.lld: undefined symbol: __sync_lock_test_and_set_1
    note: referenced by kiesel
    note:               /home/linus/Dev/kiesel/.zig-cache/o/b4dc1cb84146a4bec8e0e93957b1843c/kiesel.o:(once.Once((function 'do')).callSlow)

No problem, we can stub those out. One can get very far with function stubs alone :^)

export fn __sync_fetch_and_add_1(_: *u8, _: u8) callconv(.C) u8 {
    return 0;
}
export fn __sync_fetch_and_sub_1(_: *u8, _: u8) callconv(.C) u8 {
    return 0;
}
export fn __sync_val_compare_and_swap_1(_: *u8, _: u8, _: u8) callconv(.C) u8 {
    return 0;
}
export fn __sync_val_compare_and_swap_4(_: *u32, _: u32, _: u32) callconv(.C) u32 {
    return 0;
}
export fn __sync_lock_test_and_set_1(_: *u8, _: u8) callconv(.C) u8 {
    return 0;
}

Panicked during a panic. Aborting.

Now we get further but still crash, with a somewhat amusing message:

# ./kiesel
Illegal instruction at address 0x5bf4d4
Unable to dump stack trace: debug info stripped
Panicked during a panic. Aborting.
Aborted

Turns out this goes away when building with both -Denable-intl=false and -Denable-libgc=false. Intl is not needed for what we're trying to achieve here, and thanks to Zig's allocator pattern the GC is easily replaced with a std.heap.c_allocator, at the expense of leaking memory.

A non-stripped release build would probably offer some insights, but I didn't look further.

It Works!

As you saw in the blog post linked at the very beginning, this adventure has a good ending:


This article is part of a series called "Linus runs JS on things that absolutely shouldn't, but it's fun and Zig quite portable so he does it anyway". Previous entries:

To quote what I said to CanadaHonk afterwards:

It's a printer. I ran JavaScript on a printer.


Loading posts...