ELF, PE and Mach-O Viewer Online: Is Your Binary Stripped?
On this page
Short answer: the first four bytes of a native binary tell you its format. 7F 45 4C 46 is ELF (Linux, Android, most firmware), 4D 5A (“MZ”) followed by a PE\0\0 header is PE (Windows EXE, DLL, SYS), and CF FA ED FE or CE FA ED FE is Mach-O (macOS, iOS). To see everything else, drop the file into this site’s native binary overview. It shows the architecture, file type, entry point and linked libraries, the sections with their entropy, the imports, exports and strings, and for ELF whether the binary is stripped. It runs in your browser and nothing is uploaded or executed.
Who this is for: developers who received a .so, .dll or a binary without an extension and want to know what it is before they ship, link or debug it. It is also for release engineers who need to confirm that a build is stripped (or that it still carries symbols for crash reports).
What you will answer:
- Is this ELF, PE or Mach-O, and for which CPU?
- What does it link against, and what does it export?
- Which sections does it have, and does any of them look compressed?
- Does it still carry a symbol table, or was it stripped?
- Which command-line tools give the same answers?
Samples used: one small C program of my own, built on macOS for three platforms. ELF and PE come from the Android NDK r29 toolchain (clang and lld-link), Mach-O from Xcode’s clang:
#include <stdio.h>
#define HELLO_VERSION "hello 1.2.0"
int add(int a, int b) { return a + b; }
int main(void) { printf("%s: %d\n", HELLO_VERSION, add(2, 3)); return 0; }
B=$ANDROID_HOME/ndk/29.0.14206865/toolchains/llvm/prebuilt/darwin-x86_64/bin
$B/aarch64-linux-android29-clang -O2 -o hello-arm64-android hello.c
$B/llvm-strip -o hello-arm64-android.stripped hello-arm64-android
clang -O2 -arch arm64 -mmacosx-version-min=12.0 -o arm64 hello.c
clang -O2 -arch x86_64 -mmacosx-version-min=10.15 -o x86_64 hello.c
lipo -create arm64 x86_64 -output hello-macos-universal
The Windows build (hello-x64.exe) is a similar program built without the C runtime and linked with lld-link against kernel32.dll only. Use the same steps on your own builds or on open-source binaries. If you go on to run or attach to software you did not build, make sure you are authorized to, and read REA’s DISCLAIMER.md.
Step 1: Tell the three formats apart
All three formats start with a fixed “magic” value, so you can identify a file even without an extension:
| First bytes | Format | Notes |
|---|---|---|
7F 45 4C 46 (\x7fELF) |
ELF | Byte 4 is the class (1 = 32-bit, 2 = 64-bit), byte 5 the byte order |
4D 5A (MZ) |
PE | The DOS header points (at offset 0x3C) to the real PE\0\0 header |
CF FA ED FE / CE FA ED FE |
Mach-O | 64-bit / 32-bit, little-endian (the byte-swapped forms are big-endian) |
CA FE BA BE |
Universal Mach-O or Java class | The same magic is used by both |
The last row catches people out. A universal (“fat”) Mach-O stores the number of architectures right after the magic, while a Java .class file stores its version there. The viewer applies the same rule as file(1): more than 30 “architectures” means it is not a universal binary. I compiled a one-line Hello.java with javac (its header is cafe babe 0000 003d), dropped the class file in, and the tool refused it with “Not an ELF, PE or Mach-O file.” and the technical detail “cafebabe magic but not a universal binary (Java class file?)”.
Step 2: Read the identity block
Open the file in the native binary overview. The Identity block comes first. These are the values it reported for three of my samples:
| Field | hello-arm64-android |
hello-x64.exe |
hello-macos-universal (slice 1) |
|---|---|---|---|
| Format | ELF64 | PE32+ | Mach-O 64-bit |
| Architecture | AArch64 | x64 (0x8664) | arm64 |
| Bitness | 64-bit | 64-bit | 64-bit |
| File type | ET_DYN | EXE | MH_EXECUTE |
| Role | executable | executable | executable |
| Entry point | 0x4650 | RVA 0x1010 (VA 0x140001010) | entryoff 0x468 (VA 0x100000468) |
| Interpreter | /system/bin/linker64 | — | /usr/lib/dyld |
| Format-specific | OS/ABI UNIX - System V | Subsystem WINDOWS_CUI, linker 14.0 | macOS, minimum OS 12.0, SDK 27.0, UUID |
A few things are worth knowing when you read this block:
- ET_DYN does not always mean “shared library”. PIE executables are ET_DYN too. The tool calls the file an executable when it has a
PT_INTERPinterpreter or theDF_1_PIEflag, which is how my Android binary got the role executable. - The interpreter tells you the platform.
/system/bin/linker64is Android’s 64-bit dynamic linker, while a desktop Linux binary would name something likeld-linux-x86-64.so.2. - Universal binaries have slices. For
hello-macos-universalthe tool lists two slices,x86_64(12,592 bytes at offset 4096) andarm64(33,464 bytes at offset 32768), and you pick the one to analyze.lipo -detailed_infoand REA report the same offsets (see Step 6).
When a PE file carries a CLI header, the identity block also says “This is a .NET assembly” and links to the .NET metadata and IL viewer. For managed code, the PE headers alone do not tell the whole story; Check a .NET DLL’s Target Framework covers the managed side.
Step 3: Sections, segments and entropy
The Sections table lists every section with its address, file offset, size, permissions and Shannon entropy in bits per byte. For the AArch64 build, .text (the code) was 308 bytes at 5.53 bits per byte, .rodata 20 bytes at 3.82 and .dynstr 73 bytes at 4.21. On Windows, hello-x64.exe has just three sections: .text, .rdata and .pdata.
Entropy is a rough measure of how random the bytes look. In my samples, code sections landed between about 5.2 and 6.8 bits per byte, while string and symbol tables sat lower. The tool flags a section as high entropy when it is at least 1 KiB and above 7.2 bits per byte, which usually means compressed or encrypted data: embedded archives, images, compressed resources or packed code. None of my samples came close (the largest, Newtonsoft.Json’s .text, was 5.93).
A high-entropy section is a fact about the bytes, not a verdict. Installers, games and apps with bundled assets often have them for ordinary reasons. Use it to decide where to look next.
ELF files also have segments (program headers): the PT_LOAD ranges the loader maps, plus PT_INTERP, PT_DYNAMIC, PT_GNU_STACK and PT_GNU_RELRO. Sections are for linkers and debuggers, segments are what the loader actually uses. That is why a binary can run without any section headers at all.
Step 4: Libraries, imports, exports and strings
Linked libraries answers “what does this need at runtime?”:
- ELF: the
DT_NEEDEDentries. My Android build needslibdl.soandlibc.so. - PE: the import directory.
hello-x64.exeimports fromkernel32.dllonly. - Mach-O:
LC_LOAD_DYLIBand friends. The macOS build loads/usr/lib/libSystem.B.dylib.
Imports and exports lists symbol names. For ELF the tool reads .dynsym, the dynamic symbol table the loader uses, and only falls back to .symtab when there is no .dynsym (object files and static executables). So my Android executable shows four imports (printf, __libc_init, __cxa_atexit, __register_atfork) and no exports, even before stripping: main and add live only in .symtab. A PE DLL lists its export table; my add-x64.dll exports exactly one function, add.
Strings finds ASCII and UTF-16LE runs of at least six characters (adjustable) and names the section each one sits in. The version string hello 1.2.0 showed up in .rodata (ELF), .rdata (PE) and __TEXT,__cstring (Mach-O). That is the quickest way to confirm which version of your library ended up in a release package. The full list can be downloaded as a .txt file.
Step 5: Is the binary stripped?
“Stripped” means the full symbol table, which debuggers and profilers use to name functions, was removed. The dynamic symbols needed for linking stay. How you check depends on the format.
ELF: look for .symtab
The Security features table has a row Symbol table stripped. It reads no when a .symtab section exists and yes when there is no .symtab or no section headers at all. On my sample, before and after llvm-strip:
hello-arm64-android |
hello-arm64-android.stripped |
|
|---|---|---|
| Symbol table stripped | no (.symtab 38 entries) |
yes (no .symtab) |
| Sections | 26 | 23 (.comment, .symtab, .strtab removed) |
| File size | 6,088 bytes | 4,248 bytes |
| Imports | 4 | 4 |
The import count does not change, because .dynsym survives stripping. Standard tools agree:
$ file hello-arm64-android hello-arm64-android.stripped
hello-arm64-android: ELF 64-bit LSB pie executable, ARM aarch64, ... not stripped
hello-arm64-android.stripped: ELF 64-bit LSB pie executable, ARM aarch64, ... stripped
$ llvm-nm hello-arm64-android.stripped
hello-arm64-android.stripped: no symbols
$ llvm-nm -D hello-arm64-android.stripped
U __cxa_atexit@LIBC
U __libc_init@LIBC
U __register_atfork@LIBC
U printf@LIBC
On Android, release builds are usually stripped by the Gradle build. Keep the unstripped .so files (or their build IDs) so you can symbolize native crash reports later.
Mach-O and PE: no single flag
The tool has no “stripped” row for Mach-O or PE, because neither format has one obvious marker. You can still judge it from the symbol counts:
- Mach-O: the tool reads the
LC_SYMTABnlist table. Beforestrip, my arm64 executable showed 3 exports (__mh_execute_header,_add,_main) and 1 import (_printf). Afterstrip, only__mh_execute_headerand_printfwere left, which matchesnm.stripalso re-applied an ad hoc signature, so the Signing identifier field changed to the new file name. - PE: MSVC-style toolchains (including
lld-link) keep debug symbols in a separate.pdbfile, so the image itself carries only the import and export tables, which stripping does not touch. The tool does not read the debug directory, so it cannot tell you whether a PDB was produced. MinGW builds are different: they can carry DWARF sections such as.debug_infoin the image, and those appear in the section list until you strip them.
Step 6: The same from the command line, and with REA
Every value above can be cross-checked with standard tools. I ran these on the same files:
$ llvm-readelf -h hello-arm64-android | grep -E "Class|Type|Machine"
Class: ELF64
Type: DYN (Shared object file)
Machine: AArch64
$ llvm-readobj --file-headers hello-x64.exe | grep -E "Machine|Magic"
Machine: IMAGE_FILE_MACHINE_AMD64 (0x8664)
Magic: 0x20B
$ otool -hv hello-macos-arm64
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 ARM64 ALL 0x00 EXECUTE 17 1056 NOUNDEFS DYLDLINK TWOLEVEL PIE
$ lipo -info hello-macos-universal
Architectures in the fat file: hello-macos-universal are: x86_64 arm64
readelf, objdump, nm and strip come with binutils on Linux, and the llvm-* equivalents ship with the Android NDK and with LLVM. otool, lipo and codesign need the Xcode command line tools on macOS.
REA, the open-source project by morluto, adds structured JSON for agents and scripts. In 6.4.0, rea list-architectures <path> and rea inspect-macho <path> read Mach-O files, but they wrap Apple’s command-line tools (file, lipo, otool, nm, dyld_info and others) and so only run on macOS with the Xcode command line tools. On my universal binary, list-architectures returned both slices with the same offsets and sizes as the browser tool, plus their alignment:
[{"name":"x86_64","cpu_type":"CPU_TYPE_X86_64","cpu_subtype":"CPU_SUBTYPE_X86_ALL","file_offset":4096,"size":12592,"alignment":4096},
{"name":"arm64","cpu_type":"CPU_TYPE_ARM64","cpu_subtype":"CPU_SUBTYPE_ARM64_ALL","file_offset":32768,"size":33464,"alignment":16384}]
inspect-macho is upfront about stripping too. Its limitations line says “Imports and exports combine dyld_info and nm; stripped or toolchain-hidden symbols may be absent.”
For ELF and PE, REA 6.4.0 has no header viewer that matches the browser tool. inspect-binary-layout reports sections, symbols and mitigations, but its initial profile only covers ELF64 x86-64 files, only on Linux x64, with pwntools 4.15.0 that you install yourself. For reading the code itself, the next step is REA with Ghidra: Free Native Binary Analysis with REA and Ghidra walks through decompiling, string search and cross-references on a binary you build.
FAQ
Is my file uploaded anywhere? No. The binary overview parses the file in a Web Worker in your browser that has no network access. Nothing is run, disassembled or uploaded.
How large a file can I open? Up to 512 MiB. Very large tables are capped (for example, 4,096 sections and 100,000 symbols), and the report notes it when that happens.
Can it open .so files from an APK?
Yes. Unzip the APK and open any file from lib/<abi>/. For checking those libraries against Android’s 16 KB page size requirement, see Check an APK for Android’s 16 KB Page Size Requirement, or open the whole APK in the APK analyzer.
Does a “yes” in Security features mean the binary is safe? No. Those rows describe how the file was built (PIE, NX, RELRO, signatures present). Signatures are detected, never verified.
What about core dumps and object files? ELF core dumps (ET_CORE) and object files (ET_REL, MH_OBJECT) are recognized and labeled by role. They have no entry point or dynamic linking information to show.
Next steps
- Open your own build: native binary overview
- Decompile and trace a feature: Free Native Binary Analysis with REA and Ghidra
- Windows targets in depth: Reverse Engineer a Windows EXE or DLL with AI, Ghidra and REA
- Check whether a Windows build is 32-bit, 64-bit or ARM64: Check if an EXE or DLL Is 32-bit, 64-bit or ARM64
- Mach-O apps on macOS: Reverse Engineer a macOS App with REA