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:

Syntax:

1frida-trace [target] -i "module!FunctionName" -S path/to/shared1.js -S path/to/shared2.js

Why Use It?

PurposeDescription
Pre-initializationRun setup code (allocate buffers, build lookup tables) before any handler fires
Shared code libraryDefine debugged, reusable functions once; call them from any handler in any project
Avoid duplicationEliminate copy-pasting boilerplate across dozens of auto-generated .js handler stubs
Namespace managementOrganize platform-specific helpers (Windows, Android, Linux) into separate files

Scope Rules

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:

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

ConceptDetail
Option name--init-session / -S
Execution timingBefore the first handler onEnter fires
Scope of defined functionsGlobal — callable by name from any handler
Shared persistent datastate object (passed to every onEnter/onLeave)
Per-call data (onEnter → onLeave)this object (use cloneArgs pattern)
Multiple filesYes — each -S path loads one file in order
File dependency orderEarlier -S files must define functions used by later -S files