The heuristic method is quick but produces lots of false positives. Another option is to manually reconstitute the call stack from the memory dump. This is relatively easy for debug builds because GCC uses R11 as a frame pointer (FP) and generates the same prologue/epilogue for every function.
For release builds, there is no generic solution. It is necessary to check the generated assembler code as there is no standard prologue/epilogue and R11 is not used as frame pointer.
A typical prologue for a debug ARM function looks like this:
mov     ip, sp
stmfd   sp!, {fp, ip, lr, pc}
sub     fp, ip, #4     /* FP now points to base of stack frame */
sub     sp, sp, #16    /* space for local variables */
noting that: SP = R13, FP = R11, IP = R12, LR = R14, and PC = R15.
This code creates the following stack frame:
Looking at the example session listed in when tracing through the stack heuristically. in which the crash is due to a panic, the FP value is the R11 value; this is 0x6571de70. This gives us the innermost stack frame:
6571de64:   e8 de 71 65 <------------- pointer to previous stack frame 
            74 de 71 65 
            74 fb 16 f8 <------------- Saved return address 
            88 28 03 f8 <------------- FP points to this word
Looking up the saved return address, 0xf816fb74, in the symbol file shows that the current function was called from DDmaChannel::DoCreate().
f816fb50    0198    DDmaTestChannel::DoCreate(int, TDesC8 const *, TVersion const &)
f816fce8    007c    DDmaTestChannel::~DDmaTestChannel(void)
f816fd64    0294    DDmaTestChannel::Request(int, void *, void *)
Using the pointer to the previous stack frame saved into the current frame, we can decode the next frame:
6571ded4:   1c c4 03 64 
            f8 02 00 64 
            10 df 71 65 <------------- pointer to previous stack frame 
            ec de 71 65 
6571dee4:   84 da 01 f8 <------------- saved return address 
            5c fb 16 f8 <------------- start of second stack frame 
            00 4e 40 00 
            00 00 00 00 
Looking up the saved return address, 0xf801da84, in the symbol file shows that DDmaTestChannel::DoCreate() was called from DLogicalDevice::ChannelCreate().
f801d9b4    00f8    DLogicalDevice::ChannelCreate(DLogicalChannelBase *&, TChannelCreateInfo &)
f801daac    01b8    ExecHandler::ChannelCreate(TDesC8 const &, TChannelCreateInfo &, int)
f801dc64    00e4    ExecHandler::ChannelRequest(DLogicalChannelBase *, int, void *, void *)
And here is the third stack frame:
6571df04:   d4 df 71 65 <------------- pointer to previous stack frame 
            14 df 71 65 
            e0 db 01 f8 <------------- saved return address 
            c0 d9 01 f8 <------------- start of third stack frame 
So DLogicalDevice::ChannelCreate() was called from ExecHandler::ChannelCreate().
Note that this mechanical way of walking the stack is valid only for debug functions. For release functions, it is necessary to study the code generated by the compiler.
For completness, this is a typical prologue for a debug THUMB function:
push    { r7, lr }
sub     sp, #28
add     r7, sp, #12 /* R7 is THUMB frame pointer */
and this creates the following stack frame:
A call stack can mix ARM and THUMB frames. Odd return addresses are used for THUMB code and even ones for ARM code.