[Guide]: Setup Remote-debugging Android native code with RustRover + lldb-server (gdbserver mode) Standalone

Introduction

This guide shows how to:

  • Build a Rust binary for Android.
  • Run lldb-server in gdbserver mode inside an Android app.
  • Attach RustRover’s Remote Debug (LLDB) configuration.
  • Diagnose typical errors (linker issues, "Operation not permitted", "connection shut down…", breakpoints not hit, etc.).

It is based on the concrete attached example:

  • Host: macOS (Apple Silicon)
  • NDK: installed under ~/Android/Sdk/ndk/28.2.13676358
  • App ID (package): com.example.nativetestapp
  • Rust project: LLDBRemoteDebug under ~/RustroverProjects/LLDBRemoteDebug
  • Target: aarch64-linux-android (for an arm64-v8a device/emulator, ABI). For the specific example, it was used:
    • Device: Pixel 7 Pro
    • Android: Android 14.0
    • API level: API 34
    • Services: Android Open Source

Adjust paths and names as needed for your setup.


1. Prerequisites

1.1 Tools on macOS

  1. Android Studio (with SDK + NDK)
  2. Rust + rustup
  3. RustRover 2025.2+ (in this example, 2026.1.2 was used). (The Remote Debug configuration type must exist).
  4. ADB on PATH

1.1.1 Android SDK / NDK

Open Android Studio and go to Settings/Preferences | Language & Frameworks | Android SDK:

  • SDK Platforms tab: install at least one recent API (e.g., Android 14).
  • SDK Tools tab: enable the Android SDK Build-Tools and NDK (Side by side) developer tools.

On macOS, the SDK is usually under ~/Library/Android/sdk. To match tools that expect ~/Android/Sdk, create a symlink:

mkdir -p "$HOME/Android"
ln -s "$HOME/Library/Android/sdk" "$HOME/Android/Sdk"

Check NDK:

ls "$HOME/Android/Sdk/ndk"

Versions should be seen like 28.2.13676358, 30.0.14904198, etc.


1.1.2 ADB on PATH

Verify:

ls "$HOME/Android/Sdk/platform-tools/adb"

If present, add to ~/.zshrc:

export ANDROID_SDK_ROOT="$HOME/Android/Sdk"
export PATH="$ANDROID_SDK_ROOT/platform-tools:$PATH"

Reload:

source ~/.zshrc
adb devices

The device/emulator should be listed.

1.2 Android device/emulator

For the smoothest experience:

  • Use an emulator with a "Google APIs" system image (not "Google Play"), e.g.:
    • Device: Pixel 7 Pro
    • System image: Android 14 (API 34), arm64-v8a, Google APIs

Why: "Google Play" and many retail builds have stricter SELinux rules, often blocking apps from opening listening sockets, which breaks gdbserver/lldb-server gdbserver even when everything else is correct.

1.3 Sample Android app (com.example.nativetestapp)

Either use a real app, or create a simple sample app:

  1. In Android Studio: New Project → Native C++.

  2. Name: NativeTestApp.

  3. ApplicationId: e.g. com.example.nativetestapp.

  4. Ensure a debuggable build (default for Debug).

  5. Important: Add INTERNET permission so the app can open sockets:

    In app/src/main/AndroidManifest.xml:

<manifest ...>

    <uses-permission android:name="android.permission.INTERNET"/>

    <application

        ...>

        ...

    </application>

</manifest>
  1. Build & run it once on the device/emulator (Run ▶).

Confirm:

adb shell ps | grep com.example.nativetestapp
adb shell getprop ro.product.cpu.abi

Example result:

  • ABI: arm64-v8a → use aarch64 binaries from NDK.
  • PID: e.g., 5278.

1.4 Rust project for the native binary

In RustRover or via CLI, create a Rust binary project, e.g.:

cd ~/RustroverProjects
cargo new lldbremotedebug
  • This is a binary crate (no [lib] section).
  • We will build it for Android and run it under lldb-server.

1.4.1 Install Rust Android target

rustup target add aarch64-linux-android

1.4.2 Configure cross-linker (.cargo/config.toml)

Rust needs to use the Android NDK toolchain instead of the macOS system linker.

From the project root:

cd ~/RustroverProjects/LLDBRemoteDebug
mkdir -p .cargo
nano .cargo/config.toml

Put:

[target.aarch64-linux-android]
linker = "/Users/<user>/Android/Sdk/ndk/28.2.13676358/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang"
ar     = "/Users/<user>/Android/Sdk/ndk/28.2.13676358/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar"

Check that both paths exist:

ls "/Users/<user>/Android/Sdk/ndk/28.2.13676358/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang"
ls "/Users/<user>/Android/Sdk/ndk/28.2.13676358/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar"

If the NDK version differs, adjust 28.2.13676358 accordingly.


1.4.3 Build for Android

cd ~/RustroverProjects/LLDBRemoteDebug
cargo clean
cargo build --target aarch64-linux-android

Check output:

ls target/aarch64-linux-android/debug

Example:

LLDBRemoteDebug  LLDBRemoteDebug.d  build  deps  examples  incremental

Use target/aarch64-linux-android/debug/LLDBRemoteDebug as both the symbol file and the binary that runs on the device.


2. Preparing lldb-server and the binary on the device

2.1 Choose the correct lldb-server (ABI)

From the NDK:

  • Device ABI: arm64-v8a → use aarch64 folder.

Path example:

/Users/<user>/Android/Sdk/ndk/28.2.13676358/toolchains/llvm/prebuilt/darwin-x86_64/lib/clang/19/lib/linux/aarch64/lldb-server

2.2 Push lldb-server and move it into the app dir

# Push lldb-server to /data/local/tmp
adb push "/Users/<user>/Android/Sdk/ndk/28.2.13676358/toolchains/llvm/prebuilt/darwin-x86_64/lib/clang/19/lib/linux/aarch64/lldb-server" /data/local/tmp/lldb-server
adb shell chmod 755 /data/local/tmp/lldb-server
# Copy into app's internal storage and secure it
adb shell "run-as com.example.nativetestapp cp /data/local/tmp/lldb-server /data/data/com.example.nativetestapp/lldb-server"
adb shell "run-as com.example.nativetestapp chmod 700 /data/data/com.example.nativetestapp/lldb-server"

Why move it? On many devices, the app user can’t run binaries directly from /data/local/tmp and may get a Permission denied error. Copying into /data/data/appID/ under the app user bypasses this.

2.3 Push the Rust binary into the app dir

# From project root
cd ~/RustroverProjects/LLDBRemoteDebug
# Push the binary
adb push "target/aarch64-linux-android/debug/LLDBRemoteDebug" /data/local/tmp/lldbremotedebug
# Move into app's dir and make it executable
adb shell "run-as com.example.nativetestapp cp /data/local/tmp/lldbremotedebug /data/data/com.example.nativetestapp/lldbremotedebug"
adb shell "run-as com.example.nativetestapp chmod 700 /data/data/com.example.nativetestapp/lldbremotedebug"

Now the app user can execute both lldb-server and the Rust binary inside its sandbox.


3. Starting lldb-server in gdbserver mode

Let the lldb-server launch the Rust binary; that gives the cleanest 1:1 mapping between the binary on the device and the symbol file on the host.

3.1 Forward the port

adb forward tcp:5039 tcp:5039

3.2 Launch lldb-server and the Rust binary

Run this from Mac:

adb shell "run-as com.example.nativetestapp /data/data/com.example.nativetestapp/lldb-server gdbserver :5039 /data/data/com.example.nativetestapp/lldbremotedebug"
  • If everything is OK, this command will "hang" with no error: lldb-server is now:
    • listening on: 5039 inside the app context, and
    • has launched and suspended lldbremotedebug at the entry point.
  • If Operation not permitted is shown or another error, see the Troubleshooting section.

4. Configuring RustRover Remote Debug (LLDB)

In RustRover:

  1. Run | Edit Configurations…
  2. Click + → Remote Debug.
  3. Fill the fields:
  • Name: RemoteDebugLLDB (or any name)
  • Debugger: LLDB-21 (bundled)
  • 'process connect' URL: connect://localhost:5039

Important: the connect:// prefix must be included. Omitting it leads to "unsupported connection URL:..." or similar errors.

4.1 Symbol file

Use the binary built for Android:

/Users/<user>/RustroverProjects/LLDBRemoteDebug/target/aarch64-linux-android/debug/lldbremotedebug

(Use the file chooser in RustRover to avoid typos.)

  • This executable contains debug info, so it’s enough as a symbol file.

4.2 Sysroot

  • Leave empty for this simple case (NDK sysroot can be configured later if system libraries need to be debugged).

4.3 Path mappings

Add one row and use the folder picker:

  • Remote: /data/data/com.example.nativetestapp/ (or narrower, e.g.,/data/data/com.example.nativetestapp/lldbremotedebug)
  • Local: /Users/<user>/RustroverProjects/LLDBRemoteDebug/

RustRover uses this to translate paths found in debug info (remote) into local file system locations so it can open the source files and resolve breakpoints.

If RustRover shows "Path mapping is invalid: local path does not exist", verify the path in Terminal:

cd /Users/<user>/RustroverProjects/LLDBRemoteDebug
ls -d target/aarch64-linux-android/debug

Then reselect the directory using the folder picker to ensure the path matches exactly.


5. Running and using the debugger

  1. Make sure step 3.2 (lldb-server gdbserver: 5039 …) is still running in the terminal with no error.
  2. In RustRover, select the RemoteDebugLLDB configuration.
  3. Click Debug.

In the Debug tool window, it will typically be seen:

  • LLDB internal messages, such as:

Qt support was enabled

This just means LLDB loaded its Qt helpers and pretty-printers. It is not related to the Android app using Qt.

  • A breakpoint listed at something like:
__rustc::rust_panic at panicking.rs:...

This is LLDB’s automatic "stop on Rust panic" breakpoint. LLDB adds it so that if the program panics, the debugger stops in the panic handler, and the stack can be inspected. It appears even if it wasn't set manually, it is expected.

Seeing these messages tells that:

  • LLDB is running correctly on the host, and
  • Rust language support is active.

5.1 Setting and hitting custom breakpoints

The Goal of this step is to confirm that the process running on the device is the same binary built, that the symbol file is correct, and that path mappings are set up properly by stopping execution on a line of the Rust code (not just in the panic handler).

Follow these small steps:

  1. Open a Rust source file In RustRover, open a Rust source file from the project (for example, src/main.rs in the LLDBRemoteDebug crate).
  2. Set a breakpoint on code that runs early Pick a line that will execute soon after the program starts, for example:
fn main() {
    println!("Hello from Android!"); // ← set breakpoint here
}
  • Click in the gutter next to that line to create a breakpoint.

  • The breakpoint should appear in the Debug tool window as an enabled breakpoint.

    image.png

  • If the icon looks “hollow” or shows a warning tooltip, LLDB has not yet resolved it to any loaded code (usually a sign of wrong binary or path mapping, check the Troubleshooting section).

  1. Start or resume the debug session

    • Ensure lldb-server gdbserver is still running on the device (the command from Step 5.1 is still “hanging” with no error).
    • In RustRover, start the Remote Debug (LLDB) configuration if it is not already running.
    • If the debugger is stopped at the entry point, click Resume/Continue to let execution reach the custom breakpoint.
  2. What should be seen when everything is correct:

    • The custom breakpoint icon turns solid, indicating a resolved breakpoint (LLDB has matched it to code in the loaded lldbremotedebug binary).
    • Execution stops on that line in the Rust source.
    • The current line is highlighted in the editor.
    • The call stack shows frames from the LLDBRemoteDebug crate.
    • Local variables can be inspected and stepped through in the code.

    image1.png

  3. If the custom breakpoint never hits, check the following:

    • The correct binary is running on the device
      • The process started by lldb-server should be exactly /data/data/com.example.nativetestapp/lldbremotedebug, not some other app binary.
    • Symbol file is correct in RustRover
      • In the Remote Debug configuration, the Symbol file should point to:
target/aarch64-linux-android/debug/LLDBRemoteDebug
  • for this project.
  • Path mappings are correct
    • Remote path (where the binary and sources live on the device) should map to the local project directory, for example:
      • Remote: /data/data/com.example.nativetestapp/
      • Local: /Users/<user>/RustroverProjects/LLDBRemoteDebug/
  • Build is a Debug build
    • Use a non-optimized (Debug) build so that lines are not heavily inlined or removed by the optimizer. In highly optimized builds, some breakpoints may not resolve as expected.

6. Troubleshooting

6.1 Cargo / build errors

6.1.1 “no library targets found” / .so glob fails

  • Symptom: cargo build --target … --lib or ls target/.../*.so fails.
  • Cause: the crate is a binary-only crate (no [lib] section).
  • Fix: Build the binary instead:
cargo build --target aarch64-linux-android
  • Use the binary as the symbol file: target/aarch64-linux-android/debug/LLDBRemoteDebug.

6.1.2 “can’t find crate for std” (E0463)

  • Cause: Android target is not installed.
  • Fix:
rustup target add aarch64-linux-android

6.1.3 “linking with cc failed” on macOS

  • Symptom: linker error mentioning macOS ld options like --as-needed, -z, --gc-sections.
  • Cause: Rust is using the macOS host linker instead of the Android NDK cross‑linker.
  • Fix: configure .cargo/config.toml as in 1.4.2, then:
cargo clean
cargo build --target aarch64-linux-android

6.2 RustRover configuration issues

6.2.1 Remote Debug configuration type is “Unknown” / broken

  • Symptom: run configuration shows ? Unknown and Run Configuration Error: Broken configuration due to unavailable plugin or invalid configuration data.
  • Likely causes:
    • RustRover version older than 2025.2 (Remote Debug not available).
    • Running in a mode where the debugger plugin is disabled.
  • Fix:
    • Install RustRover 2025.2 EAP or newer.
    • Ensure the built‑in Native Debugging Support / debugger plugins are enabled.
    • Create a new Remote Debug configuration; don’t reuse an imported CLion config.

6.2.2 “unsupported connection URL: ''”

  • Symptom: error dialog when starting debug, mentioning unsupported or empty connection URL.
  • Cause: 'process connect' URL field is empty or missing connect:// prefix.
  • Fix:
    1. Run | Edit Configurations… → select the Remote Debug (LLDB).

    2. Set: connect://localhost:5039

    3. If the error persists, clear and re‑type manually to avoid hidden characters.


6.2.3 “Path mapping is invalid: local path does not exist”

  • Cause: the local directory path is wrong or doesn’t exist yet.

  • Fix:

    1. Verify in Terminal:
    cd /Users/<user>/RustroverProjects/LLDBRemoteDebug
    ls -d target/aarch64-linux-android/debug
    
    1. In the Path Mappings dialog, use the folder picker instead of typing the path.

6.3 Device / lldb-server errors

6.3.1 run-as: Package 'com.example.nativetestapp' is not debuggable

  • Cause: the app is not debuggable (release build or missing android:debuggable="true").
  • Fix: Use a Debug build from Android Studio, or set android:debuggable="true" in the manifest for test builds.

6.3.2 run-as: exec failed for /data/local/tmp/lldb-server: Permission denied

  • Cause: app user cannot execute binaries from /data/local/tmp.
  • Fix: Copy into the app’s data dir and adjust perms (see 2.2):
adb shell "run-as com.example.nativetestapp cp /data/local/tmp/lldb-server /data/data/com.example.nativetestapp/lldb-server"
adb shell "run-as com.example.nativetestapp chmod 700 /data/data/com.example.nativetestapp/lldb-server"

6.3.3 error: failed to connect to client at 'listen://...:5039': Operation not permitted

  • Symptom: when running lldb-server gdbserver :5039 ... as an app user, it is seen:
error: failed to connect to client at 'listen://localhost:5039': Operation not permitted
Attached to process NNNN...
lldb-server-local_build
  • Meaning:
    • lldb-server successfully attaches to the process,
    • but OS security policy (SELinux / app sandbox) disallows opening a listening TCP socket for that app user.
    • lldb-server exits; RustRover sees "Connection shut down by remote side while waiting for reply to initial handshake packet".
  • Checklist:
    1. Correct ABI: use aarch64 lldb-server for arm64-v8a, etc.

    2. INTERNET permission: Ensure the app manifest contains:

      <uses-permission android:name="android.permission.INTERNET"/>
      

      Rebuild and reinstall the app. Without this, newer Android versions often block any socket operations, even to localhost.

    3. Bind to a specific host: Prefer :5039 or localhost:5039 over *:.

      adb shell "run-as com.example.nativetestapp /data/data/com.example.nativetestapp/lldb-server gdbserver :5039 /data/data/com.example.nativetestapp/lldbremotedebug"
      
    4. If Operation not permitted error is still appearing, most likely it's a retail device or a “Google Play” emulator image with a strict SELinux policy that forbids listening sockets in-app contexts. Try the next solutions:

      • Switch to an emulator with a "Google APIs" image (e.g. Android 14 (Google APIs, arm64-v8a)).
      • Or use a rooted / engineering device and run lldb-server as a more privileged user (for internal testing only).
      • If neither is possible, Android Studio’s debugger may need to be used instead of that particular device.

6.3.4 "Connection shut down by remote side while waiting for reply to initial handshake packet"

  • This is RustRover’s side of the same problem:

    • TCP connection to localhost:5039 succeeds,
    • but lldb-server on the device crashed or exited immediately (e.g. because of Operation not permitted, wrong mode, etc.).
  • Also occurs if LLDB is started in platform mode (lldb-server platform --listen ...) but uses RustRover’s gdbserver mode (process connect) → protocol mismatch. Always use:

    lldb-server gdbserver :5039 ...
    

    and connect://localhost:5039 on the host.


6.4 Breakpoints not hit

If the Remote Debug session connects but the custom breakpoints never hit, review the next possible causes:

  1. Wrong binary / symbol file

    • The process on the device must actually be running the same build as in the symbol file.
    • If a Java / Kotlin app is attached and it doesn’t load the Rust binary, the custom Rust breakpoints will never resolve.
    • Using the lldb-server gdbserver :5039 /data/data/.../lldbremotedebug launch pattern ensures the target binary matches the symbol file.
  2. Path mappings

    • Ensure remote paths in the debug info map to the local source tree.
    • For the setup described:
      • Remote: /data/data/com.example.nativetestapp/
      • Local: /Users/<user>/RustroverProjects/LLDBRemoteDebug/
  3. Optimizations / inlined code

    • In highly optimized builds, some lines may be inlined or removed. For a clean debug experience, use unoptimized (Debug) profiles when building the Rust binary.

7. Summary of concrete values for this project

For the example project on macOS:

  • Rust project root: /Users/<user>/RustroverProjects/LLDBRemoteDebug

  • Android target binary (symbol file): /Users/<user>/RustroverProjects/LLDBRemoteDebug/target/aarch64-linux-android/debug/LLDBRemoteDebug

  • App ID: com.example.nativetestapp (with <uses-permission android:name="android.permission.INTERNET"/>)

  • lldb-server on device: /data/data/com.example.nativetestapp/lldb-server (copied from NDK’s aarch64 directory)

  • RustRover Remote Debug (LLDB) config:

    • Name: RemoteDebugLLDB
    • Debugger: LLDB-21
    • 'process connect' URL: connect://localhost:5039
    • Symbol file: /Users/<user>/RustroverProjects/LLDBRemoteDebug/target/aarch64-linux-android/debug/LLDBRemoteDebug
    • Path mapping:
      • Remote: /data/data/com.example.nativetestapp/
      • Local: /Users/<user>/RustroverProjects/LLDBRemoteDebug/
    • lldb-server launch (from macOS):
    adb forward tcp:5039 tcp:5039
    adb shell "run-as com.example.nativetestapp /data/data/com.example.nativetestapp/lldb-server gdbserver :5039 /data/data/com.example.nativetestapp/lldbremotedebug"
    

With these in place on a permissive (Google APIs) emulator or a suitable device, RustRover should connect cleanly, and the Rust breakpoints in lldbremotedebug will hit as expected.

0 out of 0 found this helpful

Please sign in to leave a comment.

Have more questions?

Submit a request