bdunagan

Brian Dunagan

May 15 2010
Symbolification: Shipping Symbols

nm -a path_to_executable

When Mac applications are compiled, Xcode has a setting to include or exclude debugging symbols: the “Strip Style”. When set to fully strip, the bundled executable (AppName.app/Contents/MacOS/AppName) contains very few symbols, but when all the symbols are included, the executable is a wealth of information. Unfortunately, Xcode’s default Release configuration includes all the symbols, leading to shipping applications with lots of symbol information, including method names and even source files. These are accessible with nm. Let’s see what we find for Retrospect Client (from my company):

Method Names

...
0000431d t -[PrefController awakeFromNib]
0000431d - 01 001f   FUN -[PrefController awakeFromNib]:f(0,1)
...

Source Files

...
0000276e - 01 0032    SO /Volumes/Leopard/projects/trunk/retrospect/client/mac/cpgui/AboutPanel.m
00004963 - 01 0032    SO /Volumes/Leopard/projects/trunk/retrospect/client/mac/cpgui/PasswordController.m
...

While interesting in isolation, the information is richer in aggregate. With a quick ruby script, we can rank the apps in /Applications by various metrics:

Top 10 by Methods

8857	Coda
8036	Evernote
7660	Cornerstone
6312	Pages
6054	Jing
5100	Papers
4805	Delicious Library 2
4589	Socialite
4563	Retrospect
4048	CSSEdit

Top 10 by Source Files

  304	Delicious Library 2
  295	LittleSnapper
  257	Socialite
  240	Evernote
  192	NetNewsWire
  158	Microsoft Document Connection
  144	Tweetie
  115	Retrospect
  105	Clipstart
   87	Transmit

Apple’s products are not in either list because they strip out the symbols for every major application. However, they apparently missed App Store’s Application Loader 1.1 (72), codenamed StarGazer:

...
00000000 - 00 0000    SO /Users/jfosback/jingle/iTMSTransporter/branches/iTMSTransporter-1.4/Applications/Stargazer/Source/ITunesPurpleSoftwareUploadDocumentController.m
...

Xcode Settings Over half of the apps in my /Applications had no symbol information. The developers stripped it out, as described by Apple’s documentation on symbolizing crash dumps.

As I mentioned earlier, Apple’s default Release configuration does not strip out symbols. In fact, it leaves them all in, regardless of what “Strip Style” (STRIP_STYLE) is set to. The culprit is “Deployment Postprocessing” (DEPLOYMENT_POSTPROCESSING). When not enabled, Xcode ignores the setting for “Strip Style”. Apple describes this “Deployment Postprocessing” option in Xcode Build System Guide: Build Configuration and further in Xcode Project Management Guide: Building Products.

I wrote a quick sample application in Xcode and used the default Release configuration to generate the following dumps from nm:

No symbols are stripped (Xcode’s default Release configuration)

# Deployment Postprocessing: false
# Strip Style: any
[~/Desktop] $ nm -a SampleApp/build/Release/SampleApp.app/Contents/MacOS/SampleApp 
0000000000000011 - 01 0000 ENSYM 
0000000000000011 - 00 0000   FUN 
0000000000000011 - 00 0000   FUN 
00000001000018fd - 01 0000 BNSYM 
0000000000000000 - 00 0000    SO 
00000001000018dc - 01 0000 BNSYM 
000000000000000a - 00 0000   FUN 
000000000000000a - 01 0000 ENSYM 
0000000000000000 - 01 0000    SO 
0000000000000000 - 00 0000    SO 
0000000000000000 - 01 0000    SO 
0000000000000011 - 01 0000 ENSYM 
00000001000018e6 - 01 0000 BNSYM 
0000000000000006 - 00 0000   FUN 
0000000000000006 - 01 0000 ENSYM 
00000001000018ec - 01 0000 BNSYM 
0000000100001e8b s  stub helpers
00000001000018e6 t -[SampleAppAppDelegate applicationDidFinishLaunching:]
00000001000018e6 - 01 0000   FUN -[SampleAppAppDelegate applicationDidFinishLaunching:]
00000001000018fd t -[SampleAppAppDelegate setWindow:]
00000001000018fd - 01 0000   FUN -[SampleAppAppDelegate setWindow:]
00000001000018ec - 01 0000   FUN -[SampleAppAppDelegate window]
00000001000018ec t -[SampleAppAppDelegate window]
0000000000000000 - 00 0000    SO /Users/bdunagan/Desktop/SampleApp/SampleAppAppDelegate.m
000000004be395c1 - 00 0001   OSO /Users/bdunagan/Desktop/SampleApp/build/SampleApp.build/Release/SampleApp.build/Objects-normal/x86_64/SampleAppAppDelegate.o
000000004be395c1 - 00 0001   OSO /Users/bdunagan/Desktop/SampleApp/build/SampleApp.build/Release/SampleApp.build/Objects-normal/x86_64/main.o
0000000000000000 - 00 0000    SO /Users/bdunagan/Desktop/SampleApp/main.m
                 U _NSApplicationMain
0000000100002660 D _NXArgc
0000000100002668 D _NXArgv
                 U _OBJC_CLASS_$_NSObject
0000000100002070 - 0a 0000 STSYM _OBJC_CLASS_$_SampleAppAppDelegate
0000000100002070 s _OBJC_CLASS_$_SampleAppAppDelegate
00000001000021c8 s _OBJC_IVAR_$_SampleAppAppDelegate.window
00000001000021c8 - 0b 0000 STSYM _OBJC_IVAR_$_SampleAppAppDelegate.window
                 U _OBJC_METACLASS_$_NSObject
0000000100002048 s _OBJC_METACLASS_$_SampleAppAppDelegate
0000000100002048 - 0a 0000 STSYM _OBJC_METACLASS_$_SampleAppAppDelegate
0000000100002678 D ___progname
0000000100000000 A __mh_execute_header
                 U __objc_empty_cache
                 U __objc_empty_vtable
0000000100002670 D _environ
                 U _exit
00000001000018dc t _main
00000001000018dc - 01 0000   FUN _main
                 U dyld_stub_binder
00000001000018a0 T start

Debugging symbols are stripped

# Deployment Postprocessing: true
# Strip Style: Debugging Symbols (STRIP_STYLE=debugging)
[~/Desktop] $ nm -a SampleApp/build/Release/SampleApp.app/Contents/MacOS/SampleApp 
0000000100001e8b s  stub helpers
00000001000018e6 t -[SampleAppAppDelegate applicationDidFinishLaunching:]
00000001000018fd t -[SampleAppAppDelegate setWindow:]
00000001000018ec t -[SampleAppAppDelegate window]
                 U _NSApplicationMain
0000000100002660 D _NXArgc
0000000100002668 D _NXArgv
                 U _OBJC_CLASS_$_NSObject
0000000100002070 s _OBJC_CLASS_$_SampleAppAppDelegate
00000001000021c8 s _OBJC_IVAR_$_SampleAppAppDelegate.window
                 U _OBJC_METACLASS_$_NSObject
0000000100002048 s _OBJC_METACLASS_$_SampleAppAppDelegate
0000000100002678 D ___progname
0000000100000000 A __mh_execute_header
                 U __objc_empty_cache
                 U __objc_empty_vtable
0000000100002670 D _environ
                 U _exit
00000001000018dc t _main
                 U dyld_stub_binder
00000001000018a0 T start

Non-global symbols are stripped

# Deployment Postprocessing: true
# Strip Style: Non-Global Symbols (STRIP_STYLE=non-global)
[~/Desktop] $ nm -a SampleApp/build/Release/SampleApp.app/Contents/MacOS/SampleApp 
                 U _NSApplicationMain
0000000100002660 D _NXArgc
0000000100002668 D _NXArgv
                 U _OBJC_CLASS_$_NSObject
                 U _OBJC_METACLASS_$_NSObject
0000000100002678 D ___progname
0000000100000000 A __mh_execute_header
                 U __objc_empty_cache
                 U __objc_empty_vtable
0000000100002670 D _environ
                 U _exit
                 U dyld_stub_binder
0000000005614542 - 00 0000   OPT radr://5614542
00000001000018a0 T start

All Symbols are stripped

# Deployment Postprocessing: true
# Strip Style: All Symbols (STRIP_STYLE=all) (default)
[~/Desktop] $ nm -a SampleApp/build/Release/SampleApp.app/Contents/MacOS/SampleApp 
                 U _NSApplicationMain
                 U _OBJC_CLASS_$_NSObject
                 U _OBJC_METACLASS_$_NSObject
0000000100000000 A __mh_execute_header
                 U __objc_empty_cache
                 U __objc_empty_vtable
                 U _exit
                 U dyld_stub_binder
0000000005614542 - 00 0000   OPT radr://5614542

Ruby Script To generate the statistics for the apps in /Applications, I wrote a short ruby script:

#!/usr/local/bin/ruby19

# analyze_apps.rb
# * Analyze dump from 'nm -a path_to_executable' to collect statistics: Name, Path, Lines, Archs, Files, Class Methods, Instance Methods

# Collect app paths.
apps = []
apps_folders = ["/Applications", "/Applications/iWork '08", "/Applications/iWork '09", "/Applications/Microsoft Office 2004", "/Applications/Microsoft Office 2008", "/Applications/Utilities"]
apps_folders.each do |app_folder|
  app_list = Dir.entries(app_folder)
  app_list.each do |app|
    apps << "#{app_folder}/#{app}"
  end
end

# Include source code?
PRINT_SOURCE_CODE = false

# Identify columns of data.
puts "Name, Path, Lines, Archs, Files, Class Methods, Instance Methods"

# Loop through apps.
apps.each do |app|
  # Check that the app exists.
  if Dir.exists?("#{app}") && app.include?(".app")
    app_array = app.split("/")
    app_name = app_array[app_array.count-1].split(".app")[0]
    exe_path = "#{app}/Contents/MacOS/#{app_name}"
    # Check that the executable exists.
    if File.exists?(exe_path)
      # Use 'nm -a path_to_executable' to dump symbol information.
      results = %x[nm -a "#{exe_path}"]

      # Get number of architectures supported. All other information is duplicated per architecture.
      arch_count = results.scan("(for architecture ").count
      arch_count = 1 if arch_count == 0

      # Get the number of lines, class methods, and instance methods per architecture.
      line_count = results.scan("\n").count / arch_count
      class_methods = results.scan(" t +[").count / arch_count
      instance_methods = results.scan(" t -[").count / arch_count

      # Loop through the results to more accurately count the source files (though not perfectly).
      source_lookup = []
      source_count = 0
      results.split("\n").each do |line|
        # Check that the line references "SO" or "SOL" but isn't repeated.
        if line.include?("SO ") && line.split("SO ").count > 1 && line.split("SO ")[1].include?(".m") && !source_lookup.include?(line.split("SO ")[1])
          file = line.split("SO ")[1]
          # Add file name to lookup.
          source_lookup << file
          # Increment counter.
          source_count = source_count + 1
          # Print out file name.
          puts "#{app_name}: #{file}" if PRINT_SOURCE_CODE
        elsif line.include?("SOL ") && line.split("SOL ").count > 1 && line.split("SOL ")[1].include?(".m") && !source_lookup.include?(line.split("SOL ")[1])
          file = line.split("SOL ")[1]
          # Add file name to lookup.
          source_lookup << file
          # Increment counter.
          source_count = source_count + 1
          # Print out file name.
          puts "#{app_name}: #{file}" if PRINT_SOURCE_CODE
        end
      end

      # Print out the application's stats.
      puts "#{app_name}, #{app}, #{line_count}, #{arch_count}, #{source_count}, #{class_methods}, #{instance_methods}"
    end
  end
end
Measuring Design Changes class-dump: Dumping Classes
LinkedIn GitHub Email