diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ac6010 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.dep +*.o +config.mk.local +test/fixtures/* +atosl +Guardfile diff --git a/Makefile b/Makefile index 7065575..145e229 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,9 @@ dist: clean gzip ${DIST}.tar rm -rf ${DIST} +test: all + rake + install: all mkdir -p ${DESTDIR}${PREFIX}/bin cp -f atosl ${DESTDIR}${PREFIX}/bin diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..b54076a --- /dev/null +++ b/Rakefile @@ -0,0 +1,7 @@ +require 'rake/testtask' + +task :default => [:test] + +Rake::TestTask.new do |t| + t.pattern = "test/*_test.rb" +end diff --git a/atosl.c b/atosl.c index 604f6da..5db7ad9 100755 --- a/atosl.c +++ b/atosl.c @@ -197,6 +197,13 @@ char *demangle(const char *sym) return demangled; } +static int compare_symbols(const void *a, const void *b) +{ + struct symbol_t *sym_a = (struct symbol_t *)a; + struct symbol_t *sym_b = (struct symbol_t *)b; + return sym_a->addr - sym_b->addr; +} + int parse_uuid(dwarf_mach_object_access_internals_t *obj, uint32_t cmdsize) { int i; @@ -300,7 +307,7 @@ int parse_section_64(dwarf_mach_object_access_internals_t *obj) } struct dwarf_section_64_t *sec = obj->sections_64; - + if (!sec) { obj->sections_64 = s; } else { @@ -414,6 +421,10 @@ int parse_symtab(dwarf_mach_object_access_internals_t *obj, uint32_t cmdsize) int i; char *strtable; + union { + struct nlist_t nlist32; + struct nlist_64 nlist64; + } nlist; struct symtab_command_t symtab; struct symbol_t *current; @@ -453,6 +464,7 @@ int parse_symtab(dwarf_mach_object_access_internals_t *obj, uint32_t cmdsize) context.symlist = malloc(sizeof(struct symbol_t) * symtab.nsyms); if (!context.symlist) fatal("unable to allocate memory"); + current = context.symlist; for (i = 0; i < symtab.nsyms; i++) { @@ -470,32 +482,6 @@ int parse_symtab(dwarf_mach_object_access_internals_t *obj, uint32_t cmdsize) current++; } - ret = lseek(obj->handle, pos, SEEK_SET); - if (ret < 0) - fatal("error seeking: %s", strerror(errno)); - - return 0; -} - -static int compare_symbols(const void *a, const void *b) -{ - struct symbol_t *sym_a = (struct symbol_t *)a; - struct symbol_t *sym_b = (struct symbol_t *)b; - return sym_a->addr - sym_b->addr; -} - -int print_symtab_symbol(Dwarf_Addr slide, Dwarf_Addr addr) -{ - union { - struct nlist_t nlist32; - struct nlist_64 nlist64; - } nlist; - struct symbol_t *current; - int found = 0; - - int i; - - addr = addr - slide; current = context.symlist; for (i = 0; i < context.nsymbols; i++) { @@ -537,7 +523,26 @@ int print_symtab_symbol(Dwarf_Addr slide, Dwarf_Addr addr) current++; } - qsort(context.symlist, context.nsymbols, sizeof(*current), compare_symbols); + /* Use a stable sort to preserve existing orders of symbols with the same address + * Unity3D has symbols with the same address where we the first one is generally the better one + * FIXME: We rely on the fact, that GNU uses mergesort to implement qsort */ + #ifdef __APPLE__ + mergesort(context.symlist, context.nsymbols, sizeof(*current), compare_symbols); + #else + qsort(context.symlist, context.nsymbols, sizeof(*current), compare_symbols); + #endif + + ret = lseek(obj->handle, pos, SEEK_SET); + if (ret < 0) + fatal("error seeking: %s", strerror(errno)); + + return 0; +} + +struct symbol_t *find_symtab_symbol(Dwarf_Addr slide, Dwarf_Addr addr) { + struct symbol_t *current; + int i; + addr = addr - slide; current = context.symlist; for (i = 0; i < context.nsymbols; i++) { @@ -549,29 +554,66 @@ int print_symtab_symbol(Dwarf_Addr slide, Dwarf_Addr addr) break; } - struct symbol_t *prev = (current - 1); + struct symbol_t *prev; - char *demangled = demangle(prev->name); - const char *name = demangled ? demangled : prev->name; + /* Unity3D applications contain _m_ symbols with the same address + * as the method name + * FIXME: Change code to support more than 2 symbols with the same address */ + if((current - 2)->addr == (current - 1)->addr) { + return (current - 2); + } + else { + return (current - 1); + } - if (name[0] == '_') - name++; + return prev; + } + current++; + } - printf("%s%s (in %s) + %d\n", - name, - demangled ? "()" : "", - basename((char *)options.dsym_filename), - (unsigned int)(addr - prev->addr)); - found = 1; + return NULL; +} - if (demangled) - free(demangled); - break; +int print_symtab_symbol(Dwarf_Addr slide, Dwarf_Addr addr) { + struct symbol_t *symbol = find_symtab_symbol(slide, addr); + if(symbol){ + char *demangled = demangle(symbol->name); + const char *name = demangled ? demangled : symbol->name; + + if (name[0] == '_') + name++; + + printf("%s%s (in %s) + %d\n", + name, + demangled ? "()" : "", + basename((char *)options.dsym_filename), + (unsigned int)(addr - symbol->addr)); + + if (demangled){ + free(demangled); } - current++; + return DW_DLV_OK; + } + else { + return DW_DLV_NO_ENTRY; } - return found ? DW_DLV_OK : DW_DLV_NO_ENTRY; +} + +char* get_name_from_symbol_table(Dwarf_Addr slide, Dwarf_Addr addr) { + struct symbol_t *symbol = find_symtab_symbol(slide, addr); + if(symbol){ + const char *name = symbol->name; + + if (name[0] == '_') + { + name++; + } + return (char *) name; + } + else { + return "UNKNOWN"; + } } int parse_command( @@ -633,7 +675,7 @@ static int dwarf_mach_object_access_internals_init( ret = _read(obj->handle, &header, sizeof(header)); if (ret < 0) fatal_file(ret); - + /* Need to skip 4 bytes of the reserved field of mach_header_64 */ if (header.cputype == CPU_TYPE_ARM64 && header.cpusubtype == CPU_SUBTYPE_ARM64_ALL) { context.is_64 = 1; @@ -721,7 +763,7 @@ static int dwarf_mach_object_access_get_section_info( *error = DW_DLE_MDE; return DW_DLV_ERROR; } - + if (obj->sections_64) { struct dwarf_section_64_t *sec = obj->sections_64; for (i = 0; i < section_index; i++) { @@ -797,7 +839,7 @@ static int dwarf_mach_object_access_load_section( ret = _read(obj->handle, addr, sec->mach_section.size); if (ret < 0) fatal_file(ret); - + } *section_data = addr; @@ -890,8 +932,8 @@ const char *lookup_symbol_name(Dwarf_Addr addr) subprogram = subprogram->next; } + return "unknown"; - return "(unknown)"; } int print_subprogram_symbol(Dwarf_Addr slide, Dwarf_Addr addr) @@ -918,9 +960,11 @@ int print_subprogram_symbol(Dwarf_Addr slide, Dwarf_Addr addr) } if (match) { - demangled = demangle(match->name); + /* somehow get the symbol name from the symbol table */ + char *name_from_symbol_table = get_name_from_symbol_table(slide, addr + slide); + demangled = demangle(name_from_symbol_table); printf("%s (in %s) + %d\n", - demangled ?: match->name, + demangled ? demangled : name_from_symbol_table, basename((char *)options.dsym_filename), (unsigned int)(addr - match->lowpc)); if (demangled) @@ -1027,7 +1071,7 @@ int print_dwarf_symbol(Dwarf_Debug dbg, Dwarf_Addr slide, Dwarf_Addr addr) ret = dwarf_diename(cu_die, &diename, &err); DWARF_ASSERT(ret, err); - symbol = lookup_symbol_name(addr); + symbol = get_name_from_symbol_table(slide, slide + addr); demangled = demangle(symbol); printf("%s (in %s) (%s:%d)\n", diff --git a/test/fixtures/.gitkeep b/test/fixtures/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/integration_test.rb b/test/integration_test.rb new file mode 100644 index 0000000..68419cc --- /dev/null +++ b/test/integration_test.rb @@ -0,0 +1,79 @@ +require_relative 'test_helper' + +class TestAtosl < MiniTest::Unit::TestCase + + def setup + purge_disk_cache + end + + def test_unsorted_symbol_table_of_libsystem_kernel + load_address = '0x194a38000' + symbol_address = '0000000194a53270' + cmd = "#{ATOSL} --no-cache -A arm64 -l #{load_address} -o '#{FIXTURES['libsystem_kernel.dylib']}' #{symbol_address} 2>&1" + expected = '__pthread_kill (in libsystem_kernel.dylib) + 33488904' + actual = `#{cmd}`.chomp + + assert_equal(expected, actual) + end + + # A symboltable might contain several symbols with the same address + # Always take the first one. This is way we need a stable sort for + # the symbols in the symbol table. Example: + # + # m_UnityEngine... + # m_41 + def test_subprogram_always_take_the_first_symbol + load_address = '0x9d000' + symbol_address = '0x003d6880' + cmd = "#{ATOSL} --no-cache -A armv7 -l #{load_address} -o '#{FIXTURES['CrashUnityTest']}' #{symbol_address} 2>&1" + expected = 'm_UnityEngine_EventSystems_ExecuteEvents_Execute_UnityEngine_EventSystems_IPointerClickHandler_UnityEngine_EventSystems_BaseEventData (in CrashUnityTest) + 3358836' + actual = `#{cmd}`.chomp + + assert_equal(expected, actual) + end + + def test_unity_il2cpp_subprograms_always_take_the_first_symbol + load_address = '0x1000ec000' + symbol_address = '0x00000001007e0844' + cmd = "#{ATOSL} --no-cache -A arm64 -l #{load_address} -o '#{FIXTURES['CrashUnityTest_4_6_IL2CPP_64bit_slow_and_safe']}' #{symbol_address} 2>&1" + expected = 'FMOD::Thread::callback (in CrashUnityTest_4_6_IL2CPP_64bit_slow_and_safe) + 489640' + actual = `#{cmd}`.chomp + + assert_equal(expected, actual) + end + + # IL2CPP introduces weird mangling issues but we are consistent with apple's atos + # on this one + # Different behavior between OSX and Linux + # Going with the Linux behavior + def test_unity_il2cpp_user_scripts + load_address = '0x1000ec000' + symbol_address = '0x0000000100103160' + cmd = "#{ATOSL} --no-cache -A arm64 -l #{load_address} -o '#{FIXTURES['CrashUnityTest_4_6_IL2CPP_64bit_slow_and_safe']}' #{symbol_address} 2>&1" + #FIXME: On OSX a dfferent source file might be returned. However + # This is not important, since the main target is LINUX. If we'd be running + # on OSX, we could use the apple atos anyway + expected = if(/darwin/ =~ RUBY_PLATFORM) != nil + 'AssemblyU002DCSharp_ButtonControllerScript_m_finallyDoTheCrash (in Bulk_Assembly-CSharp_0.cpp) (Bulk_Assembly-CSharp_0.cpp:245)' + else + 'AssemblyU002DCSharp_ButtonControllerScript_m_finallyDoTheCrash (in CrashUnityTest_4_6_IL2CPP_64bit_slow_and_safe) (Bulk_Assembly-CSharp_0.cpp:245)' + end + + actual = `#{cmd}`.chomp + + assert_equal(expected, actual) + end + + def test_subprograms_returns_the_same_result_when_using_cache + load_address = '0x1000ec000' + symbol_address = '0x00000001007e0844' + cmd = "#{ATOSL} -A arm64 -l #{load_address} -o '#{FIXTURES['CrashUnityTest_4_6_IL2CPP_64bit_slow_and_safe']}' #{symbol_address} 2>&1" + + # the cache is populated during the first run + without_cache = `#{cmd}`.chomp + with_cache = `#{cmd}`.chomp + + assert_equal(without_cache, with_cache) + end + +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..9752e45 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,62 @@ +require 'minitest/autorun' +require 'open-uri' + +class FixtureLoader + + MANIFEST = { + "libsystem_kernel.dylib" => "https://s3-eu-west-1.amazonaws.com/test-fixtures.atosl.github.com/libsystem_kernel.dylib", + "CrashUnityTest" => "https://s3-eu-west-1.amazonaws.com/test-fixtures.atosl.github.com/CrashUnityTest", + "CrashUnityTest_4_6_IL2CPP_64bit_slow_and_safe" => "https://s3-eu-west-1.amazonaws.com/test-fixtures.atosl.github.com/CrashUnityTest_4_6_IL2CPP_64bit_slow_and_safe", + } + + FIXTURE_PATH = File.join(File.dirname(File.realdirpath(__FILE__)), 'fixtures') + + def load + fixtures = {} + + MANIFEST.each do |filename, url| + path = fixture_path_for_filename(filename) + + unless File.exists?(path) + puts "Downloading #{url}" + download(url, path) + end + + fixtures[filename] = path + end + + fixtures + end + + def download(url, target_path) + File.open(target_path, "wb") do |saved_file| + open(url, "rb") do |read_file| + saved_file.write(read_file.read) + end + end + end + + def fixture_path_for_filename(filename) + File.join(File.dirname(File.realdirpath(__FILE__)), 'fixtures', filename) + end +end + +class MiniTest::Unit::TestCase + def purge_disk_cache + dir = ENV['HOME'] + '/.atosl-cache/' + if Dir.exists?(dir) + `rm -r #{dir}` + end + end + + # turn of the magic diff, because it tries to be smart + # with the output + # https://github.com/seattlerb/minitest/issues/494 + def mu_pp_for_diff obj + mu_pp(obj).gsub(/\\n/, "\n") + end +end + +FIXTURES = FixtureLoader.new.load +ATOSL = File.join(File.dirname(File.realdirpath(__FILE__)), '..', 'atosl') +