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.
Build
cd winstrings
go build -o winstrings.exe .
For a release binary, stamp the version in:
go build -ldflags "-X main.version=v1.0.0" -o winstrings.exe .
Like the rest of the win* suite, the tool takes its identity from argv[0]:
rename winstrings.exe to strings.exe and its usage and error messages say
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 on IMAGE_SCN_CNT_CODE or IMAGE_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 /N and //B section
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_ALLOC with a type other than SHT_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's show/-U s typo.
- The
-n overflow diagnostics. This tool follows 2.45/2.46, which added
minimum string length is too big: N and minimum string length is too small: N; 2.38 has neither check and silently accepts -n -1 as 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 escape mis-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 locale prints only the lead byte of each multibyte character, because
upstream formats it with printf("%.1s").
- A bare
-NUMBER overrides -n regardless of order. strings -7 -n 3 and
strings -n 3 -7 both 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 invalid is
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.