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

FeatureValue
Full nameShared Object (ELF shared library)
File extension.so
MIME typeapplication/x-sharedlib
Format typeELF (Executable and Linkable Format) shared library — compiled machine code
DeveloperUnix/Linux ecosystem; ELF defined by the System V ABI / TIS
IntroducedELF: 1990s (System V Release 4)
Standard / specELF (System V ABI); Tool Interface Standard
Open standardYes
Byte orderEither — declared by EI_DATA (byte 5): little- or big-endian
Class32-bit or 64-bit — declared by EI_CLASS (byte 4)
Magic number7F 45 4C 46 (\x7FELF) at offset 0
ELF typeET_DYN (0x0003) at e_type, offset 16
Loaded byDynamic linker/loader ld.so / ld-linux.so, or dlopen()
Windows equivalentDLL (PE); macOS equivalent: .dylib (Mach-O)
CategorySystem Files
Related extensions.dll, .dylib, .a, .o, .elf, .ko
Specification URLhttps://refspecs.linuxfoundation.org/elf/elf.pdf
File signature (magic bytes)
7F 45 4C 46

Offset 0. The four bytes 0x7F then ASCII E L F form the ELF magic number, shared by every ELF file (shared libraries, executables and object files alike). The next bytes of e_ident distinguish them: EI_CLASS at offset 4 is 1 for 32-bit or 2 for 64-bit; EI_DATA at offset 5 is 1 for little-endian or 2 for big-endian. A .so is identified as a shared object by its e_type field at offset 16, which is ET_DYN (0x0003), not by a distinct magic number.

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 tagWhat it holds
DT_SONAMEThe library’s canonical name, e.g. libssl.so.3
DT_NEEDEDEach shared object this one depends on (one entry per dependency)
DT_SYMTAB / DT_STRTABDynamic symbol table and its string table
DT_RELA / DT_JMPRELRelocation entries to patch at load time
DT_INIT / DT_FINIInitialiser 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.solibssl.so.3libssl.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