352a2eade2154a4737e937fac81a629d
断点调试的实现原理

断点调试的实现原理

对于经常使用LLDB的朋友来说,breakpoint系列的指令肯定是耳熟能详的,常见的有对地址或者函数符号下断点等等。

但是在这表象背后,究竟断点调试的逻辑是如何实现的?可能绝大多数人都不能比较详细的回答出来。因此今天就让我们一起从LLDB的源码中了解断点的实现原理。

CPU 执行流

稍微了解汇编的同学可能都知道,不论我们采用的是哪种高级编程语言进行程序开发,对于CPU来说,他执行的永远是二进制流对应的机器码,而机器码转换为人可以阅读的助记符就成了汇编。

CPU来说,从执行逻辑来说是相对直白的,就是按照PC寄存器指向的指令地址顺序一条条执行。当然,中间如果遇到跳转指令,如ARM64中的bbrblr等等,也会有一系列上的地址跳转发生。

所以从某种程度上说,我们要能够使用断点,相当于我们对原有的程序执行流进行了修改,插入了某些特定功能的指令片段,从而让程序能够在触发断点的时候转移到断点的处理上。

那么,断点究竟是怎么插入到原有实现当中的呢?同时,断点究竟插入的是什么样的代码才能起到改变CPU执行流的效果呢?

让我们通过LLDB来一探究竟吧。

LLDB的断点实现

限于篇幅有限,本文以breakpoint set --name 来进行举例,其他断点设置方式都大同小异,读者可以自行摸索

我们先实现一个命令行测试工程,命名为TestLLDB,架构x86_64,代码如下:

#import <Foundation/Foundation.h>

void testBreakpoint()
{
    NSLog(@"wuziqi is veryhandsome");
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        testBreakpoint();
        NSLog(@"Hello, World!");
    }
    return 0;
}

用我们之前在使用 Xcode 构建 LLDB 源码中构建好的LLDB进行调试验证。

执行如下命令:

  • lldb -f TestLLDB

  • breakpoint set --name testBreakpoint

  • run

上述这段指令做了什么呢?其实就是首先挂载我们的测试程序,通过函数名testBreakpoint下断点,最后触发程序运行。

好,做完了这些步骤以后,你会发现你的调试过程成功停在了testBreakpoint中的NSLog语句上。

由于我们的测试程序是带有调试选项参数的(默认),所以展现的是源码而非汇编。

同时,我们的LLDB源码会停留在CommandObjectBreakpointSetdoExecute

case eSetTypeFunctionName: // Breakpoint by function name
    {
      uint32_t name_type_mask = m_options.m_func_name_type_mask;

      if (name_type_mask == 0)
        name_type_mask = eFunctionNameTypeAuto;

      bp = target
               ->CreateBreakpoint(
                   &(m_options.m_modules), &(m_options.m_filenames),
                   m_options.m_func_names, name_type_mask, m_options.m_language,
                   m_options.m_offset_addr, m_options.m_skip_prologue, internal,
                   m_options.m_hardware)
               .get();
    } break;

接下来,我们需要通过调试,解决心中的疑惑:设置断点就行对被调试的程序进行了哪些操作。

对函数名称的解析

在通过名称设置断点的时候,首先要解决的一个问题就是,这个名称究竟是什么函数?

之所以要解决这样的问题,在于编译后的编译单元(module)其实不知道究竟函数是什么玩意,它只了解一个个符号(如全局变量等)。

对于不同的语言来说,编译过程中对函数名称的编码方式也不同(mangling),因此我们从输入的函数名到真正的符号之间要进行一次决议。

LLDB中承担断点符号决议的是BreakpointResolverName。首先每一次对名称的查询都会触发这段逻辑,这段逻辑本质上是为了加速从名称到后续映射的查询过程的。

void BreakpointResolverName::AddNameLookup(const ConstString &name,
                                           uint32_t name_type_mask) {
  ObjCLanguage::MethodName objc_method(name.GetCString(), false);
  if (objc_method.IsValid(false)) {
    std::vector<ConstString> objc_names;
    objc_method.GetFullNames(objc_names, true);
    for (ConstString objc_name : objc_names) {
      Module::LookupInfo lookup;
      lookup.SetName(name);
      lookup.SetLookupName(objc_name);
      lookup.SetNameTypeMask(eFunctionNameTypeFull);
      m_lookups.push_back(lookup);
    }
  } else {
    Module::LookupInfo lookup(name, name_type_mask, m_language);
    m_lookups.push_back(lookup);
  }
}

而真正的查询决议逻辑实质上在BreakpointResolverName::SearchCallback,对应的代码是moduleSP->findFunctions

对进程做了什么

我们通过LLDB下了断点,但是断点肯定不会是作用于LLDB本身;其应该修改的是对应的被调试程序TestLLDB中的机器码,所以我们肯定需要侵入到源程序进行断点相关的代码的插入,这个逻辑在Process::CreateBreakpointSite里:

lldb::break_id_t
Process::CreateBreakpointSite(const BreakpointLocationSP &owner,
                              bool use_hardware) {
  addr_t load_addr = LLDB_INVALID_ADDRESS;

  bool show_error = true;
  switch (GetState()) {
  case eStateInvalid:
  case eStateUnloaded:
  case eStateConnected:
  case eStateAttaching:
  case eStateLaunching:
  case eStateDetached:
  case eStateExited:
    show_error = false;
    break;

  case eStateStopped:
  case eStateRunning:
  case eStateStepping:
  case eStateCrashed:
  case eStateSuspended:
    show_error = IsAlive();
    break;
  }

  // Reset the IsIndirect flag here, in case the location changes from
  // pointing to a indirect symbol to a regular symbol.
  owner->SetIsIndirect(false);

  if (owner->ShouldResolveIndirectFunctions()) {
    Symbol *symbol = owner->GetAddress().CalculateSymbolContextSymbol();
    if (symbol && symbol->IsIndirect()) {
      Status error;
      Address symbol_address = symbol->GetAddress();
      load_addr = ResolveIndirectFunction(&symbol_address, error);
      if (!error.Success() && show_error) {
        GetTarget().GetDebugger().GetErrorFile()->Printf(
            "warning: failed to resolve indirect function at 0x%" PRIx64
            " for breakpoint %i.%i: %s\n",
            symbol->GetLoadAddress(&GetTarget()),
            owner->GetBreakpoint().GetID(), owner->GetID(),
            error.AsCString() ? error.AsCString() : "unknown error");
        return LLDB_INVALID_BREAK_ID;
      }
      Address resolved_address(load_addr);
      load_addr = resolved_address.GetOpcodeLoadAddress(&GetTarget());
      owner->SetIsIndirect(true);
    } else
      load_addr = owner->GetAddress().GetOpcodeLoadAddress(&GetTarget());
  } else
    load_addr = owner->GetAddress().GetOpcodeLoadAddress(&GetTarget());

  if (load_addr != LLDB_INVALID_ADDRESS) {
    BreakpointSiteSP bp_site_sp;

    // Look up this breakpoint site.  If it exists, then add this new owner,
    // otherwise
    // create a new breakpoint site and add it.

    bp_site_sp = m_breakpoint_site_list.FindByAddress(load_addr);

    if (bp_site_sp) {
      bp_site_sp->AddOwner(owner);
      owner->SetBreakpointSite(bp_site_sp);
      return bp_site_sp->GetID();
    } else {
      bp_site_sp.reset(new BreakpointSite(&m_breakpoint_site_list, owner,
                                          load_addr, use_hardware));
      if (bp_site_sp) {
        Status error = EnableBreakpointSite(bp_site_sp.get());
        if (error.Success()) {
          owner->SetBreakpointSite(bp_site_sp);
          return m_breakpoint_site_list.Add(bp_site_sp);
        } else {
          if (show_error) {
            // Report error for setting breakpoint...
            GetTarget().GetDebugger().GetErrorFile()->Printf(
                "warning: failed to set breakpoint site at 0x%" PRIx64
                " for breakpoint %i.%i: %s\n",
                load_addr, owner->GetBreakpoint().GetID(), owner->GetID(),
                error.AsCString() ? error.AsCString() : "unknown error");
          }
top Created with Sketch.