Получаем стек вызовов в Mac OS X

Я давно хотел добавить поддержку получения стека вызовов к своему Tasks Explorer, но никакой публичной информации на этот счет мне не попадалось, а ковырять систему было некогда да и лень (зима, чтоб ее). С первыми лучиками весеннего солнца задача с получением стека вызов стала казаться не такой уж и скучной и дело сдвинулось.
Начал я с того, что нашел утилиту stackshot разработки Apple, только вот исходники приложения закрыты. Как вариант, stackshot можно было использовать напрямую, но в целом такая идея мне не понравилась, да и никаких новых и полезных знаний это не даст. Тем не менее, stackshot вполне сгодился в качестве отправной точки.

Изначально я думал что придется прибегать к дизассемберу, но все оказалось куда проще. Если посмотреть таблицу экспорта stackshot (nm /usr/libexec/stackshot), то единственный “подозрительный” вызов это syscall. Ведь возможность сбора информации о стеке вызов на уровне пользователя выглядит крайне мало вероятной.

Дальше в ход пошли исходники XNU. В файле xnu/bsd/kern/syscalls.master есть описание фунции stack_snapshot:

365 AUE_STACKSNAPSHOT ALL { int stack_snapshot(pid_t pid, user_addr_t tracebuf, uint32_t tracebuf_size, uint32_t flags, uint32_t dispatch_offset) NO_SYSCALL_STUB; }

Судя по описанию, этот метод не экспортируется для использования конечным пользователем (NO_SYSCALL_STUB), но ничто не мешает вызвать его через syscall. Полагаю что stackshot от Apple именно так и работает.

Далее, в файле xnu/bsd/kern/kdebug.c можно найти функцию stack_snapshot, с довольно подробным описанием что, как и почему.

/*
 * stack_snapshot:   Obtains a coherent set of stack traces for all threads
 *           on the system, tracing both kernel and user stacks
 *           where available. Uses machine specific trace routines
 *           for ppc, ppc64 and x86.
 * Inputs:       uap->pid - process id of process to be traced, or -1
 *           for the entire system
 *           uap->tracebuf - address of the user space destination
 *           buffer
 *           uap->tracebuf_size - size of the user space trace buffer
 *           uap->options - various options, including the maximum
 *           number of frames to trace.
 * Outputs:      EPERM if the caller is not privileged
 *           EINVAL if the supplied trace buffer isn't sanely sized
 *           ENOMEM if we don't have enough memory to satisfy the
 *           request
 *           ENOENT if the target pid isn't found
 *           ENOSPC if the supplied buffer is insufficient
 *           *retval contains the number of bytes traced, if successful
 *           and -1 otherwise. If the request failed due to
 *           tracebuffer exhaustion, we copyout as much as possible.
 */

int stack_snapshot(struct proc *p, register struct stack_snapshot_args *uap, int32_t *retval)

Остается не так уж и много, понять в каком формате в tracebuf будут выходные данные.
Функция kdp_stackshot отвечает за заполнение буфера следующим образом. В начало буфера помещается структура task_snapshot:

struct task_snapshot
{
   uint32_t snapshot_magic;
   int32_t pid;
   uint32_t nloadinfos;
   char ss_flags;
   char p_comm[17];
} __attribute__ ((packed));
typedef struct task_snapshot task_snapshot_t;

За которой может идти несколько структур dyld_uuid_info64 либо dyld_uuid_info в зависимости от архитектуры ОС.

struct dyld_uuid_info64
{
   user64_addr_t imageLoadAddress; /* base address image is mapped into */
   uuid_t imageUUID;           /* UUID of image */
};
struct dyld_uuid_info
{
   user32_addr_t imageLoadAddress; /* base address image is mapped into */
   uuid_t imageUUID;           /* UUID of image */
};

Количество структур содержится в поле nloadinfos, а будет ли структура 32 или 64 битной можно понять по наличию флага kUser64_p в поле ss_flags.

#define kUser64_p 1

При этом, я ни для одного приложения не получил структуры task_snapshot со значением поля nloadinfos отличным от 0.

Далее в буфере размещается структура thread_snapshot, которая служит для описания потока.

struct thread_snapshot
{
   uint32_t snapshot_magic;
   uint32_t nkern_frames;
   uint32_t nuser_frames;
   uint64_t wait_event;
   uint64_t continuation;
   uint64_t thread_id;
   int32_t state;
   char ss_flags;
} __attribute__ ((packed));

И сразу за thread_snapshot идет последовательность из stack_record. Количество записей stack_record определяется на основании значения поля nuser_frames из структуры thread_snapshot.

struct stack_record
{
   addr64_t return_addr;
   addr64_t frame_addr;
} __attribute__ ((packed));

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

Следующим шагом идет преобразование полученных адресов в информацию об именах символов. Этот шаг я решил не усложнять и воспользоваться не менее закрытым приложением atos. atos, несмотря на свою закрытость, довольно прост в использовании и не прихотлив, не то что stackshot:

atos -p PID addr1 addr2 ... addrN

В результате вызова, приведенного выше получается что-то типа такого (адреса должны быть в шестнадцатеричном представлении):

atos -p 170 0x7FFF8353E2DA 0x7FFF83577536 0x7FFF835773E9
mach_msg_trap (in libSystem.B.dylib) + 10
_pthread_start (in libSystem.B.dylib) + 331
thread_start (in libSystem.B.dylib) + 13

Соответственно, достаточно просто создать строку с необходимой командой и выполнить ее при помощи popen.
Вот, собственно, и пример:

#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>

#define TRACEBUFF_LEN 101024

struct task_snapshot
{
    uint32_t snapshot_magic;
    int32_t pid;
    uint32_t nloadinfos;
    char ss_flags;
    char p_comm[17];
} __attribute__ ((packed));
typedef struct task_snapshot task_snapshot_t;

#define STACKSHOT_TASK_SNAPSHOT_MAGIC 0xdecafbad

struct thread_snapshot
{
    uint32_t snapshot_magic;
    uint32_t nkern_frames;
    uint32_t nuser_frames;
    uint64_t wait_event;
    uint64_t continuation;
    uint64_t thread_id;
    int32_t state;
    char ss_flags;
} __attribute__ ((packed));
typedef struct thread_snapshot thread_snapshot_t;

#define STACKSHOT_THREAD_SNAPSHOT_MAGIC 0xfeedface

typedef uint64_t addr64_t;

enum thread_state
{
    TH_WAIT = 0x01,
    TH_SUSP = 0x02,
    TH_RUN = 0x04,
    TH_UNINT = 0x08,
    TH_TERMINATE = 0x10,
    TH_TERMINATE2 = 0x20,
    TH_IDLE = 0x80
};
typedef enum thread_state thread_state_t;

#define kUser64_p 1

typedef __uint64_t user64_addr_t __attribute__((aligned(8)));
typedef __uint32_t user32_addr_t;

struct dyld_uuid_info64
{
    user64_addr_t imageLoadAddress; /* base address image is mapped into */
    uuid_t imageUUID;           /* UUID of image */
};

struct dyld_uuid_info
{
    user32_addr_t imageLoadAddress; /* base address image is mapped into */
    uuid_t imageUUID;           /* UUID of image */
};

#define PROCESS_PID 9460

struct stack_record
{
    addr64_t return_addr;
    addr64_t frame_addr;
} __attribute__ ((packed));
typedef struct stack_record stack_record_t;

int main (int argc, const char * argv[])
{
    if( argc == 1 )
    {
        printf( "Usage: stack_snapshot PIDn" );
        exit(-1);
    }
    pid_t pid = atoi( argv[1] );
   
    char tracebuf[TRACEBUFF_LEN];
    char *tracepos = tracebuf;
    int result = syscall(SYS_stack_snapshot, pid, tracebuf, TRACEBUFF_LEN, 0);
    if( -1 == result )
    {
        return -1;
    }

    task_snapshot_t *snapshot = (task_snapshot_t*)tracepos;
    if (STACKSHOT_TASK_SNAPSHOT_MAGIC != snapshot->snapshot_magic
        || pid != snapshot->pid)
    {
        return -1;
    }

    tracepos += sizeof(task_snapshot_t);
    if (0 < snapshot->nloadinfos)
    {
        int uuid_info_size = snapshot->ss_flags & kUser64_p ?
            sizeof(struct dyld_uuid_info64) :
            sizeof(struct dyld_uuid_info);
        tracepos += snapshot->nloadinfos * uuid_info_size;
    }

    char buff[1024];
    char *cmd_fmt = "atos -p %d ";
    while (tracepos-tracebuf >= sizeof(task_snapshot_t))
    {

        thread_snapshot_t *tsnap = (thread_snapshot_t*)tracepos;
        if (STACKSHOT_THREAD_SNAPSHOT_MAGIC != tsnap->snapshot_magic)
        {
            break;
        }

        tracepos += sizeof(thread_snapshot_t);
        int i;
       
        char cmd_line[1024];
        sprintf( cmd_line, cmd_fmt, pid );
        for (i = 0; i<tsnap->nuser_frames; ++i)
        {
            stack_record_t *addr = (stack_record_t*)tracepos;
            sprintf(buff, "%llx ", addr->return_addr);
            strcat(cmd_line, buff);
            tracepos += sizeof(stack_record_t);
        }
        FILE *out = popen(cmd_line, "r");
       
        while (!fgetc(out)) {}
        while (fgets(buff, 1024, out))
        {
            printf("%s", buff);
        }
        pclose(out);
       
        for (i = 0; i<tsnap->nkern_frames; ++i)
        {
            stack_record_t *addr = (stack_record_t*)tracepos;
            tracepos += sizeof(stack_record_t);
        }
    }
   
    printf("result == %dn", result);
    return 0;
}

Leave a Reply