Understanding Xcode Link Map Files and Mach‑O Layout
Enable Link Map output in Xcode by setting Target → Build Settings → Write Link Map File to YES. The destination path can be customized; by default it expands to:
$(TARGET_TEMP_DIR)/$(PRODUCT_NAME)-LinkMap-$(CURRENT_VARIANT)-$(CURRENT_ARCH).txt
Example (simulator, arm64):
/Users/dev/Library/Developer/Xcode/DerivedData/LinkMapSample-a1b2c3d4/Build/Intermediates.noindex/LinkMapSample.build/Release-iphonesimulator/LinkMapSample.build/LinkMapSample-LinkMap-normal-arm64.txt
Link Map files describe how the final Mach-O executable is composed: the contributing object files and libraries, how code/data are laid out, and per-symbol placement/size. They are structured in several labeled sections.
Path header
# Path: /Users/dev/Library/Developer/Xcode/DerivedData/LinkMapSample-a1b2c3d4/Build/Products/Release-iphonesimulator/LinkMapSample.app/LinkMapSample
The path of the generated Mach-O binary.
Arch header
# Arch: arm64
The target architecture for the linked image.
Object files table
# Object files:
[ 0] linker synthesized
[ 1] /Users/dev/Library/.../LinkMapSample.app-Simulated.xcent
[ 2] /Users/dev/Library/.../Objects-normal/arm64/HomeViewController.o
[ 3] /Users/dev/Library/.../Objects-normal/arm64/main.o
[ 4] /Users/dev/Library/.../Objects-normal/arm64/ApplicationDelegate.o
[ 5] /Applications/Xcode.app/.../System/Library/Frameworks/Foundation.framework/Foundation.tbd
[ 6] /Applications/Xcode.app/.../usr/lib/libobjc.tbd
[ 7] /Applications/Xcode.app/.../System/Library/Frameworks/UIKit.framework/UIKit.tbd
Every contributing object or text-based stub (tbd) is assigned an index in brackets. These indices are referenced in the symbol table for size attribusion.
Sections and Mach-O context
# Sections:
# Address Size Segment Section
0x1000036B0 0x00000420 __TEXT __text
0x100003AD0 0x0000002C __TEXT __stubs
0x100003AFC 0x00000060 __TEXT __stub_helper
0x100003B5C 0x00000990 __TEXT __objc_methname
0x1000044EC 0x00000044 __TEXT __objc_classname
0x100004530 0x00000820 __TEXT __objc_methtype
0x100004D50 0x00000085 __TEXT __cstring
0x100004DD8 0x00000170 __TEXT __entitlements
0x100004F48 0x00000058 __TEXT __unwind_info
0x100005000 0x00000018 __DATA __nl_symbol_ptr
0x100005018 0x00000018 __DATA __got
0x100005030 0x00000040 __DATA __la_symbol_ptr
0x100005070 0x00000010 __DATA __objc_classlist
0x100005080 0x00000010 __DATA __objc_protolist
0x100005090 0x00000008 __DATA __objc_imageinfo
0x100005098 0x00000C10 __DATA __objc_const
0x100005CA8 0x00000018 __DATA __objc_selrefs
0x100005CC0 0x00000010 __DATA __objc_classrefs
0x100005CD0 0x00000008 __DATA __objc_superrefs
0x100005CD8 0x00000008 __DATA __objc_ivar
0x100005CE0 0x000000B8 __DATA __objc_data
0x100005D98 0x000000D0 __DATA __data
The binary is a Mach-O image. Verify with the file tool:
file LinkMapSample
Possible output:
LinkMapSample: Mach-O 64-bit executable arm64
Mach-O organizes virtual memory into segments, commonly:
- __TEXT: read-only, executable code and immutable metadata
- __DATA: writable data such as globals and Objective‑C runtime blobs
- __LINKEDIT: loader metadata (e.g., symbol/string tables), read-only
Each segment is subdivided into sections. For example, __objc_methname stores Objective‑C selector strings. The columns above are the start address, byte size (hex), segment name, and section name.
Symbols
# Address Size File Name
0x1000036B0 0x00000044 [ 2] -[HomeViewController viewDidLoad]
0x1000036F4 0x00000098 [ 3] _main
0x10000378C 0x00000084 [ 4] -[ApplicationDelegate application:didFinishLaunchingWithOptions:]
0x100003810 0x00000040 [ 4] -[ApplicationDelegate applicationWillResignActive:]
0x100003850 0x00000040 [ 4] -[ApplicationDelegate applicationDidEnterBackground:]
0x100003890 0x00000040 [ 4] -[ApplicationDelegate applicationWillEnterForeground:]
0x1000038D0 0x00000040 [ 4] -[ApplicationDelegate applicationDidBecomeActive:]
0x100003910 0x00000040 [ 4] -[ApplicationDelegate applicationWillTerminate:]
0x100003950 0x00000020 [ 4] -[ApplicationDelegate window]
0x100003970 0x00000040 [ 4] -[ApplicationDelegate setWindow:]
0x1000039B0 0x00000030 [ 4] -[ApplicationDelegate .cxx_destruct]
0x100003AD0 0x00000006 [ 5] _NSStringFromClass
0x100003AD6 0x00000006 [ 7] _UIApplicationMain
0x100003ADC 0x00000006 [ 6] _objc_autoreleasePoolPop
0x100003AE2 0x00000006 [ 6] _objc_autoreleasePoolPush
0x100003AE8 0x00000006 [ 6] _objc_msgSendSuper2
0x100003AEE 0x00000006 [ 6] _objc_retainAutoreleasedReturnValue
0x100003AF4 0x00000006 [ 6] _objc_storeStrong
0x100003AFC 0x00000010 [ 0] helper helper
0x100003B0C 0x0000000A [ 5] _NSStringFromClass
0x100003B16 0x0000000A [ 6] _objc_autoreleasePoolPop
0x100003B20 0x0000000A [ 6] _objc_autoreleasePoolPush
0x100003B2A 0x0000000A [ 6] _objc_msgSendSuper2
0x100003B34 0x0000000A [ 6] _objc_retainAutoreleasedReturnValue
0x100003B3E 0x0000000A [ 6] _objc_storeStrong
0x100003B48 0x0000000A [ 7] _UIApplicationMain
0x100003B5C 0x0000000C [ 2] literal string: viewDidLoad
...
Interpretation:
- Address: symbol start (hex virtual address)
- Size: byte length (hex)
- File: object index as defined in the object file table
- Name: symbol or string literal descriptor
To attribuet size to a given .o or framework stub, sum the sizes of symbols whose File column equals that object’s index. The ranges can also be grouped by section: for example, any text symbols between the __text start and the next section’s start belong to the text section.
Dead stripped symbols
# Dead Stripped Symbols:
# Size File Name
<<dead>> 0x00000018 [ 2] CIE
<<dead>> 0x00000018 [ 3] CIE
<<dead>> 0x00000006 [ 4] literal string: class
<<dead>> 0x00000008 [ 4] literal string: v16@0:8
...
Entries the linker removed during dead‑code elimination. These do not contribute to the final image size but can be informative when reviewing what was discarded.
Minimal Ruby parser to summarize per-file sizes
#!/usr/bin/env ruby
# frozen_string_literal: true
abort('usage: ruby linkmap_size.rb <path-to-LinkMap.txt>') if ARGV.empty?
path = ARGV.first
raise "File not found: #{path}" unless File.file?(path)
section = nil
index_to_name = {}
size_by_index = Hash.new(0)
File.foreach(path) do |line|
line.rstrip!
case line
when /^# Object files:/
section = :objects
next
when /^# Sections:/
section = :sections
next
when /^# Address\s+Size\s+File\s+Name/
section = :symbols
next
when /^# Dead Stripped Symbols:/
section = :dead
next
end
if section == :objects
# [ 12] /path/to/Thing.o
if line =~ /\[\s*(\d+)\]\s+(.*)/
idx = Integer(Regexp.last_match(1))
index_to_name[idx] = Regexp.last_match(2)
end
elsif section == :symbols
# 0xADDR 0xSIZE [ N] name
if line =~ /^0x([0-9a-fA-F]+)\s+0x([0-9a-fA-F]+)\s+\[\s*(\d+)\]\s+/
size_hex = Regexp.last_match(2)
idx = Integer(Regexp.last_match(3))
size_by_index[idx] += size_hex.to_i(16)
end
end
end
# Convert to sorted list and print
fmt = ->(b){
if b >= 1024 * 1024
format('%.2fMB', b.to_f / (1024 * 1024))
elsif b >= 1024
format('%.2fKB', b.to_f / 1024)
else
format('%dB', b)
end
}
total = size_by_index.values.reduce(0, :+)
size_by_index
.sort_by { |(_i, bytes)| -bytes }
.each do |idx, bytes|
name = index_to_name[idx] || "[#{idx}]"
puts format('%-50s %10s', name, fmt.call(bytes))
end
puts format('%-50s %10s', 'Total (approx):', fmt.call(total))
Sample output for the examples above:
/Users/dev/.../ApplicationDelegate.o 8.83KB
/Users/dev/.../HomeViewController.o 792B
/Users/dev/.../LinkMapSample.app-Simulated.xcent 392B
/Users/dev/.../main.o 208B
linker synthesized 128B
.../usr/lib/libobjc.tbd 112B
.../Foundation.framework/Foundation.tbd 32B
.../UIKit.framework/UIKit.tbd 32B
Total (approx): 10.28KB