You may not always have gdb(1) at hand. Here are a couple of other options at your disposal.

#1 Use addr2line to get the crash location

$ cat badmem.c
void function_c() { int *i = (int*)0xdeadbeef; *i = 123; } // <-- line 1
void function_b() { function_c(); }
void function_a() { function_b(); }
int main() { function_a(); return 0; }
$ gcc -g badmem.c -o badmem
$ ./badmem
Segmentation fault

No core dump? You can still get some info.

$ tail -n1 /var/log/syslog
... badmem[1171]: segfault at deadbeef ip 00000000004004da sp 00007fff8825dcd0 error 6 in badmem[400000+1000]
$ echo 00000000004004da | addr2line -Cfe ./badmem
function_c
/home/walter/srcelf/bt/badmem.c:1

#2 Do platform specific stack wizardry

We extend the badmem.c example from above and add non-portable backtrace facilities. (Tested on an x86_64.)

$ cat poormanbt.c 
#include <ucontext.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#define REG_RSP 15
#define REG_RIP 16
void function_c() { int *i = (int*)0xdeadbeef; *i = 123; } // <-- line 7
void function_b(int a, int b, int c) { function_c(); } // <-- line 8
void function_a() { function_b(1, 2, 3); } // <-- line 9

void poormanbt(int signum, siginfo_t *info, void *data) {
  struct ucontext *uc = (struct ucontext*)data;
  unsigned long *l = (unsigned long*)uc->uc_mcontext.gregs[REG_RSP];
  printf("%lx (ip) %lx (sp)\n", uc->uc_mcontext.gregs[REG_RIP], (unsigned long)l);
  for (; *l; l = (unsigned long*)*l) { printf("%lx (ip) %lx (sp)\n", l[1], l[0]); }
  fflush(stdout);
  _exit(1);
}
int main() {
  // prepare bt
  struct sigaction sa = {0,};
  sa.sa_sigaction = poormanbt;
  sigfillset(&sa.sa_mask);
  sa.sa_flags = SA_SIGINFO;
  sigaction(SIGSEGV, &sa, (void*)0);
  // fire badmem
  function_a();
  return 0; // <-- line 28
}

We have it output instruction pointer addresses from the stack at SIGSEGV. This example uses the SA_SIGINFO flag that enables signal stack info to be passed into the signal handler.

$ gcc poormanbt.c -g -o poormanbt
$ ./poormanbt 
4006ba (ip) 7fff61feb150 (sp)
4006dd (ip) 7fff61feb170 (sp)
4006f7 (ip) 7fff61feb180 (sp)
40080e (ip) 7fff61feb240 (sp)
$ ./poormanbt | addr2line -Cfe ./poormanbt
function_c
/home/walter/srcelf/bt/poormanbt.c:7
function_b
/home/walter/srcelf/bt/poormanbt.c:8
function_a
/home/walter/srcelf/bt/poormanbt.c:9
main
/home/walter/srcelf/bt/poormanbt.c:28

But you really don't want to do the above. Enter execinfo.

#3 Use the special purpose execinfo backtrace

This is far more portable.

$ cat libbt.c 
#include <execinfo.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void function_c() { int *i = (int*)0xdeadbeef; *i = 123; } // line 5
void function_b(int a, int b, int c) { function_c(); } // line 6
void function_a() { function_b(1, 2, 3); } // line 7

void libbt(int signum, siginfo_t *info, void *data) {
  int i, j;
  void *buffer[16];
  i = backtrace((void**)&buffer, 16); // <-- line 12
  for (j = 0; j < i; ++j) { printf("%p (ip)\n", buffer[j]); }
  fflush(stdout);
  _exit(1);
}
int main() {
  // prepare bt
  struct sigaction sa = {0,};
  sa.sa_sigaction = libbt;
  sigfillset(&sa.sa_mask);
  sigaction(SIGSEGV, &sa, (void*)0);
  // fire badmem
  function_a();
  return 0; // line 25
}

We changed poormanbt.c around a bit and are using backtrace(3) instead of the ucontext_t. This function seems to know what it's doing.

$ gcc libbt.c -g -o libbt
$ ./libbt 
0x40077c (ip)
0x7f538e77baf0 (ip)
0x40070a (ip)
0x40072d (ip)
0x400747 (ip)
0x40083d (ip)
0x7f538e766c4d (ip)
0x400639 (ip)
$ ./libbt | addr2line -Cfe ./libbt
libbt
/home/walter/srcelf/bt/libbt.c:12
??
??:0
function_c
/home/walter/srcelf/bt/libbt.c:5
function_b
/home/walter/srcelf/bt/libbt.c:6
function_a
/home/walter/srcelf/bt/libbt.c:7
main
/home/walter/srcelf/bt/libbt.c:26
??
??:0
_start
??:0

Useful? Not so often. But it's good to know these methods exist.

registers backtrace c shell