From 22e100cebbefd657f495378a1b9580b0bf1e908a Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 19 Sep 2024 08:29:35 +0000 Subject: [PATCH] Add support for hot command to show hot threads --- Dockerfile | 8 + assets/target/target.c | 49 +++++ package | 14 +- package-lock.json | 4 +- package.json | 2 +- src/cmdlets/breakpoints/bp.ts | 23 +-- src/cmdlets/development/js.ts | 2 + src/cmdlets/misc/grep.ts | 1 + src/cmdlets/thread/hot.ts | 342 ++++++++++++++++++++++++++++++++++ src/commands/cmdlet.ts | 11 ++ src/commands/cmdlets.ts | 2 + src/io/output.ts | 4 +- src/misc/format.ts | 2 +- 13 files changed, 435 insertions(+), 29 deletions(-) create mode 100644 src/cmdlets/thread/hot.ts diff --git a/Dockerfile b/Dockerfile index 70f8759..2a343d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -264,6 +264,8 @@ RUN gcc \ -Wall \ -Werror \ -no-pie \ + -D_GNU_SOURCE \ + -lpthread \ -o target \ /root/target.c RUN mkdir /root/x86 @@ -283,6 +285,8 @@ RUN gcc \ -Werror \ -no-pie \ -m32 \ + -D_GNU_SOURCE \ + -lpthread \ -o target \ /root/target.c RUN mkdir /root/arm @@ -302,6 +306,8 @@ RUN arm-none-linux-gnueabihf-gcc \ -Werror \ -no-pie \ -marm \ + -D_GNU_SOURCE \ + -lpthread \ -o target \ /root/target.c RUN mkdir /root/arm64 @@ -321,6 +327,8 @@ RUN aarch64-none-linux-gnu-gcc \ -Werror \ -no-pie \ -march=armv8-a \ + -D_GNU_SOURCE \ + -lpthread \ -o target \ /root/target.c diff --git a/assets/target/target.c b/assets/target/target.c index 4599b9d..935cd0b 100644 --- a/assets/target/target.c +++ b/assets/target/target.c @@ -1,10 +1,13 @@ #include +#include #include #include #include #include #include +#define NUM_THREADS 5 + typedef unsigned int uint; __attribute__((noinline)) void my_memcpy(void *dest, const void *src, size_t n) @@ -106,14 +109,51 @@ void my_a(uint i) } } +static void *busy_loop(void *arg) +{ + char thread_name[16] = {0}; + int index = *(int *)arg; + printf("Thread %d started\n", index); + + snprintf(thread_name, sizeof(thread_name), "Child-%d", index); + pthread_setname_np(pthread_self(), thread_name); + + long limit = (index + 1) * 10000000L; + + while (true) + { + for (volatile long i = 0; i < limit; i++) + ; + + usleep(500000); + } + + return 0; +} + int main(int argc, char **argv, char **envp) { + pthread_t threads[NUM_THREADS] = {0}; + int thread_indices[NUM_THREADS] = {0}; + int fd = open("/dev/null", O_RDWR); dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); close(fd); + pthread_setname_np(pthread_self(), "Parent"); + + for (int i = 0; i < NUM_THREADS; i++) + { + thread_indices[i] = i; + if (pthread_create(&threads[i], NULL, busy_loop, &thread_indices[i]) != 0) + { + perror("Failed to create thread"); + return 1; + } + } + while (true) { @@ -129,6 +169,15 @@ int main(int argc, char **argv, char **envp) puts(buf); free(buf); + + for (volatile long i = 0; i < 100000000L; i++) + ; + usleep(500000); } + + for (int i = 0; i < NUM_THREADS; i++) + { + pthread_join(threads[i], NULL); + } } diff --git a/package b/package index 6e971b1..e5db61d 100755 --- a/package +++ b/package @@ -9,7 +9,7 @@ echo 'EOF' >> $TEMPDIR/frida-cshell echo ')' >> $TEMPDIR/frida-cshell cat <> $TEMPDIR/frida-cshell -verbose= +debug= file= name= pid= @@ -25,7 +25,7 @@ show_help () { echo " -f spawn FILE" echo " -n attach to NAME" echo " -p attach to PID" - echo " -V enable verbose mode" + echo " -d enable debug mode" echo } @@ -34,7 +34,7 @@ if [[ "\$1" == "--help" ]]; then exit 0 fi -while getopts ":f:hn:p:V:" opt; do +while getopts ":f:hn:p:d" opt; do case \$opt in f) file=\$OPTARG @@ -48,8 +48,8 @@ while getopts ":f:hn:p:V:" opt; do p) pid=\$OPTARG ;; - V) - verbose=true + d) + debug=true ;; :) echo "Option - \$OPTARG requires an argument." @@ -76,8 +76,8 @@ then exit 1 fi -if [ \${verbose} ]; then - opt="{\"verbose\":true}" +if [ \${debug} ]; then + opt="{\"debug\":true}" else opt="{}" fi diff --git a/package-lock.json b/package-lock.json index 9de5b7a..d5d7f92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "frida-cshell", - "version": "1.4.4", + "version": "1.4.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frida-cshell", - "version": "1.4.4", + "version": "1.4.5", "devDependencies": { "@eslint/js": "^9.10.0", "@types/frida-gum": "^18.7", diff --git a/package.json b/package.json index 23bbaae..6795d6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "frida-cshell", - "version": "1.4.4", + "version": "1.4.5", "description": "Frida's CShell", "scripts": { "prepare": "npm run build && npm run version && npm run package && npm run copy", diff --git a/src/cmdlets/breakpoints/bp.ts b/src/cmdlets/breakpoints/bp.ts index 92d049c..47e98a8 100644 --- a/src/cmdlets/breakpoints/bp.ts +++ b/src/cmdlets/breakpoints/bp.ts @@ -7,7 +7,6 @@ import { Token } from '../../io/token.js'; import { Var } from '../../vars/var.js'; const NUM_CHAR: string = '#'; -const UNLIMITED_CHAR: string = '*'; abstract class TypedBpCmdLet extends CmdLet implements InputInterceptLine { public abstract readonly bpType: BpType; @@ -45,16 +44,6 @@ abstract class TypedBpCmdLet extends CmdLet implements InputInterceptLine { return val; } - protected parseHits(token: Token): number | null { - if (token.getLiteral() === UNLIMITED_CHAR) return -1; - - const v = token.toVar(); - if (v === null) return null; - - const hits = v.toU64().toNumber(); - return hits; - } - private runDelete(tokens: Token[]): Var | null { const vars = this.transform(tokens, [this.parseIndex, this.parseDelete]); if (vars === null) return null; @@ -140,7 +129,7 @@ abstract class CodeBpCmdLet const vars = this.transformOptional( tokens, [this.parseVar], - [this.parseHits], + [this.parseNumberOrAll], ); if (vars === null) return null; const [[addr], [hits]] = vars as [[Var], [number | null]]; @@ -164,7 +153,7 @@ abstract class CodeBpCmdLet const vars = this.transformOptional( tokens, [this.parseIndex, this.parseVar], - [this.parseHits], + [this.parseNumberOrAll], ); if (vars === null) return null; const [[index, addr], [hits]] = vars as [[number, Var], [number | null]]; @@ -224,7 +213,7 @@ abstract class MemoryBpCmdLet const vars = this.transformOptional( tokens, [this.parseVar, this.parseVar], - [this.parseHits], + [this.parseNumberOrAll], ); if (vars === null) return null; const [[addr, length], [hits]] = vars as [[Var, Var], [number | null]]; @@ -249,7 +238,7 @@ abstract class MemoryBpCmdLet const vars = this.transformOptional( tokens, [this.parseIndex, this.parseVar, this.parseVar], - [this.parseHits], + [this.parseNumberOrAll], ); if (vars === null) return null; const [[index, addr, length], [hits]] = vars as [ @@ -325,7 +314,7 @@ abstract class TraceBpCmdLet const vars = this.transformOptional( tokens, [this.parseVar, this.parseVar], - [this.parseHits], + [this.parseNumberOrAll], ); if (vars === null) return null; const [[addr, depth], [hits]] = vars as [[Var, Var], [number | null]]; @@ -360,7 +349,7 @@ abstract class TraceBpCmdLet const vars = this.transformOptional( tokens, [this.parseIndex, this.parseVar, this.parseVar], - [this.parseHits], + [this.parseNumberOrAll], ); if (vars === null) return null; const [[index, addr, depth], [hits]] = vars as [ diff --git a/src/cmdlets/development/js.ts b/src/cmdlets/development/js.ts index d50392b..390ec58 100644 --- a/src/cmdlets/development/js.ts +++ b/src/cmdlets/development/js.ts @@ -58,6 +58,7 @@ import { WriteCmdLet } from '../data/write.js'; import { GrepCmdLet } from '../misc/grep.js'; import { CatCmdLet } from '../files/cat.js'; import { LogCmdLet } from '../misc/log.js'; +import { HotCmdLet } from '../thread/hot.js'; const USAGE: string = `Usage: js @@ -104,6 +105,7 @@ export class JsCmdLet extends CmdLet { HelpCmdLet: HelpCmdLet, History: History, HistoryCmdLet: HistoryCmdLet, + HotCmdLet: HotCmdLet, Input: Input, InsnBpCmdLet: InsnBpCmdLet, LdCmdLet: LdCmdLet, diff --git a/src/cmdlets/misc/grep.ts b/src/cmdlets/misc/grep.ts index 3b74cd8..f21c513 100644 --- a/src/cmdlets/misc/grep.ts +++ b/src/cmdlets/misc/grep.ts @@ -18,6 +18,7 @@ export class GrepCmdLet extends CmdLet { public runSync(tokens: Token[]): Var { const vars = this.transformOptional(tokens, [], [this.parseLiteral]); if (vars === null) return this.usage(); + // eslint-disable-next-line prefer-const let [_, [filter]] = vars as [[], [string | null]]; if (filter === null) { Output.clearFilter(); diff --git a/src/cmdlets/thread/hot.ts b/src/cmdlets/thread/hot.ts new file mode 100644 index 0000000..338d40e --- /dev/null +++ b/src/cmdlets/thread/hot.ts @@ -0,0 +1,342 @@ +import { Numeric } from '../../misc/numeric.js'; +import { CmdLet } from '../../commands/cmdlet.js'; +import { Output } from '../../io/output.js'; +import { Token } from '../../io/token.js'; +import { Var } from '../../vars/var.js'; +import { Format } from '../../misc/format.js'; +import { Input } from '../../io/input.js'; + +const MAX_DURATION: number = 10; +const USAGE: string = `Usage: t + +hot * - show execution time for all threads + +hot * duration - show execution time for all threads during a given time period + duration the duration in seconds (maximum of ${MAX_DURATION}) over which to time the threads + +hot id - show execution time for given thread + id the id of the thread to show information for + +hot id duration - show execution time for given thread during a given time period + id the id of the thread to show information for + duration the duration in seconds (maximum of ${MAX_DURATION}) over which to time the thread + +hot name - show execution time for given thread + name the name of the thread to show information for + +hot name duration - show execution time for given thread during a given time period + name the name of the thread to show information for + duration the duration in seconds (maximum of ${MAX_DURATION}) over which to time the thread`; + +const FIELD_NAMES = [ + 'pid', + 'comm', + 'state', + 'ppid', + 'pgrp', + 'session', + 'tty_nr', + 'tpgid', + 'flags', + 'minflt', + 'cminflt', + 'majflt', + 'cmajflt', + 'utime', + 'stime', + 'cutime', + 'cstime', + 'priority', + 'nice', + 'num_threads', + 'itrealvalue', + 'starttime', + 'vsize', + 'rss', + 'rsslim', + 'startcode', + 'endcode', + 'startstack', + 'kstkesp', + 'kstkeip', + 'signal', + 'blocked', + 'sigignore', + 'sigcatch', + 'wchan', + 'nswap', + 'cnswap', + 'exit_signal', + 'processor', + 'rt_priority', + 'policy', + 'delayacct_blkio_ticks', + 'guest_time', + 'cguest_time', + 'start_data', + 'end_data', + 'start_brk', + 'arg_start', + 'arg_end', + 'env_start', + 'env_end', + 'exit_code', +]; + +export class HotCmdLet extends CmdLet { + name = 'hot'; + category = 'thread'; + help = 'display thread execution time information'; + + private static readonly _SC_CLK_TCK: number = 2; + private pSysConf: NativePointer | null = null; + private ticksPerSecond: UInt64 | null = null; + + public runSync(tokens: Token[]): Var { + if (this.ticksPerSecond === null) { + this.ticksPerSecond = this.getTicksPerSecond(); + } + + if (this.ticksPerSecond === null) { + throw Error('failed to get ticks per second'); + } + + const retWithId = this.runShowId(tokens); + if (retWithId !== null) return retWithId; + + const retWithName = this.runShowName(tokens); + if (retWithName !== null) return retWithName; + + return this.usage(); + } + + private getTicksPerSecond(): UInt64 | null { + if (this.pSysConf === null) return null; + const sysConf = new NativeFunction(this.pSysConf, 'pointer', ['int']); + const ret = sysConf(HotCmdLet._SC_CLK_TCK); + const val = uint64(ret.toString()); + if (val === uint64('0xffffffffffffffff')) return null; + return val; + } + + private runShowId(tokens: Token[]): Var | null { + const vars = this.transformOptional( + tokens, + [this.parseNumberOrAll], + [this.parseDuration], + ); + if (vars === null) return null; + const [[id], [duration]] = vars as [[number], [number | null]]; + + Output.debug(`id: ${id}`); + + const matches = Process.enumerateThreads().filter( + t => id === -1 || t.id === id, + ); + const search = id === -1 ? null : `#${id}`; + return this.printThreads(matches, duration, search); + } + + private runShowName(tokens: Token[]): Var | null { + const vars = this.transformOptional( + tokens, + [this.parseLiteral], + [this.parseDuration], + ); + if (vars === null) return null; + const [[name], [duration]] = vars as [[string], [number | null]]; + + const matches = Process.enumerateThreads().filter(t => t.name === name); + return this.printThreads(matches, duration, name); + } + + private parseDuration(token: Token): number | null { + const v = token.toVar(); + if (v === null) return null; + const num = v.toU64().toNumber(); + if (num > MAX_DURATION) return null; + return num; + } + + private printThreads( + threads: ThreadDetails[], + duration: number | null, + search: string | null = null, + ): Var { + if (threads.length !== 0) { + if (duration === null) { + const startTimes = threads.reduce>( + (times, t, _index) => { + times[t.id] = uint64('0'); + return times; + }, + {}, + ); + const endTimes = this.getThreadTimes(threads); + this.printThreadTimes(threads, startTimes, endTimes); + } else { + Output.writeln(`Statictics will be displayed in ${duration} seconds`); + const startTimes = this.getThreadTimes(threads); + setTimeout(() => { + const endTimes = this.getThreadTimes(threads); + Output.clearLine(); + Output.writeln(Output.yellow('-'.repeat(80))); + Output.writeln( + `${Output.yellow('|')} Displaying hot thread statistics:`, + ); + Output.writeln(Output.yellow('-'.repeat(80))); + Input.suppressIntercept(true); + Output.setIndent(true); + Output.writeln(); + try { + this.printThreadTimes(threads, startTimes, endTimes); + Output.writeln(); + } finally { + Output.setIndent(false); + Input.suppressIntercept(false); + Output.writeln(Output.yellow('-'.repeat(80))); + Input.prompt(); + } + }, duration * 1000); + } + } + + switch (threads.length) { + case 0: + if (search === null) { + Output.writeln('No threads found'); + } else { + Output.writeln(`Thread: ${search} not found`); + } + return Var.ZERO; + case 1: { + const t = threads[0] as ThreadDetails; + return new Var(uint64(t.id), `Thread: ${t.id}`); + } + default: + return Var.ZERO; + } + } + + private getThreadTimes( + threads: ThreadDetails[], + ): Record { + const result = threads.reduce>( + (times, t, _index) => { + try { + const path = `/proc/${Process.id}/task/${t.id}/stat`; + Output.debug(`path: ${path}`); + const data = File.readAllText(path); + Output.debug(`data: ${data}`); + const fields = data.split(' '); + const stats: Record = FIELD_NAMES.reduce< + Record + >((acc, key, index) => { + acc[key] = fields[index]; + return acc; + }, {}); + + Object.keys(stats).forEach((key, index) => { + const val = stats[key]; + const valString = + val === undefined ? Output.red('undefined') : Output.yellow(val); + Output.debug( + [ + `${Output.green(index.toString().padStart(3, ' '))}.`, + `${Output.blue(key)}:`, + `${valString}`, + ].join(' '), + ); + }); + + const val = stats['utime'] ?? null; + const utime = val == null ? null : Numeric.parse(val); + times[t.id] = utime; + } catch { + times[t.id] = null; + } + return times; + }, + {}, + ); + + return result; + } + + private printThreadTimes( + threads: ThreadDetails[], + startTimes: Record, + endTimes: Record, + ) { + const ticks = this.ticksPerSecond; + if (ticks === null) { + throw Error('failed to get ticks per second'); + } + + const deltas = threads.reduce>( + (acc, t, _index) => { + const startTime = startTimes[t.id] as UInt64 | null; + const endTime = endTimes[t.id] as UInt64 | null; + if (startTime === null) { + acc[t.id] = null; + } else if (endTime === null) { + acc[t.id] = null; + } else { + const delta = endTime.sub(startTime); + acc[t.id] = delta; + } + return acc; + }, + {}, + ); + + const sorted = threads + .map(t => { + return { thread: t, time: deltas[t.id] as UInt64 | null }; + }) + .sort((a, b) => { + if (a.time === null) { + return 1; + } else if (b.time === null) { + return -1; + } else { + return b.time.compare(a.time); + } + }); + + sorted.forEach(t => { + let timeString = Output.red('unknown'); + if (t.time !== null) { + const millis = (t.time.toNumber() * 1000) / ticks.toNumber(); + timeString = `${Output.yellow(Format.toDecString(millis))} ms`; + } + + Output.writeln( + [ + `${Output.yellow(t.thread.id.toString().padStart(5, ' '))}:`, + `${Output.green((t.thread.name ?? '[UNNAMED]').padEnd(15, ' '))}`, + `[state: ${Output.blue(t.thread.state)}]`, + `[time: ${timeString}]`, + ].join(' '), + threads.length > 1, + ); + }); + } + + public usage(): Var { + Output.writeln(USAGE); + return Var.ZERO; + } + + public override isSupported(): boolean { + switch (Process.platform) { + case 'linux': + this.pSysConf = Module.findExportByName(null, 'sysconf'); + return true; + case 'windows': + case 'barebone': + default: + return false; + } + } +} diff --git a/src/commands/cmdlet.ts b/src/commands/cmdlet.ts index ca02bf5..0c74126 100644 --- a/src/commands/cmdlet.ts +++ b/src/commands/cmdlet.ts @@ -2,6 +2,7 @@ import { Token } from '../io/token.js'; import { Var } from '../vars/var.js'; const DELETE_CHAR: string = '#'; +const UNLIMITED_CHAR: string = '*'; export abstract class CmdLet { public abstract readonly category: string; @@ -95,4 +96,14 @@ export abstract class CmdLet { if (literal !== DELETE_CHAR) return null; return literal; } + + protected parseNumberOrAll(token: Token): number | null { + if (token.getLiteral() === UNLIMITED_CHAR) return -1; + + const v = token.toVar(); + if (v === null) return null; + + const hits = v.toU64().toNumber(); + return hits; + } } diff --git a/src/commands/cmdlets.ts b/src/commands/cmdlets.ts index faa4915..3fe48b7 100644 --- a/src/commands/cmdlets.ts +++ b/src/commands/cmdlets.ts @@ -49,6 +49,7 @@ import { DebugCmdLet } from '../cmdlets/development/debug.js'; import { GrepCmdLet } from '../cmdlets/misc/grep.js'; import { CatCmdLet } from '../cmdlets/files/cat.js'; import { LogCmdLet } from '../cmdlets/misc/log.js'; +import { HotCmdLet } from '../cmdlets/thread/hot.js'; export class CmdLets { private static byName: Map = new Map(); @@ -75,6 +76,7 @@ export class CmdLets { this.registerCmdletType(FunctionExitBpCmdLet); this.registerCmdletType(HelpCmdLet); this.registerCmdletType(HistoryCmdLet); + this.registerCmdletType(HotCmdLet); this.registerCmdletType(InsnBpCmdLet); this.registerCmdletType(JsCmdLet); this.registerCmdletType(LdCmdLet); diff --git a/src/io/output.ts b/src/io/output.ts index f91a881..a4c3a22 100644 --- a/src/io/output.ts +++ b/src/io/output.ts @@ -102,7 +102,9 @@ export class Output { } if (this.log !== null) { - this.log.write(text); + // eslint-disable-next-line no-control-regex + const uncoloured = text.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''); + this.log.write(uncoloured); } send(['frida:stderr', text]); } diff --git a/src/misc/format.ts b/src/misc/format.ts index 753cdbc..f5335b1 100644 --- a/src/misc/format.ts +++ b/src/misc/format.ts @@ -24,7 +24,7 @@ export class Format { ptr: NativePointer | UInt64 | number | null, ): string { if (ptr === null) return '[UNDEFINED]'; - return ptr.toString(10); + return ptr.toString(10).replace(/\B(?=(\d{3})+(?!\d))/g, ','); } public static toSize(ptr: NativePointer | UInt64 | number | null): string {