strace — strace syscall tracer — bash/Linux ptrace, macOS dtruss, Windows procmon, pwsh Trace-Command + Get-WinEvent ETW equivalents across all 5 shells
Equivalents in every shell
strace -f -e trace=openat,connect ./myprogramLinux's syscall tracer using `ptrace(2)`. `-f` follow forks/threads (CRITICAL — without it you miss child processes); `-e trace=<list>` filter to specific syscalls (`openat`, `read`, `connect`, `execve` etc.); `-e trace=%file` macro for all file-related syscalls; `-e trace=%network` for network; `-p <PID>` attach to running process. `-o file.log` write to file (not stderr — useful when the traced program also writes stderr). `-s 1024` increase string truncation length (default 32 — strings get `"...truncated"...` mid-output without it). Distro install: `apt install strace`, `dnf install strace`, `pacman -S strace`. Linux-only — does NOT work on macOS or Windows native.
strace -f -e trace=openat,connect ./myprogramSame `strace` binary. On macOS, `strace` does NOT exist — Apple does not ship ptrace-based tracing tools. Use `dtruss` (DTrace wrapper) instead: `sudo dtruss -f ./myprogram` (requires SIP partially disabled on Apple Silicon; `csrutil enable --without dtrace` from Recovery Mode). For non-disabled-SIP macOS: `sample <PID>` gives a sampled stack snapshot — not syscall-level but useful for "what is this process doing right now". `fs_usage` and `sc_usage` are also macOS syscall-adjacent tools.
strace -f -e trace=openat,connect ./myprogramNo fish-specific wrinkle — `strace` is a separate Linux binary. CAVEAT: `strace`'s long output is BUFFERED with `\n` separators that fish handles cleanly, but when watching live (`strace -f ./myprogram`), terminal-resize SIGWINCH can interrupt mid-output. Use `strace -f -o /tmp/strace.log ./myprogram &` and `tail -F /tmp/strace.log` for sane scrollback.
Trace-Command -Name ParameterBinding -Expression { Get-ChildItem } -PSHostpwsh has NO syscall-level tracer — `Trace-Command` operates at the cmdlet level (parameter binding, pipeline construction), NOT at the kernel-syscall level. For Windows syscall-level tracing: `procmon.exe` (Sysinternals, GUI; ProcessName / Operation / Path filters) is the canonical equivalent. For ETW (Event Tracing for Windows) at the command line: `logman.exe create trace MyTrace -p Microsoft-Windows-Kernel-File -o trace.etl` + `logman start MyTrace` / `logman stop MyTrace`; analyze with `Microsoft.Diagnostics.Tracing.PerfView`. For LINUX pwsh: `& strace -f pwsh ./script.ps1` works — strace traces pwsh's syscalls (revealing assembly loading, file opens, etc.).
procmon.exe /BackingFile C:\trace.pmlcmd has no native syscall tracer. `procmon.exe` (Sysinternals — `winget install Microsoft.Sysinternals.ProcessMonitor`) is the Windows-canonical tool — GUI-driven but supports CLI: `/BackingFile <file.pml>` saves to a file, `/Quiet` suppresses launch dialog, `/Terminate` stops a running session. For older/lighter-weight tracing: `tracerpt.exe` (analyze ETW files), `xperf.exe` (Windows Performance Toolkit). For the simplest "what files is this exe opening": `handle.exe -p <process> -a` (Sysinternals — snapshots open handles).
Worked examples
Trace all syscalls of a command and write to a log
strace -f -o trace.log ./myprogramstrace -f -o trace.log ./myprogramwsl strace -f -o trace.log ./myprogramprocmon.exe /BackingFile C:\trace.pml /QuietTrace only file-system syscalls (debug "file not found" errors)
strace -f -e trace=%file ./myprogram 2>&1 | grep ENOENTstrace -f -e trace=%file ./myprogram 2>&1 | grep ENOENTwsl bash -c "strace -f -e trace=%file ./myprogram 2>&1 | grep ENOENT"procmon.exe /BackingFile C:\trace.pmlAttach to an already-running process
sudo strace -f -p $(pidof myprogram)sudo strace -f -p $(pidof myprogram)wsl sudo strace -f -p $(pidof myprogram)handle.exe -p myprogram.exe -aTrace network syscalls only
strace -f -e trace=%network -e read=all -s 256 ./myprogramstrace -f -e trace=%network -e read=all -s 256 ./myprogramGet-NetTCPConnectionnetstat -anoGotchas
- **`-f` (follow forks) is almost always what you want — without it you miss children.** A bare `strace ./myprogram` traces ONLY the initial process. If `myprogram` forks a child (very common — `sh -c "cmd"`, `exec`, daemon double-fork), the child's syscalls are NOT traced and the parent's trace usually shows the `fork()` then a `wait()`. Result: "I straced it but I see nothing useful". Always pass `-f` unless you have a specific reason not to. CAVEAT: `-f` produces interleaved output from all processes — each line is prefixed with `[pid 1234]` for disambiguation. For complex multi-process traces, save to file and post-process by PID: `strace -ff -o trace ./myprogram` writes one file per PID (`trace.1234`, `trace.1235`, etc.).
- **strace slows the traced program by ~50–500x for syscall-heavy workloads.** Each syscall trap requires ptrace round-trips between strace and the kernel. CPU-bound code (no syscalls) sees minimal slowdown; I/O-heavy or fork-heavy code grinds. If your bug is timing-related (race conditions, deadlocks), strace WILL distort it — Heisenbug. Faster alternative for production-style tracing: `perf trace` (kernel's built-in perf tool, lower overhead via tracepoints), `bpftrace` (eBPF-based, near-zero overhead), or `sysdig` (commercial-friendly equivalent). For "is this program syscall-bound at all": `strace -c ./myprogram` (`-c` summary mode) prints a per-syscall time/count table — production-safe-ish, ~2x slowdown.
- **String truncation: default `-s 32` is too short for HTTP / SQL payloads.** strace truncates string arguments to syscalls at 32 bytes by default — useful for terse output, painful for actually reading what was sent: `write(7, "POST /api/v1/users HTTP/1.1\r\nHo"..., 256) = 256`. Bump with `-s 1024` (or `-s 4096` for full HTTP headers). For "show me the FULL contents read/written on fd 7": `strace -e read=7 -e write=7 -s 4096 ./myprogram`. Without these, debugging HTTP / database / RPC issues via strace is mostly guessing.
- **Permission gotchas: strace needs ptrace permissions; Yama may block.** On hardened Linux (Ubuntu since 22.04, Debian 11+, Fedora since 35), `/proc/sys/kernel/yama/ptrace_scope` is `1` by default — a process can only be traced by its OWN parent. Attaching to an unrelated running process (`strace -p <PID>` to a process YOU started in another terminal) FAILS with `Operation not permitted` even as the same user. Fix: `sudo strace -p <PID>` (root bypasses yama), or temporarily lower yama: `echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope`. Containers running with `--cap-drop=SYS_PTRACE` strip the capability entirely — add it back with `docker run --cap-add=SYS_PTRACE`. For strace on docker exec sessions: `docker run --security-opt seccomp=unconfined --cap-add=SYS_PTRACE`.
- **`ltrace` is the library-call sibling — different abstraction layer.** `strace` traces SYSCALLS (kernel boundary). `ltrace` traces LIBRARY CALLS (libc / libssl / etc — userspace boundary). For "what arguments was `malloc` called with?": `ltrace -f ./myprogram`. For "what crypto operations happened": `ltrace -e 'libssl.so.*' -f ./myprogram`. ltrace catches a different bug class: misuse of library APIs (wrong argument, missing free), whereas strace catches syscall-level issues (file-not-found, permission-denied, network refused). Often you want BOTH: `strace -f` first to find the failing syscall, then `ltrace -f` to trace the library call that issued it.
WSL & PowerShell Core notes
Related glossary
- Process identity
Every running program has multiple identifiers beyond its PID. They show up in `ps`, `kill`, `setsid`, job control, and every "why is my Ctrl-C being eaten" debug session.
- Streams & file descriptors
Every Unix process is born with three open files. Knowing which one you are writing to (or reading from) is the difference between a script that works and a script that silently swallows errors.
- Signals
Signals are tiny inter-process messages. Knowing which ones can be caught, which cannot, and which clean up vs. terminate ungracefully prevents a lot of "service won’t restart" bugs.