README
¶
winstrings
strings for Windows — a clone of the GNU binutils strings(1) command, in pure Go.
It prints the sequences of printable characters found in a file: the classic
way to get a first look at a binary. Every option from the
strings(1) man page is
implemented, including the object-file modes (-d, -T), the wide-character
encodings (-e), the UTF-8 display modes (-U), and @file option files.
Written for Windows, but the source is portable and builds and runs on Linux too.
Installation
Option 1 — Download binary
Download the binary for your platform from the
latest release and
place it anywhere on your PATH. Windows and Linux are published, for both
amd64 and arm64, along with a SHA256SUMS.txt.
| Platform | File |
|---|---|
| Windows (amd64) | winstrings-windows-amd64.exe |
| Windows (arm64) | winstrings-windows-arm64.exe |
| Linux (amd64) | winstrings-linux-amd64 |
| Linux (arm64) | winstrings-linux-arm64 |
Rename it to winstrings.exe (or winstrings on Linux) — the tool reports
itself by whatever the file is called, so it is worth renaming rather than
running under the download name.
Option 2 — Go install
If you have Go 1.22+ installed:
go install github.com/fermat-tech/winstrings@latest
This builds and places winstrings.exe in your GOBIN directory (typically
%USERPROFILE%\go\bin), which should already be on your PATH.
Option 3 — Build from source
git clone https://github.com/fermat-tech/winstrings.git
cd winstrings
go build -o winstrings.exe .
That is the whole build. winstrings --version reports the right version by
itself: the Go toolchain stamps it from the repository's tag, so there is no
-ldflags incantation to remember and nothing to keep in sync. A build with
uncommitted changes reports the version with +dirty appended, and a build
from a source archive with no repository in it falls back to the version
compiled into the source.
To cross-compile, set GOOS and GOARCH. This is exactly how the release
binaries are produced — -trimpath keeps local paths out of the binary and
-s -w drops the symbol table:
$env:GOOS="linux"; $env:GOARCH="arm64"
go build -trimpath -ldflags "-s -w" -o winstrings-linux-arm64 .
Like the rest of the win* suite, the tool takes its identity from argv[0]:
whatever you name the file is what it calls itself in its usage and error
messages. Name it strings.exe and it reports itself as strings.
Usage
winstrings [option(s)] [file(s)]
With no file arguments it reads standard input.
| Option | Meaning |
|---|---|
-a, --all, - |
Scan the entire file, not just the data sections. Default. |
-d, --data |
Only scan initialized, loaded data sections of object files. |
-f, --print-file-name |
Print the file name before each string. |
-n N, --bytes=N, -N |
Minimum string length; default 4. |
-t {o,d,x}, --radix= |
Print each string's file offset in octal, decimal or hex. |
-o |
Alias for -t o. |
-w, --include-all-whitespace |
Treat every whitespace character as part of a string. |
-e {s,S,b,l,B,L}, --encoding= |
Character size and endianness. |
-U MODE, --unicode=MODE |
How to display UTF-8 multibyte characters. |
-T NAME, --target=NAME |
Object file format to assume (only affects -d). |
-s STR, --output-separator=STR |
Output record separator; default is a newline. |
@FILE |
Read further options from FILE. |
-h, --help |
Print usage and exit. |
-v, -V, --version |
Print version information and exit. |
By default a string ends at any non-printable character. Tab and space are
always part of a string; other whitespace, including newline and carriage
return, is not, unless -w is given.
-n accepts C integer syntax, so -n 0x10 is 16 and -n 010 is 8. A bare
-NUMBER sets the same thing.
Encodings (-e)
| Value | Meaning |
|---|---|
s |
single 7-bit-byte characters (ASCII) — the default |
S |
single 8-bit-byte characters (every byte above 127 counts as printable) |
b / l |
16-bit big-/little-endian, for UTF-16 and UCS-2 text |
B / L |
32-bit big-/little-endian |
Unicode display (-U)
These control what happens to valid UTF-8 multibyte sequences. Anything other
than default implies -e S, and minimum length is then counted in
characters rather than bytes.
| Value | Short | Effect |
|---|---|---|
default |
d |
No special treatment; -e decides. |
invalid |
i |
Multibyte characters are non-printable, so they end a string. |
locale |
l |
Display in the current locale. |
hex |
x |
Display as <0xc3a9>. |
escape |
e |
Display as \u00e9. |
highlight |
h |
Like escape, but coloured red when stdout is a terminal. |
Note that show / -U s is not accepted, despite appearing in the
--help text of binutils 2.38 and earlier. That was a documentation bug
upstream — the real binary rejects it too — and it was corrected in binutils
2.45. This implementation follows the man page and the corrected help text.
Object files (-d)
-d restricts the scan to sections that are allocated, loaded and have
contents, which usually cuts out a lot of noise. Understood formats:
- PE / COFF —
.exe,.dll,.sys,.obj, both PE32 and PE32+. Sections are selected onIMAGE_SCN_CNT_CODEorIMAGE_SCN_CNT_INITIALIZED_DATA;.debug*,.zdebug*,.stab*and the other names BFD classifies as debug information are excluded, and section sizes are clamped to the virtual size so the file-alignment padding is not scanned. Long/Nand//Bsection names are resolved through the COFF string table, which is what keeps the debug sections out of a Go- or clang-built binary. - ELF — 32- and 64-bit, either endianness. Sections are selected on
SHF_ALLOCwith a type other thanSHT_NOBITS.
Anything else — a plain file, an ar archive, a .NET bundle — is not an
object file as far as -d is concerned, and the whole file is scanned instead.
That matches GNU strings, which does the same for archives.
-T binary presents the whole file as one loaded section. A target naming a
format that does not match the file is only a preference: as in BFD, the file
is then probed normally. An unrecognised target name makes -d fall back to a
whole-file scan.
A caveat about target names
--help lists the formats -d can actually read. Six further BFD target
names are recognised but cannot be read by this build:
srec symbolsrec verilog tekhex ihex plugin
Passing one of these is accepted and has no effect: the file is probed and
scanned as whatever it really is. That is not a special case — it is precisely
what BFD does with any target that does not match the file, silently, and GNU
strings behaves the same way. They are kept out of the --help list on purpose,
because honouring a name is not the same as parsing the format, and listing
srec as supported would imply this tool reads S-records. It does not.
Worth knowing either way: -T has no effect at all without -d, in this
tool and in GNU strings alike, because the target is only consulted when
opening a file as an object.
Environment
| Variable | Effect |
|---|---|
NO_COLOR |
If set and non-empty, -U highlight emits no colour. |
WINSTRINGS_COLOR |
always / never overrides the terminal check. |
Exit status
0 on success, 1 if any file could not be read or an option was invalid.
Compatibility
The scanning engine is a direct port of binutils/strings.c (2.45), so output
is intended to be byte-for-byte identical to GNU strings. That is verified by
differential testing rather than asserted: the same argument lists are run
against this tool on Windows and against GNU strings on a Linux box, and the
outputs, stderr and exit statuses are compared byte for byte.
| Suite | Cases | Identical |
|---|---|---|
| Synthetic fixtures, ELF executable, Go-built PE | 170 | 154 |
Real MSVC binaries (64- and 32-bit kernel32.dll, notepad.exe) |
12 | 12 |
ELF .o object and ar archive |
8 | 8 |
All 16 remaining differences are accounted for below: 11 are the deliberate divergences, and 5 are version drift between binutils 2.38 and 2.46.
Deliberate divergences
1. Digits anywhere in a short-option cluster set the minimum length.
strings -a6 and strings -6a both mean "scan everything, minimum length 6".
GNU strings misparses these: it reads the minimum length from the wrong argv
element and fails with invalid integer argument a6 — or, for -6a, with
invalid integer argument trings, having read the program's own name. A bare
-6 works correctly in both.
2. Offsets after a UTF-8 pushback are correct. When -U is in effect and a
byte has been pushed back after a failed UTF-8 sequence, GNU strings underflows
an unsigned int counter and reports an offset 2³² too large — 4294967340
where the string is actually at offset 44. This prints the true offset.
Affects the streaming path only; -d was never wrong.
3. Backslashes in @file option files are literal unless they escape
something. libiberty lets a backslash escape any character, which on
Windows quietly destroys every path in an options file: C:\Users\na\tools
comes back as C:Usersnatools. Here a backslash is an escape only before a
quote, another backslash, or whitespace — every case the man page's own
description needs — and is a literal character otherwise.
4. Standard output is flushed before anything is written to standard error. GNU strings lets stdio buffering decide, so redirecting both streams to one file puts all the warnings before all the output. Here warnings appear in sequence with the strings around them.
Of the 16 differing cases: divergence 1 accounts for 2, divergence 2 for 9, and
the 5 remaining are version drift (below). Divergences 3 and 4 account for none
— no case exercises a Windows path in an @file, and the harness captures
stdout and stderr separately. The unreadable target names described under
A caveat about target names are not a
divergence: all seven cases covering them match GNU byte for byte.
Two further differences show up only against binutils 2.38, the version used for differential testing, and are version drift rather than divergence:
- The supported-target line printed at the end of the usage message, which
reflects how each build was configured. This accounts for the 5 usage-text
cases above; the rest of the usage message matches to the byte, including the
--unicode=line, where the tool follows 2.45's corrected wording rather than 2.38'sshow/-U stypo. - The
-noverflow diagnostics. This tool follows 2.45/2.46, which addedminimum string length is too big: Nandminimum string length is too small: N; 2.38 has neither check and silently accepts-n -1as a minimum of 4294967295.
GNU quirks reproduced on purpose
These are upstream bugs that are kept, because their output is well-defined, loses no information, and a user diffing against GNU strings would otherwise see spurious differences:
-U escapemis-packs 4-byte UTF-8. U+1F600 prints as\u07c600, not\u1f600: upstream repacks the 21 payload bits into three bytes on the wrong boundaries. No information is lost — the escape still uniquely identifies the character — so the exact output is reproduced.-U localeprints only the lead byte of each multibyte character, because upstream formats it withprintf("%.1s").- A bare
-NUMBERoverrides-nregardless of order.strings -7 -n 3andstrings -n 3 -7both use 7, because upstream applies the numeric option after the option loop has finished. - The pushback order after an invalid 4-byte sequence under
-U invalidis upstream's (bytes 4, 2, 3 rather than 4, 3, 2), so that scanning resumes at the same place GNU strings resumes at.
Tests
go test ./... # unit tests
go vet ./...
The unit tests cover the scanner in every encoding, the -U modes through both
the buffered and streaming paths, option parsing, @file expansion, C
strtoul semantics, and PE/ELF section selection against synthesized object
files. The differential suite against real GNU strings lives outside the repo;
see Compatibility above for what it covers.
Licence
MIT — see LICENSE.
Documentation
¶
Overview ¶
winstrings prints the sequences of printable characters in files.
Usage:
winstrings [option(s)] [file(s)]
winstrings is a Windows clone of GNU strings(1) from binutils. For each file given it prints the printable character sequences that are at least 4 characters long (or the number given with -n) and are followed by an unprintable character or end of file.
Options:
-a - --all scan the entire file (default)
-d --data only scan initialized, loaded data sections
-f --print-file-name print the file name before each string
-n N --bytes=N minimum string length (default 4)
-N same, as a bare number (e.g. -6)
-t --radix={o,d,x} print the offset of each string
-o alias for --radix=o
-w --include-all-whitespace treat all whitespace as string characters
-e --encoding={s,S,b,l,B,L} character size and endianness
-U --unicode=MODE how to display UTF-8 multibyte characters
-T --target=BFDNAME object file format for -d
-s --output-separator=STR output record separator (default newline)
@FILE read further options from FILE
-h --help display usage and exit
-v -V --version output version information and exit
The scanning engine is a direct port of binutils' strings.c so that output is byte-for-byte identical to GNU strings; see README.md for the handful of deliberate divergences.