Google-breakpad linux源码解析(一)

3,238 阅读7分钟

Google-Breadpad linux源码解析 (一)

前言

阅读本文你需要简单了解breakpad的原理以及使用方式,在前面一篇文档有对这一部分内容进行介绍google-breakpad - 使用篇

整体概要

这篇文档将会从breakpadclient端的源码开始分析,在篇幅(一)中将不会具体分析如何写入dump文件,而是分析整个breakpad执行的流程,当然仅仅针对linux源码

源码分析

ExceptionHandler 构造函数

源码文件:src/client/linux/handler/exception_handler.cc:219

// 源代码中会去除一些无用代码,如Android特定处理代码,v8特定处理代码,以及大量注释
ExceptionHandler::ExceptionHandler(const MinidumpDescriptor& descriptor,
                                   FilterCallback filter,
                                   MinidumpCallback callback,
                                   void* callback_context,
                                   bool install_handler,
                                   const int server_fd)
    : filter_(filter),
      callback_(callback),
      callback_context_(callback_context),
      minidump_descriptor_(descriptor),
      crash_handler_(NULL) {
  if (server_fd >= 0)
    crash_generation_client_.reset(CrashGenerationClient::TryCreate(server_fd));

  if (!IsOutOfProcess() && !minidump_descriptor_.IsFD() &&
      !minidump_descriptor_.IsMicrodumpOnConsole())
    minidump_descriptor_.UpdatePath();

  pthread_mutex_lock(&g_handler_stack_mutex_);

  memset(&g_crash_context_, 0, sizeof(g_crash_context_));

  if (!g_handler_stack_)
    g_handler_stack_ = new std::vector<ExceptionHandler*>;
  if (install_handler) {
    InstallAlternateStackLocked();
    InstallHandlersLocked();
  }
  g_handler_stack_->push_back(this);
  pthread_mutex_unlock(&g_handler_stack_mutex_);
}

参数解释:

  • descriptor是一个MinidumpDescriptor描述符,他可以描述你dump写入的形式,如采用microdump(最小的dump信息),或者传入一个文件描述符、socket套接字、dump最终输入文件名等等
  • filter是一个回调函数,这个回调函数将会在触发异常之后breakpad没有做任何实质操作之前被调用,如果filter返回false,将会终止breakpad接下来的动作
  • callback也是一个回调函数,不过它在dump写入之后调用,并且会返回一些写入结果,如dumper是否生成成功,生成路径在哪等
  • install_handler为true的时候将会在发生异常终止的时候写入minidump文件,为false的时候将只会在手动调用WriteMinidump时候才会写入
  • server_fdout-process模式的时候传入服务端的fd,在in-process的时候传入-1

构造函数流程:

  1. 首先通过判断传入进来的server_fd是否大于等于0,如果server_fd大于等于表示形式是out-process,将会初始化crash_generation_client_对象
  2. 接下来判断是in-process, 并且minidump的描述符不是采用一个文件描述符形式和minidump写入不是采用microdump形式,就表示这个minidump描述符的初始化采用的是目录方式,然后minidump_descriptor_.UpdatePath这一行代码其实只是干了生成一个uuid然后拼接上原有的dump生成目录。
  3. 初始化g_handler_stack_, g_handler_stack_是一个vector<ExceptionHandler*>, ExcetipnHandler的实例可能有多个,所以用一个容器来保存起来。
  4. 判断install_handler是否为true,为true会调用两个函数InstallAlternateStackLocked, InstallHandlersLocaled, 后面再分析这两个函数
  5. 将当前ExceptionHandler对象插入到g_handler_stack_中

ExceptionHandler构造函数的整个初始化流程并不复杂,去除out-process的这种情况,它就只干了几件事情。

InstallHandlersLocaled 函数

源码文件:src/client/linux/handler/exception_handler.cc:131

// 源代码中会去除一些无用代码,如Android特定处理代码,v8特定处理代码,以及大量注释
void InstallAlternateStackLocked() {
  if (stack_installed)
    return;

  memset(&old_stack, 0, sizeof(old_stack));
  memset(&new_stack, 0, sizeof(new_stack));

  static const unsigned kSigStackSize = std::max(16384, SIGSTKSZ);

  if (sys_sigaltstack(NULL, &old_stack) == -1 || !old_stack.ss_sp ||
      old_stack.ss_size < kSigStackSize) {
    new_stack.ss_sp = calloc(1, kSigStackSize);
    new_stack.ss_size = kSigStackSize;

    if (sys_sigaltstack(&new_stack, NULL) == -1) {
      free(new_stack.ss_sp);
      return;
    }
    stack_installed = true;
  }
}

InstallAlternateStackLocaked这个函数只是在做一件事情,建立一个alternative stack,给后续signal处理handler使用,为什么要建立一个alternative stack?原因是这个信号的产生可能是由于堆栈溢出,所以在执行signal的handler时使用一个alternatvie stack是很有必要的

alternate signal stack建立与使用分为三个步骤:

  1. 分配一块内存给alternate signal stack使用
  2. 使用sigaltstack系统调用设置alternate signal stack的首地址和stack大小
  3. 在建立signal handler(sigaction api)的时候通过SA_ONSTACK flag告知系统这个处理handler使用alternate stack

具体请阅读sigaltstack(2) - Linux manual page (man7.org)

InstallAlternateStackLocaked做了alternate signal stack的前两个步骤,最后一个步骤会在InstallHandlersLocakd函数里面完成。

ExceptionHandler::InstallHandlersLocked 函数

源码文件:src/client/linux/handler/exception_handler.cc:275

// 源代码中会去除一些无用代码,如Android特定处理代码,v8特定处理代码,以及大量注释

bool ExceptionHandler::InstallHandlersLocked() {
  if (handlers_installed)
    return false;

  for (int i = 0; i < kNumHandledSignals; ++i) {
    if (sigaction(kExceptionSignals[i], NULL, &old_handlers[i]) == -1)
      return false;
  }

  struct sigaction sa;
  memset(&sa, 0, sizeof(sa));
  sigemptyset(&sa.sa_mask);

  for (int i = 0; i < kNumHandledSignals; ++i)
    sigaddset(&sa.sa_mask, kExceptionSignals[i]);

  sa.sa_sigaction = SignalHandler;
  sa.sa_flags = SA_ONSTACK | SA_SIGINFO;

  for (int i = 0; i < kNumHandledSignals; ++i) {
    if (sigaction(kExceptionSignals[i], &sa, NULL) == -1) {
      // 忽略错误
    }
  }
  handlers_installed = true;
  return true;
}

InstallHandlersLocaked函数主要作用是给需要捕获的signal设置处理handler,并且完成上面剩下的alternate signal stack的最后一步。

  1. 保留之前所有的旧的处理handler,如果保留失败返回
  2. 在某个信号处理handler时阻塞所有kExcetpionSignals触发的signal(kExceptionSignals = SIGSEGV, SIGABRT, SIGFPE, SIGILL, SIGBUS, SIGTRAP)。
  3. 设置SignalHandler到sa.sa_sigaction, 设置sa.sa_flags为SA_ONSTACK | SIGINFO, 并将所有的kExcetptionSignals里的信号处理handler都设置成sa.sa_sigaction, sa.sa_flags。这里面sa.sa_flags的SA_ONSTACK flag就是告知系统这个处理handler采用alternate stack,而SA_SIGINFO flag告诉系统使用sa_sigaction来代替sa.sa_handler参数。

到这里整个ExceptionHandler的初始化流程就完成了,可以看到如果触发了kExceptionSignals里面的任何一个信号都会走到SignalHandler函数里面进行处理,在前面ExceptionHandler的整个初始化都是在一个正常环境运行,而后面SignalHandler函数开始都是运行在一个崩溃环境中


ExceptionHandler::SignalHandler 函数

源码文件:src/client/linux/handler/exception_handler.cc:275

// 源代码中会去除一些无用代码,如Android特定处理代码,v8特定处理代码,以及大量注释
void ExceptionHandler::SignalHandler(int sig, siginfo_t* info, void* uc) {

  if (g_first_chance_handler_ != nullptr &&
      g_first_chance_handler_(sig, info, uc)) {
    return;
  }

  pthread_mutex_lock(&g_handler_stack_mutex_);

  
  struct sigaction cur_handler;
  if (sigaction(sig, NULL, &cur_handler) == 0 &&
      cur_handler.sa_sigaction == SignalHandler &&
      (cur_handler.sa_flags & SA_SIGINFO) == 0) {
    sigemptyset(&cur_handler.sa_mask);
    sigaddset(&cur_handler.sa_mask, sig);

    cur_handler.sa_sigaction = SignalHandler;
    cur_handler.sa_flags = SA_ONSTACK | SA_SIGINFO;

    if (sigaction(sig, &cur_handler, NULL) == -1) {
      InstallDefaultHandler(sig);
    }
    pthread_mutex_unlock(&g_handler_stack_mutex_);
    return;
  }

  bool handled = false;
  for (int i = g_handler_stack_->size() - 1; !handled && i >= 0; --i) {
    handled = (*g_handler_stack_)[i]->HandleSignal(sig, info, uc);
  }

  if (handled) {
    InstallDefaultHandler(sig);
  } else {
    RestoreHandlersLocked();
  }

  pthread_mutex_unlock(&g_handler_stack_mutex_);
}

SignalHandler函数运行在一个被破坏的线程中,而且还是运行在alternate stack环境中。

在触发崩溃时这个SignalHandler函数将会被调用,SignalHandler主要做了:

  1. 第一个if是怕外部使用者又使用了signal函数对之前设置的信号做了处理,导致不可必要的崩溃,这个if会具体判断是否是由最开始ExceptionHandler初始化时sigaction设置的信号处理函数,如果不是将重新设置这个信号的处理方式到正确处理方式。
  2. 调用HandleSignal函数
  3. 进行后续清理工作

SignalHandler里面核心就是调用HandleSignal函数做具体的逻辑处理

ExceptionHandler::HandleSignal 函数

源码文件:src/client/linux/handler/exception_handler.cc:440

bool ExceptionHandler::HandleSignal(int /*sig*/, siginfo_t* info, void* uc) {
  if (filter_ && !filter_(callback_context_))
    return false;
 
  bool signal_trusted = info->si_code > 0;
  bool signal_pid_trusted = info->si_code == SI_USER ||
      info->si_code == SI_TKILL;
  if (signal_trusted || (signal_pid_trusted && info->si_pid == getpid())) {
    sys_prctl(PR_SET_DUMPABLE, 1, 0, 0, 0);
  }
 
  memset(&g_crash_context_, 0, sizeof(g_crash_context_));
  memcpy(&g_crash_context_.siginfo, info, sizeof(siginfo_t));
  memcpy(&g_crash_context_.context, uc, sizeof(ucontext_t));
 

  ucontext_t* uc_ptr = (ucontext_t*)uc;
  if (uc_ptr->uc_mcontext.fpregs) {
    memcpy(&g_crash_context_.float_state, uc_ptr->uc_mcontext.fpregs,
           sizeof(g_crash_context_.float_state));
  }
 
  g_crash_context_.tid = syscall(__NR_gettid);
  if (crash_handler_ != NULL) {
    if (crash_handler_(&g_crash_context_, sizeof(g_crash_context_),
                       callback_context_)) {
      return true;
    }
  }
  return GenerateDump(&g_crash_context_);
}

HandlSignal时一个非常重要的函数,它和上面的一样运行在一个崩溃环境。

  1. 调用ExceptionHandler构造时传入的filter_回调,如果回调返回false将终止后续的处理
  2. 检查信号是否为可信的,可信将利用pcrtl设置RP_SETDUMPABLE,表示允许dump生成
  3. 初始化g_crash_context_全局变量
  4. 调用GenrateDump函数做实际dump生成操作

ExceptionHandler::GenerateDump 函数

源码文件:src/client/linux/handler/exception_handler.cc:498

bool ExceptionHandler::GenerateDump(CrashContext* context) {
  if (IsOutOfProcess())
    return crash_generation_client_->RequestDump(context, sizeof(*context));
 
  static const unsigned kChildStackSize = 16000;
  PageAllocator allocator;
  uint8_t* stack = reinterpret_cast<uint8_t*>(allocator.Alloc(kChildStackSize));
  if (!stack)
    return false;

  stack += kChildStackSize;
  my_memset(stack - 16, 0, 16);
 
  ThreadArgument thread_arg;
  thread_arg.handler = this;
  thread_arg.minidump_descriptor = &minidump_descriptor_;
  thread_arg.pid = getpid();
  thread_arg.context = context;
  thread_arg.context_size = sizeof(*context);
 
  
  if (sys_pipe(fdes) == -1) {
    fdes[0] = fdes[1] = -1;
  }
 
  const pid_t child = sys_clone(
      ThreadEntry, stack, CLONE_FS | CLONE_UNTRACED, &thread_arg, NULL, NULL,
      NULL);
  if (child == -1) {
    sys_close(fdes[0]);
    sys_close(fdes[1]);
    return false;
  }
 
  sys_close(fdes[0]);
  sys_prctl(PR_SET_PTRACER, child, 0, 0, 0);
  SendContinueSignalToChild();
  int status = 0;
  const int r = HANDLE_EINTR(sys_waitpid(child, &status, __WALL));
 
  sys_close(fdes[1]);
 
  bool success = r != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0;
  if (callback_)
    success = callback_(minidump_descriptor_, callback_context_, success);
  return success;
}

这个函数也是一个非常核心的函数,但是它其实呢只干了一件事情,利用clone系统调用建立一个子进程,然后等待子进程终止。

  1. 创建一块stack内存区域用于clone函数使用
  2. 填充thread_arg参数为了clone函数
  3. 开启一个管道为了实现子进程和父进程的同步,这里由于子进程在写dump之前需要父进程允许子进程跟踪自己的ptrace,所以采用了管道来做这个同步,子进程会一直等待父进程设置完ptrace之后再开始后续工作。
  4. 调用clone函数,设置处理函数ThreadEntry
  5. 父进程一直等待子进程结束,子进程结束之后,判断子进程运行状态,并且调用ExceptionHandler里面设置的会调用函数即Minidumpcallback告知运行状态。

ExceptionHandler::ThreadEntry 函数

源码文件:src/client/linux/handler/exception_handler.cc:422

int ExceptionHandler::ThreadEntry(void* arg) {
  const ThreadArgument* thread_arg = reinterpret_cast<ThreadArgument*>(arg);

  sys_close(thread_arg->handler->fdes[1]);

  thread_arg->handler->WaitForContinueSignal();
  sys_close(thread_arg->handler->fdes[0]);

  return thread_arg->handler->DoDump(thread_arg->pid, thread_arg->context,
                                     thread_arg->context_size) == false;
}

ThreadEntry函数实现特别简单,首先等待父进程的ptrace设置完成,然后调用DoDump函数做dump写入操作。

ExceptionHandler::DoDump 函数

源码文件:src/client/linux/handler/exception_handler.cc:601

bool ExceptionHandler::DoDump(pid_t crashing_process, const void* context,
                              size_t context_size) {
  const bool may_skip_dump =
      minidump_descriptor_.skip_dump_if_principal_mapping_not_referenced();
  const uintptr_t principal_mapping_address =
      minidump_descriptor_.address_within_principal_mapping();
  const bool sanitize_stacks = minidump_descriptor_.sanitize_stacks();
  if (minidump_descriptor_.IsMicrodumpOnConsole()) {
    return google_breakpad::WriteMicrodump(
        crashing_process,
        context,
        context_size,
        mapping_list_,
        may_skip_dump,
        principal_mapping_address,
        sanitize_stacks,
        *minidump_descriptor_.microdump_extra_info());
  }
  if (minidump_descriptor_.IsFD()) {
    return google_breakpad::WriteMinidump(minidump_descriptor_.fd(),
                                          minidump_descriptor_.size_limit(),
                                          crashing_process,
                                          context,
                                          context_size,
                                          mapping_list_,
                                          app_memory_list_,
                                          may_skip_dump,
                                          principal_mapping_address,
                                          sanitize_stacks);
  }
  return google_breakpad::WriteMinidump(minidump_descriptor_.path(),
                                        minidump_descriptor_.size_limit(),
                                        crashing_process,
                                        context,
                                        context_size,
                                        mapping_list_,
                                        app_memory_list_,
                                        may_skip_dump,
                                        principal_mapping_address,
                                        sanitize_stacks);
}

DoDump函数实现也非常简单,只是根据minidump_descirptor_描述符类型来做不同的WriteMinidump操作,我们呢现在只关心他是一个path的情况,也就是DoDump的最后一个WriteMinidump函数调用。

目前整个Breakpad的整体流程就已经完成。

整体流程图


在下一篇的源码分析中将会主力分析WriteMinidump