Session Initialization Primer
frida-trace --init-session / -S Primer
What Is --init-session?
The --init-session (-S) option executes one or more user-written JavaScript files during the frida-trace engine initialization stage, before the first function handler is called.
Key properties:
- Top-level functions defined in session scripts become globally visible to all handlers
- Data can be stored in the global
stateobject, which is passed as a parameter to every handler statepersists across function calls — all handlers share the samestateinstance- Multiple
-Sflags are accepted; each flag takes a path to a JS source file
Syntax:
1frida-trace [target] -i "module!FunctionName" -S path/to/shared1.js -S path/to/shared2.js
Why Use It?
| Purpose | Description |
|---|---|
| Pre-initialization | Run setup code (allocate buffers, build lookup tables) before any handler fires |
| Shared code library | Define debugged, reusable functions once; call them from any handler in any project |
| Avoid duplication | Eliminate copy-pasting boilerplate across dozens of auto-generated .js handler stubs |
| Namespace management | Organize platform-specific helpers (Windows, Android, Linux) into separate files |
Scope Rules
- Functions defined at the top level of a session script are globally accessible by name in all handlers
- Data objects should be stored on
state(e.g.,state.myLookupTable = ...) to make them accessible acrossonEnter/onLeavecalls - Per-invocation state (data passed from
onEntertoonLeavefor the same call) usesthis, notstate
Example: Tracing ExtTextOutW on Windows
Target Function
ExtTextOutW() in gdi32full.dll:
1BOOL ExtTextOutW(
2 HDC hdc,
3 int x,
4 int y,
5 UINT options,
6 const RECT *lprect,
7 LPCWSTR lpString,
8 UINT c,
9 const INT *lpDx
10);
Invocation
1frida-trace -p 6980 --decorate -i "gdi32full.dll!ExtTextOutW" -S core.js -S ms-windows.js
Enhanced Trace Output
Instrumenting...
ExtTextOutW: Loaded handler at "c:\project\__handlers__\gdi32full.dll\ExtTextOutW.js"
Started tracing 1 function. Press Ctrl+C to stop.
/* TID 0x3ab8 */
2695 ms ---------------------------------------------
2695 ms ExtTextOutW() [gdi32full.dll]
2695 ms x: 0
2695 ms y: 0
2695 ms options: ETO_OPAQUE
2695 ms lprect [20 bytes]
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00b9c81c 00 00 00 00 01 00 00 00 01 00 00 00 98 02 00 00 ................
00b9c82c 26 90 b2 b3 &...
2695 ms lprect: (left, top, right, bottom) = (0, 1, 1, 664)
2695 ms c: 0
2696 ms x (exit): 0
2696 ms y (exit): 0
Enhancements over the default trace:
optionsDWORD decoded to named flag strings (ETO_OPAQUE,ETO_CLIPPED | ETO_IGNORELANGUAGE, etc.)- Pointer arguments (
lprect,lpString,lpDx) shown as both a hex memory dump and a human-readable value lpStringdecoded viareadUtf16String()
Handler: ExtTextOutW.js
1{
2 onEnter(log, args, state) {
3 log('---------------------------------------------');
4 log('ExtTextOutW() [gdi32full.dll]');
5
6 // cloneArgs() is from core.js — copies args into this.args for onLeave access
7 cloneArgs(args, 8, this);
8 const [, x, y, options, lprect, lpString, c, lpDx] = this.args;
9
10 log(`x: ${x.toInt32()}`);
11 log(`y: ${y.toInt32()}`);
12
13 // decodeExttextoutOptions() is from ms-windows.js
14 log(`options: ${decodeExttextoutOptions(options)}`);
15
16 if (!lprect.isNull()) {
17 prettyHexdump(log, 'lprect', lprect, 20); // from core.js
18 log(`lprect: ${rectStructToString(lprect)}`); // from ms-windows.js
19 }
20
21 if (!lpString.isNull()) {
22 prettyHexdump(log, 'lpString', lpString, 50);
23 log(`*lpString: "${lpString.readUtf16String()}"`);
24 }
25
26 log(`c: ${c.toUInt32()}`);
27
28 if (!lpDx.isNull()) {
29 prettyHexdump(log, 'lpDx', lpDx, 4);
30 log(`*lpDx: ${lpDx.readU32()}`);
31 }
32 },
33
34 onLeave(log, retval, state) {
35 // this.args is available because cloneArgs() saved it in onEnter
36 const [, x, y] = this.args;
37 log(`x (exit): ${x.toInt32()}`);
38 log(`y (exit): ${y.toInt32()}`);
39 }
40}
Shared Library: core.js
General-purpose utilities for any platform.
1/**
2 * Copies args[] into invCtx.args as a real JS array, making args accessible in onLeave().
3 * @param {NativePointer[]} args - The args virtual array from onEnter()
4 * @param {number} numArgs - Number of args to copy (args has no .length)
5 * @param {InvocationContext} invCtx - The `this` object of the calling onEnter()
6 */
7function cloneArgs(args, numArgs, invCtx) {
8 const items = [];
9 for (let i = 0; i !== numArgs; i++)
10 items.push(args[i]);
11 invCtx.args = items;
12}
13
14/**
15 * Decodes a bitmask value into a '|'-delimited string of flag names.
16 * @param {number} value - Integer bitmask to decode
17 * @param {Map<number,string>} spec - Map of { flagValue -> flagName }
18 * @returns {string} e.g. "ETO_CLIPPED | ETO_OPAQUE", or "0" if no flags set
19 *
20 * Example spec:
21 * new Map([[0x0004, 'ETO_CLIPPED'], [0x0002, 'ETO_OPAQUE'], ...])
22 */
23function decodeBitflags(value, spec) {
24 if (value === 0) return '0';
25 const flags = [];
26 let pending = value;
27 for (const [flagValue, flagName] of spec.entries()) {
28 if ((value & flagValue) !== 0) {
29 flags.push(flagName);
30 pending &= ~flagValue;
31 if (pending === 0) break;
32 }
33 }
34 if (pending !== 0)
35 flags.push(`0x${pending.toString(16)}`);
36 return flags.join(' | ');
37}
38
39/**
40 * Logs a labeled hex dump of `length` bytes at `address`.
41 * @param {function} log - The frida-trace log function
42 * @param {string} desc - Label to print before the dump
43 * @param {NativePointer} address - Start address
44 * @param {number} length - Number of bytes to dump
45 */
46function prettyHexdump(log, desc, address, length) {
47 const lines = [];
48 prettyHexdumpLines(lines, desc, address, length);
49 log(lines.join('\n'));
50}
51
52/**
53 * Collects hex dump lines into an array without printing.
54 * @param {string[]} lines - Output array (mutated in place)
55 * @param {string} desc - Label
56 * @param {NativePointer} address - Start address
57 * @param {number} length - Number of bytes
58 * @param {string} [indent='\t\t'] - Prefix for each dump line
59 */
60function prettyHexdumpLines(lines, desc, address, length, indent = '\t\t') {
61 lines.push(`${desc} [${length} bytes]`);
62 try {
63 const s = hexdump(address, { length });
64 for (const line of s.split('\n'))
65 lines.push(`${indent}${line}`);
66 } catch (e) {
67 lines.push(`${indent}WARNING: address is NOT VALID (${address})`);
68 }
69}
Shared Library: ms-windows.js
Windows-specific helpers, depends on core.js being loaded first via an earlier -S flag.
1const extTextOptionsSpec = new Map([
2 [0x00004, 'ETO_CLIPPED'],
3 [0x00010, 'ETO_GLYPH_INDEX'],
4 [0x01000, 'ETO_IGNORELANGUAGE'],
5 [0x00800, 'ETO_NUMERICSLATIN'],
6 [0x00400, 'ETO_NUMERICSLOCAL'],
7 [0x00002, 'ETO_OPAQUE'],
8 [0x02000, 'ETO_PDY'],
9 [0x00080, 'ETO_RTLREADING'],
10 [0x10000, 'ETO_REVERSE_INDEX_MAP'],
11]);
12
13/**
14 * Decodes the `options` parameter of ExtTextOutW() into flag names.
15 * @param {number} flags - DWORD options value
16 * @returns {string} '|'-delimited flag names, or '0'
17 */
18function decodeExttextoutOptions(flags) {
19 return decodeBitflags(flags, extTextOptionsSpec); // decodeBitflags from core.js
20}
21
22/**
23 * Reads a Windows RECT struct and returns it as a readable string.
24 * RECT layout: four contiguous LONG (4-byte) values: left, top, right, bottom
25 * @param {NativePointer} lprect - Pointer to RECT
26 * @returns {string} "(left, top, right, bottom) = (0, 0, 77, 15)"
27 */
28function rectStructToString(lprect) {
29 if (lprect.isNull()) return 'LPRECT is null';
30 const left = lprect.readU32();
31 const top = lprect.add(4).readU32();
32 const right = lprect.add(8).readU32();
33 const bottom = lprect.add(12).readU32();
34 return `(left, top, right, bottom) = (${left}, ${top}, ${right}, ${bottom})`;
35}
Design Patterns
Pattern 1: Separate files by platform/category
-S core.js # general utilities (cloneArgs, decodeBitflags, prettyHexdump)
-S ms-windows.js # Windows-specific (RECT, HRESULT, Win32 flag maps)
-S android.js # Android-specific (JNI helpers, ART internals)
Load order matters: later files can call functions defined in earlier files.
Pattern 2: Namespace objects to avoid collisions
When mixing third-party shared libraries, use a named global object instead of bare global functions:
1// In mylib.js
2global.MyLibrary = {
3 doX() { /* ... */ },
4 doY() { /* ... */ },
5};
Call as MyLibrary.doX() in handlers. Prevents conflicts when two libraries define a function with the same name.
Pattern 3: State storage
1// In init script
2state.lookupTable = buildMyTable(); // available to all handlers via state.lookupTable
3
4// In handler onEnter
5onEnter(log, args, state) {
6 const result = state.lookupTable[args[0].toInt32()];
7}
Summary Table
| Concept | Detail |
|---|---|
| Option name | --init-session / -S |
| Execution timing | Before the first handler onEnter fires |
| Scope of defined functions | Global — callable by name from any handler |
| Shared persistent data | state object (passed to every onEnter/onLeave) |
| Per-call data (onEnter → onLeave) | this object (use cloneArgs pattern) |
| Multiple files | Yes — each -S path loads one file in order |
| File dependency order | Earlier -S files must define functions used by later -S files |