Binary & Executable Formats

A linked executable isn't a formless blob of bytes — it follows a well-defined binary file format the operating system's loader knows how to parse, organizing code, data, and metadata into clearly labeled sections.

An executable file format is a standardized structure for organizing a compiled program's machine code, data, and metadata (like which functions/symbols it defines, and what it still needs from elsewhere) so the operating system knows exactly how to load and run it. ELF, PE, and Mach-O are the three most widely used formats.

The operating system's loader (covered in the next section) needs a predictable, well-defined structure to know where the actual executable code is, where initial data lives, what memory permissions each part needs, and where execution should begin — a standardized binary format is what makes that possible.

ELF (Executable and Linkable Format) is used by Linux and most other Unix-like systems; PE (Portable Executable) is used by Windows; Mach-O is used by macOS and iOS — the same compiled C program, targeting the same CPU architecture, still needs to be packaged in a different binary format for each of these operating systems.

Think of a shipping manifest attached to a cargo container. The manifest doesn't just say "here's some cargo" — it specifies exactly what's in each labeled compartment, which compartments are fragile (read-only), which are refrigerated, and where the unloading crew should start. An executable file format is exactly this kind of manifest for a compiled program, telling the OS's loader precisely what's inside and how to handle each part.

Every one of these sections is given specific memory permissions when loaded — .text is typically executable but not writable, .rodata is readable but not writable, and .data/.bss are both readable and writable — a security-relevant separation that prevents, for example, a program's own instructions from being accidentally (or maliciously) overwritten at runtime.

A section (like .text, .data, .bss, .rodata) is how an executable file organizes its content into logically and functionally distinct pieces; a segment is a related but distinct concept the loader actually uses at load time, typically grouping one or more sections that share the same required memory permissions (readable/writable/executable) into one contiguous mapped region.

The .bss section is unusual in that it takes up (almost) no actual space in the executable file on disk — since it only holds uninitialized data, the file just records how large that section should be, and the loader is responsible for allocating and zero-filling that much memory at load time, rather than the file needing to store a large block of zero bytes itself.

An executable's symbol table lists the functions and variables it defines and/or still needs from elsewhere, along with debug symbols (if the file was compiled with debug information) that let a debugger map raw addresses in the running program back to meaningful source-level names, files, and line numbers.

Despite fulfilling the same fundamental role, ELF, PE, and Mach-O differ meaningfully in their exact structure, terminology, and specific capabilities — a tool built to parse or generate one of these formats generally cannot handle the others without dedicated, format-specific support.

Assuming a compiled Linux program's executable could run unmodified on Windows just by copying the file over — even with an identical CPU architecture, the operating system's loader expects the specific binary format (ELF vs. PE) it knows how to parse, and a mismatched format simply won't be recognized as a valid executable at all.

Confusing a section (a file-level, purely structural organizational unit) with a segment (a load-time, memory-permission-driven grouping) — related concepts, but genuinely serving different purposes at different stages.

Assuming an executable's file size on disk directly reflects how much memory it will use once running — the .bss section is a clear counterexample, taking essentially no file space while still requiring real memory once loaded.

Using a tool like readelf or objdump (on Linux) to inspect a real compiled program's actual sections, symbol table, and headers, turning the abstract format description into something concretely visible.

Stripping debug symbols from a release binary (removing the extra symbol/debug information used only for debugging) specifically to reduce the shipped executable's file size, once active debugging of that particular build is no longer needed.

Recognizing, when cross-compiling software for a different target operating system, that the correct binary format for that target OS is produced automatically by using the correct toolchain — this isn't something a programmer typically manages by hand.

ELF's structural overview An ELF file begins with a fixed-format ELF header (identifying it as a valid ELF file, its target architecture, and its entry point address), followed by a program header table (describing segments, used by the loader) and a section header table (describing sections, used mainly by the linker and debugging tools) — the same underlying file can be viewed through either lens depending on whether the reader cares about load-time segments or link-time/debug-time sections.

Shared libraries as a special case of the same format ELF (and PE, and Mach-O) uses essentially the same overall file format for both ordinary executables and shared libraries, distinguished mainly by a type field in the header and by additional metadata (like a dynamic symbol table and relocation information specific to runtime linking) that a shared library needs but a simple static executable typically doesn't.

Why format differences matter for cross-platform toolchains A cross-compiler targeting a different operating system than the one it runs on must not only generate the correct machine code for the target CPU architecture, but also correctly emit the target OS's specific executable format — which is precisely why a full cross-compilation toolchain (covered further in the future Embedded Programming Toolchain module) bundles a matched compiler, assembler, and linker specifically configured for both the target CPU and the target OS's binary format together, rather than these being freely interchangeable independent pieces.

Name the three major executable formats and the operating systems that use them.

ELF (Executable and Linkable Format) on Linux and most Unix-like systems, PE (Portable Executable) on Windows, and Mach-O on macOS and iOS.

What's the difference between a section and a segment in an executable file?

A section is a file-level organizational unit (like .text or .data) used mainly by the linker and debugging tools. A segment is a load-time grouping, typically of one or more sections sharing the same required memory permissions, that the OS loader actually maps into memory as one contiguous region.

Why does the .bss section take up very little space in the file on disk?

Because it holds only uninitialized data — the file just records how large the section needs to be, and the loader allocates and zero-fills that much memory at load time, rather than the file storing that much actual (zero-valued) data itself.

Why can't a Linux ELF executable run directly on Windows, even for the same CPU architecture?

Because Windows's loader expects the PE format, not ELF — the two operating systems' loaders are built to parse their own specific binary format's headers and structure, and don't recognize the other's format as a valid executable at all, regardless of whether the underlying machine code would otherwise be compatible with the CPU.

.text     -- the actual machine instructions (the program's code)
.rodata   -- read-only data (string literals, const globals)
.data     -- initialized global/static variables
.bss      -- uninitialized global/static variables (space reserved, zero-filled at load)