SO File Documentation
Summary
A file with the .so extension is an ELF shared object: a dynamically linked shared library on Linux and other Unix-like systems, containing compiled native machine code. It is the direct counterpart of a Windows DLL and a macOS .dylib. Programs load a .so at run time through the dynamic linker rather than double-clicking it. Its MIME type is application/x-sharedlib, and every ELF file begins with the 4-byte magic 7F 45 4C 46 (\x7FELF). A missing .so error is fixed by installing the package that provides it, not by downloading the file.
Technical details
| Feature | Value |
|---|---|
| Full name | Shared Object (ELF shared library) |
| File extension | .so |
| MIME type | application/x-sharedlib |
| Format type | ELF (Executable and Linkable Format) shared library — compiled machine code |
| Developer | Unix/Linux ecosystem; ELF defined by the System V ABI / TIS |
| Introduced | ELF: 1990s (System V Release 4) |
| Standard / spec | ELF (System V ABI); Tool Interface Standard |
| Open standard | Yes |
| Byte order | Either — declared by EI_DATA (byte 5): little- or big-endian |
| Class | 32-bit or 64-bit — declared by EI_CLASS (byte 4) |
| Magic number | 7F 45 4C 46 (\x7FELF) at offset 0 |
| ELF type | ET_DYN (0x0003) at e_type, offset 16 |
| Loaded by | Dynamic linker/loader ld.so / ld-linux.so, or dlopen() |
| Windows equivalent | DLL (PE); macOS equivalent: .dylib (Mach-O) |
| Category | System Files |
| Related extensions | .dll, .dylib, .a, .o, .elf, .ko |
| Specification URL | https://refspecs.linuxfoundation.org/elf/elf.pdf |
What is a .so file?
A .so file is a shared object: a dynamically linked shared library on Linux and other Unix-like systems (the BSDs, illumos, Android). It contains compiled native machine code stored in the Executable and Linkable Format (ELF), the standard binary format across Unix since System V Release 4 in the late 1980s. A shared library packages functions that many programs use, so each program links against one shared copy at run time instead of embedding its own. That saves memory and disk, and lets a single library update reach every program that depends on it. It is the direct counterpart of a Windows DLL and a macOS .dylib.
A .so is not meant to be opened or double-clicked, and it has no standalone entry point. It exports functions for other programs to call. The dynamic linker/loader (ld.so / ld-linux.so) maps it into a process either when a program starts or on demand through dlopen(). Understanding a .so means understanding the ELF structure below, because the .so designation comes from an ELF field, not from a separate file type.
ELF identification: e_ident, class and endianness
Every ELF file opens with a 16-byte identification array, e_ident, that a loader reads before anything else.
Offset Field Meaning
0 EI_MAG0..3 7F 45 4C 46 -- '\x7F','E','L','F'
4 EI_CLASS 1 = ELFCLASS32, 2 = ELFCLASS64
5 EI_DATA 1 = little-endian, 2 = big-endian
6 EI_VERSION 1 = current
7 EI_OSABI target ABI (0 = System V, 3 = Linux, ...)
8 EI_ABIVERSION
9..15 EI_PAD reserved, zero
EI_CLASS decides whether the rest of the file uses 32-bit or 64-bit widths for addresses and offsets, which changes the size and layout of every following structure. EI_DATA fixes the byte order for all multi-byte fields, so an ELF file is self-describing about endianness rather than assuming the host’s. Because the same 7F 45 4C 46 magic marks shared libraries, executables and unlinked object files alike, the magic alone does not tell you it is a library; that comes from the next structure.
The ELF header and e_type = ET_DYN
Immediately after e_ident, the ELF header proper continues. For a 64-bit file the fields are:
Offset Field Purpose
16 e_type object type: 1 REL, 2 EXEC, 3 DYN, 4 CORE
18 e_machine architecture: 0x3E x86-64, 0xB7 AArch64, ...
20 e_version 1
24 e_entry entry-point address (0 for a pure library)
32 e_phoff program-header table offset
40 e_shoff section-header table offset
... e_flags, e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx
The field that makes a file a shared object is e_type at offset 16, which holds ET_DYN (value 3). The same value covers both shared libraries and position-independent executables, which is why a modern PIE binary and a .so can share a type. e_machine names the CPU the code targets, so an AArch64 .so cannot load into an x86-64 process. e_entry is typically zero for a library, since it is not launched directly. e_phoff points at the program headers the loader needs, and e_shoff at the section headers the linker and tools use.
Program headers: what the loader maps
The program header table describes segments — contiguous regions the loader maps into memory. A shared object’s program headers include one or more PT_LOAD segments (the code and data to map), a PT_DYNAMIC segment (the dynamic-linking information, below), and often PT_GNU_RELRO to mark regions made read-only after relocation. Each PT_LOAD entry carries a file offset, a virtual address, a size in the file, a size in memory, and permission flags (read/write/execute). The loader mmaps each segment at the requested offset from wherever it places the library, which is possible because the code is position-independent. This is the run-time view of the file; it is deliberately separate from the section view that tools use.
The .dynamic section: SONAME, NEEDED and symbol resolution
The heart of a shared object is the .dynamic section (reached through PT_DYNAMIC), an array of tagged entries that tells ld.so everything it needs to wire the library into a process:
| Dynamic tag | What it holds |
|---|---|
DT_SONAME | The library’s canonical name, e.g. libssl.so.3 |
DT_NEEDED | Each shared object this one depends on (one entry per dependency) |
DT_SYMTAB / DT_STRTAB | Dynamic symbol table and its string table |
DT_RELA / DT_JMPREL | Relocation entries to patch at load time |
DT_INIT / DT_FINI | Initialiser and finaliser routines run on load/unload |
The DT_SONAME is the versioned name embedded inside the file, which is why a library’s filenames form a symlink chain: libssl.so → libssl.so.3 → libssl.so.3.0.2. A program records the SONAME it was built against, and the loader picks any file that presents a compatible SONAME, so old programs keep working across minor library updates. The exported and imported symbols live in .dynsym with names in .dynstr; the machine code is in .text, read-only constants in .rodata, and writable data in .data.
The PLT and GOT: run-time symbol resolution
Because a shared library’s calls to functions in other libraries cannot be resolved until load time, ELF uses two tables for indirection. The Global Offset Table (.got) holds the resolved addresses of external symbols; the Procedure Linkage Table (.plt) holds small stubs that call through the GOT. On the first call to an external function the PLT stub invokes the linker’s resolver, which looks up the symbol, writes its address into the GOT slot, and jumps to it; subsequent calls read the now-filled GOT slot directly. This “lazy binding” spreads resolution cost over the program’s run and is the mechanism that lets independently built libraries call each other without fixed addresses.
Inspecting a shared object
Because ELF is a documented format, standard tools read a .so without running it. readelf -h lib.so prints the ELF header (confirming ET_DYN, class and machine); readelf -d lib.so dumps the dynamic section, showing the SONAME and every DT_NEEDED dependency. nm -D lib.so lists the exported dynamic symbols. objdump -d lib.so disassembles the machine code. And ldd ./program or ldd lib.so resolves the whole dependency graph and reports which shared objects are found and which are missing — the first command to run when a program fails to start with a missing-library error. For reverse engineering, a disassembler such as Ghidra imports the .so for full analysis.
Security considerations: library hijacking and where .so lives
A .so is executable native code that runs inside whatever process loads it, so a malicious shared library can do anything the host program can do. Two concrete attack mechanics matter. The first is library hijacking: the loader searches a defined list of directories, and the environment variables LD_PRELOAD and LD_LIBRARY_PATH can force a rogue .so to load ahead of the genuine one, injecting attacker code into a legitimate program before main() even runs. A world-writable directory early in the search path is enough to plant such a library. The second is the “fix my error” trap: downloading an individual .so from a random website to satisfy a “cannot open shared object file” message may install malicious or ABI-incompatible code system-wide.
The correct response to a missing library is to install the package that provides it through the distribution’s signed repositories (apt install, dnf install, pacman -S), then refresh the loader cache with ldconfig. Deleting the shared objects in /lib or /usr/lib is dangerous: programs and the operating system depend on them, and removing libc.so or similar can leave the system unbootable. Libraries should only be removed by uninstalling their package.
References
- Linux Foundation — ELF (Executable and Linkable Format) specification
- Linux man-pages — ld.so(8) dynamic linker/loader
- Linux man-pages — dlopen(3), loading shared objects at run time
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.