Debugging with ELF Core Dumps on i.MX RT Targets
1. Overview
This application note describes how to collect and analyse ELF core dumps from user-space applications running on the Emcraft uClinux BSP for NXP i.MX RT targets.
When a user-space process on the target terminates from an uncaught fatal signal (SIGSEGV, SIGABRT, SIGBUS, SIGFPE, ...), the kernel can be configured to write an ELF core file capturing the process's register state, memory layout, and loaded shared-library map. The core file is later opened on the development host with the BSP's cross gdb, which reconstructs the full backtrace and variable state as they were at the moment of the crash.
This workflow is the primary tool for diagnosing crashes that cannot be reproduced under a live gdbserver session -- field failures, timing-sensitive bugs, and crashes inside third-party libraries.
target crash --> /var/cores/core.* --> scp to host --> cross-gdb --> btThe i.MX RT targets are ARM Cortex-M7 microcontrollers without an MMU. User-space binaries run in the FDPIC (Function Descriptor Position-Independent Code) format, and the kernel uses fs/binfmt_elf_fdpic.c rather than the mainline ELF loader. All core-dump mechanics described below are the FDPIC variants of the familiar Linux tooling.
2. Target-Side Prerequisites
2.1. Kernel Configuration
Three Linux kernel options control core-dump support:
Option | Required | Purpose |
|---|---|---|
| yes | Run and core-dump FDPIC binaries on no-MMU ARM |
| yes | Master switch for core-dump support |
| yes | Emit ELF-format core files on fatal signals |
Inspect the kernel configuration of your project for these options and enable any that are not set:
$ cd projects/rootfs
$ make kmenuconfigIn the resulting menu, navigate to:
General setup --->
[*] Enable core dump support
[*] Enable ELF core dumpsSave the configuration, then rebuild the kernel:
$ make2.2. Run-Time Settings
Two kernel knobs control when and where a core file is produced. Both have to be set per boot unless you persist them in an init script under etc/:
Allow cores of any size (defaults to 0, i.e. no core).
/ # ulimit -c unlimitedChoose where the core goes. A plain filename places it in the crashing process's current working directory. An absolute path centralises cores in one place. (The
%e(executable name) and%p(PID) substitutions incore_patternare documented incore(5); use them to keep multiple cores from overwriting one another.)
/ # echo '/var/cores/core.%e.%p' > /proc/sys/kernel/core_pattern
/ # mkdir -p /var/cores2.3. Memory Budget
No-MMU Linux does not use demand paging; a process's virtual address space is backed one-to-one by physical RAM. The core file therefore contains the actual memory image of the process and is the size of its resident footprint. On targets with limited RAM (the 1050/1060 ship with 32 MB or less), make sure core_pattern points at a filesystem with enough free space -- an SD card mount or a network mount -- rather than tmpfs, which would consume SDRAM a second time.
2.4. Keep an Unstripped Copy on the Host
Stripping a binary does not affect core-dump generation on the target: a stripped executable produces exactly the same core as its unstripped counterpart, and the GNU build-id (.note.gnu.build-id) that GDB uses to match the two is preserved by strip. The target-side copy can therefore remain stripped -- which is the normal configuration for size-constrained i.MX RT rootfs images.
What matters is the host-side copy that you load into gdb for analysis. GDB's post-mortem backtrace on FDPIC relies on:
ARM EHABI unwind tables (
.ARM.exidx/.ARM.extab), which survivestrip-- they are allocatable sections required at runtime by the C++ exception machinery;function prologue scanning for frames that EHABI does not describe -- works on stripped code;
DWARF debug info for file names, line numbers, local variables and function arguments -- removed by
strip.
In other words, a stripped binary still back-traces correctly on the host but yields hex PCs only. Source listings (list), local variables (info locals), and function arguments require the unstripped copy. Keep the unstripped build output of each user-space application on the host alongside its core files.
For BSP-provided packages this is already taken care of. The pre-built toolchain under tools/arm-buildroot-uclinuxfdpiceabi_sdk-buildroot/ ships a full sysroot containing unstripped copies of the C library, the dynamic loader, and every user-space binary of every enabled package. ACTIVATE.sh exports the sysroot path as $TOOLS_LIBS; pointing gdb set sysroot at it (§4.2) gives post-mortem sessions full debug info for all BSP-provided code without any extra setup.
If you rebuild the toolchain from the BSP's Buildroot tree with make buildroot-sdk, the freshly built sysroot appears under buildroot/output_<defconfig>/host/arm-buildroot-uclinuxfdpiceabi/sysroot/
(for example buildroot/output_arm_cortexm_sdk_lite/host/arm-buildroot-uclinuxfdpiceabi/sysroot/). Point set sysroot there instead of $TOOLS_LIBS.
For your own applications, build with -g and keep the unstripped build output on the host. Only strip the copy that you install into the target image.
3. Triggering and Retrieving a Core
3.1. A Minimal Reproducer
Any process that terminates from an uncaught fatal signal produces a core, so the reproducer can be very small. Save the following as crash_fdpic.c on the host:
/* crash_fdpic.c -- minimal core-dump reproducer */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
static void __attribute__((noinline)) level2(char *p)
{
raise(SIGSEGV);
}
static void __attribute__((noinline)) level1(void)
{
level2((char *)0);
}
int main(void)
{
printf("pid=%d, about to crash\n", (int)getpid());
level1();
return 0;
}The two __attribute__((noinline)) markers keep level1 and level2 visible as separate frames even at higher optimisation levels, which makes the backtrace in §4.3 easier to read.
Build it with the BSP cross-compiler after sourcing the activation script:
$ cd linux-cm-imxrt117x-<version>
$ source ACTIVATE.sh
$ arm-buildroot-uclinuxfdpiceabi-gcc -g -O0 crash_fdpic.c -o crash_fdpic
$ file crash_fdpic
crash_fdpic: ELF 32-bit LSB pie executable, ARM, EABI5 version 1, dynamically linked, interpreter /lib/ld-uClibc.so.0, with debug_info, not stripped-g embeds DWARF debug information (used by GDB on the host for source listings and local variables); -O0 disables optimisations so that stack frames and variables line up with the source code.
The toolchain defaults to the FDPIC binary format required by the no-MMU kernel -- no extra flag is needed.
Keep the resulting crash_fdpic binary on the host unchanged; it is the unstripped host-side copy GDB will later match to the core by build-id (§2.4).
Copy the binary to the target via scp:
$ scp crash_fdpic root@<target-ip>:/tmp/3.2. Capturing a Core
On the target:
/ # ulimit -c unlimited
/ # echo '/var/cores/core.%e.%p' > /proc/sys/kernel/core_pattern
/ # mkdir -p /var/cores
/ # /tmp/crash_fdpic
pid=75, about to crash
Segmentation fault
/ # ls -l /var/cores/
-rw------- 1 root root 544768 Jan 1 09:57 core.crash_fdpic.75The default BSP shell is /bin/hush, which prints just Segmentation fault without the (core dumped) suffix that bash and busybox ash append. The shell message is therefore not a reliable indicator of whether a core was written. Confirm success by the ls -l /var/cores/ output above -- if no file appears there, re-check the prerequisites from §2, most commonly a missing ulimit -c unlimited or a read-only / full filesystem pointed at by core_pattern.
3.3. Copying the Core to the Host
The rootfs project ships dropbear (SSH server), so scp from the host work out of the box:
$ scp root@<target-ip>:/var/cores/core.crash_fdpic.75 .Only the core file has to travel from the target to the host. The executable passed to gdb is the unstripped host-side build output from §3.1 -- the crash_fdpic produced by the cross-compiler, still sitting in your working directory. GDB matches it to the core by build-id, which strip preserves on the target copy, so the host-side build output and the stripped target copy are a valid pair.
If you no longer have the matching build output on the host (for example because the tree has since been rebuilt from modified sources, which changes the build-id), GDB will warn with warning: exec file is newer than core file and symbol resolution will be unreliable. In that case, rebuild from the exact revision that was on the target, or pull the binary back from the target with a second scp -- but remember that the target copy may be stripped, so source listings and local variables will not be available.
4. Host-Side Analysis with Cross-GDB
4.1. Selecting the Right GDB
Use the BSP cross debugger, accessible after sourcing the BSP activation script:
$ cd linux-cm-imxrt117x-<version>
$ source ACTIVATE.sh
$ which arm-buildroot-uclinuxfdpiceabi-gdb
.../tools/arm-buildroot-uclinuxfdpiceabi_sdk-buildroot/bin/arm-buildroot-uclinuxfdpiceabi-gdbIf you have rebuilt the toolchain from the BSP's Buildroot tree (see Using Buildroot to Customize the Toolchain and Applications), the equivalent binary lives at buildroot/output_<defconfig>/host/bin/arm-buildroot-uclinuxfdpiceabi-gdb. Both are functionally identical.
This BSP-specific build of gdb contains four FDPIC core-dump patches (tracked as RM-7623) without which post-mortem sessions either fail to open the core or produce an incorrect backtrace. A stock arm-linux-gnueabi-gdb downloaded from a distribution package repository or the ARM Developer site will not work for these cores.
4.2. Opening a Core
From the BSP top-level directory, with ACTIVATE.sh sourced:
$ arm-buildroot-uclinuxfdpiceabi-gdb -ex "set sysroot $TOOLS_LIBS" -ex "file ./crash_fdpic" -ex "core-file ./core.crash_fdpic.75"Breaking that down:
Command | Why |
|---|---|
| Points GDB at the target sysroot ( |
| The unstripped executable that produced the core. |
| Load the core; GDB reconstructs the process's address space, register state and shared-library map. |
4.3. A Typical Session
(gdb) bt
#0 0x8173af6e in raise () from .../libc.so.0
#1 0x81f4c7a4 in level2 (p=0x0) at crash_fdpic.c:8
#2 0x81f4c7ba in level1 () at crash_fdpic.c:13
#3 0x81f4c7e0 in main () at crash_fdpic.c:19
(gdb) frame 2
#2 0x81f4c7ba in level1 () at crash_fdpic.c:13
13 level2((char *)0);
(gdb) info frame
Stack level 2, frame at 0x81effc38:
pc = 0x81f4c7ba in level1 (crash_fdpic.c:13); saved pc = 0x81f4c7e0
called by frame at 0x81effc48, caller of frame at 0x81effc28
source language c.
Arglist at 0x81effc28, args:
Locals at 0x81effc28, Previous frame's sp is 0x81effc38
Saved registers:
r3 at 0x81effc28, r4 at 0x81effc2c, r7 at 0x81effc30, lr at 0x81effc34
(gdb) info registers
r0 0x0 0
r1 0x81effbe0 2179988448
r2 0x0 0
r3 0xffff0000 4294901760
r4 0x81ed1a00 2179799552
r5 0x81ee13f0 2179863536
r6 0x0 0
r7 0x81effc28 2179988520
r8 0x81effe68 2179989096
r9 0x81ee03fc 2179859452
r10 0x81ed4f78 2179813240
r11 0x0 0
r12 0x81ed1a3c 2179799612
sp 0x81effc28 0x81effc28
lr 0x81f4c7bb -2114664517
pc 0x81f4c7ba 0x81f4c7ba <level1+12>
cpsr 0x1000000 16777216
fpscr <unavailable>bt full, info locals, info args, list, and disassemble all work on frames that carry DWARF information. Frames inside stripped libraries still resolve PC to symbol name but do not show source.
5. Known Cosmetic Messages
The following diagnostics are expected on a healthy FDPIC core-dump session and can be ignored:
warning: shared library handler failed to enable breakpoint- GDB tries to plant a breakpoint in the dynamic loader to observe futuredlopen()calls. In a post-mortem session there are no future events to observe, so the failure is harmless.cpsr 0x01000000 ...- The core target synthesises a generic ARM register set that names the status registercpsr. On a Cortex-M target this field actually holds thexPSR; bit 24 is the T (Thumb) bit rather than bit 5. The BSP cross-GDB is aware of the difference and interprets the value correctly for unwinding; only the label remainscpsr.Empty
.eh_frame- FDPIC executables carry ARM EHABI tables (.ARM.exidx/.ARM.extab) rather than DWARF.eh_frame. The cross-GDB selects EHABI automatically; no extra option is needed.fpscr <unavailable>ininfo registers- The FDPIC core dumper in the current kernel writes only theNT_PRSTATUSand (legacy FPA)NT_PRFPREGregister notes. The Cortex-M VFP state, which would be carried in anNT_ARM_VFPnote, is not emitted; GDB's target description still includesfpscrand therefore reports it as unavailable. Integer registers, PC, LR, SP, and stack memory are fully captured, so backtrace and variable inspection are unaffected. If VFP state is specifically required, attach live withgdbserver(which uses ptrace and returns the full VFP register set).
6. Troubleshooting
Symptom | Likely cause |
|---|---|
|
|
No file appears under |
|
| The binary on the host was rebuilt after the core was captured -- fetch the exact binary that ran on the target. |
| Using an upstream (non-BSP) GDB that is missing the FDPIC core-dump support. Switch to |
Source lines not shown, only hex PCs | Binary was stripped, or |
| The crash corrupted the frame pointer chain before the unwinder reached that frame; higher frames may still be valid -- inspect them with |
7. Further Reading
core(5)- kernel manual page on core file format and/proc/sys/kernel/core_pattern.GDB User Manual, chapter "Core Files" -- generic post-mortem workflow.
Using Buildroot to Customize the Toolchain and Applications -- BSP-level overview of the Buildroot integration and how to rebuild the cross
gdbused in this note.For live-attach debugging,
gdbserveris installed in the rootfs at/usr/bin/gdbserverand complements, rather than replaces, post-mortem core dumps.