Mobile Development 16 min read

Remote Debugging of Android Apps via Custom JDWP Channels

The article describes how to remotely debug Android apps by creating a custom JDWP socket channel that bypasses Android 7.0’s library restrictions using a fake dlsym ELF parser, enabling developers to retrieve runtime state from production builds without prior instrumentation or user cooperation.

Meituan Technology Team
Meituan Technology Team
Meituan Technology Team
Remote Debugging of Android Apps via Custom JDWP Channels

Mobile developers often encounter bugs that cannot be reproduced in a local development environment after a product is released. Traditional approaches such as log collection or pre‑instrumented tracing have clear drawbacks, prompting the need for a tool that can locate online issues without deep user cooperation or prior instrumentation.

The debugging principle is based on the Java Platform Debugger Architecture (JPDA). JPDA consists of three relatively independent modules: JVM TI (the Java Virtual Machine Tool Interface, the debuggee), JDWP (Java Debug Wire Protocol, the communication channel), and JDI (Java Debug Interface, the debugger).

These modules decompose the debugging process into three natural concepts: the debuggee runs on the target VM and can be monitored via JVM TI; the debugger defines the interfaces that users can invoke; JDWP transports messages between the debugger and the debuggee.

JDWP packets are divided into Command Packets (sent from debugger to VM to control execution or query state) and Reply Packets (responses from VM). The protocol defines 18 command groups covering VM, reference types, objects, threads, methods, stacks, events, etc.

On Android, the JPDA framework is realized by adapting JVM TI to the Dalvik/ART VM and supporting JDWP over both ADB and socket transports. To enable remote debugging, a custom JDWP channel based on a socket is required.

Hack‑Native‑JDWP : By modifying the global variable gJdwpOptions to use socket mode and restarting the JDWP thread, a custom communication path can be established. This involves dynamic loading of libart.so or libdvm.so and invoking internal functions such as SetJdwpAllowed, StopJdwp, ParseJdwpOptions, and StartJdwp:

void *handler = dlopen("/system/lib/libart.so", RTLD_NOW);   if (handler == NULL){ LOGD(LOG_TAG,env->NewStringUTF(dlerror())); }   // enable debugging for non‑debuggable builds
 void (*allowJdwp)(bool);
 allowJdwp = (void (*)(bool)) dlsym(handler, "_ZN3art3Dbg14SetJdwpAllowedEb");
 allowJdwp(true);
 void (*pfun)();   // stop existing JDWP thread
 pfun = (void (*)()) dlsym(handler, "_ZN3art3Dbg8StopJdwpEv");
 pfun();   // reconfigure gJdwpOptions
 bool (*parseJdwpOptions)(const std::string&);
 parseJdwpOptions = (bool (*)(const std::string&)) dlsym(handler, "_ZN3art3Dbg16ParseJdwpOptionsERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE");
 std::string options = "transport=dt_socket,address=8000,server=y,suspend=n";
 parseJdwpOptions(options);
 // restart JDWP
 pfun = (void (*)()) dlsym(handler, "_ZN3art3Dbg9StartJdwpEv");
 pfun();

Starting with Android 7.0, the system blocks applications from dynamically linking non‑public NDK libraries, causing dlopen to fail. To bypass this restriction, a custom fake_dlsym implementation can be built by parsing the ELF structure of the loaded library in memory, extracting the dynamic symbol table, string table, and program bits to compute the actual function addresses.

void *fake_dlsym(void *handle, const char *name) {
    int k;
    struct ctx *ctx = (struct ctx *) handle;
    Elf_Sym *sym = (Elf_Sym *) ctx->dynsym;
    char *strings = (char *) ctx->dynstr;
    for (k = 0; k < ctx->nsyms; k++, sym++)
        if (strcmp(strings + sym->st_name, name) == 0) {
            // base address + symbol offset - bias
            return (char *)ctx->load_addr + sym->st_value - ctx->bias;
        }
    return 0;
}

Using this approach, the custom socket channel can be established, and JDWP messages can be forwarded between the debugger and the Android VM. Additional challenges such as Proguard obfuscation (loss of LineNumberTable and LocalVariableTable) are addressed by preserving line number information to allow breakpoint setting and variable inspection.

The front‑end of the debugging tool adopts concepts from LLDB, providing a command‑driven interface for developers. After the message‑forwarding layer is functional, the next step is to wrap the JDI specification to expose higher‑level debugging commands.

Summary : The article presents a comprehensive exploration of remote debugging for Android applications, covering JPDA fundamentals, JDWP protocol details, dynamic library manipulation, ELF parsing to emulate dlopen / dlsym, handling of Android 7.0 restrictions, and the impact of Proguard. The proposed solution enables developers to quickly obtain runtime state from production apps without requiring user cooperation or pre‑embedded instrumentation.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Androiddynamic linkingELFremote debuggingjdwp
Meituan Technology Team
Written by

Meituan Technology Team

Over 10,000 engineers powering China’s leading lifestyle services e‑commerce platform. Supporting hundreds of millions of consumers, millions of merchants across 2,000+ industries. This is the public channel for the tech teams behind Meituan, Dianping, Meituan Waimai, Meituan Select, and related services.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.