Skip to content
Merged
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
318 changes: 261 additions & 57 deletions StikJIT/Amethyst.js
Original file line number Diff line number Diff line change
@@ -1,89 +1,293 @@
// Universal JIT Script, last updated 2025-10-10
/*
// JIT "syscalls"
__attribute__((noinline,optnone,naked))
void JIT26Detach(void) {
asm("mov x16, #0 \n"
"brk #0xf00d \n"
"ret");
}
__attribute__((noinline,optnone,naked))
void* JIT26PrepareRegion(void *addr, size_t len) {
asm("mov x16, #1 \n"
"brk #0xf00d \n"
"ret");
}

__attribute__((noinline,optnone,naked))
void BreakSendJITScript(char* script, size_t len) {
asm("mov x16, #2 \n"
"brk #0xf00d \n"
"ret");
}
*/
const CMD_DETACH = 0;
const CMD_PREPARE_REGION = 1;
const CMD_NEW_BREAKPOINTS = 2;
const commands = {
[CMD_DETACH]: JIT26Detach,
[CMD_PREPARE_REGION]: JIT26PrepareRegion,
[CMD_NEW_BREAKPOINTS]: JIT26NewBreakpoints
};
const legacyCommands = {
[0x68]: JIT26NewBreakpoints,
[0x69]: JIT26HandleBrk0x69,
[0xf00d]: JIT26HandleBrk0xf00d
};

// Log levels
//const LOG_NONE = 0;
const LOG_INFO = 1;
const LOG_VERBOSE = 2;
let logLevel = LOG_VERBOSE;
function log_verbose(msg) {
if (logLevel >= LOG_VERBOSE) {
log(msg);
}
}

// To avoid having to re-parse these in each function, we save some registers here
let tid, x0, x1, x16, pc;
let detached = false;
let pid = get_pid();
let attachResponse = send_command(`vAttach;${pid.toString(16)}`);

log(`pid = ${pid}`);
log(`attach_response = ${attachResponse}`);

let totalBreakpoints = 0;
while (!detached) {
totalBreakpoints++;
log(`Handling signal ${totalBreakpoints}`);

let brkResponse = send_command(`c`);
log_verbose(`brkResponse = ${brkResponse}`);

// extract tid, pc, x16
let tmpMatch = /T[0-9a-f]+thread:(?<tid>[0-9a-f]+);/.exec(brkResponse);
tid = tmpMatch ? tmpMatch.groups['tid'] : null;
tmpMatch = /20:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
pc = tmpMatch ? tmpMatch.groups['reg'] : null;
tmpMatch = /10:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
x16 = tmpMatch ? tmpMatch.groups['reg'] : null;
if (!tid || !pc || !x16) {
log(`Failed to extract registers: tid=${tid}, pc=${pc}, x16=${x16}`);
continue;
}
pc = littleEndianHexStringToNumber(pc);
x16 = littleEndianHexStringToNumber(x16);

let instructionResponse = send_command(`m${pc.toString(16)},4`);
log(`instruction at pc: ${instructionResponse}`);
let instrU32 = littleEndianHexToU32(instructionResponse);

// check if this is a brk
if ((instrU32 & 0xFFE0001F)>>>0 != 0xD4200000) {
log(`Skipping: instruction was not a brk (was 0x${instrU32.toString(16)})`);
let signum = /^T(?<sig>[a-z0-9;]{2})/.exec(brkResponse);
signum = signum ? signum.groups['sig'] : null;
if (!signum) {
log(`Failed to extract signal number: ${signum}`);
continue;
}
log(`Continuing with signal 0x${signum}`);
send_command(`vCont;S${signum}:${tid}`);
continue;
}

let brkImmediate = extractBrkImmediate(instrU32);
log(`BRK immediate: 0x${brkImmediate.toString(16)} (${brkImmediate})`);
if (legacyCommands[brkImmediate] != undefined) {
// when we find a valid brk immediate command, parse x0 and x1
tmpMatch = /00:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
x0 = tmpMatch ? tmpMatch.groups['reg'] : null;
tmpMatch = /01:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
x1 = tmpMatch ? tmpMatch.groups['reg'] : null;
if (!x0 || !x1) {
log(`Failed to extract registers: x0=${x0}, x1=${x1}`);
continue;
}
x0 = littleEndianHexStringToNumber(x0);
x1 = littleEndianHexStringToNumber(x1);

// jump over brk
let pcPlus4 = numberToLittleEndianHexString(pc + 4n);
let pcPlus4Response = send_command(`P20=${pcPlus4};thread:${tid};`);
log(`pcPlus4Response = ${pcPlus4Response}`);

// dispatch brk-immediate command
const command = legacyCommands[brkImmediate];
command(brkResponse);
} else {
log(`Skipping breakpoint: brk immediate 0x${brkImmediate.toString(16)} was not handled by this script. You could add it by evaluating legacyCommands[0x${brkImmediate.toString(16)}] = yourFunction;`);
continue;
}
}

function JIT26Detach() {
let detachResponse = send_command(`D`);
log_verbose(`detachResponse = ${detachResponse}`);
detached = true;
}

// brk 0x68
function JIT26NewBreakpoints(brkResponse) {
let instructionResponse = send_command(`m${pc.toString(16)},4`);
log(`instruction at pc: ${instructionResponse}`);
let instrU32 = littleEndianHexToU32(instructionResponse);
let brkImmediate = extractBrkImmediate(instrU32);

let memResponse = send_command(`m${x0.toString(16)},${x1}`);

let scriptText = hexToAscii(memResponse);
log_verbose(`Script text: ${scriptText}`);

const res = runScriptAndCapture(scriptText);
if (res.ok) {
log('Script succeeded:', res.value);
} else {
log('Script failed:', res.name, res.message);
log(res.stack);
}
}

// brk 0x69
function JIT26HandleBrk0x69(brkResponse) {
// in the old script we chose 0x69, so now we check here and return error
// if you wish to keep using this, you can set your own handler like `legacyCommands[0x69] = yourHandler;` using BreakSendJITScript
log(`Error: It seems you are using legacy breakpoint 0x69. Please set your legacy handler using \`legacyCommands[0x69] = yourHandler;\` or migrate to universal jitcalls to use this script. The function will now return 0xE0000069.`);
let putX0Response = send_command(`P0=E0000069;thread:${tid};`);
log(`putX0Response = ${putX0Response}`);
}

// brk 0xf00d
function JIT26HandleBrk0xf00d(brkResponse) {
// dispatch command via x16
const command = commands[x16];
if (command === undefined) {
log(`Unknown command ${x16.toString(16)}`);
return;
}
log(`Invoking command ${x16.toString(16)}`);
command(brkResponse);
}

function JIT26PrepareRegion(brkResponse) {
let instructionResponse = send_command(`m${pc.toString(16)},4`);
log(`instruction at pc: ${instructionResponse}`);
let instrU32 = littleEndianHexToU32(instructionResponse);
let brkImmediate = extractBrkImmediate(instrU32);

if (x0 == 0n && x1 == 0n) {
return;
}

let jitPageAddress = x0;
if (x0 == 0n) {
let requestRXResponse = send_command(`_M${x1.toString(16)},rx`);
log_verbose(`requestRXResponse = ${requestRXResponse}`);

if (!requestRXResponse || requestRXResponse.length === 0) {
log(`Failed to allocate RX memory`);
return;
}

jitPageAddress = BigInt(`0x${requestRXResponse}`);
log(`Allocated JIT page at address: 0x${jitPageAddress.toString(16)}`);
}

let prepareJITPageResponse = prepare_memory_region(jitPageAddress, x1);
log(`prepareJITPageResponse = ${prepareJITPageResponse}`);

let putX0Response = send_command(`P0=${numberToLittleEndianHexString(jitPageAddress)};thread:${tid};`);
log(`putX0Response = ${putX0Response}`);
}

// utilities
function littleEndianHexStringToNumber(hexStr) {
// Convert hex string to byte array
const bytes = [];
for (let i = 0; i < hexStr.length; i += 2) {
bytes.push(parseInt(hexStr.substr(i, 2), 16));
}

// Assemble number from first 5 bytes (little-endian)
let num = 0n;
for (let i = 4; i >= 0; i--) {
num = (num << 8n) | BigInt(bytes[i]);
}

return num; // BigInt: 0x01016644fc
return num;
}

function numberToLittleEndianHexString(num) {
// Extract 5 bytes in little-endian order
const bytes = [];
for (let i = 0; i < 5; i++) {
bytes.push(Number(num & 0xFFn));
num >>= 8n;
}

// Pad with 3 zero bytes to make 8 bytes
while (bytes.length < 8) {
bytes.push(0);
}

// Convert to hex string
return bytes.map(b => b.toString(16).padStart(2, '0')).join('');
}

function attach() {
let pid = get_pid();
log(`pid = ${pid}`)

// attach
let attachResponse = send_command(`vAttach;${pid.toString(16)}`)
log(`attach_response = ${attachResponse}`)

// wait for brk
let brkResponse = send_command(`c`)
log(`brkResponse = ${brkResponse}`)

let tid = /T[0-9a-f]+thread:(?<tid>[0-9a-f]+);/.exec(brkResponse).groups['tid']
let pc = /20:(?<reg>[0-9a-f]{16});/.exec(brkResponse).groups['reg']
let x0 = /00:(?<reg>[0-9a-f]{16});/.exec(brkResponse).groups['reg']
function littleEndianHexToU32(hexStr) {
return parseInt(hexStr.match(/../g).reverse().join(''), 16);
}

log(`tid = ${tid}, pc = ${littleEndianHexStringToNumber(pc).toString(16)}, x0 = ${littleEndianHexStringToNumber(x0).toString(16)}`)
function extractBrkImmediate(u32) {
return (u32 >> 5) & 0xFFFF;
}

pc = numberToLittleEndianHexString(littleEndianHexStringToNumber(pc) + BigInt(4))
let x0Num = littleEndianHexStringToNumber(x0)
function hexToAscii(hexStr) {
let str = '';
for (let i = 0; i < hexStr.length; i += 2) {
const byte = parseInt(hexStr.substr(i, 2), 16);
if (byte === 0) break;
str += String.fromCharCode(byte);
}
return str;
}

// pc+4
let pcPlus4Response = send_command(`P20=${pc};thread:${tid};`)
log(`pcPlus4Response = ${pcPlus4Response}`)
function runScriptAndCapture(scriptText) {
try {
const value = eval(scriptText);
return { ok: true, value };
} catch (err) {
return {
ok: false,
name: err && err.name,
message: err && err.message,
stack: err && err.stack
};
}
}

// apply for jit page of requested size
let requestRXResponse = send_command(`_M${x0Num.toString(16)},rx`)
log(`requestRXResponse = ${requestRXResponse}`)
let jitPageAddress = BigInt(`0x${requestRXResponse}`)
// For making your own script / adding your own breakpoints. you can send this string to BreakSendJITScript and it'll add it for any subsequent breakpoints
// x0, x1, x16, pc and tid are global variables. If you need more registers, parse them like:
// tmpMatch = /02:(?<reg>[0-9a-f]{16});/.exec(brkResponse); // x2
// let x2 = tmpMatch ? tmpMatch.groups['reg'] : null;
// if (!x2) {
// log(`Failed to extract registers: x2=${x2}`);
// return;
// }
// x2 = littleEndianHexStringToNumber(x2);
//
/*
commands[3] = wowBreakPoint;

// TODO fill the page
let a = 0;
try {
for(let i = 0n; i < x0Num; i += 16384n) {
let curPointer = jitPageAddress + i;
let response = send_command(`M${curPointer.toString(16)},1:69`)
if(a % 1000 == 0) {
log(`progress: ${a}/${Math.ceil(Number(x0Num / BigInt(16384)))}, response = ${response}`)
}
++a;
}
log(`memory write completed. command executed: ${a}`)
}catch(e) {
log(`memory write failed at command ${a}`)
function wowBreakPoint(brekpoint) {
let instructionResponse = send_command(`m${pc.toString(16)},4`);
log(`instruction at pc: ${instructionResponse}`);
let instrU32 = littleEndianHexToU32(instructionResponse);
let brkImmediate = extractBrkImmediate(instrU32);

if (x0 == 0n && x1 == 0n) {
return;
}

// put page address in x0
let putX0Response = send_command(`P0=${numberToLittleEndianHexString(jitPageAddress)};thread:${tid};`)
log(`putX0Response = ${putX0Response}`)
// detach
let detachResponse = send_command(`D`)
log(`detachResponse = ${detachResponse}`)
}
let jitPageAddress = x0;
let prepareJITPageResponse = prepare_memory_region(jitPageAddress, x1);
log(`prepareJITPageResponse = ${prepareJITPageResponse}`);

attach()
let putX0Response = send_command(`P0=${numberToLittleEndianHexString(jitPageAddress)};thread:${tid};`);
log(`putX0Response = ${putX0Response}`);
}
*/
2 changes: 1 addition & 1 deletion StikJIT/JSSupport/ScriptEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ struct ScriptEditorView: View {
}
}
.preferredColorScheme(preferredScheme)
.tint(Color.white)
.tint(colorScheme == .dark ? .white : .black)
.toolbar(.hidden, for: .tabBar)
}

Expand Down