Skip to content

Commit cb5ff5c

Browse files
Fix Amethyst support (#311)
* Add download section to README Added download section with links and images for AltSource and StikDebug. * Update download link in README.md * Update FeatureFlags.swift * Update ScriptEditorView.swift * Implement Universal Script for Amethyst I don't know if melo has added support, but if so we could change the name and reassign * Disable Mini Tools feature flag
1 parent 77b8810 commit cb5ff5c

File tree

2 files changed

+262
-58
lines changed

2 files changed

+262
-58
lines changed

StikJIT/Amethyst.js

Lines changed: 261 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,293 @@
1+
// Universal JIT Script, last updated 2025-10-10
2+
/*
3+
// JIT "syscalls"
4+
__attribute__((noinline,optnone,naked))
5+
void JIT26Detach(void) {
6+
asm("mov x16, #0 \n"
7+
"brk #0xf00d \n"
8+
"ret");
9+
}
10+
__attribute__((noinline,optnone,naked))
11+
void* JIT26PrepareRegion(void *addr, size_t len) {
12+
asm("mov x16, #1 \n"
13+
"brk #0xf00d \n"
14+
"ret");
15+
}
116
17+
__attribute__((noinline,optnone,naked))
18+
void BreakSendJITScript(char* script, size_t len) {
19+
asm("mov x16, #2 \n"
20+
"brk #0xf00d \n"
21+
"ret");
22+
}
23+
*/
24+
const CMD_DETACH = 0;
25+
const CMD_PREPARE_REGION = 1;
26+
const CMD_NEW_BREAKPOINTS = 2;
27+
const commands = {
28+
[CMD_DETACH]: JIT26Detach,
29+
[CMD_PREPARE_REGION]: JIT26PrepareRegion,
30+
[CMD_NEW_BREAKPOINTS]: JIT26NewBreakpoints
31+
};
32+
const legacyCommands = {
33+
[0x68]: JIT26NewBreakpoints,
34+
[0x69]: JIT26HandleBrk0x69,
35+
[0xf00d]: JIT26HandleBrk0xf00d
36+
};
37+
38+
// Log levels
39+
//const LOG_NONE = 0;
40+
const LOG_INFO = 1;
41+
const LOG_VERBOSE = 2;
42+
let logLevel = LOG_VERBOSE;
43+
function log_verbose(msg) {
44+
if (logLevel >= LOG_VERBOSE) {
45+
log(msg);
46+
}
47+
}
48+
49+
// To avoid having to re-parse these in each function, we save some registers here
50+
let tid, x0, x1, x16, pc;
51+
let detached = false;
52+
let pid = get_pid();
53+
let attachResponse = send_command(`vAttach;${pid.toString(16)}`);
54+
55+
log(`pid = ${pid}`);
56+
log(`attach_response = ${attachResponse}`);
57+
58+
let totalBreakpoints = 0;
59+
while (!detached) {
60+
totalBreakpoints++;
61+
log(`Handling signal ${totalBreakpoints}`);
62+
63+
let brkResponse = send_command(`c`);
64+
log_verbose(`brkResponse = ${brkResponse}`);
65+
66+
// extract tid, pc, x16
67+
let tmpMatch = /T[0-9a-f]+thread:(?<tid>[0-9a-f]+);/.exec(brkResponse);
68+
tid = tmpMatch ? tmpMatch.groups['tid'] : null;
69+
tmpMatch = /20:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
70+
pc = tmpMatch ? tmpMatch.groups['reg'] : null;
71+
tmpMatch = /10:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
72+
x16 = tmpMatch ? tmpMatch.groups['reg'] : null;
73+
if (!tid || !pc || !x16) {
74+
log(`Failed to extract registers: tid=${tid}, pc=${pc}, x16=${x16}`);
75+
continue;
76+
}
77+
pc = littleEndianHexStringToNumber(pc);
78+
x16 = littleEndianHexStringToNumber(x16);
79+
80+
let instructionResponse = send_command(`m${pc.toString(16)},4`);
81+
log(`instruction at pc: ${instructionResponse}`);
82+
let instrU32 = littleEndianHexToU32(instructionResponse);
83+
84+
// check if this is a brk
85+
if ((instrU32 & 0xFFE0001F)>>>0 != 0xD4200000) {
86+
log(`Skipping: instruction was not a brk (was 0x${instrU32.toString(16)})`);
87+
let signum = /^T(?<sig>[a-z0-9;]{2})/.exec(brkResponse);
88+
signum = signum ? signum.groups['sig'] : null;
89+
if (!signum) {
90+
log(`Failed to extract signal number: ${signum}`);
91+
continue;
92+
}
93+
log(`Continuing with signal 0x${signum}`);
94+
send_command(`vCont;S${signum}:${tid}`);
95+
continue;
96+
}
97+
98+
let brkImmediate = extractBrkImmediate(instrU32);
99+
log(`BRK immediate: 0x${brkImmediate.toString(16)} (${brkImmediate})`);
100+
if (legacyCommands[brkImmediate] != undefined) {
101+
// when we find a valid brk immediate command, parse x0 and x1
102+
tmpMatch = /00:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
103+
x0 = tmpMatch ? tmpMatch.groups['reg'] : null;
104+
tmpMatch = /01:(?<reg>[0-9a-f]{16});/.exec(brkResponse);
105+
x1 = tmpMatch ? tmpMatch.groups['reg'] : null;
106+
if (!x0 || !x1) {
107+
log(`Failed to extract registers: x0=${x0}, x1=${x1}`);
108+
continue;
109+
}
110+
x0 = littleEndianHexStringToNumber(x0);
111+
x1 = littleEndianHexStringToNumber(x1);
112+
113+
// jump over brk
114+
let pcPlus4 = numberToLittleEndianHexString(pc + 4n);
115+
let pcPlus4Response = send_command(`P20=${pcPlus4};thread:${tid};`);
116+
log(`pcPlus4Response = ${pcPlus4Response}`);
117+
118+
// dispatch brk-immediate command
119+
const command = legacyCommands[brkImmediate];
120+
command(brkResponse);
121+
} else {
122+
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;`);
123+
continue;
124+
}
125+
}
126+
127+
function JIT26Detach() {
128+
let detachResponse = send_command(`D`);
129+
log_verbose(`detachResponse = ${detachResponse}`);
130+
detached = true;
131+
}
132+
133+
// brk 0x68
134+
function JIT26NewBreakpoints(brkResponse) {
135+
let instructionResponse = send_command(`m${pc.toString(16)},4`);
136+
log(`instruction at pc: ${instructionResponse}`);
137+
let instrU32 = littleEndianHexToU32(instructionResponse);
138+
let brkImmediate = extractBrkImmediate(instrU32);
139+
140+
let memResponse = send_command(`m${x0.toString(16)},${x1}`);
141+
142+
let scriptText = hexToAscii(memResponse);
143+
log_verbose(`Script text: ${scriptText}`);
144+
145+
const res = runScriptAndCapture(scriptText);
146+
if (res.ok) {
147+
log('Script succeeded:', res.value);
148+
} else {
149+
log('Script failed:', res.name, res.message);
150+
log(res.stack);
151+
}
152+
}
153+
154+
// brk 0x69
155+
function JIT26HandleBrk0x69(brkResponse) {
156+
// in the old script we chose 0x69, so now we check here and return error
157+
// if you wish to keep using this, you can set your own handler like `legacyCommands[0x69] = yourHandler;` using BreakSendJITScript
158+
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.`);
159+
let putX0Response = send_command(`P0=E0000069;thread:${tid};`);
160+
log(`putX0Response = ${putX0Response}`);
161+
}
162+
163+
// brk 0xf00d
164+
function JIT26HandleBrk0xf00d(brkResponse) {
165+
// dispatch command via x16
166+
const command = commands[x16];
167+
if (command === undefined) {
168+
log(`Unknown command ${x16.toString(16)}`);
169+
return;
170+
}
171+
log(`Invoking command ${x16.toString(16)}`);
172+
command(brkResponse);
173+
}
174+
175+
function JIT26PrepareRegion(brkResponse) {
176+
let instructionResponse = send_command(`m${pc.toString(16)},4`);
177+
log(`instruction at pc: ${instructionResponse}`);
178+
let instrU32 = littleEndianHexToU32(instructionResponse);
179+
let brkImmediate = extractBrkImmediate(instrU32);
180+
181+
if (x0 == 0n && x1 == 0n) {
182+
return;
183+
}
184+
185+
let jitPageAddress = x0;
186+
if (x0 == 0n) {
187+
let requestRXResponse = send_command(`_M${x1.toString(16)},rx`);
188+
log_verbose(`requestRXResponse = ${requestRXResponse}`);
189+
190+
if (!requestRXResponse || requestRXResponse.length === 0) {
191+
log(`Failed to allocate RX memory`);
192+
return;
193+
}
194+
195+
jitPageAddress = BigInt(`0x${requestRXResponse}`);
196+
log(`Allocated JIT page at address: 0x${jitPageAddress.toString(16)}`);
197+
}
198+
199+
let prepareJITPageResponse = prepare_memory_region(jitPageAddress, x1);
200+
log(`prepareJITPageResponse = ${prepareJITPageResponse}`);
201+
202+
let putX0Response = send_command(`P0=${numberToLittleEndianHexString(jitPageAddress)};thread:${tid};`);
203+
log(`putX0Response = ${putX0Response}`);
204+
}
205+
206+
// utilities
2207
function littleEndianHexStringToNumber(hexStr) {
3-
// Convert hex string to byte array
4208
const bytes = [];
5209
for (let i = 0; i < hexStr.length; i += 2) {
6210
bytes.push(parseInt(hexStr.substr(i, 2), 16));
7211
}
8-
9-
// Assemble number from first 5 bytes (little-endian)
10212
let num = 0n;
11213
for (let i = 4; i >= 0; i--) {
12214
num = (num << 8n) | BigInt(bytes[i]);
13215
}
14-
15-
return num; // BigInt: 0x01016644fc
216+
return num;
16217
}
17218

18219
function numberToLittleEndianHexString(num) {
19-
// Extract 5 bytes in little-endian order
20220
const bytes = [];
21221
for (let i = 0; i < 5; i++) {
22222
bytes.push(Number(num & 0xFFn));
23223
num >>= 8n;
24224
}
25-
26-
// Pad with 3 zero bytes to make 8 bytes
27225
while (bytes.length < 8) {
28226
bytes.push(0);
29227
}
30-
31-
// Convert to hex string
32228
return bytes.map(b => b.toString(16).padStart(2, '0')).join('');
33229
}
34230

35-
function attach() {
36-
let pid = get_pid();
37-
log(`pid = ${pid}`)
38-
39-
// attach
40-
let attachResponse = send_command(`vAttach;${pid.toString(16)}`)
41-
log(`attach_response = ${attachResponse}`)
42-
43-
// wait for brk
44-
let brkResponse = send_command(`c`)
45-
log(`brkResponse = ${brkResponse}`)
46-
47-
let tid = /T[0-9a-f]+thread:(?<tid>[0-9a-f]+);/.exec(brkResponse).groups['tid']
48-
let pc = /20:(?<reg>[0-9a-f]{16});/.exec(brkResponse).groups['reg']
49-
let x0 = /00:(?<reg>[0-9a-f]{16});/.exec(brkResponse).groups['reg']
231+
function littleEndianHexToU32(hexStr) {
232+
return parseInt(hexStr.match(/../g).reverse().join(''), 16);
233+
}
50234

51-
log(`tid = ${tid}, pc = ${littleEndianHexStringToNumber(pc).toString(16)}, x0 = ${littleEndianHexStringToNumber(x0).toString(16)}`)
235+
function extractBrkImmediate(u32) {
236+
return (u32 >> 5) & 0xFFFF;
237+
}
52238

53-
pc = numberToLittleEndianHexString(littleEndianHexStringToNumber(pc) + BigInt(4))
54-
let x0Num = littleEndianHexStringToNumber(x0)
239+
function hexToAscii(hexStr) {
240+
let str = '';
241+
for (let i = 0; i < hexStr.length; i += 2) {
242+
const byte = parseInt(hexStr.substr(i, 2), 16);
243+
if (byte === 0) break;
244+
str += String.fromCharCode(byte);
245+
}
246+
return str;
247+
}
55248

56-
// pc+4
57-
let pcPlus4Response = send_command(`P20=${pc};thread:${tid};`)
58-
log(`pcPlus4Response = ${pcPlus4Response}`)
249+
function runScriptAndCapture(scriptText) {
250+
try {
251+
const value = eval(scriptText);
252+
return { ok: true, value };
253+
} catch (err) {
254+
return {
255+
ok: false,
256+
name: err && err.name,
257+
message: err && err.message,
258+
stack: err && err.stack
259+
};
260+
}
261+
}
59262

60-
// apply for jit page of requested size
61-
let requestRXResponse = send_command(`_M${x0Num.toString(16)},rx`)
62-
log(`requestRXResponse = ${requestRXResponse}`)
63-
let jitPageAddress = BigInt(`0x${requestRXResponse}`)
263+
// 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
264+
// x0, x1, x16, pc and tid are global variables. If you need more registers, parse them like:
265+
// tmpMatch = /02:(?<reg>[0-9a-f]{16});/.exec(brkResponse); // x2
266+
// let x2 = tmpMatch ? tmpMatch.groups['reg'] : null;
267+
// if (!x2) {
268+
// log(`Failed to extract registers: x2=${x2}`);
269+
// return;
270+
// }
271+
// x2 = littleEndianHexStringToNumber(x2);
272+
//
273+
/*
274+
commands[3] = wowBreakPoint;
64275
65-
// TODO fill the page
66-
let a = 0;
67-
try {
68-
for(let i = 0n; i < x0Num; i += 16384n) {
69-
let curPointer = jitPageAddress + i;
70-
let response = send_command(`M${curPointer.toString(16)},1:69`)
71-
if(a % 1000 == 0) {
72-
log(`progress: ${a}/${Math.ceil(Number(x0Num / BigInt(16384)))}, response = ${response}`)
73-
}
74-
++a;
75-
}
76-
log(`memory write completed. command executed: ${a}`)
77-
}catch(e) {
78-
log(`memory write failed at command ${a}`)
276+
function wowBreakPoint(brekpoint) {
277+
let instructionResponse = send_command(`m${pc.toString(16)},4`);
278+
log(`instruction at pc: ${instructionResponse}`);
279+
let instrU32 = littleEndianHexToU32(instructionResponse);
280+
let brkImmediate = extractBrkImmediate(instrU32);
281+
282+
if (x0 == 0n && x1 == 0n) {
283+
return;
79284
}
80285
81-
// put page address in x0
82-
let putX0Response = send_command(`P0=${numberToLittleEndianHexString(jitPageAddress)};thread:${tid};`)
83-
log(`putX0Response = ${putX0Response}`)
84-
// detach
85-
let detachResponse = send_command(`D`)
86-
log(`detachResponse = ${detachResponse}`)
87-
}
286+
let jitPageAddress = x0;
287+
let prepareJITPageResponse = prepare_memory_region(jitPageAddress, x1);
288+
log(`prepareJITPageResponse = ${prepareJITPageResponse}`);
88289
89-
attach()
290+
let putX0Response = send_command(`P0=${numberToLittleEndianHexString(jitPageAddress)};thread:${tid};`);
291+
log(`putX0Response = ${putX0Response}`);
292+
}
293+
*/

StikJIT/JSSupport/ScriptEditorView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ struct ScriptEditorView: View {
5858
}
5959
}
6060
.preferredColorScheme(preferredScheme)
61-
.tint(Color.white)
61+
.tint(colorScheme == .dark ? .white : .black)
6262
.toolbar(.hidden, for: .tabBar)
6363
}
6464

0 commit comments

Comments
 (0)