diff --git a/runtime/druntime/src/core/internal/backtrace/dwarf.d b/runtime/druntime/src/core/internal/backtrace/dwarf.d index 5da98948000..635bc635749 100644 --- a/runtime/druntime/src/core/internal/backtrace/dwarf.d +++ b/runtime/druntime/src/core/internal/backtrace/dwarf.d @@ -43,7 +43,7 @@ * Reference: http://www.dwarfstd.org/ * Copyright: Copyright Digital Mars 2015 - 2015. * License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). - * Authors: Yazan Dabain, Sean Kelly + * Authors: Yazan Dabain, Sean Kelly, Luna Nielsen * Source: $(DRUNTIMESRC core/internal/backtrace/dwarf.d) */ @@ -69,6 +69,7 @@ else import core.internal.container.array; import core.stdc.string : strlen, memcpy; +import core.attribute : weak; //debug = DwarfDebugMachine; debug(DwarfDebugMachine) import core.stdc.stdio : printf; @@ -184,7 +185,6 @@ int traceHandlerOpApplyImpl(size_t numFrames, startIdx = idx + 1; } - if (!image.isValid()) return locations[startIdx .. $].processCallstack(null, 0, dg); @@ -244,11 +244,6 @@ int processCallstack(Location[] locations, const(ubyte)[] debugLineSectionData, { if (debugLineSectionData) resolveAddresses(debugLineSectionData, locations, baseAddress); - version (Darwin) - { - if (!debugLineSectionData) - resolveAddressesWithAtos(locations); - } TraceInfoBuffer buffer; foreach (idx, const ref loc; locations) @@ -267,141 +262,6 @@ int processCallstack(Location[] locations, const(ubyte)[] debugLineSectionData, return 0; } -version (Darwin) { - /** - * Resolve the addresses of `locations` using `atos` (executable that ships with XCode) - * - * Spawns a child process that calls `atos`. Communication is through stdin/stdout pipes. - * - * After this function successfully completes, `locations` will contain - * file / lines informations. - * - * The lifetime of the `Location` data surpases function return (strndup is used). - * - * Params: - * locations = The locations to resolve - */ - private void resolveAddressesWithAtos(Location[] locations) @nogc nothrow - { - import core.stdc.stdio : fclose, fflush, fgets, fprintf, printf, snprintf; - import core.stdc.stdlib : exit; - import core.sys.posix.stdio : fdopen; - import core.sys.posix.unistd : close, dup2, execlp, fork, getpid, pipe; - // Create in/out pipes to communicate with the forked exec - int[2] dummy_pipes; // these dummy pipes are there to prevent funny issues when stdin/stdout is closed and pipe returns id 0 or 1 - int[2] pipes_to_atos; - int[2] pipes_from_atos; - if ( pipe(dummy_pipes) < 0 || pipe(pipes_to_atos) < 0 || pipe(pipes_from_atos) < 0 ) { - printf("some pipe creation error!\n"); - return; - } - close(dummy_pipes[0]); - close(dummy_pipes[1]); - auto write_to_atos = pipes_to_atos[1]; - auto read_from_atos = pipes_from_atos[0]; - auto atos_stdin = pipes_to_atos[0]; - auto atos_stdout = pipes_from_atos[1]; - auto self_pid = cast(int) getpid(); - // Spawn a child process that calls atos, reads/writes from the pipes, and then exits. - auto child_id = fork(); - if (child_id == -1) - { - printf("some fork error!\n"); - return; - } - else if (child_id == 0) - { - // We are in the child process, spawn atos and link pipes - // Close unused read/write ends of pipes - close(write_to_atos); - close(read_from_atos); - // Link pipes to stdin/stdout - dup2(atos_stdin, 0); - close(atos_stdin); - dup2(atos_stdout, 1); - close(atos_stdout); - char[10] pid_str; - snprintf(pid_str.ptr, pid_str.sizeof, "%d", cast(int) self_pid); - const(char)* atos_executable = "atos"; - const(char)* atos_p_arg = "-p"; - const(char)* atos_fullpath_arg = "-fullPath"; - execlp(atos_executable, atos_executable, atos_fullpath_arg, atos_p_arg, pid_str.ptr, null); - // If exec returns, an error occurred, need to exit the forked process here. - printf("some exec error!\n"); - exit(0); - } - // Parent process just continues from here. - // Close unused pipes - close(atos_stdin); - close(atos_stdout); - auto to_atos = fdopen(write_to_atos, "w"); - auto from_atos = fdopen(read_from_atos, "r"); - // buffer for atos reading. Note that symbol names can be super large... - static char[16 * 1024] read_buffer = void; - char* status_ptr = null; - foreach (ref loc; locations) - { - fprintf(to_atos, "%p\n", loc.address); - fflush(to_atos); - read_buffer[0] = '\0'; - status_ptr = fgets(read_buffer.ptr, read_buffer.sizeof, from_atos); - if (!status_ptr) - break; - Location parsed_loc = parseAtosLine(read_buffer.ptr); - if (parsed_loc.line != -1) - { - // Only update the file:line info, keep the procedure name as found before (preserving the standard truncation). - loc.file = parsed_loc.file; - loc.line = parsed_loc.line; - } - } - if (!status_ptr) - printf("\nDid not succeed in using 'atos' for extra debug information.\n"); - fclose(to_atos); - fclose(from_atos); - close(write_to_atos); - close(read_from_atos); - } - private Location parseAtosLine(char* buffer) @nogc nothrow - { - // The line from `atos` is in one of these formats: - // myfunction (in library.dylib) (sourcefile.c:17) - // myfunction (in library.dylib) + 0x1fe - // myfunction (in library.dylib) + 15 - // 0xdeadbeef (in library.dylib) + 0x1fe - // 0xdeadbeef (in library.dylib) + 15 - // 0xdeadbeef (in library.dylib) - // 0xdeadbeef - import core.stdc.stdlib : atoi; - import core.stdc.string : strchr, strstr; - import core.sys.posix.string : strndup; - Location loc; - if (!buffer) - return loc; - if (buffer[0] == '0' && buffer[1] == 'x') - // no named symbol found - return loc; - const symbolname_end = strstr(buffer, " (in "); - if (!symbolname_end) - return loc; - const symbolname_size = symbolname_end - buffer; - loc.procedure = strndup(buffer, symbolname_size)[0..symbolname_size]; - const filename_start = strstr(symbolname_end, ") (") + 3; - if (cast(size_t)filename_start < 4) - return loc; - const colon_location = strchr(filename_start, ':'); - if (!colon_location) - return loc; - const filename_size = colon_location - filename_start; - loc.file = strndup(filename_start, filename_size)[0..filename_size]; - const final_paren = strchr(colon_location+1, ')'); - if (!final_paren) - return loc; - loc.line = atoi(colon_location+1); - return loc; - } -} - /** * Resolve the addresses of `locations` using `debugLineSectionData` * diff --git a/runtime/druntime/src/core/internal/backtrace/macho.d b/runtime/druntime/src/core/internal/backtrace/macho.d index ff6a31b7faa..2f66f991f13 100644 --- a/runtime/druntime/src/core/internal/backtrace/macho.d +++ b/runtime/druntime/src/core/internal/backtrace/macho.d @@ -20,32 +20,39 @@ else version (WatchOS) version (Darwin): import core.stdc.config : c_ulong; -import core.sys.darwin.crt_externs : _NSGetMachExecuteHeader; -import core.sys.darwin.mach.getsect : mach_header_64, getsectiondata; - -struct Image -{ - private mach_header_64* self; +import core.stdc.stdlib : free; +import core.internal.macho.dl; +import core.internal.macho.io; + +struct Image { + SharedObject self; + SharedObject debugObj; + + this(SharedObject self) { + this.self = self; + this.debugObj = self; + + if (!self.hasSection("__DWARF", "__debug_line")) { + auto dsymPath = getDsymDefaultPath(); + this.debugObj = SharedObject.fromFile(dsymPath.ptr); + } + } - static Image openSelf() - { - return Image(_NSGetMachExecuteHeader()); + T processDebugLineSectionData(T)(scope T delegate(const(ubyte)[]) processor) { + return processor(debugObj.getSection("__DWARF", "__debug_line")); } - @property bool isValid() - { - return self !is null; + static Image openSelf() { + return Image(SharedObject.thisExecutable()); } - T processDebugLineSectionData(T)(scope T delegate(const(ubyte)[]) processor) - { - c_ulong size; - auto data = getsectiondata(self, "__DWARF", "__debug_line", &size); - return processor(data[0 .. size]); + @property bool isValid() { + return self.isValid; } - @property size_t baseAddress() - { + @property size_t baseAddress() { + if (debugObj.slide == -1) + return self.slide; return 0; } } diff --git a/runtime/druntime/src/core/internal/macho/dl.d b/runtime/druntime/src/core/internal/macho/dl.d new file mode 100644 index 00000000000..66b3afeb083 --- /dev/null +++ b/runtime/druntime/src/core/internal/macho/dl.d @@ -0,0 +1,228 @@ +/** + * Simplifies working with shared Macho-O objects of the current process. + * + * Copyright: Copyright Kitsunebi Games 2025 + * License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). + * Authors: Luna (the Foxgirl) Nielsen + * Source: $(DRUNTIMESRC core/internal/macho/dl.d) + */ + +module core.internal.macho.dl; + +version (OSX) + version = Darwin; +else version (iOS) + version = Darwin; +else version (TVOS) + version = Darwin; +else version (WatchOS) + version = Darwin; + +version (Darwin): + +import core.sys.darwin.mach.getsect : mach_header_64, getsectiondata, getsectbynamefromheader_64; +import core.sys.darwin.dlfcn; +import core.sys.darwin.fcntl; +import core.sys.darwin.sys.mman; +import core.sys.posix.sys.stat; +import core.sys.darwin.mach.loader; +import core.sys.darwin.mach.dyld : + _dyld_image_count, + _dyld_get_image_name, + _dyld_get_image_header, + _dyld_get_image_vmaddr_slide; + +/** + Enables iterating over the process' currently loaded shared objects. +*/ +struct SharedObjects { +@nogc nothrow: + /// + alias Callback = int delegate(SharedObject); + + /// + static int opApply(scope Callback dg) + { + foreach(i; 0.._dyld_image_count) { + if (int result = dg(SharedObject.fromIndex(i))) + return result; + } + return 0; + } +} + +/** + A loaded mach-o binary. +*/ +struct SharedObject { +@nogc nothrow: +private: + mach_header_64* _header; + ptrdiff_t vmaddr_slide; + const(char)* _name; + +public: + + /** + Returns the shared object with the given index. + */ + static SharedObject fromIndex(uint idx) { + return SharedObject( + cast(mach_header_64*)_dyld_get_image_header(idx), + _dyld_get_image_vmaddr_slide(idx), + _dyld_get_image_name(idx) + ); + } + + /** + Returns the shared object with the given dlopen handle. + + Params: + dlhandle = Handle returned by dlopen + + Returns: + A SharedObject instance matching the given dlhandle, + or an empty handle on failure. + */ + static SharedObject fromHandle(void* dlhandle) { + foreach(so; SharedObjects) { + if (auto hndl = dlopen(so.name, RTLD_NOLOAD)) { + dlclose(hndl); + + if (hndl is dlhandle) + return so; + } + } + return SharedObject.init; + } + + /** + Returns the shared object with the given dlopen handle. + + Params: + path = Path to the object to load. + + Returns: + A SharedObject instance matching the given path, + or an empty handle on failure. + */ + static SharedObject fromFile(const(char)* path) { + if (auto hndl = dlopen(path, 0)) + return SharedObject.fromHandle(hndl); + + mach_header_64* base_header = cast(mach_header_64*)_dyld_get_image_header(0); + + // Try opening and mapping the file. + int fd = open(path, O_RDONLY); + if (fd == -1) + return SharedObject.init; + + stat_t fdInfo; + if (fstat(fd, &fdInfo) == -1) + return SharedObject.init; + + void* data = mmap(null, fdInfo.st_size, PROT_READ, MAP_SHARED, fd, 0); + if (data == MAP_FAILED) + return SharedObject.init; + + // Non-fat mach-o object. + mach_header* hdr = cast(mach_header*)data; + if (hdr.magic == MH_MAGIC || hdr.magic == MH_MAGIC_64) { + if (hdr.cputype != base_header.cputype || hdr.cpusubtype != base_header.cpusubtype) { + munmap(data, fdInfo.st_size); + return SharedObject.init; + } + + return SharedObject(cast(mach_header_64*)data, -1, path); + } + + // Fat binary. + fat_header* fat = cast(fat_header*)data; + if (fat.magic == [0xca, 0xfe, 0xba, 0xbe]) { + fat_entry* entry = cast(fat_entry*)(data+fat_header.sizeof); + foreach(i; 0..fat.count) { + if (entry.cputype == base_header.cputype && entry.cpusubtype == base_header.cpusubtype) { + return SharedObject(cast(mach_header_64*)(data+entry.file_offset), -1, path); + } + + entry++; + } + } + + // Remember to unmap the file. + munmap(data, fdInfo.st_size); + return SharedObject.init; + } + + /** + Returns the object of the current process' executable. + */ + static SharedObject thisExecutable() { + return SharedObject.fromIndex(0); + } + + /** + Whether the object is valid. + */ + @property bool isValid() { + return _header !is null; + } + + /** + The name of this object. + */ + @property const(char)* name() { + return _name; + } + + /** + The base address of this object. + */ + @property void* baseAddress() { + return cast(void*)_header; + } + + /** + The mach header of the image. + */ + @property mach_header_64* header() { + return _header; + } + + /** + The virtual memory slide for the image. + */ + @property ptrdiff_t slide() { + return vmaddr_slide; + } + + /** + Gets whether the given section is present. + */ + bool hasSection(const(char)* segname, const(char)* sectname) { + return getSection(segname, sectname).length > 0; + } + + /** + Gets the given section within the shared object/image. + */ + ubyte[] getSection(const(char)* segname, const(char)* sectname) { + + // mmapped mach-o + if (vmaddr_slide == -1 && _header) { + if (auto sect = getsectbynamefromheader_64(_header, segname, sectname)) { + return (cast(ubyte*)_header+sect.offset)[0..sect.size]; + } + return null; + } + + // linked mach-o + if (_header) { + size_t len; + if (auto data = getsectiondata(_header, segname, sectname, &len)) + return data[0..len]; + } + + return null; + } +} diff --git a/runtime/druntime/src/core/internal/macho/io.d b/runtime/druntime/src/core/internal/macho/io.d new file mode 100644 index 00000000000..b2afc2a9b5e --- /dev/null +++ b/runtime/druntime/src/core/internal/macho/io.d @@ -0,0 +1,77 @@ +/** + * Provides (read-only) memory-mapped I/O for Mach-O files. + * + * + * Copyright: Copyright Kitsunebi Games 2025 + * License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). + * Authors: Luna (the Foxgirl) Nielsen + * Source: $(DRUNTIMESRC core/internal/macho/io.d) + */ + +module core.internal.macho.io; + +version (OSX) + version = Darwin; +else version (iOS) + version = Darwin; +else version (TVOS) + version = Darwin; +else version (WatchOS) + version = Darwin; + +version (Darwin): + +import core.stdc.stdio : snprintf; +import core.stdc.stdlib : free, malloc; +import core.sys.darwin.mach.dyld : _NSGetExecutablePath; + +/** + Returns the path to the process' executable as newly allocated slice. +*/ +char[] thisExePath() { + uint len; + if (_NSGetExecutablePath(null, &len) != -1) + return null; + + auto buffer = cast(char*) malloc(len); + if (!buffer) + return null; + + if (_NSGetExecutablePath(buffer, &len) != 0) { + free(buffer); + return null; + } + + return buffer[0..len]; +} + +/** + Gets the base file name of the given path. +*/ +char[] getBaseName(char[] path) { + size_t i; + foreach_reverse(j; 0..path.length) { + if (path[j] == '/') { + i = j; + break; + } + } + return path[i+1..$]; +} + +/** + Gets the default path of a dSYM file. +*/ +char[] getDsymDefaultPath() { + enum DSYM_SUBPATH_FMT = "%s.dSYM/Contents/Resources/DWARF/%s"; + + char[] exePath = thisExePath(); + char[] exeBase = exePath.getBaseName(); + int len = snprintf(null, 0, DSYM_SUBPATH_FMT, exePath.ptr, exeBase.ptr); + + char* fullname = cast(char*)malloc(len+1); + len = snprintf(fullname, len+1, DSYM_SUBPATH_FMT, exePath.ptr, exeBase.ptr); + + free(exePath.ptr); + return fullname[0..len]; +} \ No newline at end of file diff --git a/runtime/druntime/src/core/sys/darwin/mach/loader.d b/runtime/druntime/src/core/sys/darwin/mach/loader.d index 7713eea9785..a3b2e530730 100644 --- a/runtime/druntime/src/core/sys/darwin/mach/loader.d +++ b/runtime/druntime/src/core/sys/darwin/mach/loader.d @@ -3423,3 +3423,18 @@ struct note_command ulong offset; ulong size; } + +struct fat_header +{ + ubyte[4] magic; + uint count; +} + +struct fat_entry +{ + uint cputype; + uint cpusubtype; + uint file_offset; + uint size; + uint alignment; +} diff --git a/runtime/druntime/src/rt/sections_elf_shared.d b/runtime/druntime/src/rt/sections_elf_shared.d index 83c0f236ef2..40d7919b032 100644 --- a/runtime/druntime/src/rt/sections_elf_shared.d +++ b/runtime/druntime/src/rt/sections_elf_shared.d @@ -84,7 +84,6 @@ else version (Darwin) else static assert(0, "Not implemented for this architecture"); - extern(C) intptr_t _dyld_get_image_slide(const mach_header*) nothrow @nogc; } else version (NetBSD) { diff --git a/runtime/druntime/test/exceptions/Makefile b/runtime/druntime/test/exceptions/Makefile index 7565e425060..5ea164d01b0 100644 --- a/runtime/druntime/test/exceptions/Makefile +++ b/runtime/druntime/test/exceptions/Makefile @@ -67,6 +67,18 @@ ifeq ($(OS),windows) endif include ../common.mak +# Generate dSYMs on macOS. +$(OBJDIR)/%$(DOTEXE).dSYM: $(OBJDIR)/%$(DOTEXE) + dsymutil $< + +ifeq ($(OS),osx) +tests_without_exe = line_trace_21656 rt_trap_exceptions_drt_gdb +exes = $(filter-out $(tests_without_exe),$(TESTS)) +$(exes:%=$(OBJDIR)/%.done): $(OBJDIR)/%.done: $(OBJDIR)/%$(DOTEXE) $(OBJDIR)/%$(DOTEXE).dSYM +$(OBJDIR)/line_trace_21656.done: $(OBJDIR)/line_trace$(DOTEXE).dSYM +$(OBJDIR)/rt_trap_exceptions_drt_gdb.done: $(OBJDIR)/rt_trap_exceptions_drt$(DOTEXE).dSYM +endif + $(ROOT)/line_trace.done: $(ROOT)/line_trace$(DOTEXE) @echo Testing line_trace $(TIMELIMIT)$(ROOT)/line_trace > $@