Android - Overview: Debugging Native Applications

07 Apr, 21
Tags:
18
0
Android Debugging Toolkit

One question we often get asked is how to get started with native programming and debugging. This article is designed to help set you up for native programming on Android.

Required items

The Android NDK contains the tools required to compile C. Among other things, it also contains GDB, which is required to attach and debug remote processes on Android devices. It's available here:
https://developer.android.com/ndk/downloads

Additionally, ADB is required to get a shell on Androids. This is available in the platform-tools package here:
https://developer.android.com/studio/releases/platform-tools

Enabling ADB on device

ADB is enabled by tapping on the Build Version in About Phone 9 times. Some vendors move or rename the Build Version however it’s generally available on all phones.

There are many in-depth instructions for enabling ADB available online.

If running ADB from Windows, some devices require a Windows device driver to be installed. It’s available here: https://developer.android.com/studio/run/win-usb

To get a shell on the phone, execute adb shell. This shell runs as the shell user which is slightly more privileged than apps.

Example Program

 The following is a small example that will be used to step through via GDB

#include <stdlib.h>
#include <stdio.h>

static
void
    Hello
    (
        char    *Name
    )
{
    printf( "Hello %s\n", Name );
}

int
    main
    (
        int     Argc,
        char    *Argv[]
    )
{
    if( Argc != 2 )
    {
        fprintf( stderr, "Usage: %s <Name>\n", Argv[ 0 ] );
        exit( EXIT_FAILURE );
    }

    Hello( Argv[ 1 ] );

    return EXIT_SUCCESS;
}

This is compiled using the android toolchain. Debugging symbols are included using -g.

$ aarch64-linux-android21-clang -pie -g -o example example.c

Running the example

Android provides the directory /data/local/tmp/ for the shell user to use as a scratch area. While the directory is named tmp, this directory is not wiped each boot.

To run the example, the adb push command it used to upload the file to the device:

$ adb push ./example /data/local/tmp/

Once pushed, it can be run using adb shell:

$ adb shell
android$ cd /data/local/tmp/
android$ ./example World
Hello World

Remote GDB

gdb can be used in conjunction with gdbserver, where gdb is executed locally and gdbserver is run remotely on the phone.

To do this, gdb needs to be able to connect to gdbserver. ADB provides the forward functionality, which allows a port on the local machine to be forwarded to a listening port on Android. Port 12345 will be used for this.

$ adb forward tcp:12345 tcp:12345

 

gdbserver is available in the NDK. It needs to be pushed to the Android device:

adb push ./android-ndk-r21e/prebuilt/android-arm64/gdbserver/gdbserver /data/local/tmp/

 

gdbserver arguments:

$ gdbserver :12345 ./example World

 

gdb is located at android-ndk-r21e/prebuilt/linux-x86_64/bin/gdb. (or <platform>-x86_64 if not using linux). The command target remote :<port> is used to instruct gdb where the remote gdbserver is.

$ ./gdb
(gdb) target remote :12345
Remote debugging using :12345
Reading /data/local/tmp/example from remote target...
warning: File transfers from remote targets can be slow. Use "set sysroot" to access files locally instead.
Reading /data/local/tmp/example from remote target...
Reading symbols from target:/data/local/tmp/example...
Reading /system/bin/linker64 from remote target...
Reading /system/bin/linker64 from remote target...
Reading symbols from target:/system/bin/linker64...
(No debugging symbols found in target:/system/bin/linker64)
0x0000007ff7f53ac0 in __dl__start () from target:/system/bin/linker64

After connecting to remote, gdb downloads the files that are loaded in the remote process. This

can be slow, so to speed up debugging it's possible to keep a shadow copy of shared libraries on the local host, and set their location with set sysroot

For this example, next lets set a breakpoint on the Hello function, and then continue execution:

(gdb) break Hello
Breakpoint 1 at 0x55555566f0: file ./example.c, line 12.
(gdb) continue
Continuing.
Reading /apex/com.android.runtime/lib64/bionic/libdl.so from remote target...
Reading /apex/com.android.runtime/lib64/bionic/libc.so from remote target...
Reading /system/lib64/libnetd_client.so from remote target...
Reading /system/lib64/libc++.so from remote target...
Reading /apex/com.android.runtime/lib64/bionic/libm.so from remote target...

Breakpoint 1, Hello (Name=0x7ffffff6d0 "World") at ./example.c:12
12	    printf( "Hello %s\n", Name );

Finally, lets step over (next) until end of program.

(gdb) next
13	}
(gdb) next
main (Argc=2, Argv=0x7ffffff488) at ./example.c:30
30	    return EXIT_SUCCESS;
(gdb) continue
Continuing.
[Inferior 1 (process 21386) exited normally]
(gdb) 

As expected, the program exits normally. Looking at the shell running gdbserver, we can see the program output:

$ ./gdbserver :12345 ./example World
Process ./example created; pid = 21386
Listening on port 12345
Remote debugging from host 127.0.0.1, port 39129
Hello World

Child exited with status 0