wrong ELF class: ELFCLASS32 or ELFCLASS64 — what it means
$ python3 -c "import ctypes; ctypes.CDLL('./libsensor.so')"
OSError: ./libsensor.so: wrong ELF class: ELFCLASS32
or, at link time:
$ gcc main.o sensor.o -o app
/usr/bin/ld: sensor.o: relocations in generic ELF (EM: 3)
/usr/bin/ld: sensor.o: error adding symbols: file in wrong format
Both are the same underlying fact: an ELF file’s class — 32-bit or 64-bit — is fixed in the file header, and it cannot be mixed within one address space.
Class is not architecture
These are two independent header fields, and confusing them sends people down the wrong path:
| Field | Values | Answers |
|---|---|---|
EI_CLASS | ELFCLASS32, ELFCLASS64 | how wide are pointers and offsets in this file |
e_machine | EM_X86_64, EM_AARCH64, EM_386, … | which instruction set the code is for |
An EM_X86_64 binary is always ELFCLASS64. An EM_386 binary is always ELFCLASS32. But
EM_MIPS and EM_PPC exist in both classes, which is why “32-bit MIPS” and “64-bit MIPS” are
different targets with incompatible ABIs. Checking file output handles both fields at once:
$ file libsensor.so
libsensor.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked
ELF 32-bit is the class; Intel 80386 is the machine.
Why this is a different failure from Exec format error
Both end with the same practical result — the code does not run — but they fail at different layers:
Exec format erroris the kernel rejecting an executable. Nothing was loaded, no loader ran.wrong ELF classis reported after something already accepted the file. At link time it is the linker refusing to combine objects of different widths. At run time it isdlopen()refusing to map a library whose class differs from the calling process.
That second case is the one that confuses people, because the error arrives from application code —
Python’s ctypes, a Java JNI loader, a plugin system — not from the shell:
$ python3 -c "import platform; print(platform.architecture())"
('64bit', 'ELF')
$ file ./vendor/lib/liblegacy.so
./vendor/lib/liblegacy.so: ELF 32-bit LSB shared object, Intel 80386
A 64-bit Python process cannot dlopen() a 32-bit library. There is no flag that changes this; the
pointer sizes in the two pieces of code disagree, so the call ABI cannot be reconciled at all.
The four cases, and which fix applies
Mixing object files of different classes
$ gcc -c -m32 lib.c -o lib.o
$ gcc -c main.c -o main.o
$ gcc main.o lib.o -o app
/usr/bin/ld: lib.o: error adding symbols: file in wrong format
Compile the whole program with the same -m32/-m64 flag. Partial application does not work at the
object level. If you need 32-bit builds on a 64-bit host, install multilib:
sudo apt install gcc-multilib g++-multilib
Loading a 64-bit library into a 32-bit process
The mirror image of the ctypes example above, common when a Python or Node installation is 32-bit
(the default on some Windows-derived distributions and on some embedded boards) while the library
vendor ships only x86-64 builds. Check the interpreter’s word size, not the CPU’s — a 64-bit CPU
runs 32-bit userlands happily.
A prebuilt wheel or binary for the wrong target
$ pip install somepkg
$ python3 -c "import somepkg"
ImportError: .../somepkg/_core.cpython-311-x86_64-linux-gnu.so: wrong ELF class: ELFCLASS64
The filename is the giveaway: x86_64 in a wheel tag on an aarch64 host, or an i686 tag on
x86_64. pip normally selects the right wheel; it does not when the tag was mis-declared, when a
wheel was built locally with the wrong flags, or when the platform tag was overridden with
--platform. Verify with pip debug --verbose | grep -A20 'Compatible tags'.
NOBITS and linker scripts — a false alarm
Occasionally wrong ELF class appears for a linker script rather than an object:
/usr/bin/ld: /usr/lib/libc.so: invalid ELF header
On glibc systems /usr/lib/libc.so is a text linker script, not an ELF file. This is expected; if you
are passing it explicitly to a custom link command, use -lc and let the driver resolve it instead.
Diagnosing quickly
$ readelf -h libsensor.so | grep -E 'Class|Machine|Type'
Class: ELF32
Type: DYN (Shared object file)
Machine: Intel 80386
Then compare against the consumer:
$ uname -m # kernel architecture
$ getconf LONG_BIT # userland width: 32 or 64
$ python3 -c "import platform; print(platform.architecture()[0])"
uname -m tells you the CPU, not the userland. That gap is where most of these incidents live: a
64-bit kernel with a 32-bit userland answers x86_64 to uname and 32 to getconf LONG_BIT.
Should you fix it or work around it?
The honest answer is usually neither — use the correct build. Multilib exists and works, but it doubles the dependency surface: a 32-bit binary needs 32-bit versions of every library it links, and on modern distributions those packages are increasingly unavailable or unmaintained. If a vendor only ships a 32-bit library in 2026, that is a signal about the vendor.
When you have no choice, a container with the matching userland is cleaner than installing multilib across the host. It also makes the constraint explicit and reproducible.
The ELF Inspector reports class, machine and type for any binary without a shell, which is handy when the machine that has the problem is the one you cannot install binutils on. For the case where the classes match but the flat binary still will not execute, see Exec format error.
Every command in this guide was run against the distribution named in the output. If your output differs, the mechanism section explains why — and the ELF Inspector shows the same fields without needing a shell.