Как получить трассировку стека для С++ с использованием gcc с информацией о номере линии?

Мы используем трассировку стека в проприетарном assert, например, макросе, чтобы отлавливать ошибки разработчика - при обнаружении ошибки трассировка стека печатается.

Я считаю, что gcc-пары backtrace()/backtrace_symbols() недостаточно:

  1. Имена искажены
  2. Нет информации о строке

Первая проблема может быть решена с помощью abi :: __ cxa_demangle.

Однако вторая проблема более сложная. Я нашел замену для backtrace_symbols(). Это лучше, чем gcc backtrace_symbols(), поскольку он может извлекать номера строк (если они скомпилированы с -g), и вам не нужно компилировать с -rdynamic.

Однако, этот код лицензирован по GNU, поэтому я не могу использовать его в коммерческом коде.

Любое предложение?

P.S.

GDB способен распечатывать аргументы, передаваемые функциям.    Наверное, это уже слишком много, чтобы просить :)

PS 2

Подобный вопрос (спасибо, nobar)

Ответ 1

Не так давно я ответил на похожий вопрос. Вам следует взглянуть на исходный код, доступный для метода # 4, который также печатает номера строк и имена файлов.

  • Способ № 4:

Небольшое улучшение я сделал в методе № 3 для печати номеров строк. Это также можно скопировать для работы с методом № 2.

По сути, он использует addr2line для преобразования адресов в имена файлов и номера строк.

Исходный код ниже печатает номера строк для всех локальных функций. Если вызывается функция из другой библиотеки, вы можете увидеть пару ??:0 вместо имен файлов.

#include <stdio.h>
#include <signal.h>
#include <stdio.h>
#include <signal.h>
#include <execinfo.h>

void bt_sighandler(int sig, struct sigcontext ctx) {

  void *trace[16];
  char **messages = (char **)NULL;
  int i, trace_size = 0;

  if (sig == SIGSEGV)
    printf("Got signal %d, faulty address is %p, "
           "from %p\n", sig, ctx.cr2, ctx.eip);
  else
    printf("Got signal %d\n", sig);

  trace_size = backtrace(trace, 16);
  /* overwrite sigaction with caller address */
  trace[1] = (void *)ctx.eip;
  messages = backtrace_symbols(trace, trace_size);
  /* skip first stack frame (points here) */
  printf("[bt] Execution path:\n");
  for (i=1; i<trace_size; ++i)
  {
    printf("[bt] #%d %s\n", i, messages[i]);

    /* find first occurence of '(' or ' ' in message[i] and assume
     * everything before that is the file name. (Don't go beyond 0 though
     * (string terminator)*/
    size_t p = 0;
    while(messages[i][p] != '(' && messages[i][p] != ' '
            && messages[i][p] != 0)
        ++p;

    char syscom[256];
    sprintf(syscom,"addr2line %p -e %.*s", trace[i], p, messages[i]);
        //last parameter is the file name of the symbol
    system(syscom);
  }

  exit(0);
}


int func_a(int a, char b) {

  char *p = (char *)0xdeadbeef;

  a = a + b;
  *p = 10;  /* CRASH here!! */

  return 2*a;
}


int func_b() {

  int res, a = 5;

  res = 5 + func_a(a, 't');

  return res;
}


int main() {

  /* Install our signal handler */
  struct sigaction sa;

  sa.sa_handler = (void *)bt_sighandler;
  sigemptyset(&sa.sa_mask);
  sa.sa_flags = SA_RESTART;

  sigaction(SIGSEGV, &sa, NULL);
  sigaction(SIGUSR1, &sa, NULL);
  /* ... add any other signal here */

  /* Do something */
  printf("%d\n", func_b());
}

Этот код должен быть скомпилирован как: gcc sighandler.c -o sighandler -rdynamic

Программа выводит:

Got signal 11, faulty address is 0xdeadbeef, from 0x8048975
[bt] Execution path:
[bt] #1 ./sighandler(func_a+0x1d) [0x8048975]
/home/karl/workspace/stacktrace/sighandler.c:44
[bt] #2 ./sighandler(func_b+0x20) [0x804899f]
/home/karl/workspace/stacktrace/sighandler.c:54
[bt] #3 ./sighandler(main+0x6c) [0x8048a16]
/home/karl/workspace/stacktrace/sighandler.c:74
[bt] #4 /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6) [0x3fdbd6]
??:0
[bt] #5 ./sighandler() [0x8048781]
??:0

Ответ 2

Итак, вам нужна автономная функция, которая печатает трассировку стека со всеми функциями, имеющимися в трассировке стека GDB, и которая не завершает работу вашего приложения. Ответ заключается в том, чтобы автоматизировать запуск GDB в неинтерактивном режиме, чтобы выполнять только те задачи, которые вы хотите.

Это делается путем выполнения gdb в дочернем процессе с использованием fork() и создания сценариев для отображения трассировки стека, пока ваше приложение ожидает его завершения. Это может быть выполнено без использования дампа памяти и без прерывания работы приложения. Я узнал, как это сделать, взглянув на этот вопрос: Как лучше вызывать gdb из программы для печати его стека?

Пример, опубликованный с этим вопросом, не работал для меня в точности так, как написано, поэтому здесь моя "исправленная" версия (я запускал ее в Ubuntu 9.04).

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

void print_trace() {
    char pid_buf[30];
    sprintf(pid_buf, "%d", getpid());
    char name_buf[512];
    name_buf[readlink("/proc/self/exe", name_buf, 511)]=0;
    int child_pid = fork();
    if (!child_pid) {           
        dup2(2,1); // redirect output to stderr
        fprintf(stdout,"stack trace for %s pid=%s\n",name_buf,pid_buf);
        execlp("gdb", "gdb", "--batch", "-n", "-ex", "thread", "-ex", "bt", name_buf, pid_buf, NULL);
        abort(); /* If gdb failed to start */
    } else {
        waitpid(child_pid,NULL,0);
    }
}

Как показано в указанном вопросе, GDB предоставляет дополнительные опции, которые вы можете использовать. Например, использование "bt full" вместо "bt" создает еще более подробный отчет (локальные переменные включаются в вывод). Справочные страницы для GDB довольно просты, но полная документация доступна здесь.

Поскольку это основано на GDB, выходные данные включают в себя разделенные имена, номера строк, аргументы функции и, возможно, даже локальные переменные. Кроме того, GDB поддерживает потоки, поэтому вы должны быть в состоянии извлечь некоторые специфичные для потока метаданные.

Вот пример вида трассировки стека, который я вижу с помощью этого метода.

0x00007f97e1fc2925 in waitpid () from /lib/libc.so.6
[Current thread is 0 (process 15573)]
#0  0x00007f97e1fc2925 in waitpid () from /lib/libc.so.6
#1  0x0000000000400bd5 in print_trace () at ./demo3b.cpp:496
2  0x0000000000400c09 in recursive (i=2) at ./demo3b.cpp:636
3  0x0000000000400c1a in recursive (i=1) at ./demo3b.cpp:646
4  0x0000000000400c1a in recursive (i=0) at ./demo3b.cpp:646
5  0x0000000000400c46 in main (argc=1, argv=0x7fffe3b2b5b8) at ./demo3b.cpp:70

Примечание. Я обнаружил, что это несовместимо с использованием valgrind (возможно, из-за использования виртуальной машины Valgrind). Он также не работает, когда вы запускаете программу внутри сеанса GDB (невозможно применить второй экземпляр "ptrace" к процессу).

Ответ 3

Существует серьезное обсуждение по существу того же вопроса: Как сгенерировать stacktrace при сбое моего приложения gcc С++. Предоставляется множество предложений, включая много обсуждений о том, как создавать трассировки стека во время выполнения.

Мой личный любимый ответ из этого потока состоял в том, чтобы включить core dumps, который позволяет вам просмотреть состояние приложения в момент сбоя (включая аргументы функции, номера строк и распущенные имена). Дополнительным преимуществом этого подхода является то, что он не только работает для утверждений, но также и для ошибок сегментации и необработанных исключений.

Различные оболочки Linux используют разные команды для включения дампов ядра, но вы можете сделать это из своего кода приложения с помощью чего-то вроде этого...

#include <sys/resource.h>
...
struct rlimit core_limit = { RLIM_INFINITY, RLIM_INFINITY };
assert( setrlimit( RLIMIT_CORE, &core_limit ) == 0 ); // enable core dumps for debug builds

После сбоя запустите свой любимый отладчик, чтобы проверить состояние программы.

$ kdbg executable core

Вот пример вывода...

alt text

Также можно извлечь трассировку стека из дампа ядра в командной строке.

$ ( CMDFILE=$(mktemp); echo "bt" >${CMDFILE}; gdb 2>/dev/null --batch -x ${CMDFILE} temp.exe core )
Core was generated by `./temp.exe'.
Program terminated with signal 6, Aborted.
[New process 22857]
#0  0x00007f4189be5fb5 in raise () from /lib/libc.so.6
#0  0x00007f4189be5fb5 in raise () from /lib/libc.so.6
#1  0x00007f4189be7bc3 in abort () from /lib/libc.so.6
#2  0x00007f4189bdef09 in __assert_fail () from /lib/libc.so.6
#3  0x00000000004007e8 in recursive (i=5) at ./demo1.cpp:18
#4  0x00000000004007f3 in recursive (i=4) at ./demo1.cpp:19
#5  0x00000000004007f3 in recursive (i=3) at ./demo1.cpp:19
#6  0x00000000004007f3 in recursive (i=2) at ./demo1.cpp:19
#7  0x00000000004007f3 in recursive (i=1) at ./demo1.cpp:19
#8  0x00000000004007f3 in recursive (i=0) at ./demo1.cpp:19
#9  0x0000000000400849 in main (argc=1, argv=0x7fff2483bd98) at ./demo1.cpp:26

Ответ 4

Используйте для него библиотеку glog google. Он имеет новую лицензию BSD.

Он содержит функцию GetStackTrace в файле stacktrace.h.

ИЗМЕНИТЬ

Я нашел здесь http://blog.bigpixel.ro/2010/09/09/stack-unwinding-stack-trace-with-gcc/, что есть утилита addr2line, которая переводит адреса программ в имена файлов и номера строк.

http://linuxcommand.org/man_pages/addr2line1.html

Ответ 5

Поскольку лицензионный код GPL призван помочь вам во время разработки, вы можете просто не включать его в конечный продукт. GPL ограничивает вас распространением кода лицензий GPL, связанного с кодом, не совместимым с GPL. До тех пор, пока вы используете только код GPL внутри, вы должны быть в порядке.

Ответ 6

Здесь альтернативный подход. Макрос debug_assert() программно устанавливает условную точку останова. Если вы работаете в отладчике, вы попадете в точку останова, если выражение assert будет ложным - и вы можете проанализировать текущий стек (программа не заканчивается). Если вы не работаете в отладчике, неудачный debug_assert() заставляет программу прерываться и вы получаете дамп ядра, из которого вы можете проанализировать стек (см. Мой более ранний ответ).

Преимущество этого подхода, по сравнению с обычными утверждениями, заключается в том, что вы можете продолжить запуск программы после запуска debug_assert (при запуске в отладчике). Другими словами, debug_assert() немного более гибкий, чем assert().

   #include <iostream>
   #include <cassert>
   #include <sys/resource.h> 

// note: The assert expression should show up in
// stack trace as parameter to this function
void debug_breakpoint( char const * expression )
   {
   asm("int3"); // x86 specific
   }

#ifdef NDEBUG
   #define debug_assert( expression )
#else
// creates a conditional breakpoint
   #define debug_assert( expression ) \
      do { if ( !(expression) ) debug_breakpoint( #expression ); } while (0)
#endif

void recursive( int i=0 )
   {
   debug_assert( i < 5 );
   if ( i < 10 ) recursive(i+1);
   }

int main( int argc, char * argv[] )
   {
   rlimit core_limit = { RLIM_INFINITY, RLIM_INFINITY };
   setrlimit( RLIMIT_CORE, &core_limit ); // enable core dumps
   recursive();
   }

Примечание. Иногда установка условных точек останова в отладчиках может быть медленной. Установив точку останова программно, производительность этого метода должна быть эквивалентна производительности обычного assert().

Примечание. Как написано, это относится к архитектуре Intel x86 - у других процессоров могут быть разные инструкции для генерации точки останова.

Ответ 7

Немного поздно, но вы можете использовать libbfb, чтобы получить имя файла и linenumber, например refdbg, в symsnarf.c. libbfb внутренне используется addr2line и gdb

Ответ 8

Одним из решений является запуск gdb с "bt" - script в неудавшемся обработчике. Не так просто интегрировать такой запуск gdb, но он даст вам как обратную линию, так и имена args и demangle (или вы можете передать выход gdb через программу С++ filt).

Обе программы (gdb и С++ filt) не будут привязаны к вашему приложению, поэтому GPL не потребует от вас приложения с открытым исходным кодом.

Тот же подход (выполняйте программу GPL), которую вы можете использовать с обратными символами. Просто сгенерируйте список ascii% eip и карту файла exec (/proc/self/maps) и передайте его в отдельный двоичный файл.

Ответ 9

Вы можете использовать DeathHandler - небольшой класс С++, который делает все для вас надежным.

Ответ 10

Я полагаю, что номера строк связаны с текущим значением eip, правильно?

РЕШЕНИЕ 1:
Затем вы можете использовать что-то вроде GetThreadContext(), за исключением того, что вы работаете с linux. Я немного искал Google и нашел нечто похожее, ptrace():

Системный вызов ptrace() обеспечивает означает, что родительский процесс может наблюдать и контролировать выполнение другого процесса, а также изучить и изменить его основной образ и регистры. [...] Родитель может инициировать след вызывающий fork (2) и имеющий результирующий ребенок делает PTRACE_TRACEME, (обычно) с помощью exec (3). В качестве альтернативы родитель может начать след существующего процесса с использованием PTRACE_ATTACH.

Теперь я подумал: вы можете сделать "главную" программу, которая проверяет сигналы, которые отправляются его ребенку, реальной программе, над которой вы работаете. после fork() он вызывает waitid():

Все эти системные вызовы используются для дождитесь изменений состояния у ребенка из вызывающего процесса, и получить информация о ребенке состояние изменилось.

и если SIGSEGV (или что-то подобное) пойман вызов ptrace(), чтобы получить значение eip.

PS: Я никогда не пользовался этими системными вызовами (ну, на самом деле, я их никогда не видел раньше), поэтому я не знаю, возможно ли это, что не может вам помочь. По крайней мере, я надеюсь, что эти ссылки полезны.;)

РЕШЕНИЕ 2: Первое решение довольно сложно, не так ли? Я придумал гораздо более простой способ: используя signal() поймать интересующие вас сигналы и вызвать простую функцию, которая читает eip значение, хранящееся в стеке:

...
signal(SIGSEGV, sig_handler);
...

void sig_handler(int signum)
{
    int eip_value;

    asm {
        push eax;
        mov eax, [ebp - 4]
        mov eip_value, eax
        pop eax
    }

    // now you have the address of the
    // **next** instruction after the
    // SIGSEGV was received
}

Этот синтаксис asm Borland один, просто адаптируйте его к GAS.;)

Ответ 11

Вот мой третий ответ - по-прежнему пытаюсь воспользоваться базовыми дампами.

В вопросе не было совершенно ясно, должны ли макросы "assert-like" прервать приложение (как это делает assert), или они должны были продолжить выполнение после генерации трассировки стека.

В этом ответе я обращаюсь к случаю, когда вы хотите показать трассировку стека и продолжить выполнение. Я написал функцию coredump() ниже для создания дампа ядра, автоматически извлечь трассировку стека из него, а затем продолжить выполнение программы.

Использование такое же, как и для assert(). Разумеется, разница состоит в том, что assert() завершает программу, но coredump_assert() не делает.

   #include <iostream>
   #include <sys/resource.h> 
   #include <cstdio>
   #include <cstdlib>
   #include <boost/lexical_cast.hpp>
   #include <string>
   #include <sys/wait.h>
   #include <unistd.h>

   std::string exename;

// expression argument is for diagnostic purposes (shows up in call-stack)
void coredump( char const * expression )
   {

   pid_t childpid = fork();

   if ( childpid == 0 ) // child process generates core dump
      {
      rlimit core_limit = { RLIM_INFINITY, RLIM_INFINITY };
      setrlimit( RLIMIT_CORE, &core_limit ); // enable core dumps
      abort(); // terminate child process and generate core dump
      }

// give each core-file a unique name
   if ( childpid > 0 ) waitpid( childpid, 0, 0 );
   static int count=0;
   using std::string;
   string pid = boost::lexical_cast<string>(getpid());
   string newcorename = "core-"+boost::lexical_cast<string>(count++)+"."+pid;
   string rawcorename = "core."+boost::lexical_cast<string>(childpid);
   int rename_rval = rename(rawcorename.c_str(),newcorename.c_str()); // try with core.PID
   if ( rename_rval == -1 ) rename_rval = rename("core",newcorename.c_str()); // try with just core
   if ( rename_rval == -1 ) std::cerr<<"failed to capture core file\n";

  #if 1 // optional: dump stack trace and delete core file
   string cmd = "( CMDFILE=$(mktemp); echo 'bt' >${CMDFILE}; gdb 2>/dev/null --batch -x ${CMDFILE} "+exename+" "+newcorename+" ; unlink ${CMDFILE} )";
   int system_rval = system( ("bash -c '"+cmd+"'").c_str() );
   if ( system_rval == -1 ) std::cerr.flush(), perror("system() failed during stack trace"), fflush(stderr);
   unlink( newcorename.c_str() );
  #endif

   }

#ifdef NDEBUG
   #define coredump_assert( expression ) ((void)(expression))
#else
   #define coredump_assert( expression ) do { if ( !(expression) ) { coredump( #expression ); } } while (0)
#endif

void recursive( int i=0 )
   {
   coredump_assert( i < 2 );
   if ( i < 4 ) recursive(i+1);
   }

int main( int argc, char * argv[] )
   {
   exename = argv[0]; // this is used to generate the stack trace
   recursive();
   }

Когда я запускаю программу, она отображает три трассировки стека...

Core was generated by `./temp.exe'.                                         
Program terminated with signal 6, Aborted.
[New process 24251]
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#1  0x00007f2818acbbc3 in abort () from /lib/libc.so.6
#2  0x0000000000401a0e in coredump (expression=0x403303 "i < 2") at ./demo3.cpp:29
#3  0x0000000000401f5f in recursive (i=2) at ./demo3.cpp:60
#4  0x0000000000401f70 in recursive (i=1) at ./demo3.cpp:61
#5  0x0000000000401f70 in recursive (i=0) at ./demo3.cpp:61
#6  0x0000000000401f8b in main (argc=1, argv=0x7fffc229eb98) at ./demo3.cpp:66
Core was generated by `./temp.exe'.
Program terminated with signal 6, Aborted.
[New process 24259]
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#1  0x00007f2818acbbc3 in abort () from /lib/libc.so.6
#2  0x0000000000401a0e in coredump (expression=0x403303 "i < 2") at ./demo3.cpp:29
#3  0x0000000000401f5f in recursive (i=3) at ./demo3.cpp:60
#4  0x0000000000401f70 in recursive (i=2) at ./demo3.cpp:61
#5  0x0000000000401f70 in recursive (i=1) at ./demo3.cpp:61
#6  0x0000000000401f70 in recursive (i=0) at ./demo3.cpp:61
#7  0x0000000000401f8b in main (argc=1, argv=0x7fffc229eb98) at ./demo3.cpp:66
Core was generated by `./temp.exe'.
Program terminated with signal 6, Aborted.
[New process 24267]
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#1  0x00007f2818acbbc3 in abort () from /lib/libc.so.6
#2  0x0000000000401a0e in coredump (expression=0x403303 "i < 2") at ./demo3.cpp:29
#3  0x0000000000401f5f in recursive (i=4) at ./demo3.cpp:60
#4  0x0000000000401f70 in recursive (i=3) at ./demo3.cpp:61
#5  0x0000000000401f70 in recursive (i=2) at ./demo3.cpp:61
#6  0x0000000000401f70 in recursive (i=1) at ./demo3.cpp:61
#7  0x0000000000401f70 in recursive (i=0) at ./demo3.cpp:61
#8  0x0000000000401f8b in main (argc=1, argv=0x7fffc229eb98) at ./demo3.cpp:66

Ответ 12

вот мое решение:

#include <execinfo.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
#include <zconf.h>
#include "regex"

std::string getexepath() {
    char result[PATH_MAX];
    ssize_t count = readlink("/proc/self/exe", result, PATH_MAX);
    return std::string(result, (count > 0) ? count : 0);
}

std::string sh(std::string cmd) {
    std::array<char, 128> buffer;
    std::string result;
    std::shared_ptr<FILE> pipe(popen(cmd.c_str(), "r"), pclose);
    if (!pipe) throw std::runtime_error("popen() failed!");
    while (!feof(pipe.get())) {
        if (fgets(buffer.data(), 128, pipe.get()) != nullptr) {
            result += buffer.data();
        }
    }
    return result;
}


void print_backtrace(void) {
    void *bt[1024];
    int bt_size;
    char **bt_syms;
    int i;

    bt_size = backtrace(bt, 1024);
    bt_syms = backtrace_symbols(bt, bt_size);
    std::regex re("\\[(.+)\\]");
    auto exec_path = getexepath();
    for (i = 1; i < bt_size; i++) {
        std::string sym = bt_syms[i];
        std::smatch ms;
        if (std::regex_search(sym, ms, re)) {
            std::string addr = ms[1];
            std::string cmd = "addr2line -e " + exec_path + " -f -C " + addr;
            auto r = sh(cmd);
            std::regex re2("\\n$");
            auto r2 = std::regex_replace(r, re2, "");
            std::cout << r2 << std::endl;
        }
    }
    free(bt_syms);
}

void test_m() {
    print_backtrace();
}

int main() {
    test_m();
    return 0;
}

выход:

/home/roroco/Dropbox/c/ro-c/cmake-build-debug/ex/test_backtrace_with_line_number
test_m()
/home/roroco/Dropbox/c/ro-c/ex/test_backtrace_with_line_number.cpp:57
main
/home/roroco/Dropbox/c/ro-c/ex/test_backtrace_with_line_number.cpp:61
??
??:0

"??" и "??: 0", так как этот след находится в libc, а не в моем источнике

Ответ 13

AFAICS - все предоставленные решения не будут печатать имена функций и номера строк из общих библиотек. Это то, что мне было нужно, поэтому я изменил решение karlphillip (и некоторые другие ответы на аналогичный вопрос), чтобы разрешить адреса совместно используемых библиотек, используя /proc/id/maps.

#include <stdlib.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <execinfo.h>
#include <stdbool.h>

struct Region { // one mapped file, for example a shared library
    uintptr_t start;
    uintptr_t end;
    char* path;
};

static struct Region* getRegions(int* size) { 
// parse /proc/self/maps and get list of mapped files 
    FILE* file;
    int allocated = 10;
    *size = 0;
    struct Region* res;
    uintptr_t regionStart = 0x00000000;
    uintptr_t regionEnd = 0x00000000;
    char* regionPath = "";
    uintmax_t matchedStart;
    uintmax_t matchedEnd;
    char* matchedPath;

    res = (struct Region*)malloc(sizeof(struct Region) * allocated);
    file = fopen("/proc/self/maps", "r");
    while (!feof(file)) {
        fscanf(file, "%jx-%jx %*s %*s %*s %*s%*[ ]%m[^\n]\n",  &matchedStart, &matchedEnd, &matchedPath);
        bool bothNull = matchedPath == 0x0 && regionPath == 0x0;
        bool similar = matchedPath && regionPath && !strcmp(matchedPath, regionPath);
        if(bothNull || similar) {
            free(matchedPath);
            regionEnd = matchedEnd;
        } else {
            if(*size == allocated) {
                allocated *= 2;
                res = (struct Region*)realloc(res, sizeof(struct Region) * allocated);
            }

            res[*size].start = regionStart;
            res[*size].end = regionEnd;
            res[*size].path = regionPath;
            (*size)++;
            regionStart = matchedStart;
            regionEnd = matchedEnd;
            regionPath = matchedPath;
        }
    }
    return res;
}

struct SemiResolvedAddress {
    char* path;
    uintptr_t offset;
};
static struct SemiResolvedAddress semiResolve(struct Region* regions, int regionsNum, uintptr_t address) {
// convert address from our address space to
// address suitable fo addr2line 
    struct Region* region;
    struct SemiResolvedAddress res = {"", address};
    for(region = regions; region < regions+regionsNum; region++) {
        if(address >= region->start && address < region->end) {
            res.path = region->path;
            res.offset = address - region->start;
        }
    }
    return res;
}

void printStacktraceWithLines(unsigned int max_frames)
{
    int regionsNum;
    fprintf(stderr, "stack trace:\n");

    // storage array for stack trace address data
    void* addrlist[max_frames+1];

    // retrieve current stack addresses
    int addrlen = backtrace(addrlist, sizeof(addrlist) / sizeof(void*));
    if (addrlen == 0) {
        fprintf(stderr, "  <empty, possibly corrupt>\n");
        return;
    }
    struct Region* regions = getRegions(&regionsNum); 
    for (int i = 1; i < addrlen; i++)
    {
        struct SemiResolvedAddress hres =
                semiResolve(regions, regionsNum, (uintptr_t)(addrlist[i]));
        char syscom[256];
        sprintf(syscom, "addr2line -C -f -p -a -e %s 0x%jx", hres.path, (intmax_t)(hres.offset));
        system(syscom);
    }
    free(regions);
}