Autogenerated documentation with DeepWiki
Minimalistic and lightweight shell-based hex editor.
- Print file content in various formats.
- Write/overwrite data into the file.
- Transform a range of bytes in place (xor, add, rotate, byte-swap, reverse, ...).
- Undo writes until committed.
- Enumerate (ASCII) strings.
- Search strings or binary data.
- Calculate hashes, checksums, and CRCs.
- Execute template files using a custom language (bhe), see examples in the
templates/subdirectory. - Disassemble opcodes (using Capstone).
- Assemble opcodes (using Keystone).
- Identify the ISA of a byte block using bundled AI models, or graph code ranges across a file.
Just run bhex <file> to start the shell.
Supported flags:
Usage: bhex [ options ] inputfile
-h --help Print help
-w --write Open the file in write mode
-b --backup Backup original file in "filename.bk"
-2 --no_warning Disable warnings
-n --no_history Do not save command history
-C --no_color Do not use colors
-U --no_unicode Draw with ASCII only, never with unicode
-c "c1; c2; ..." Execute the commands given as argument and exit
-s --script Script mode (commands from raw stdin)
exit code is 1 when bhex cannot start (bad command line, missing or unopenable input file) and 2 when a command of a "-c" batch or of a "-s" script fails
command history is saved in "$HOME/.bhex_history", it can be changed setting BHEX_HISTORY_FILE environment variable
Colors are disabled automatically when the output is not a terminal, or when the NO_COLOR
environment variable is set (set CLICOLOR_FORCE to keep them through a pipe); unicode is used
only when the locale of the environment is a UTF-8 one.
The project can be compiled using cmake. Without Capstone and Keystone, it has no runtime dependencies (apart from libc), so it should be quite straightforward:
$ mkdir build
$ cd build
$ cmake ..
$ make
To enable the disassembler command, use "-DENABLE_CAPSTONE=on".
To enable the assembler command, use "-DENABLE_KEYSTONE=on".
To enable an ASAN build, use "-DASAN=on -DCMAKE_BUILD_TYPE=Debug".
To enable tests, use "-DENABLE_TESTS=on".
To enable fuzz test binaries, use "-DENABLE_FUZZ_TESTS=on".
To statically link libc, use "-DSTATIC_BUILD=on".
The cosmocc toolchain builds bhex as a single
APE binary (bhex.exe in the releases) that runs on Linux, macOS, Windows,
FreeBSD, OpenBSD and NetBSD, on both x86_64 and aarch64:
$ mkdir build_cosmopolitan
$ cd build_cosmopolitan
$ cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/cosmopolitan.cmake -DCMAKE_BUILD_TYPE=Release ..
$ make
The toolchain is located from "-DCOSMOCC_HOME=
", the COSMOCC_HOME environment variable, or a cosmocc on PATH.Every command has the following structure:
$ command_name/mod1/mod2/mod3/... arg1 arg2 ...
where the modifiers (e.g., mod1) are optional parameters of the command.
The documentation for each command can be accessed by typing "?" after the name of the command.
In the shell, a "*" before the "$" of the prompt ([0x0000000] * $) means that there are writes that have not been committed yet.
If you type "help" (or "h"), you get the list of commands:
[0x0000000] $ h
Available commands:
assemble [as]
checksum [cs]
commit [c]
crc [cr]
delete [d]
diff [df]
disas [ds]
echo [ec]
entropy [e]
export [ex]
findbase [fba]
hash [hh]
help [h]
hist [hi]
identify [id]
import [im]
info [i]
interactive [tui]
isa_identify [ii]
map [m]
print [p]
search [src]
seek [s]
setbase [sb]
strings [str]
template [t]
transform [tr]
undo [u]
write [w]
Based on isadetect.
[0x0000000] $ ii?
isa_identify: identify the ISA of a block of bytes using bundled AI models
ii[/s] [<size>]
s: name one ISA for the whole range, without looking for ranges
size: number of bytes to analyze starting from the current offset
(if omitted, use the whole file)
By default the region is scanned in 1024-byte chunks: each chunk is classified as code or not, contiguous chunks of the same kind are merged, and only the code ranges are printed, each with the top ISA for that range. On a whole file that is the useful answer -- naming one ISA for a file that is mostly headers, strings and data says little.
/s is the other question: given a range already known to hold code, what is
it? It prints the top ISA candidates for the range as a whole.
Example:
[0x0000000] $ ii 4096
ISA graph (4096 bytes analyzed, 1024-byte chunks):
[0x0000000000000000, 0x0000000000000800): x64, le (confidence: 96.42%)
[0x0000000000000c00, 0x0000000000001000): x86, le (confidence: 91.87%)
[0x0000000] $ sb?
setbase: set or display the base address
sb <base>
base: the new base address (if omitted, display current base)
[0x0000000] $ ec?
echo: print arguments to stdout
echo [/x|/d] <arg1> [arg2] ...
/x: force hexadecimal output for numbers (default)
/d: force decimal output for numbers
Expressions in backticks are evaluated before printing.
[0x0000000] $ i?
info: prints information about the opened binary
[0x0000000] $ e?
entropy: display an entropy graph
e [<rows> <len>]
rows: number of points in the graph (if omitted or '-', auto mode)
len: number of bytes to include starting from the current offset (if omitted, use the whole file)
[0x0000000] $ e - 8
[ 00000000 - 000277c8 ] (5.980) ---------------------------------+
[ 000277c8 - 0004ef90 ] (6.398) -----------------------------------+
[ 0004ef90 - 00076758 ] (6.492) ------------------------------------+
[ 00076758 - 0009df20 ] (4.491) -------------------------+
[ 0009df20 - 000c56e8 ] (6.441) ------------------------------------+
[ 000c56e8 - 000eceb0 ] (6.477) ------------------------------------+
[ 000eceb0 - 00114678 ] (6.495) ------------------------------------+
[ 00114678 - 0013be40 ] (4.388) ------------------------+
The distribution of the byte values in a range: one row per value, with the count, the share of the
range it takes and a bar scaled against the most frequent value. Where e summarises a range into
one number, this is the whole picture that number came from, so a block of text, a table of pointers
and a compressed blob look nothing alike here.
[0x0000000] $ hi?
hist: draw the distribution of the byte values in a range
hi[/z/s] [<len>]
z: include the values that never occur
s: sort by count
len: number of bytes to include starting from the current offset (if omitted, use the whole file)
[0x0000000] $ hi 64
00 16 25.00% ---------------------------------------------+
03 1 1.56% --+
08 1 1.56% --+
0a 2 3.12% -----+
0d 2 3.12% -----+
19 1 1.56% --+
1a 1 1.56% --+
20 ' ' 3 4.69% --------+
...
A value that never occurs is left out unless /z asks for it, and /s sorts by count, putting the
most frequent value last so that the prompt does not push it off the screen. The rows are colored
the way the bytes of a dump are: gray for 00, red for ff, green for printable ASCII. The bars
are drawn like the ones of the entropy graph, - up to a + tip and on the same 45 column scale,
so that the two commands can be read side by side.
The whole file as one character per slice, each named after what the bytes in
it look like. Where e gives a number per region and hist the distribution
of the whole, this says what kind of thing is where -- and the distinction it
makes that entropy alone cannot is text against structured binary, which sit
at the same entropy.
[0x0000000] $ m 6
[ 00000000 ] ......................................................:AAAAAAAAA
[ 00009580 ] AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:::::::::::::::::::
[ 00012b00 ] ::::::::::::::::::::::::::::::::::::############################
[ 0001c040 ] ################################################################
[ 00025580 ] ##################:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
[ 0002eac0 ] FFFFFFFFF:......................................................
. zeroes F 0xff A text # high entropy : mixed
597 bytes per cell, 384 cells
A slice is called after what fills it: nine tenths 00 or ff makes it
padding or filler, 85% printable makes it text, and bytes that cannot be told
apart from random make it compressed or encrypted. Everything else (headers,
tables, code) is "mixed".
The bar for "random" moves with the size of a slice. A short slice has too few bytes to visit all 256 values, so it cannot score 8 bits however random it is: 64 bytes top out near 5.8. A fixed threshold would call none of them compressed, so the map compares each slice against what random bytes of that size would actually measure, and does not guess at all below 64 bytes.
Start an interactive session.
[0x0000000] $ src?
search: search a string or a sequence of bytes in the file
src[/{x, s}/sk/p/1] <what> [<len>]
x: data is a hex string
s: data is a string (default)
sk: seek to first match
p: print context
1: stop at the first match
what: either a string or a hex string. A hex string may carry '?'
in place of a digit, matching any value for that nibble
(e.g. "e8 ?? ?? ?? ??")
len: number of bytes to search starting from the current offset
(if omitted, search the whole file)
[0x0000000] $ str?
enumerate the strings in the file (i.e., sequences of printable ascii characters with 8 or 16 bits)
str[/n/{a,w}] [ <pattern> <num> ]
n: look for null-terminated strings
a: 8-bit only
w: 16-bit only
pattern: print only strings that contain the pattern as substring (use * for any character)
num: minimum length (default: 3)
Based on binbloom.
[0x0000000] $ fba?
findbase: guess the base address of a raw firmware blob using binbloom heuristics
fba[/{32,64}/{le,be}]
32: assume a 32-bit blob (default)
64: assume a 64-bit blob
le: force little-endian decoding
be: force big-endian decoding
[0x0000000] $ hh?
hash: calculate the hash of <size> bytes at current offset + <off>
hash /l <algorithm> [ <size> <off> ]
l: list the supported hashing algorithms
algorithm: hashing algorithm, or a part of one to run a family ('md' runs md2 to md6-512), or '*' for all of them
size: number of bytes to include in the hash (if omitted or zero, hash the whole file starting from current offset)
off: starting offset wrt to current offset (default 0)
[0x0000000] $ hh/l
md2
md4
md5
md6-128
sm3
sha1
sha256
sha512
sha512-256
sha3-256
shake128-256
keccak-256
RipeMD-160
blake2s
blake2b
blake2b-256
blake3
gost
streebog-256
groestl-256
jh-256
skein-512
snefru-128
spectral-256
haval-128-5
tiger
whirlpool
xxh32
xxh64
xxh3-64
murmur3-32
fnv1a-32
...
The list runs to 86 entries. Alongside the cryptographic families it carries the fast,
non-cryptographic hashes that turn up inside file formats, xxh32 is what an LZ4 frame
checksums its content with, xxh64 what a Zstandard one uses, and murmur3/fnv1a are what
indices and shellcode import resolvers tend to use. Note that keccak-256 is the original
padding (what Ethereum and pre-FIPS-202 software call "SHA-3") and differs from sha3-256,
and that streebog-* is GOST R 34.11-2012 while plain gost is the superseded 34.11-94.
[0x0000000] $ t?
template: parse the file at current offset using a 'bhe' template file
t[/l/i/x] <name or file>
l: list available templates and structs
x: output in XML
i: interpret inline code
arg: its meaning depends on the mode. It could be
- the name of the pre-loaded template/struct/proc to use
- a path to a template file
- a filter (if in list mode)
- inline bhex code (if in interpret mode)
[0x0000000] $ t/l
Available templates:
gpt
lz4
lha
qcow2
jpeg
gzip
ext
...
Walks the file looking for the formats bhex knows, the way binwalk does. Each template declares
the byte patterns its format cannot appear without (_identify_magic), and the bytes it needs to
see to be sure (_identify). One pass finds every declared pattern at once, and only where one
matched does the template get asked; a template that recognises the bytes answers with the size of
what it found, and the scan resumes past it.
[0x0000000] $ id?
identify: scan the file for known formats. One pass finds every magic
the templates declared ('_identify_magic'), and only where
one matched is a template asked ('_identify')
id[/l/v/vv/n/e] [<len>]
l: list the templates that take part in the scan
v: report the numbers of the scan (templates, candidates, timings)
vv: also report the time each template cost (measuring it is not
free, the scan itself gets slower)
n: do not skip over what was identified
e: exhaustive: ignore the declared magics and ask every template
at every offset
len: number of bytes to scan starting from the current offset
(if omitted, scan up to the end of the file)
A hit reports the size the template gave for what it recognised,
and the scan resumes past it, so a format embedded in something
already identified is only found with '/n'
[0x0000000] $ id
0x00100000 png 218 bytes
0x002000da elf 324 bytes
0x0030021e zip 71 bytes
0x0040058b gzip 10 bytes
0x005005aa jpeg 20 bytes
0x00600747 mp4 262 bytes
0x0070084d squashfs 623 bytes
0x0080184d rpm 96 bytes
A plain scan prints the hits and nothing else. What it took to find them is behind /v:
[0x0000000] $ id/v
0x00100000 png 218 bytes
...
12 hits in 8395032 bytes, 46 templates
prefilter: 72 patterns -> 157 candidates in 0.030s
157 offsets, 157 runs in 0.034s
A template with no _identify_magic has to be tried at every offset, which puts a floor under the
whole scan; id/l shows which templates are prefiltered and id/vv what each one costs.
[0x0000000] $ s?
seek: change current offset
s[/{+,-}] <addr>
+: sum 'off' to current offset (wrap if greater than filesize)
-: subtract 'off' from current offset (wrap if lower than zero)
off: can be either a number or the character '-'.
In the latter case seek to the offset before the last seek.
If a base address is set (see 'sb'), absolute addresses are
relative to the base and cannot go below it.
NOTE: if called without arguments, print current offset
[0x0000000] $ crc?
import: calculate the CRC <name> at current offset + <off>
crc[/l] <name> [<size> <off>]
l: list the supported crc names
name: name of the CRC (or a partial name, or '*')
size: number of bytes to include in the crc (if omitted or zero, import the whole file starting from current offset)
offset: starting offset of the imported file (if omitted, import from current offset)
[0x0000000] $ cs?
checksum: calculate a checksum at current offset + <off>
checksum [/l] <name> [<size> <off>]
l: list the supported checksum names
name: name of the checksum (or a partial name, or '*')
size: number of bytes to include (if omitted or zero, use the whole file starting from current offset)
offset: starting offset (if omitted, use current offset)
[0x0000000] $ as?
assemble: assemble code and write it at current offset
as[/l/i/s] <arch> 'instr1; instr2; ...'
l: list supported architectures
i: insert instead of overwrite
s: seek to the end of the write
arch: the architecture to use
code: assembly code string (e.g., "inc eax; inc ecx; ret")
All ten architectures keystone assembles for are named, hexagon and evm among them. The
latter is not an LLVM target at all: keystone looks its opcodes up in a table of its own, one
mnemonic per call, so as evm push1 writes the single byte 0x60.
[0x0000000] $ ds?
disas: disassemble code at current offset
ds[/l [<filter>]|/a/o] <arch> [<nbytes>]
l: list the supported architectures, or those <filter>
names: the one called that, else the ones starting
with it, else the ones mentioning it anywhere
a: draw the branches as arrows on the left of the mnemonics.
'◂' marks a jump, '▸' where it lands, '▾' and '▴' a
target that is not part of the listing
o: print the bytes of every instruction
arch: the architecture to use
nbytes: number of opcodes to disassemble (default: up to the
instruction that returns)
Every architecture capstone can decode is named, all 23 of them, down to the variant: the table
is generated from capstone's own cstool list by scripts/gen_disas_archs.py, so a submodule
bump cannot leave bhex naming an instruction set that capstone has renamed or re-tuned. That is
168 names, which is why /l takes a filter:
[0x0000000] $ ds/l loongarch
Supported architectures matching 'loongarch' (2 of 168):
loongarch32 LoongArch 32-bit
loongarch64 LoongArch 64-bit
The filter is read the way hash reads an algorithm: the name spelled exactly wins, then the
names starting with it, then the ones mentioning it anywhere -- in their description too, so
ds/l endian and ds/l thumb are worth asking. The names bhex has always used keep their
meaning where capstone now uses the same string for something else: ppc32 and ppc64 stay big
endian (capstone means the little endian pair, which is bhex's ppcle32 and ppcle64), riscv32
and riscv64 stay without the compressed encodings, and m68k stays pinned to the 68000.
With no count, the listing is the function at the current offset: it ends with the first
instruction that gives control back to the caller, or with the file (or with the first bytes that
decode to nothing) when there is none. What a return looks like is one thing per architecture and
capstone is of little help there, so each of them has its own answer in common/disassemble: x86
has a ret, arm writes the return address into the program counter, ppc branches to the link
register, mips and sparc jump through the register holding the return address, c64x and hppa and
arc through B3 and rp and blink, evm ends the call frame with any of four opcodes, and the
handful capstone does fill CS_GRP_RET for -- m680x, mos65xx, loongarch, xtensa -- just ask it.
The delay slot goes with it, and it is not a property of the architecture alone: mips and sparc
run the one instruction behind the return, c64x the next five, a hppa bv runs its slot while a
bv,n nullifies it, and SH2A's rts/n is the undelayed spelling of rts. So the listing ends
where control really leaves, whichever of those it is.
An operand counted from the program counter -- x86's [rip + 0xcd96b], the literal pools of
arm, riscv's auipc -- is printed by capstone as the offset it carries, which says nothing about
where it lands. The address it resolves to follows the instruction as a comment:
[0x0021110] $ ds x64 3
0x00021110: endbr64
0x00021114: test edi, edi
0x00021116: mov edi, dword ptr [rip + 0x1003c0] ; 0x001214dc
A base address set with setbase is part of it, as it is part of every other address of the
listing. The architectures that resolve their own get no comment: capstone already prints the
address an aarch64 adrp, an m68k (pc) operand or an s390x larl builds, and the target of a
branch is always printed absolute.
With /o the bytes of every instruction are printed between the address and the mnemonic, the
way a dump prints them:
[0x0021110] $ ds/o x64 3
0x00021110: f3 0f 1e fa endbr64
0x00021114: 85 ff test edi, edi
0x00021116: 8b 3d c0 03 10 00 mov edi, dword ptr [rip + 0x1003c0] ; 0x001214dc
With /a, every branch whose target is disassembled too is drawn as a line going from the jump
to the instruction it lands on, so that the loops and the early exits of a function can be seen
without following the addresses by hand:
[0x0215800] $ ds/a/o m68k 40
[...]
0x00215832: 6e 08 ╭◂ bgt.b $21583c
0x00215834: 20 3c ff ff fb b5 │ move.l #$fffffbb5, d0
0x0021583a: 60 36 ╭───◂ bra.b $215872
0x0021583c: 4a 88 │ ╰▸ dc.w $4a88
0x0021583e: 67 04 │ ╭◂ beq.b $215844
0x00215840: 4a 8c │ │ dc.w $4a8c
0x00215842: 66 08 │ ╭─◂ bne.b $21584c
0x00215844: 20 3c ff ff fb b6 │ │╰▸ move.l #$fffffbb6, d0
0x0021584a: 60 26 │╭──◂ bra.b $215872
0x0021584c: 4a ad 00 34 ││╰─▸ tst.l $34(a5)
[...]
0x00215870: 70 00 │││╰▸ moveq #$0, d0
0x00215872: 4c ee 30 80 ff f4 ╰┴┴─▸ movem.l -$c(a6), d7/a4-a5
Nested branches take one column each, up to five of them. Past that, and for the branches
whose target is not part of the listing, only the direction is marked with ▾ or ▴. Where
several branches land on the same instruction the lines join, as they do on the last row above.
The arrows need the branch targets that capstone reports for the architecture: bpf and ebpf get
none, as capstone does not tell their jumps apart from the rest.
There is no limit to the number of opcodes that can be asked for: a listing is read, disassembled
and printed one 4096 bytes block at a time, so a long one costs no more memory than a short one
and starts printing right away. The arrows are drawn one block at a time as well, which is what a
long listing costs: a branch is joined to its target only when the two fall in the same block, and
is marked with ▾ or ▴ when they do not.
The drawings need a UTF-8 locale, which is looked up in LC_ALL, LC_CTYPE and LANG. Any
other locale, a TERM of dumb, or -U on the command line, and the same arrows are drawn
with ASCII instead:
0x0021583a: 60 36 /---< bra.b $215872
0x0021583c: 4a 88 | \> dc.w $4a88
0x00215872: 4c ee 30 80 ff f4 \++-> movem.l -$c(a6), d7/a4-a5
[0x0000000] $ p?
print: display the data at current offset in various formats
p[/{x,w,d,q,a,C}/{le,be}/r/W/{+,-}] <nelements>
x: hex output (default)
w: words
d: dwords
q: qwords
a: as ascii
C: as C buffer
le: little-endian (default)
be: big-endian
r: raw mode (no ascii, no header and no addresses)
W: wide mode (print 32 bytes per line)
+: seek forward after printing
-: seek backwards after printing
nelements: the number of elements to display
(default: enough to display 256 bytes, if '-' the whole file)
[0x0000000] $ df?
diff: prints the differences with another file
df[/p/w/n/c] <file>
p: print different bytes
w: wide print (rows are 16 bytes)
n: do not use colors
c: print the differences as a bhex command script that
turns the current file into the other one, instead of
the report. Replay it with '-s':
bhex -2nc "df/c new.bin" old.bin > patch.bhx
bhex -2nwbs old.bin < patch.bhx
file: path to the file to compare
[0x0000000] $ ex?
export: write <size> bytes of the file starting from current offset to <ofile>
ex <ofile> [<size>]
ofile: output file
size: number of bytes to export (if omitted, all the remaining bytes)
[0x0000000] $ im?
import: import the content of <file> at current offset
im[/{ovw,i}] <file> [<size> <offset>]
i: insert in current file (default)
ovw: overwrite current file
file: input file
size: number of bytes to import (if omitted or zero, import the whole file)
offset: starting offset of the imported file (if omitted, import from offset 0)
[0x0000000] $ w?
write: write data at current offset
w[/{s,x,b,w,d,q}/{le,be}/u/i/a] <data>
s: string input (default)
x: hex input
b: byte
w: word
d: dword
q: qword
le: little-endian (default)
be: big-endian
u: unsigned
i: insert
a: advance the offset past the data written
data: the data to write. The format depends on the type of
write. Here are some examples:
w/x "00 01 02 03"
w/s "a string"
w/q/be 0x1234
[0x0000000] $ tr?
transform: transform the bytes at current offset in place
tr[/{xor,and,or,add,sub,not,rol,ror,rev,swap}/{x,s}] [<arg>] [<size>]
xor: xor every byte with the key (default)
and: bitwise and with the key
or: bitwise or with the key
add: add the key to every byte
sub: subtract the key from every byte
not: complement every byte (takes no arg)
rol: rotate every byte left by <arg> bits
ror: rotate every byte right by <arg> bits
rev: reverse the order of the bytes (takes no arg)
swap: reverse the byte order of every group of <arg> bytes
x: the key is a hex string (default)
s: the key is a string
arg: the key of xor/and/or/add/sub, the number of bits of
rol/ror, or the group size of swap (2, 4 or 8). The key
is repeated over the region, the others are plain numbers
size: number of bytes to transform (if omitted, all the
remaining bytes)
The result is an ordinary pending write: 'c/l' lists it, 'u'
undoes it and nothing reaches the file until 'c'
Here are some examples:
tr/xor "de ad be ef"
tr/xor/s mykey 0x100
tr/not
tr/swap 4 0x40
[0x0000000] $ d?
delete: delete bytes at current offset (all remaining bytes if the argument is omitted)
d [<nbytes>]
[0x0000000] $ u?
undo: undo the last write
u[/a]
a: undo all
[0x0000000] $ c?
commit: commit all writes to file
c[/l]
l: list uncommitted changes