#!/usr/bin/tclsh
################################################################################
#
# Given a mach-o binary, use nm and gdb to size all the functions it contains
#


proc main { } {
    global argv

    set binfile [lindex $argv 0]
    if {![file readable $binfile]} {
     	puts "can't read '$binfile'"
	exit 1
    }

    # run nm and parse the output
    do_nm $binfile

    # compute text symbol sizes
    do_sizing

    # now ask GDB for the file(s) containing each symbol
    do_gdb $binfile

    # and emit results
    do_emit_sized
    do_emit_files
}


proc do_nm {binfile} {

    global symbol_addresses
    global text_symbols

    set lines [exec nm -n $binfile]

    foreach {addr type _sym} $lines {
	# record the fact that there is a symbol boundary at this address
	lappend addresses "0x$addr"

	# if the symbol is a text symbol, record its address
	# this could be "tds" to get data symbols too..
	if {[string match \[tT\] $type]} {
	    # ignore the first leading underscore in the symbol name
	    set sym [string range $_sym 1 end]

	    # save the text symbol and its address
	    set text_symbols($sym) "0x$addr"
	}
    }
    set symbol_addresses [lsort -integer -unique $addresses]
    puts "[llength [array names text_symbols]] text symbols"
}


proc do_sizing {} {

    global symbol_addresses
    global text_symbols
    global text_symbol_sizes
    global text_size_symbols

    # walk the array of text symbols
    foreach sym [array names text_symbols] {

	# get the address of the symbol
	set addr $text_symbols($sym)

	# find the address in the global list of symbol addresses
	set index [lsearch $symbol_addresses $addr]

	# find the next symbol boundary
	set nextaddr [lindex $symbol_addresses [expr $index + 1]]

	# size is the distance between the two
	set size [expr $nextaddr - $addr]
	set text_symbol_sizes($sym) $size

	# index symbols by size
	lappend text_size_symbols($size) $sym
    }
}


proc do_gdb {binfile} {

    global symbol_filenames
    global file_symbols

    # ask GDB for the output of "info functions"
    set lines [exec echo "info functions" | xcrun -sdk iphoneos.internal gdb --quiet $binfile 2>/dev/null]

    # Output from GDB includes lines announcing files, and lines describing functions
    # within those files.
    foreach line [split $lines "\n"] {
	# new file announcement
	if {[string match "File *" $line]} {
	    set current_file [string range $line 5 end-1]
	}

	# function within a file
	set paren [string first "(" $line]
	if {$paren > 0} {

	    # function name is last token before ( in the line
	    set frag [string range $line 0 [expr $paren - 1]]
	    set sym [string trimleft [lindex [split $frag] end] "*"]

	    # index filesnames by symbol
	    lappend symbol_filenames($sym) $current_file

	    # index symbols by filename
	    lappend file_symbols($current_file) $sym
	}
    }
}


proc do_emit_sized {} {

    global text_size_symbols
    global symbol_filenames

    puts "Symbols by size"
    puts "==============="
    foreach size [lsort -integer [array names text_size_symbols]] {
	foreach sym $text_size_symbols($size) {
	    if {[llength [array names symbol_filenames -exact $sym]] > 0} {
		puts [format "%8d  %-50s %s" $size $sym $symbol_filenames($sym)]
	    } else {
		puts [format "%8d  %-50s" $size $sym]
	    }
	}
    }
    puts ""
}

proc do_emit_files {} {

    global file_symbols
    global text_symbol_sizes

    puts "Symbols by file size"
    puts "===================="
    foreach fn [array names file_symbols] {
	set size 0
	foreach sym $file_symbols($fn) {
	    if {[array names text_symbol_sizes $sym] != ""} {
		incr size $text_symbol_sizes($sym)
	    } else {
		puts "WARNING: no size information for '$sym' - nm didn't see it."
	    }
	}
	lappend size_files($size) $fn
    }

    foreach size [lsort -integer [array names size_files]] {
	foreach fn $size_files($size) {
	    puts "$fn: ($size)"
	    set lines [list]
	    foreach sym $file_symbols($fn) {
		if {[array names text_symbol_sizes $sym] != ""} {
		    lappend lines [list $text_symbol_sizes($sym) $sym]
		}
	    }
	    foreach ent [lsort -index 0 -integer $lines] {
		puts [format "%8d  %-50s" [lindex $ent 0] [lindex $ent 1]]
	    }
	    puts ""
	}
    }
}








################################################################################
main