Я давно хотел добавить поддержку получения стека вызовов к своему 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:
{
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 в зависимости от архитектуры ОС.
{
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.
При этом, я ни для одного приложения не получил структуры task_snapshot со значением поля nloadinfos отличным от 0.
Далее в буфере размещается структура 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.
{
addr64_t return_addr;
addr64_t frame_addr;
} __attribute__ ((packed));
Информация о количестве потоков нигде не передается, так что ее надо рассчитывать вручную, исходя из размеров структур и фактического размера буфера.
Следующим шагом идет преобразование полученных адресов в информацию об именах символов. Этот шаг я решил не усложнять и воспользоваться не менее закрытым приложением atos. atos, несмотря на свою закрытость, довольно прост в использовании и не прихотлив, не то что stackshot:
В результате вызова, приведенного выше получается что-то типа такого (адреса должны быть в шестнадцатеричном представлении):
mach_msg_trap (in libSystem.B.dylib) + 10
_pthread_start (in libSystem.B.dylib) + 331
thread_start (in libSystem.B.dylib) + 13
Соответственно, достаточно просто создать строку с необходимой командой и выполнить ее при помощи popen.
Вот, собственно, и пример:
#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;
}