├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console ├── linkmap_ios └── setup ├── lib ├── linkmap_ios.rb └── linkmap_ios │ └── version.rb ├── linkmap_ios.gemspec └── spec ├── linkmap_ios_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .idea/ 4 | /.bundle/ 5 | /.yardoc 6 | /Gemfile.lock 7 | /_yardoc/ 8 | /coverage/ 9 | /doc/ 10 | /pkg/ 11 | /spec/reports/ 12 | /tmp/ 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.1.1 5 | before_install: gem install bundler -v 1.12.5 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in linkmap_ios.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jesse Luo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linkmap for iOS 2 | A ruby library to parse linkmap file generated by Xcode. Then ouput result with hash, json or report. Also have a command line tool. 3 | 4 | ## Installation 5 | 6 | Add this line to your application's Gemfile: 7 | 8 | ```ruby 9 | gem 'linkmap_ios' 10 | ``` 11 | 12 | And then execute: 13 | 14 | $ bundle 15 | 16 | Or install it yourself as: 17 | 18 | $ gem install linkmap_ios 19 | 20 | ## Usage 21 | ``` 22 | linkmap_ios parse [LINKMAP FILE PATH] 23 | 24 | Options: 25 | -f, [--format=FORMAT] 26 | # Possible values: hash, json, report 27 | -o, [--output-path=OUTPUT_PATH] 28 | 29 | Parse link map file and output. Will output Ruby Hash by default 30 | ``` 31 | #### Note 32 | Byte is the default unit in all kind of output. 33 | 34 | ## Release Note 35 | ### [0.1.3] 2018-06-07 36 | 1. Support .a file 37 | 2. Minor bugfixs 38 | 39 | ### [0.1.1] 2017-03-29 40 | 1. Increase the concurrency, add dead stripped symbol handle in symbol analyze 41 | 2. Add simple section analyze 42 | 3. Add uncounted section number in report 43 | 44 | ### [0.1.0] 2016-08-09 45 | 1. Finish all base functions 46 | 47 | ## TODO 48 | 1. Add test specs 49 | 2. Add full sections analysis function 50 | 51 | ## Development 52 | 53 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 54 | 55 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 56 | 57 | ## Contributing 58 | 59 | Bug reports and pull requests are welcome on GitHub at https://github.com/jesseluo/linkmap_ios. 60 | 61 | 62 | ## License 63 | 64 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 65 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "linkmap_ios" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/linkmap_ios: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "linkmap_ios" 5 | require "csv" 6 | require "thor" 7 | 8 | module LinkmapIos 9 | class CLI < Thor 10 | desc "parse [LINKMAP FILE PATH]", "Parse link map file and output. Will output Ruby Hash by default" 11 | # method_option :path, :type => :string, :required => true, :aliases => "-i" 12 | method_option :format, :type => :string, :enum => %w{hash json report csv}, :aliases => "-f" 13 | method_option :output_path, :type => :string, :aliases => "-o" 14 | def parse(path) 15 | puts "Start parsing. Link map file size is #{File.size(path)}" 16 | start_time = Time.now 17 | 18 | format = (options.format or 'hash') 19 | 20 | if format.eql? 'csv' 21 | output = LinkmapIos::LinkmapParser.new(path).hash 22 | puts "Parse file done. Time used: #{Time.now - start_time}s" 23 | 24 | CSV.open(options.output_path, "wb") do |csv| 25 | csv << ['total size', output[:total]] 26 | output[:detail].each do |lib| 27 | csv << [lib[:library], lib[:size]] 28 | end 29 | end 30 | 31 | return 32 | end 33 | 34 | output = LinkmapIos::LinkmapParser.new(path).send(format) 35 | 36 | puts "Parse file done. Time used: #{Time.now - start_time}s" 37 | start_time = Time.now 38 | 39 | if options.output_path 40 | File.write(options.output_path, output) 41 | puts "Write file done. Time used: #{Time.now - start_time}s" 42 | else 43 | puts output 44 | end 45 | end 46 | end 47 | end 48 | 49 | LinkmapIos::CLI.start 50 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/linkmap_ios.rb: -------------------------------------------------------------------------------- 1 | require "linkmap_ios/version" 2 | require "filesize" 3 | require "json" 4 | 5 | module LinkmapIos 6 | Library = Struct.new(:name, :size, :objects, :dead_symbol_size, :pod) 7 | 8 | class LinkmapParser 9 | attr_reader :id_map 10 | attr_reader :library_map 11 | 12 | def initialize(file_path) 13 | @file_path = file_path 14 | @id_map = {} 15 | @library_map = {} 16 | @section_map = {} 17 | end 18 | 19 | def hash 20 | # Cache 21 | return @result_hash if @result_hash 22 | 23 | parse 24 | 25 | total_size = @library_map.values.map(&:size).inject(:+) 26 | detail = @library_map.values.map { |lib| {:library => lib.name, :pod => lib.pod, :size => lib.size, :dead_symbol_size => lib.dead_symbol_size, :objects => lib.objects.map { |o| @id_map[o][:object] }}} 27 | total_dead_size = @library_map.values.map(&:dead_symbol_size).inject(:+) 28 | 29 | # puts total_size 30 | # puts detail 31 | 32 | @result_hash = {:total => total_size, :detail => detail, :total_dead => total_dead_size} 33 | @result_hash 34 | end 35 | 36 | def json 37 | JSON.pretty_generate(hash) 38 | end 39 | 40 | def report 41 | result = hash 42 | 43 | report = "# Total size\n" 44 | report << "#{Filesize.from(result[:total].to_s + 'B').pretty}\n" 45 | report << "# Dead Size\n" 46 | report << "#{Filesize.from(result[:total_dead].to_s + 'B').pretty}\n" 47 | report << "\n# Library detail\n" 48 | result[:detail].sort_by { |h| h[:size] }.reverse.each do |lib| 49 | report << "#{lib[:library]} #{Filesize.from(lib[:size].to_s + 'B').pretty}\n" 50 | end 51 | report << "\n# Object detail\n" 52 | @id_map.each_value do |id_info| 53 | report << "#{id_info[:object]} #{Filesize.from(id_info[:size].to_s + 'B').pretty}\n" 54 | end 55 | 56 | report << "# Uncounted Section Detail" 57 | @section_map.select do |_, value| 58 | value[:residual_size] != 0 59 | end 60 | .each do |seg_sec_name, value| 61 | report << "\n#{seg_sec_name}, start_address: #{value[:start_address]}, end_address: #{value[:end_address]}, residual_size: #{value[:residual_size]}" 62 | end 63 | report 64 | end 65 | 66 | private 67 | 68 | def parse 69 | File.foreach(@file_path).with_index do |line, line_num| 70 | begin 71 | # Deal with string like  72 | unless line.valid_encoding? 73 | line = line.encode("UTF-16", :invalid => :replace, :replace => "?").encode('UTF-8') 74 | end 75 | 76 | if line.start_with? "#" 77 | if line.start_with? "# Object files:" 78 | @subparser = :parse_object_files 79 | elsif line.start_with? "# Sections:" 80 | @subparser = :parse_sections 81 | elsif line.start_with? "# Symbols:" 82 | @subparser = :parse_symbols 83 | elsif line.start_with? '# Dead Stripped Symbols:' 84 | @subparser = :parse_dead 85 | end 86 | else 87 | send(@subparser, line) 88 | end 89 | rescue => e 90 | puts "Exception on Link map file line #{line_num}. Content is" 91 | puts line 92 | raise e 93 | end 94 | end 95 | puts "There are #{@section_map.values.map{|value| value[:residual_size]}.inject(:+)} Byte in some section can not be analyze" 96 | end 97 | 98 | def parse_object_files(text) 99 | if text =~ /\[(.*)\].*\/(.*)\((.*)\)/ 100 | # Sample: 101 | # [ 6] SomePath/Release-iphoneos/ReactiveCocoa/libReactiveCocoa.a(MKAnnotationView+RACSignalSupport.o) 102 | # So $1 is id. $2 is library 103 | id = $1.to_i 104 | @id_map[id] = {:library => $2, :object => $3} 105 | 106 | library = (@library_map[$2] or Library.new($2, 0, [], 0)) 107 | library.objects << id 108 | if text.include?('/Pods/') 109 | # Sample: 110 | # 0x108BE58D5 0x000000AD [7167] literal string: /var/folders/80/d5nlb_0d05n7bq58h2fy181h0000gp/T/d20180412-62284-c4qbkv/sandbox.SAKPodSharper-React-0.54.3.2@20180412210546/Pods/React/ReactCommon/jschelpers/JSCHelpers.cpp 111 | # podname is word after "/Pods/", example:React 112 | divstr = text.split('/Pods/').last 113 | podname = divstr.split('/').first 114 | library.pod = podname 115 | else 116 | library.pod = '' 117 | end 118 | @library_map[$2] = library 119 | elsif text =~ /\[(.*)\].*\/(.*)/ 120 | # Sample: 121 | # System 122 | # [100] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS9.3.sdk/System/Library/Frameworks//UIKit.framework/UIKit.tbd 123 | # Main 124 | # [ 3] /SomePath/Release-iphoneos/CrashDemo.build/Objects-normal/arm64/AppDelegate.o 125 | # Dynamic Framework 126 | # [9742] /SomePath/Pods/AFNetworking/Classes/AFNetworking.framework/AFNetworking 127 | id = $1.to_i 128 | if text.include?('.framework') and not $2.include?('.') 129 | lib = $2 130 | else 131 | if text.end_with?('.a') 132 | lib = $2 133 | else 134 | lib = $2.end_with?('.tbd') ? 'System' : 'Main' 135 | end 136 | end 137 | @id_map[id] = {:library => lib, :object => $2} 138 | 139 | library = (@library_map[lib] or Library.new(lib, 0, [], 0, '')) 140 | library.objects << id 141 | @library_map[lib] = library 142 | 143 | elsif text =~ /\[(.*)\]\s*([\w\s]+)/ 144 | # Sample: 145 | # [ 0] linker synthesized 146 | # [ 1] dtrace 147 | id = $1.to_i 148 | @id_map[id] = {library: 'Main', object: $2} 149 | end 150 | end 151 | 152 | def parse_sections(text) 153 | # Sample: 154 | # 0x100005F00 0x031B6A98 __TEXT __tex 155 | text_array = text.split(' ').each(&:strip) 156 | section_name = text_array[3] 157 | segment_name = text_array[2] 158 | start_address = text_array.first.to_i(16) 159 | end_address = start_address + text_array[1].to_i(16) 160 | # section name may be dulicate in different segment 161 | seg_sec_name = "#{segment_name}#{section_name}" 162 | @section_map[seg_sec_name.to_sym] = { 163 | segment_name: segment_name, 164 | section_name: section_name, 165 | start_address: start_address, 166 | end_address: end_address, 167 | symbol_size: 0, 168 | residual_size: text_array[1].to_i(16) 169 | } 170 | end 171 | 172 | def parse_symbols(text) 173 | # Sample 174 | # 0x1000055C8 0x0000003C [ 4] -[FirstViewController viewWillAppear:] 175 | if text =~ /^0x(.+?)\s+0x(.+?)\s+\[(.+?)\]/ 176 | symbol_address = $1.to_i(16) 177 | symbol_size = $2.to_i(16) 178 | symbol_file_id = $3.to_i 179 | id_info = @id_map[symbol_file_id] 180 | if id_info 181 | id_info[:size] = (id_info[:size] or 0) + symbol_size 182 | @library_map[id_info[:library]].size += symbol_size 183 | seg_sec_name = @section_map.detect {|seg_sec_name, value| (value[:start_address]...value[:end_address]).include? symbol_address}[0] 184 | @section_map[seg_sec_name.to_sym][:symbol_size] += symbol_size 185 | @section_map[seg_sec_name.to_sym][:residual_size] -= symbol_size 186 | else 187 | puts "#{text.inspect} can not found object file" 188 | end 189 | else 190 | puts "#{text.inspect} can not match symbol regular" 191 | end 192 | end 193 | 194 | def parse_dead(text) 195 | # <> 0x00000008 [ 3] literal string: v16@0:8 196 | if text =~ /^<>\s+0x(.+?)\s+\[(.+?)\]\w*/ 197 | id_info = @id_map[$2.to_i] 198 | if id_info 199 | id_info[:dead_symbol_size] = (id_info[:dead_symbol_size] or 0) + $1.to_i(16) 200 | @library_map[id_info[:library]].dead_symbol_size += $1.to_i(16) 201 | end 202 | end 203 | end 204 | 205 | end 206 | end 207 | -------------------------------------------------------------------------------- /lib/linkmap_ios/version.rb: -------------------------------------------------------------------------------- 1 | module LinkmapIos 2 | VERSION = "0.1.3" 3 | end 4 | -------------------------------------------------------------------------------- /linkmap_ios.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'linkmap_ios/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "linkmap_ios" 8 | spec.version = LinkmapIos::VERSION 9 | spec.authors = ["Jesse Luo"] 10 | spec.email = ["radishchrist@gmail.com"] 11 | 12 | spec.summary = %q{A ruby library to parse linkmap file generated by Xcode} 13 | spec.description = %q{A ruby library to parse linkmap file generated by Xcode. 14 | Then ouput result with hash, json or report. 15 | Also have a command line tool 16 | } 17 | spec.homepage = "https://github.com/jesseluo/linkmap_ios" 18 | spec.license = "MIT" 19 | 20 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 21 | # to allow pushing to a single host or delete this section to allow pushing to any host. 22 | # if spec.respond_to?(:metadata) 23 | # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'" 24 | # else 25 | # raise "RubyGems 2.0 or newer is required to protect against public gem pushes." 26 | # end 27 | 28 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 29 | spec.bindir = "bin" 30 | spec.executables = ["linkmap_ios"] 31 | spec.require_paths = ["lib"] 32 | 33 | spec.add_dependency "thor", "~> 0.19.1" 34 | spec.add_dependency "filesize", "~> 0.1.1" 35 | 36 | spec.add_development_dependency "bundler", "~> 1.12" 37 | spec.add_development_dependency "rake", "~> 10.0" 38 | spec.add_development_dependency "rspec", "~> 3.0" 39 | end 40 | -------------------------------------------------------------------------------- /spec/linkmap_ios_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe LinkmapIos do 4 | it 'has a version number' do 5 | expect(LinkmapIos::VERSION).not_to be nil 6 | end 7 | 8 | it 'does something useful' do 9 | expect(false).to eq(true) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'linkmap_ios' 3 | --------------------------------------------------------------------------------