Current implementation - syscall abstractions
I refactored my approach to test my malloc by just mocking syscalls under testing conditions to get rid of dynamic linking altogether.
This page explains why macOS (Mach-O) does not behave like Linux (ELF) with respect to allocator interposition, and how dyld interposition can be used to deliberately override functions such as malloc across the entire process. This is a part of dynamic linking investigation series.
Understanding this difference is critical for making allocator behavior deterministic across platforms.
Unlike ELF, Mach-O uses a two-level namespace model by default.
In practice, this means:
As a result:
malloc defined in the main executable does not automatically override malloc used internally by system libraries.libSystem, libc++, and other dylibs continue to call their own allocator implementation unless explicitly told otherwise.This is why the naïve strategy—statically linking allocator symbols into the executable—works on Linux but fails on macOS.
On macOS, without special handling:
libmallocThis situation is fundamentally unsafe:
Therefore, macOS must be explicitly coerced into routing all allocator calls through a single implementation.
dyld (the Dynamic Link Editor) is responsible for:
Importantly:
dyld has the final say over which function implementation is actually called at runtime.
Apple exposes this control via interposition.
Interposition allows one function implementation to replace another across all call sites within the process.
Conceptually:
Before :
------------ ------------
|Any caller | --- dyld --- | function F |
------------ ------------
After :
------------ ------------
|Any caller | | function F |
----------- \ ------------
\ |
\ |
\ |
\ ---------------
dyld --- | interposed F` |
---------------
The interposed function becomes the gatekeeper.
Interposition is achieved by placing replacement mappings into a special Mach-O section:
(__DATA, __interpose)
Each entry is a tuple:
{ replacement_function, original_function }
At load time, dyld scans this section and rewrites bindings so that:
Crucially:
This property allows the interposer to fully control access to the original function.
Interposition logic is typically packaged as a small dylib and injected at runtime via:
DYLD_INSERT_LIBRARIES=path/to/interposed.dylib
When set:
__interpose section is registeredThis mechanism is the macOS equivalent of LD_PRELOAD on Linux, though the semantics differ.
Mach-O technically supports a flat namespace mode where symbols are resolved globally, similar to ELF.
However:
Given the allocator’s need for reliability and determinism, relying on flat namespaces is not acceptable…
libmalloc (malloc/malloc.h) to ensure ABI compatibilityNote: We could include Apple’s libmalloc with malloc/malloc.h because interposing compilation does not include -Iinclude thus binds to the original header.
malloc git:(interpose) ✗ ls "$(xcrun --show-sdk-path)"/usr/include/malloc
_malloc_type.h _malloc.h _platform.h _ptrcheck.h malloc.h
On macOS:
Using dyld interposition allows us to:
The next page, Cross-Platform Implications, ties these mechanics back to concrete design decisions in the malloc project.