CTO,
Benoit Hamelin
September 23, 2016
In addition to its main use of extending Java programs with native code, the Android NDK enables the development of self-contained programs in C/C++… given a bit of elbow grease. This article presents how to set up building such programs for Android, and debugging them remotely using the NDK’s GDB build.
My main use case for making C programs is to study the system and facilitate development and testing using some scripting tools. Also, you know, an Android phone is a Linux computer (if you look at it from far away). So let’s treat it with the respect it deserves and write C for it.
The Android NDK offers a complete C/C++ toolchain, which the SDK and Android Studio uses to build shared libraries. These libraries provide hooks to be loaded into Android applications, thereby offering services implemented as native code. Since making command-line programs is not a goal pursued through the publication of the NDK, the default link script does not bring in the C runtime initialization, so that shared libraries the program depend on are not loaded implicitly. Furthermore, the build here is a cross-compilation: the target system for the executable program is distinct from the one where the program is built. Therefore, we cannot rely on trivial system dependancies: the dependancies on user-mode GNU/Linux subsystems must be explicitly stated. Therefore, if one uses the NDK tools naively, such as
$ $NDKTOOL/gcc -o mytool mytool.c
the resulting mytool
executable is valid, but crashes when it is run on the device.
In order to properly incorporate the libc
preamble and conclusion for command-line execution, we must subvert the default link script. Thus, the next two commands compile, then link mytool
:
$ $NDKTOOL/gcc -c -I$NDKPLAT/usr/include -o mytool.o mytool.c
$ $NDKTOOL/gcc -o mytool -L$NDKPLAT/usr/lib \
-dynamic-linker=/system/lib/linker \
$NDKPLAT/usr/lib/crtbegin_dynamic.o \
mytool.o \
$NDKPLAT/usr/lib/crtend_android.o -lc -lgcc
Let us go over the details of these various commands.
The toolchain programs from the Android NDK must be explicitly chosen; including this toolchain into the shell’s PATH
runs the possibility of confusing between the NDK toolchain and the system’s native toolchain. To avoid this, we may set the following environment variables:
$ export NDK=/home/benoit/android-ndk-r11c
$ export NDKVER=android-5
$ export NDKPLAT=$NDK/platforms/$NDKVER/arch-arm
$ export NDKTOOL=$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64/arm-linux-androideabi/bin
The first environment variable should be modified to reflect the path obtained from the extraction of the NDK archive. The second variable introduces an interesting aspect of the NDK: it recapitulates (and, therefore, contains) the most recent Android platform, as well as all previous ones. For instance, platform version android-5
selected here corresponds to the application binary interface (ABI) of Android 2.2 (Froyo). As far as my experience has gone, recent platforms are still back-compatible with android-5
, so although I have left behind anything prior to Android 5.0, I still build using the Froyo ABI. Variables 3 and 4 point to the platform (header files and libraries) and build tool directory, respectively.
While the compiler invocation is easily understood (at least by seasoned C programmers), the link editor command line is certainly more complex. Let us break down the parameter list:
-o hello_world
— Generated program will be called hello_world.-L$NDKPLAT/usr/lib
— Link against the libraries of the Android platform.-dynamic-linker=/system/bin/linker
— Find the dynamic linker of the target (Android) system at path /system/bin/linker. This is important, as typically, the link editor for Linux systems is /lib/ld-linux.so.2.$NDKPLAT/usr/lib/crtbegin_dynamic.o
— This is the preamble for self-contained executables that make use of shared objects (including the shared version of libc
).mytool.o
— Object code for the program to build.$NDKPLAT/usr/lib/crtend_android.o
— Common C runtime self-contained executable postamble.That one is easy. We simply use the Android Debug Bridge (ADB) to upload the program on an Android device (or emulator) and then run it from a shell started from ADB. In a nutshell, upload:
$ adb push mytool /data/local/tmp
$ adb shell
root@generic:/ $ cd /data/local/tmp
root@generic:/data/local/tmp $ chmod 755 mytool
root@generic:/data/local/tmp $ ./mytool
/data/local/tmp
is a temporary file directory exactly for this kind of experimental purpose.
In addition to the build toolchain, the NDK comes with a corresponding version of the GNU Debugger, GDB. We use GDB all the time to debug on Linux without much issue (beyond the fact that it’s a command-line debugger), so this should not pose any problem on Android?
Well, there are some difficulties to take into account due to the fact that we’re debugging from a computer host remotely, over an ADB/USB link. In addition, the target computer (the Android device) has a different system organization compared to the host system (the computer from which we build and debug). Thus, by default, GDB does not find the files in the directory structure reported by the debugging server (which runs on the device), and fails to build coherent stack backtraces. This makes the investigation of crashes overwhelmingly difficult.
The solution to this problem is to rebuild a sufficient part of the Android system our program runs in on the host, so that GDB can find it and properly account for stack organization. We start from a shell on the device:
root@generic:/data/local/tmp $ mkdir -p system/bin
root@generic:/data/local/tmp $ cp /system/bin/linker system/bin
root@generic:/data/local/tmp $ cp -R /system/lib system/
root@generic:/data/local/tmp $ chmod -R 666 system
root@generic:/data/local/tmp $ ls system/lib > alllibs
root@generic:/data/local/tmp $ chmod 666 alllibs
We thus created a copy of the dynamic linker and all basic dynamic libraries on the Android device; in my experience, this has proven sufficient to debug my programs. Unfortunately, the ADB file transfer tools do not handle file bundles: this is why we created the listing of the system/lib
directory and stored it in alllibs
. We will use it to download each file in turn.
$ adb pull /data/local/tmp/alllibs alllibs
$ mkdir -p system/bin
$ mkdir -p system/lib
$ adb pull /data/local/tmp/system/bin/linker system/bin
$ for L in `cat alllibs`; do adb pull -p /data/local/tmp/system/lib/$L system/lib/$L; done
$ rm alllibs
We can now delete the transferred partial snapshot that lives on the Android device:
$ adb shell rm -rf /data/local/tmp/system
We are now ready to debug the program remotely, using GDB. The term remotely suggests that the debugger interface runs on a distinct system than the debugged program. This means that the actual debugger runs on the device, and communicates with the client over some link. In this case, the link is a TCP/IP connection. It can be done using wi-fi networking between the host and the device. However, Android emulators do not expose a wi-fi interface to the network where the host lives; as for the host itself, it might be a virtual machine that shares connection with the host system, therefore standing behind NAT. To alleviate all these problems and provide a more private and reliable link, ADB can forward local TCP ports of the host over to TCP ports on the device, through a USB link. We will use this feature.
The first step consists in uploading the GDB server program over to the device, using ADB:
$ adb push $NDK/prebuilt/android-arm/gdbserver/gdbserver /data/local/tmp
$ adb shell chmod 777 /data/local/tmp/gdbserver
We now initiate the forwarding of a TCP port on which GDB will serve hooks for the debugger interface. Let’s use port 10000; it could be any other non-privileged port.
$ adb forward tcp:10000 tcp:10000
Let us now start a shell from which we will run the GDB server. We will debug program mytool
with parameters asdf
and 5432
, running in directory /data/local/tmp
.
$ adb shell
root@generic:/ $ cd /data/local/tmp
root@generic:/data/local/tmp $ ./gdbserver :10000 ./mytool asdf 5432
The Android shell then gets output indicating that the server is ready to initiate the debugging session. The program does not start before the client says so. From another terminal, let we start the client and connect it to the server. Please mind that the host system I run it from is a Linux host; for Windows or Mac OS X, the path would be slightly different. From the GDB prompt, we connect to the debugging server:
$ $NDK/prebuilt/linux-x86_64/bin/gdb
[GDB banner and license info]
(gdb) target remote localhost:10000
[Output indicating connection succeeds.]
GDB now complains that it cannot find all the dynamic libraries it needs to assess how the program interacts with the system. To help it locate all this, we need to set the sysroot
and solib-search-path
GDB variables.
(gdb) set sysroot . # Where we downloaded the system/ directory.
(gdb) set solib-search-path system/lib
At this stage, GDB accounts for all the dynamic libraries used by our Android program. We can make sure by running
(gdb) info sharedlibrary
We may finally set breakpoints and resume program execution that was suspended on start by the debugging server:
(gdb) c
From then on, GDB will be able to track call stacks properly, and report accurate stack backtraces with command bt
.
As with anything GDB, all these settings (from starting the debugging server to resuming program execution) have to be redone every time we initiate a debugging session. Fortunately, all client-side commands can be written to a file, which may then be sourced in GDB on startup (using the source
GDB command).
Core dumps are snapshots of a program state as it is terminated by the system because of an exception (e.g. memory access violation). They are tremendously useful when used in conjunction with programs that bear debugging information, because they contain the stack trace of all running threads at the moment of the crash. These stack traces may be observed using GDB, provided it has been set up as described above.
The first step to using core dumps for debugging is to enable their generation and capture. In the Android shell where the program is meant to run:
root@generic:/ $ ulimit -c unlimited
root@generic:/ $ echo '/data/local/tmp/core_pid%p_user%u_group%g_sig%s_time%t' > /proc/sys/kernel/core_pattern
The latter command enables core generation for all programs, but the former only affects the programs forked from the shell it’s run in. Once you have this, run your buggy program until it crashes. Once this crash happens, you will find the core dump in /data/local/tmp
. To analyze it, download it to the host system, then open the GDB client. From there:
(gdb) file mytool
(gdb) core-file core_[...]
The first command indicates that whatever we do next concerns program mytool
; the second loads the core dump we downloaded from the Android device. At this moment, GDB complains that it cannot find all necessary dynamic libraries, so run the setup steps from the last section that follow the target remote
command.