From a4ba0fbb328277bd15c0de8cf306acf901e3d7cd Mon Sep 17 00:00:00 2001 From: Nikolay Igotti Date: Thu, 25 May 2017 15:50:18 +0300 Subject: [PATCH] Add Linux backtrace support. (#605) --- build.gradle | 259 ++++++++++++++-------------- runtime/src/main/cpp/Exceptions.cpp | 71 +++++++- runtime/src/main/cpp/ExecFormat.cpp | 141 +++++++++++++++ runtime/src/main/cpp/ExecFormat.h | 26 +++ 4 files changed, 364 insertions(+), 133 deletions(-) create mode 100644 runtime/src/main/cpp/ExecFormat.cpp create mode 100644 runtime/src/main/cpp/ExecFormat.h diff --git a/build.gradle b/build.gradle index 607ee79b803..0bb74ec4593 100644 --- a/build.gradle +++ b/build.gradle @@ -23,11 +23,11 @@ convention.plugins.platformInfo = new PlatformInfo() allprojects { if (path != ":dependencies") { - evaluationDependsOn(":dependencies") + evaluationDependsOn(":dependencies") } repositories { - mavenCentral() + mavenCentral() } loadCommandLineProperties() @@ -41,22 +41,22 @@ void setupClang(Project project) { project.convention.plugins.execClang = new org.jetbrains.kotlin.ExecClang(project) project.plugins.withType(NativeComponentPlugin) { - project.model { - toolChains { - clang(Clang) { - clangPath.each { - path it - } + project.model { + toolChains { + clang(Clang) { + clangPath.each { + path it + } - eachPlatform { // TODO: will not work when cross-compiling - [cCompiler, cppCompiler, linker].each { - it.withArguments { it.addAll(hostClangArgs) } - } + eachPlatform { // TODO: will not work when cross-compiling + [cCompiler, cppCompiler, linker].each { + it.withArguments { it.addAll(hostClangArgs) } + } - } - } - } - } + } + } + } + } } } @@ -67,54 +67,56 @@ void setupCompilationFlags() { final String binDir if (isLinux()) { - ext.host = "linux" - ext.targetArgs << - [(ext.host): - ["--sysroot=${gccToolchainDir}/${gnuTriplet}/sysroot"], - "raspberrypi": - ["-target", "armv6-unknown-linux-gnueabihf", - "-mfpu=vfp", "-mfloat-abi=hard", - "--sysroot=$raspberryPiSysrootDir", - // TODO: those two are hacks. - "-I$raspberryPiSysrootDir/usr/include/c++/4.8.3", - "-I$raspberryPiSysrootDir/usr/include/c++/4.8.3/arm-linux-gnueabihf"] - ] + ext.host = "linux" + ext.targetArgs << + [(ext.host): + ["--sysroot=${gccToolchainDir}/${gnuTriplet}/sysroot", + "-DUSE_GCC_UNWIND=1", "-DELFSIZE=64"], + "raspberrypi": + ["-target", "armv6-unknown-linux-gnueabihf", + "-mfpu=vfp", "-mfloat-abi=hard", + "-DUSE_GCC_UNWIND=1", "-DELFSIZE=32", + "--sysroot=$raspberryPiSysrootDir", + // TODO: those two are hacks. + "-I$raspberryPiSysrootDir/usr/include/c++/4.8.3", + "-I$raspberryPiSysrootDir/usr/include/c++/4.8.3/arm-linux-gnueabihf"] + ] } else { - ext.host = "macbook" - ext.targetArgs << - [(ext.host): - ["--sysroot=$macbookSysrootDir", "-mmacosx-version-min=$minMacOsVersion"], - "iphone": - ["-stdlib=libc++", "-arch", "arm64", "-isysroot", "$iphoneSysrootDir", - "-miphoneos-version-min=8.0.0"] + ext.host = "macbook" + ext.targetArgs << + [(ext.host): + ["--sysroot=$macbookSysrootDir", "-mmacosx-version-min=$minMacOsVersion"], + "iphone": + ["-stdlib=libc++", "-arch", "arm64", "-isysroot", "$iphoneSysrootDir", + "-miphoneos-version-min=8.0.0"] // TODO: re-enable after simulator sysroot is available in the dependencies // "iphone_sim": // ["-stdlib=libc++", "-isysroot", "$iphoneSimSysrootDir", "-miphoneos-version-min=8.0.0"], - ] + ] } ext.targetArgs << [ "android_arm32": - ["-target", "arm-linux-androideabi", - "--sysroot=$android_arm32SysrootDir", - // HACKS! - /*"-D_LIBCPP_HAS_MUSL_LIBC",*/ "-DOMIT_BACKTRACE", "-D__ANDROID__", - "-I$android_arm32SysrootDir/usr/include/c++/4.9.x", - "-I$android_arm32SysrootDir/usr/include/c++/4.9.x/arm-linux-androideabi" - ] + ["-target", "arm-linux-androideabi", + "--sysroot=$android_arm32SysrootDir", + "-D__ANDROID__", "-DUSE_GCC_UNWIND=1", "-DELFSIZE=32", + // HACKS! + "-I$android_arm32SysrootDir/usr/include/c++/4.9.x", + "-I$android_arm32SysrootDir/usr/include/c++/4.9.x/arm-linux-androideabi" + ] ] ext.targetList = ext.targetArgs.keySet() as List if (isLinux()) { - binDir = "$gccToolchainDir/$gnuTriplet/bin" - ext.clangArgs.addAll([ - "--gcc-toolchain=$gccToolchainDir", - "-L$llvmDir/lib" - ]) + binDir = "$gccToolchainDir/$gnuTriplet/bin" + ext.clangArgs.addAll([ + "--gcc-toolchain=$gccToolchainDir", + "-L$llvmDir/lib" + ]) } else { - binDir = "$macbookSysrootDir/usr/bin" + binDir = "$macbookSysrootDir/usr/bin" } ext.clangArgs << "-B$binDir" - ext.hostClangArgs = ext.clangArgs.clone() + ext.hostClangArgs = ext.clangArgs.clone() ext.hostClangArgs.addAll(ext.targetArgs[host]) ext.clangPath << binDir @@ -123,22 +125,22 @@ void setupCompilationFlags() { void loadLocalProperties() { if (new File("$project.rootDir/local.properties").exists()) { - Properties props = new Properties() - props.load(new FileInputStream("$project.rootDir/local.properties")) - props.each {prop -> project.ext.set(prop.key, prop.value)} + Properties props = new Properties() + props.load(new FileInputStream("$project.rootDir/local.properties")) + props.each {prop -> project.ext.set(prop.key, prop.value)} } } void loadCommandLineProperties() { if (project.hasProperty("konanc_flags")) { - throw new Error("Specify either -Ptest_flags or -Pbuild_flags.") + throw new Error("Specify either -Ptest_flags or -Pbuild_flags.") } ext.globalBuildArgs = project.hasProperty("build_flags") - ? ext.build_flags.split(' ') : [] + ? ext.build_flags.split(' ') : [] ext.globalTestArgs = project.hasProperty("test_flags") - ? ext.test_flags.split(' ') : [] + ? ext.test_flags.split(' ') : [] ext.testTarget = project.hasProperty("test_target") - ? ext.test_target : null + ? ext.test_target : null } class PlatformInfo { @@ -146,33 +148,33 @@ class PlatformInfo { private final String osArch = System.properties['os.arch'] boolean isMac() { - return osName == 'Mac OS X' + return osName == 'Mac OS X' } boolean isLinux() { - return osName == 'Linux' + return osName == 'Linux' } boolean isAmd64() { - return osArch in ['x86_64', 'amd64'] + return osArch in ['x86_64', 'amd64'] } String getGnuTriplet() { - if (isLinux() && isAmd64()) { - return "x86_64-unknown-linux-gnu" - } else { - throw unsupportedPlatformException() - } + if (isLinux() && isAmd64()) { + return "x86_64-unknown-linux-gnu" + } else { + throw unsupportedPlatformException() + } } String simpleOsName() { - if (isMac()) return 'macos' - if (isLinux()) return 'linux' - throw unsupportedPlatformException() + if (isMac()) return 'macos' + if (isLinux()) return 'linux' + throw unsupportedPlatformException() } Throwable unsupportedPlatformException() { - return new Error("unsupported platform: $osName/$osArch") + return new Error("unsupported platform: $osName/$osArch") } } @@ -188,53 +190,53 @@ task distCompiler(type: Copy) { destinationDir file('dist') from(project(':backend.native').file('build/libs')) { - into('konan/lib') + into('konan/lib') } from(project('Interop').file('Runtime/build/libs')) { - into('konan/lib') + into('konan/lib') } from(project('Interop').file('Indexer/build/libs')) { - into('konan/lib') + into('konan/lib') } from(project('Interop').file('StubGenerator/build/libs')) { - into('konan/lib') + into('konan/lib') } from(project(':backend.native').file('build/external_jars')) { - into('konan/lib') + into('konan/lib') } from(project(':backend.native').file('build/nativelibs')) { - into('konan/nativelib') + into('konan/nativelib') } from(project(':Interop').file('Indexer/build/nativelibs')) { - into('konan/nativelib') + into('konan/nativelib') } from(project(':Interop').file('Runtime/build/nativelibs')) { - into('konan/nativelib') + into('konan/nativelib') } from(project(':llvmDebugInfoC').file('build/libs/debugInfo/shared')) { - into('konan/nativelib') + into('konan/nativelib') } from(file('cmd')) { - fileMode(0755) - into('bin') + fileMode(0755) + into('bin') } from(project(':backend.native').file('konan.properties')) { - into('konan') + into('konan') } // TODO: check if we use native libraries copied to dist (see above) or not. from(project(':tools:helpers').file('build/libs')) { - into('konan/lib') + into('konan/lib') } } @@ -252,8 +254,8 @@ task commonDistRuntime(type: Copy) { // Target independant common part. from(project(':runtime').file("build/${host}Stdlib")) { - include('**') - into('klib/stdlib') + include('**') + into('klib/stdlib') } } @@ -264,25 +266,25 @@ task crossDistRuntime(type: Copy) { targetList.each { target -> task("${target}CrossDistRuntime", type: Copy) { - dependsOn ":runtime:${target}Runtime" - dependsOn ":backend.native:${target}Stdlib" - dependsOn ":backend.native:${target}Start" + dependsOn ":runtime:${target}Runtime" + dependsOn ":backend.native:${target}Stdlib" + dependsOn ":backend.native:${target}Start" - destinationDir file('dist') + destinationDir file('dist') - from(project(':runtime').file("build/$target")) { - include("*.bc") - into("klib/stdlib/$target/native") - } - from(project(':runtime').file("build/${target}Stdlib")) { - include('**') - into('klib/stdlib') - } - from(project(':runtime').file("build/${target}Start/$target/kotlin")) { - include("program.kt.bc") - rename('program.kt.bc', 'start.kt.bc') - into("klib/stdlib/$target/native") - } + from(project(':runtime').file("build/$target")) { + include("*.bc") + into("klib/stdlib/$target/native") + } + from(project(':runtime').file("build/${target}Stdlib")) { + include('**') + into('klib/stdlib') + } + from(project(':runtime').file("build/${target}Start/$target/kotlin")) { + include("program.kt.bc") + rename('program.kt.bc', 'start.kt.bc') + into("klib/stdlib/$target/native") + } } } @@ -298,33 +300,33 @@ task bundle(type: Tar) { dependsOn('crossDist') baseName = "kotlin-native-${simpleOsName()}-${project.konanVersion}" from("$project.rootDir/dist") { - include '**' - exclude 'dependencies' - into baseName + include '**' + exclude 'dependencies' + into baseName } from(project.rootDir) { - include 'DISTRO_README.md' - rename { - return "README.md" - } - into baseName + include 'DISTRO_README.md' + rename { + return "README.md" + } + into baseName } from(project.rootDir) { - include 'samples/**' - include 'INTEROP.md' - include 'RELEASE_NOTES.md' - include 'GRADLE_PLUGIN.md' - exclude '**/gradle.properties' - exclude '**/build' - exclude '**/.gradle' - rename { String fileName -> - if (fileName == "gradle.properties.for_bundle") { - return "gradle.properties" - } else { - return fileName - } - } - into baseName + include 'samples/**' + include 'INTEROP.md' + include 'RELEASE_NOTES.md' + include 'GRADLE_PLUGIN.md' + exclude '**/gradle.properties' + exclude '**/build' + exclude '**/.gradle' + rename { String fileName -> + if (fileName == "gradle.properties.for_bundle") { + return "gradle.properties" + } else { + return fileName + } + } + into baseName } destinationDir = file('.') extension = 'tar.gz' @@ -339,10 +341,9 @@ task demo(type: Exec) { task clean { doLast { - file('dist').traverse(type: FileType.ANY, excludeNameFilter: "dependencies", maxDepth: 0) { - delete it - } - delete bundle.outputs.files + file('dist').traverse(type: FileType.ANY, excludeNameFilter: "dependencies", maxDepth: 0) { + delete it + } + delete bundle.outputs.files } } - diff --git a/runtime/src/main/cpp/Exceptions.cpp b/runtime/src/main/cpp/Exceptions.cpp index 4690f016138..fe2183f502a 100644 --- a/runtime/src/main/cpp/Exceptions.cpp +++ b/runtime/src/main/cpp/Exceptions.cpp @@ -13,19 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#ifndef OMIT_BACKTRACE -#include -#endif +#include #include #include +#ifndef OMIT_BACKTRACE +#if USE_GCC_UNWIND +// GCC unwinder for backtrace. +#include +#else +// Glibc backtrace function. +#include +#endif +#endif // OMIT_BACKTRACE #include "Assert.h" #include "Exceptions.h" +#include "ExecFormat.h" #include "Memory.h" #include "Natives.h" #include "KString.h" #include "Types.h" +namespace { + class KotlinException { public: @@ -53,6 +63,50 @@ class AutoFree { } }; +#if USE_GCC_UNWIND +struct Backtrace { + Backtrace(int count, int skip) : index(0), skipCount(skip), array(nullptr) { + auto result = AllocArrayInstance( + theArrayTypeInfo, count - skipCount, &array); + RuntimeAssert(result != nullptr, "Cannot create backtrace array"); + } + + void setNextElement(const char* element) { + CreateStringFromCString( + element, ArrayAddressOfElementAt(array->array(), index++)); + } + + int index; + int skipCount; + ObjHeader* array; +}; + +_Unwind_Reason_Code depthCountCallback( + struct _Unwind_Context * context, void* arg) { + int* result = reinterpret_cast(arg); + (*result)++; + return _URC_NO_REASON; +} + +_Unwind_Reason_Code unwindCallback( + struct _Unwind_Context* context, void* arg) { + Backtrace* backtrace = reinterpret_cast(arg); + if (backtrace->skipCount > 0) { + backtrace->skipCount--; + return _URC_NO_REASON; + } + unsigned long ip = _Unwind_GetIP(context); + const char* symbol = AddressToSymbol(ip); + char line[512]; + snprintf(line, sizeof(line) - 1, "%s (0x%lx)", + symbol != nullptr ? symbol : "", ip); + backtrace->setNextElement(line); + return _URC_NO_REASON; +} +#endif + +} // namespace + #ifdef __cplusplus extern "C" { #endif @@ -65,6 +119,14 @@ OBJ_GETTER0(GetCurrentStackTrace) { ArrayHeader* array = result->array(); CreateStringFromCString("", ArrayAddressOfElementAt(array, 0)); return result; +#else +#if USE_GCC_UNWIND + int depth = 0; + _Unwind_Backtrace(depthCountCallback, &depth); + // Skips first 3 elements as irrelevant. + Backtrace result(depth, 3); + _Unwind_Backtrace(unwindCallback, &result); + RETURN_OBJ(result.array); #else const int maxSize = 32; void* buffer[maxSize]; @@ -82,11 +144,12 @@ OBJ_GETTER0(GetCurrentStackTrace) { } return result; #endif +#endif // !OMIT_BACKTRACE } void ThrowException(KRef exception) { RuntimeAssert(exception != nullptr && IsInstance(exception, theThrowableTypeInfo), - "Throwing something non-throwable"); + "Throwing something non-throwable"); throw KotlinException(exception); } diff --git a/runtime/src/main/cpp/ExecFormat.cpp b/runtime/src/main/cpp/ExecFormat.cpp new file mode 100644 index 00000000000..cc57ca23a6f --- /dev/null +++ b/runtime/src/main/cpp/ExecFormat.cpp @@ -0,0 +1,141 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if USE_GCC_UNWIND + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "Assert.h" + +namespace { + +#if !defined(ELFSIZE) +#error "Define ELFSIZE to 32 or 64" +#endif + +#define CONCAT(x,y) __CONCAT(x,y) +#define ELFNAME(x) CONCAT(elf,CONCAT(ELFSIZE,CONCAT(_,x))) +#define ELFNAME2(x,y) CONCAT(x,CONCAT(_elf,CONCAT(ELFSIZE,CONCAT(_,y)))) +#define ELFNAMEEND(x) CONCAT(x,CONCAT(_elf,ELFSIZE)) +#define ELFDEFNNAME(x) CONCAT(ELF,CONCAT(ELFSIZE,CONCAT(_,x))) + +#if ELFSIZE == 32 +#define Elf_Ehdr Elf32_Ehdr +#define Elf_Phdr Elf32_Phdr +#define Elf_Shdr Elf32_Shdr +#define Elf_Sym Elf32_Sym +#elif ELFSIZE == 64 +#define Elf_Ehdr Elf64_Ehdr +#define Elf_Phdr Elf64_Phdr +#define Elf_Shdr Elf64_Shdr +#define Elf_Sym Elf64_Sym +#else +#error "Impossible ELFSIZE" +#endif + +struct SymRecord { + Elf_Sym* symtabBegin; + Elf_Sym* symtabEnd; + char* strtab; +}; + +std::vector* symbols = nullptr; +char* mapAddress = nullptr; + +// Unfortunately, symbol tables are stored in ELF sections not mapped +// during regular execution, so we have to map binary ourselves. +Elf_Ehdr* findElfHeader() { + int fd = open("/proc/self/exe", O_RDONLY); + if (fd < 0) return nullptr; + struct stat fd_stat; + if (fstat(fd, &fd_stat) < 0) return nullptr; + void* result = mmap(nullptr, fd_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (result == MAP_FAILED) return nullptr; + return (Elf_Ehdr*)result; +} + +void initSymbols() { + RuntimeAssert(symbols == nullptr, "Init twice"); + symbols = new std::vector(); + Elf_Ehdr* ehdr = findElfHeader(); + if (ehdr == nullptr) return; + RuntimeAssert(strncmp((const char*)ehdr->e_ident, ELFMAG, SELFMAG) == 0, "Must be an ELF"); + char* mapAddress = (char*)ehdr; + Elf_Shdr* shdr = (Elf_Shdr*)(mapAddress + ehdr->e_shoff); + for (int i = 0; i < ehdr->e_shnum; i++) { + if (shdr[i].sh_type == SHT_SYMTAB) { // Static symbol table. + SymRecord record; + record.symtabBegin = (Elf_Sym*)(mapAddress + shdr[i].sh_offset); + record.symtabEnd = (Elf_Sym*)((char*)record.symtabBegin + shdr[i].sh_size); + record.strtab = (char *)(mapAddress + shdr[shdr[i].sh_link].sh_offset); + symbols->push_back(record); + } + if (shdr[i].sh_type == SHT_DYNSYM) { // Dynamic symbol table. + SymRecord record; + record.symtabBegin = (Elf_Sym*)(mapAddress + shdr[i].sh_offset); + record.symtabEnd = (Elf_Sym*)((char*)record.symtabBegin + shdr[i].sh_size); + record.strtab = (char*)(mapAddress + shdr[shdr[i].sh_link].sh_offset); + symbols->push_back(record); + } + } +} + +} // namespace + +extern "C" const char* AddressToSymbol(unsigned long address) { + if (address == 0) return nullptr; + + // First, look up in dynamically loaded symbols. + Dl_info info; + if (dladdr((const void*)address, &info) != 0 && info.dli_sname != nullptr) { + return info.dli_sname; + } + + // Otherwise, consult symbol table of the file. + if (symbols == nullptr) { + initSymbols(); + } + + for (auto record : *symbols) { + auto begin = record.symtabBegin; + auto end = record.symtabEnd; + while (begin < end) { + if (address >= (unsigned long)mapAddress + begin->st_value && + address < (unsigned long)mapAddress + begin->st_value + begin->st_size) { + return &record.strtab[begin->st_name]; + } + begin++; + } + } + return nullptr; +} + +#else + +extern "C" const char* AddressToSymbol(unsigned long address) { + return nullptr; +} + +#endif // USE_GCC_UNWIND diff --git a/runtime/src/main/cpp/ExecFormat.h b/runtime/src/main/cpp/ExecFormat.h new file mode 100644 index 00000000000..ef3d295bb86 --- /dev/null +++ b/runtime/src/main/cpp/ExecFormat.h @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RUNTIME_EXECFORMAT_H +#define RUNTIME_EXECFORMAT_H + +extern "C" { + +const char* AddressToSymbol(unsigned long address); + +} // extern "C" + +#endif // RUNTIME_EXECFORMAT_H