Try   HackMD

Tarantool backtraces design

Проблема

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

Аппрув

@alyapunov @unera

Как сделано сейчас

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

void
backtrace_foreach(backtrace_cb cb, coro_context *coro_ctx, void *cb_ctx);

backtrace_foreach() для стека чужой корутины реализуется через сбор ее libunwind-контекста с последующим проходом по этому контексту в своем стеке для получения информации о фреймах.

callback_cb тип callback'а для обработки информации о фрейме (одинаковый для любого API бэктрейсов):

typedef int (backtrace_cb)(int frameno, void *frameret,
                           const char *func, size_t offset, void *cb_ctx);

Почему нужно переделать

  • На Arm (в том числе M1) реализация libunwind-контекста не позволяет собирать бэктрейсы стеков других корутин из libcoro. В таком случае backtrace_foreach() работает корректно только еслиcoro_context == NULL.
  • Текущий API не разделяет получение контекста для трассировки и резолвинг информации о фреймах с помощью данного контекста. Без этого сложно реализовать 4002.
  • На данный момент Lua-бэктрейсы логически не связаны с С-бэктрейсами, хотя в коде всегда используются вместе с ними.

Новый API

Основная идея API - отделение сбора общего (для Lua и С) контекста бэктрейса (backtrace_collect()) от резолвинга информации о фреймах по данному контексту (backtrace_foreach()) и объединения контекстов (backtrace_append()).
API реализуется через такие же процедуры для Lua-контекста и C-контекста.

/**
 * Backtrace context
 */
struct backtrace {
    /** Number of frames. */
    int frames_count;
    /** Buffer of Lua frame data. */
    struct backtrace_frame frames[BACKTRACE_MAX_FRAMES];
};

/**
 * Fills @a ctx with RIP addresses of frames from
 * @a coro or current stack if @a coro == NULL.
 * @returns number of frames traversed.
 */
int
backtrace_collect(struct backtrace *ctx, coro_context *coro);

/**
 * Resolves frame information from @a ctx
 * and passes it along with @a cb to @a cb callback.
 * @returns number of frames traversed.
 */
int
backtrace_foreach(const struct backtrace *bt, backtrace_cb cb, void *cb_ctx);

/**
 * Appends @a add to @a to
 * @returns number of frames appended.
 */
int
backtrace_append(struct backtrace *to, const struct backtrace *add)

Данные фреймов

Для Lua-бэктрейсов фрейм хранит всю необходимую информацию о функции, для C-бэктрейсов сохраняется адрес исполняемого кода (значений регистра RIP) по аналогии с подходом backtrace() и backtrace_symbols() из <execinfo.h> в Linux.

struct backtrace_frame {
    int is_lua;
    union {
        /* C frame data */
        void *rip;
        /* Lua frame data */
        struct {
            char name[BACKTRACE_LUA_MAX_NAME];
            char source[BACKTRACE_LUA_MAX_NAME];
            int line;
        }
    }
};

Количество сохраняемых фреймов

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

enum {
    BACKTRACE_MAX_FRAMES = 32,
    BACKTRACE_LUA_MAX_NAME = 64,
};

Бэктрейс предков файбера

Файбер хранит в себе экземпляр bt_ctx, содержащий информацию о фиксированном числе фреймов его предков.
При выводе в fiber.info собирается контекст текущего файбера, сливается с контекстом его предков и с помощью backtrace_foreach_ctx() последовательно добавляется в нужную Lua - таблицу.

Возможные проблемы

  • Получение информации о фреймах по адресу исполняемого кода все еще не гарантирует полную работоспособность нового API на всех системах.
  • На текущий момент начало участка Lua-фреймов определяется по имени процедуры lj_BC_FUNCC. В новой реализации необходимо изменить этот подход.

Реализация

Реализация разбивается на 3 основных этапа, первые 2 уже частично реализованы в патче для 4002 (см. PR)
Основная часть реализации будет привязана к патчсету для 6060, так как он вызвал необходимость поменять API.

Подготовительный патч

  • Реализуем тесты на совместимость резолвинга фреймов через контекст libunwind и через буфер значений RIP с корутинами из libcoro.
  • Рефакторим текущую реализации бэктрейсов для удобства добавления нового API.

Реализация API

  • Используем уже существующий кэш фреймов и деманглинг С++-имен для сохранения совместимости со старым API.
  • Для сбора RIP регистров используем unw_backtrace(), для резолвинга фреймов unw_get_accessors()->get_proc_name() из внутреннего функционала libunwind. В случае отсутствия такого функционала используем unw_get_reg(UNW_REG_IP) и dladdr() соответственно.

Переход на новый API

  • Реализуем backtrace_foreach() через новый API.
  • Реализуем патч для 4002 через новый API.
  • При наличии конфигураций, в которых возникают проблемы с новой реализацией, оставляем старый API, если и он не работает, отключаем бэктрейсы в данной реализации.