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:
- We can disable
Atomics
andSharedArrayBuffer
, the two JS features needing support for atomics by the compiler - There is an option that tells the compiler to assume single-threaded code, which turns atomic operations into non-atomic ones in a bunch of places (e.g.
std.Thread.Mutex
) - Building in release mode reduced the number of errors even further
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.