#895: Rewrote jfrconv to make it a statically linked executable

This commit is contained in:
Andrei Pangin
2024-06-16 22:16:50 +01:00
parent dbcaa4d81a
commit 33a65c8dac
4 changed files with 83 additions and 379 deletions
+7 -9
View File
@@ -44,6 +44,7 @@ ifeq ($(OS),Darwin)
CXXFLAGS += $(FAT_BINARY_FLAGS)
PACKAGE_NAME=async-profiler-$(PROFILER_VERSION)-$(OS_TAG)
MERGE=false
SKIP_IN_RELEASE=true
endif
else
CXXFLAGS += -Wl,-z,defs
@@ -102,7 +103,8 @@ $(PACKAGE_NAME).tar.gz: $(PACKAGE_DIR)
rm -r $(PACKAGE_DIR)
$(PACKAGE_NAME).zip: $(PACKAGE_DIR)
codesign -s "Developer ID" -o runtime --timestamp -v $(PACKAGE_DIR)/$(ASPROF) $(PACKAGE_DIR)/$(LIB_PROFILER)
codesign -s "Developer ID" -o runtime --timestamp -v $(PACKAGE_DIR)/$(ASPROF) $(PACKAGE_DIR)/$(JFRCONV) $(PACKAGE_DIR)/$(LIB_PROFILER)
cat build/$(CONVERTER_JAR) >> $(PACKAGE_DIR)/$(JFRCONV)
ditto -c -k --keepParent $(PACKAGE_DIR) $@
rm -r $(PACKAGE_DIR)
@@ -119,14 +121,10 @@ build/$(ASPROF): src/main/* src/jattach/* src/fdtransfer.h
$(CC) $(CPPFLAGS) $(CFLAGS) -DPROFILER_VERSION=\"$(PROFILER_VERSION)\" -o $@ src/main/*.cpp src/jattach/*.c
strip $@
build/$(JFRCONV): src/launcher/* src/incbin.h $(JAVA_HELPER_CLASSES) build/$(CONVERTER_JAR)
$(CC) $(CPPFLAGS) $(CFLAGS) -DPROFILER_VERSION=\"$(PROFILER_VERSION)\" $(INCLUDES) -o $@ src/launcher/*.cpp -ldl
build/$(JFRCONV).exe: src/launcher/* src/incbin.h $(JAVA_HELPER_CLASSES) build/$(CONVERTER_JAR)
mkdir -p build/bin build/gensrc
(echo -n "const unsigned char CLASS_BYTES[] = {" && hexdump -v -e '1/1 "%u,"' src/helper/one/profiler/EmbeddedClassLoader.class && echo "}; const unsigned char CLASS_BYTES_END = {0};") > build/gensrc/CLASS_BYTES.c
(echo -n "const unsigned char CONVERTER_JAR[] = {" && hexdump -v -e '1/1 "%u,"' build/$(CONVERTER_JAR) && echo "}; const unsigned char CONVERTER_JAR_END = {0};") > build/gensrc/CONVERTER_JAR.c
cmd.exe /C cl /O2 /DPROFILER_VERSION=\"$(PROFILER_VERSION)\" src/launcher/*.cpp build/gensrc/*.c /Fo:build/gensrc/ /Fe:$@
build/$(JFRCONV): src/launcher/* build/$(CONVERTER_JAR)
$(CC) $(CPPFLAGS) $(CFLAGS) -DPROFILER_VERSION=\"$(PROFILER_VERSION)\" -o $@ src/launcher/*.cpp
strip $@
$(SKIP_IN_RELEASE) cat build/$(CONVERTER_JAR) >> $@
build/$(LIB_PROFILER): $(SOURCES) $(HEADERS) $(RESOURCES) $(JAVA_HELPER_CLASSES)
ifeq ($(MERGE),true)
Binary file not shown.
@@ -1,143 +0,0 @@
/*
* Copyright The async-profiler authors
* SPDX-License-Identifier: Apache-2.0
*/
package one.profiler;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
/**
* Loads classes from a JAR file embedded in a shared library.
*/
public class EmbeddedClassLoader extends ClassLoader {
private final ByteBuffer jar;
private final Map<String, int[]> directory;
public EmbeddedClassLoader(ClassLoader parent, ByteBuffer jar) {
super(parent);
this.jar = jar;
this.directory = new HashMap<>();
String specVersion = System.getProperty("java.specification.version");
int currentVersion = specVersion == null || specVersion.startsWith("1.") ? 8 : Integer.parseInt(specVersion);
jar.order(ByteOrder.LITTLE_ENDIAN);
int eocd = jar.limit() - 22;
if (jar.getInt(eocd) != 0x06054b50) {
throw new IllegalStateException("EOCD signature not found");
}
int chdr = jar.getInt(eocd + 16);
while (jar.getInt(chdr) == 0x02014b50) {
int compressedSize = jar.getInt(chdr + 20);
int uncompressedSize = jar.getInt(chdr + 24);
int fileNameLength = jar.getShort(chdr + 28) & 0xffff;
int extraLength = jar.getInt(chdr + 30);
int fileHeaderStart = jar.getInt(chdr + 42);
byte[] fileNameBytes = new byte[fileNameLength];
((Buffer) jar).position(chdr + 46);
jar.get(fileNameBytes);
String fileName = new String(fileNameBytes, StandardCharsets.UTF_8);
if (!fileName.endsWith("/")) {
int[] entry = {fileHeaderStart, compressedSize, uncompressedSize};
if (fileName.startsWith("META-INF/versions/")) {
int p = fileName.indexOf('/', 18);
if (p > 0 && Integer.parseInt(fileName.substring(18, p)) <= currentVersion) {
directory.put(fileName.substring(p + 1), entry);
}
} else {
directory.putIfAbsent(fileName, entry);
}
}
chdr += 46 + fileNameLength + (extraLength & 0xffff) + (extraLength >>> 16);
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = unzip(name.replace('.', '/').concat(".class"));
if (data == null) {
throw new ClassNotFoundException(name);
}
return defineClass(name, data, 0, data.length);
}
@Override
public InputStream getResourceAsStream(String name) {
byte[] data = unzip(name.startsWith("/") ? name.substring(1) : name);
if (data == null) {
return super.getResourceAsStream(name);
}
return new ByteArrayInputStream(data);
}
private byte[] unzip(String name) {
int[] entry = directory.get(name);
if (entry == null) {
return null;
}
int loc = entry[0];
if (jar.getInt(loc) != 0x04034b50) {
throw new IllegalStateException("LOC signature not found");
}
byte[] compressed = new byte[entry[1]];
int extraLength = jar.getInt(loc + 26);
ByteBuffer jarCopy = jar.duplicate();
((Buffer) jarCopy).position(loc + 30 + (extraLength & 0xffff) + (extraLength >>> 16));
jarCopy.get(compressed);
short method = jar.getShort(loc + 8);
if (method == 0) {
return compressed;
} else if (method != 8) {
throw new IllegalStateException("Unsupported compression algorithm");
}
byte[] uncompressed = new byte[entry[2]];
Inflater inf = new Inflater(true);
try {
inf.setInput(compressed);
if (inf.inflate(uncompressed) != uncompressed.length) {
throw new IllegalStateException("Uncompressed size mismatch");
}
return uncompressed;
} catch (DataFormatException e) {
throw new IllegalStateException("Invalid compressed data");
} finally {
inf.end();
}
}
public static Class<?> loadMainClass(ByteBuffer jar) throws ClassNotFoundException {
EmbeddedClassLoader loader = new EmbeddedClassLoader(EmbeddedClassLoader.class.getClassLoader(), jar);
byte[] manifest = loader.unzip("META-INF/MANIFEST.MF");
if (manifest == null) {
throw new IllegalStateException("MANIFEST.MF not found");
}
String s = new String(manifest, StandardCharsets.UTF_8);
int p = s.indexOf("Main-Class:");
if (p < 0) {
throw new IllegalStateException("Main-Class attribute not found");
}
int q = s.indexOf('\n', p += 11);
String mainClass = (q >= 0 ? s.substring(p, q) : s.substring(p)).trim();
return loader.findClass(mainClass);
}
}
+76 -227
View File
@@ -3,273 +3,118 @@
* SPDX-License-Identifier: Apache-2.0
*/
#include <jni.h>
#include <dirent.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include "../incbin.h"
#include <unistd.h>
#ifdef _WIN32
#include <windows.h>
#include <malloc.h>
#ifdef __APPLE__
# include <mach-o/dyld.h>
# define COMMON_JVM_DIR "/Library/Java/JavaVirtualMachines/"
# define CONTENTS_HOME "/Contents/Home"
#else
#include <dirent.h>
#include <dlfcn.h>
#include <limits.h>
# define COMMON_JVM_DIR "/usr/lib/jvm/"
# define CONTENTS_HOME ""
#endif
#define JAVA_EXE "java"
#define APP_BINARY "jfrconv"
static const char VERSION_STRING[] =
"JFR converter " PROFILER_VERSION " built on " __DATE__ "\n";
INCLUDE_HELPER_CLASS(EMBEDDED_CLASS_LOADER, CLASS_BYTES, "one/profiler/EmbeddedClassLoader")
INCBIN(CONVERTER_JAR, "build/jar/jfr-converter.jar")
static char exe_path[PATH_MAX];
static char java_path[PATH_MAX];
#if defined(__APPLE__) || defined(_WIN32)
// There is no arch subdirectory in macOS JDK bundles
# define ARCH ""
#elif defined(__x86_64__)
# define ARCH "amd64"
#elif defined(__i386__)
# define ARCH "i386"
#elif defined(__arm__) || defined(__thumb__)
# define ARCH "arm"
#elif defined(__aarch64__)
# define ARCH "aarch64"
#elif defined(__PPC64__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
# define ARCH "ppc64le"
#elif defined(__riscv) && (__riscv_xlen == 64)
# define ARCH "riscv64"
#elif defined(__loongarch_lp64)
# define ARCH "loongarch64"
#endif
#if defined(__APPLE__)
# define COMMON_JVM_DIR "/Library/Java/JavaVirtualMachines/"
# define CONTENTS_HOME "/Contents/Home"
# define LIB_DIR "lib"
# define LIBJVM "libjvm.dylib"
# define PATH_SEP ":"
# define SLASH '/'
#elif defined(_WIN32)
# define COMMON_JVM_DIR "C:\\Program Files\\Java\\"
# define CONTENTS_HOME ""
# define LIB_DIR "bin"
# define LIBJVM "jvm.dll"
# define PATH_SEP ";"
# define SLASH '\\'
#else
# define COMMON_JVM_DIR "/usr/lib/jvm/"
# define CONTENTS_HOME ""
# define LIB_DIR "lib"
# define LIBJVM "libjvm.so"
# define PATH_SEP ":"
# define SLASH '/'
#endif
#ifdef _WIN32
# define PATH_MAX MAX_PATH
# define dlopen(name, _) LoadLibrary(name)
# define dlsym(lib, name) GetProcAddress((HMODULE)lib, name)
# define realpath(path, buf) _fullpath(buf, path, MAX_PATH)
#endif
typedef jint (*JNI_CreateJavaVM_t)(JavaVM **p_vm, void **p_env, void *vm_args);
static void* load_libjvm(const char* java_home, const char* subdir) {
static bool get_exe_path() {
#ifdef __APPLE__
char buf[PATH_MAX];
if (snprintf(buf, sizeof(buf), "%s/%s/" LIBJVM, java_home, subdir) >= sizeof(buf)) {
return NULL;
}
struct stat statbuf;
if (stat(buf, &statbuf) != 0) {
return NULL;
}
return dlopen(buf, RTLD_NOW);
}
static void* find_libjvm_at(const char* path, const char* path1 = "", const char* path2 = "") {
char buf[PATH_MAX];
if (snprintf(buf, sizeof(buf), "%s%s%s", path, path1, path2) >= sizeof(buf)) {
return NULL;
}
char* java_home = realpath(buf, NULL);
if (java_home == NULL) {
return NULL;
}
char* p = strrchr(java_home, SLASH);
if (p != NULL) {
*p = 0; // strip /java
}
if ((p = strrchr(java_home, SLASH)) != NULL) {
*p = 0; // strip /bin
}
void* libjvm;
if ((libjvm = load_libjvm(java_home, LIB_DIR "/server")) == NULL &&
(libjvm = load_libjvm(java_home, LIB_DIR "/client")) == NULL &&
(libjvm = load_libjvm(java_home, LIB_DIR "/" ARCH "/server")) == NULL &&
(libjvm = load_libjvm(java_home, LIB_DIR "/" ARCH "/client")) == NULL &&
(libjvm = load_libjvm(java_home, "jre/" LIB_DIR "/" ARCH "/server")) == NULL &&
(libjvm = load_libjvm(java_home, "jre/" LIB_DIR "/" ARCH "/client")) == NULL) {
// No libjvm.so found at this path
}
free(java_home);
return libjvm;
}
static void* find_libjvm() {
void* libjvm;
char* java_home = getenv("JAVA_HOME");
if (java_home != NULL && (libjvm = find_libjvm_at(java_home, "/bin/java")) != NULL) {
return libjvm;
}
char* path = getenv("PATH");
char* path_copy;
if (path != NULL && (path_copy = strdup(path)) != NULL) {
for (char* java_bin = strtok(path_copy, PATH_SEP); java_bin != NULL; java_bin = strtok(NULL, PATH_SEP)) {
if ((libjvm = find_libjvm_at(java_bin, "/java")) != NULL) {
free(path_copy);
return libjvm;
}
}
free(path_copy);
}
#ifdef __linux__
if ((libjvm = find_libjvm_at("/etc/alternatives/java")) != NULL) {
return libjvm;
}
#endif
#ifdef _WIN32
WIN32_FIND_DATA entry;
HANDLE dir = FindFirstFile(COMMON_JVM_DIR, &entry);
if (dir != INVALID_HANDLE_VALUE) {
do {
if (entry.cFileName[0] != '.' && (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
if ((libjvm = find_libjvm_at(COMMON_JVM_DIR, entry.cFileName, CONTENTS_HOME "/bin/java")) != NULL) {
CloseHandle(dir);
return libjvm;
}
}
} while (FindNextFile(dir, &entry));
CloseHandle(dir);
}
uint32_t size = sizeof(buf);
return _NSGetExecutablePath(buf, &size) == 0 && realpath(buf, exe_path) != NULL;
#else
DIR* dir = opendir(COMMON_JVM_DIR);
if (dir != NULL) {
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] != '.' && entry->d_type == DT_DIR) {
if ((libjvm = find_libjvm_at(COMMON_JVM_DIR, entry->d_name, CONTENTS_HOME "/bin/java")) != NULL) {
closedir(dir);
return libjvm;
}
}
if (realpath("/proc/self/exe", exe_path) == NULL) {
// realpath() may fail for a path like /proc/[pid]/root/bin/exe
ssize_t size = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
if (size < 0) {
return false;
}
closedir(dir);
exe_path[size] = 0;
}
return true;
#endif
return NULL;
}
static int print_exception(JavaVM* vm, JNIEnv* env) {
env->ExceptionDescribe();
vm->DestroyJavaVM();
return 1;
}
static const char* const* build_cmdline(int argc, char** argv) {
const char** cmd = (const char**)malloc((argc + 5) * sizeof(char*));
int count = 0;
static int run_jvm(void* libjvm, int argc, char** argv) {
JNI_CreateJavaVM_t JNI_CreateJavaVM = (JNI_CreateJavaVM_t)dlsym(libjvm, "JNI_CreateJavaVM");
if (JNI_CreateJavaVM == NULL) {
return 1;
}
JavaVMOption* options = (JavaVMOption*)alloca((argc + 2) * sizeof(JavaVMOption));
int o_count = 0;
options[o_count++].optionString = (char*)"-Dsun.java.command=" APP_BINARY;
options[o_count++].optionString = (char*)"-Xss2M";
cmd[count++] = JAVA_EXE;
cmd[count++] = "-Xss2M";
for (; argc > 0; argc--, argv++) {
if ((strncmp(*argv, "-D", 2) == 0 || strncmp(*argv, "-X", 2) == 0) && (*argv)[2] ||
strncmp(*argv, "-agent", 6) == 0) {
options[o_count++].optionString = *argv;
cmd[count++] = *argv;
} else if (strncmp(*argv, "-J", 2) == 0) {
options[o_count++].optionString = *argv + 2;
cmd[count++] = *argv + 2;
} else {
break;
}
}
JavaVM* vm;
JNIEnv* env;
JavaVMInitArgs args;
args.version = JNI_VERSION_1_6;
args.nOptions = o_count;
args.options = options;
args.ignoreUnrecognized = JNI_TRUE;
cmd[count++] = "-jar";
cmd[count++] = exe_path;
int res = JNI_CreateJavaVM(&vm, (void**)&env, &args);
if (res != 0) {
return res;
for (; argc > 0; argc--, argv++) {
cmd[count++] = *argv;
}
jclass loader = env->DefineClass(EMBEDDED_CLASS_LOADER, NULL, (const jbyte*)CLASS_BYTES, INCBIN_SIZEOF(CLASS_BYTES));
if (loader == NULL) {
return print_exception(vm, env);
cmd[count] = NULL;
return cmd;
}
static bool find_java_at(const char* path, const char* path1 = "", const char* path2 = "") {
if (snprintf(java_path, sizeof(java_path), "%s%s%s/" JAVA_EXE, path, path1, path2) >= sizeof(java_path)) {
return false;
}
jmethodID load_main = env->GetStaticMethodID(loader, "loadMainClass", "(Ljava/nio/ByteBuffer;)Ljava/lang/Class;");
if (load_main == NULL) {
return print_exception(vm, env);
struct stat st;
return stat(java_path, &st) == 0 && S_ISREG(st.st_mode) && (st.st_mode & S_IXUSR) != 0;
}
static void run_java(char* const* cmd) {
// 1. Get java executable from JAVA_HOME
char* java_home = getenv("JAVA_HOME");
if (java_home != NULL && find_java_at(java_home, "/bin")) {
execv(java_path, cmd);
}
jobject jar = env->NewDirectByteBuffer((void*)CONVERTER_JAR, INCBIN_SIZEOF(CONVERTER_JAR));
if (jar == NULL) {
return print_exception(vm, env);
// 2. Try to find java in PATH
execvp(JAVA_EXE, cmd);
// 3. Try /etc/alternatives/java
if (find_java_at("/etc/alternatives")) {
execv(java_path, cmd);
}
jclass main_class = (jclass)env->CallStaticObjectMethod(loader, load_main, jar);
if (main_class == NULL) {
return print_exception(vm, env);
}
jmethodID main_method = env->GetStaticMethodID(main_class, "main", "([Ljava/lang/String;)V");
if (main_method == NULL) {
return print_exception(vm, env);
}
jobjectArray main_args = env->NewObjectArray(argc, env->FindClass("java/lang/String"), NULL);
if (main_args != NULL) {
for (int i = 0; i < argc; i++) {
env->SetObjectArrayElement(main_args, i, env->NewStringUTF(argv[i]));
// 4. Look for java in the common directory
DIR* dir = opendir(COMMON_JVM_DIR);
if (dir != NULL) {
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] != '.' && entry->d_type == DT_DIR) {
if (find_java_at(COMMON_JVM_DIR, entry->d_name, CONTENTS_HOME "/bin")) {
execv(java_path, cmd);
}
}
}
closedir(dir);
}
if (env->ExceptionCheck()) {
return print_exception(vm, env);
}
env->CallStaticVoidMethod(main_class, main_method, main_args);
if (env->ExceptionCheck()) {
return print_exception(vm, env);
}
vm->DestroyJavaVM();
return 0;
}
int main(int argc, char** argv) {
@@ -278,11 +123,15 @@ int main(int argc, char** argv) {
return 0;
}
void* libjvm = find_libjvm();
if (libjvm == NULL) {
fprintf(stderr, "No JDK found. Set JAVA_HOME or ensure java executable is on the PATH.\n");
if (!get_exe_path()) {
fprintf(stderr, "Failed to get executable path\n");
return 1;
}
return run_jvm(libjvm, argc - 1, argv + 1);
const char* const* cmd = build_cmdline(argc - 1, argv + 1);
run_java((char* const*)cmd);
// May reach here only if run_java() fails
fprintf(stderr, "No JDK found. Set JAVA_HOME or ensure java executable is on the PATH\n");
return 1;
}