LLDB脚本——查看对象创建时的栈

脚本的出处

msl脚本

28-sb-examples-malloc-logging文章

脚本的诉求起因

标题中“创建时的调用栈区”指的是我们经常所说的‘Malloc Stack’,也就是项目的‘Scheme’下的‘Malloc Stack Logging’——以下简称MSL,如下图:

如果要使用MSL这项功能,我们需要在项目运行之前勾选上图中的选项。 假设我们的项目在运行中,并且在运行前Scheme中没有勾选MSL选项,可是我们又期望在运行过程随时中打开该功能,以便于从此刻记录将要生成的对象的Malloc Stack,那我们该怎么办呢?这就是该脚本的诉求。

脚本支持的功能

脚本支持在项目Debug期间,通过命令行实现以下功能:

  1. 随时开启/关闭MSL功能。
  2. 随时查看某一个对象(内存地址)的MSL。

最终的结果将会与“Debug Memory Graph”中的调用栈一致,如下图所示:

上图中标号中的命令由脚本最终实现,解释如下:

  1. “enable_logging”为Debug过程中开启MSL功能的命令。
  2. 命令msl 后输入某个内存中的对象地址,随后输出该对象的MSL。

脚本的原理分析

查找MSL的相关函数

  1. 使用lookup命令查找出 libsystem_malloc.dylib所有的函数名称

    (lldb) lookup . -m libsystem_malloc.dylib
    ****************************************************
    560 hits in: libsystem_malloc.dylib
    ****************************************************
    bitarray_size
    bitarray_create
    bitarray_get
      ... (此处省略大量所查到的函数名称)
      create_log_file
      open_log_file_from_directory
      __mach_stack_logging_get_frames
      turn_off_stack_logging
      turn_on_stack_logging
    
  2. 检索上一步的结果中包含‘logging’的函数,会发现 turn_off_stack_logging、turn_on_stack_logging、create_log_file、__mach_stack_logging_get_frames等函数。

  3. 在苹果的开源项目中搜索到如下代码:

    typedef enum {
    	stack_logging_mode_none = 0,
    	stack_logging_mode_all,
    	stack_logging_mode_malloc,
    	stack_logging_mode_vm,
    	stack_logging_mode_lite
    } stack_logging_mode_type;
       
    extern boolean_t turn_on_stack_logging(stack_logging_mode_type mode);
    extern void turn_off_stack_logging();
    
  4. git上关于OS中libmalloc的stack_logging_test.c代码给出了__mach_stack_logging_get_frames的用法:

    static void
    check_stacks(char *ptrs[], int num_ptrs, boolean_t lite_mode)
    {
    	mach_vm_address_t frames[MAX_FRAMES];
    	uint32_t frames_count;
       	
    	for (int i = 0; i < num_ptrs; i++) {
    		kern_return_t ret = (lite_mode) ?
    		__mach_stack_logging_get_frames_for_stackid(mach_task_self(), get_stack_id_from_ptr(ptrs[i]), frames, MAX_FRAMES, &frames_count, NULL) :
    		__mach_stack_logging_get_frames(mach_task_self(), (mach_vm_address_t) ptrs[i], frames, MAX_FRAMES, &frames_count);
       		
    		EXPECT_TRUE(ret == KERN_SUCCESS, "return from __mach_stack_logging_get_frames = %d\n", (int) ret);
    		EXPECT_TRUE(frames_count > 0, "number of frames returned from __mach_stack_logging_get_frames = %u\n", frames_count);
    	}
    }
       
    

脚本逻辑分析

  1. 借助命令别名开启MSL功能

    command alias enable_logging expression -lobjc -O -- extern void turn_on_stack_logging(int); turn_on_stack_logging(1);
    
  2. 使用frame.EvaluateExpression 执行相关OC++ JIT代码,JIT代码中使用‘__mach_stack_logging_get_frames’函数获取对象内存初始化的时候调用栈帧地址数组。

    /// OC JIT code
    def generateScript(addr, options):
      script = '  mach_vm_address_t addr = (mach_vm_address_t)' + str(addr) + ';\n'
      script += r'''
    typedef struct LLDBStackAddress {
        mach_vm_address_t *addresses;
        uint32_t count = 0;
    } LLDBStackAddress;
       
      LLDBStackAddress stackaddress;
      mach_vm_address_t address = (mach_vm_address_t)addr;
      void * task = mach_task_self_;
      stackaddress.addresses = (mach_vm_address_t *)calloc(100, sizeof(mach_vm_address_t));
      __mach_stack_logging_get_frames(task, address, stackaddress.addresses, 100, &stackaddress.count);
      stackaddress
      '''
      return script
       
    /// options 的配置
    def generateOptions():
        expr_options = lldb.SBExpressionOptions()
        expr_options.SetUnwindOnError(True)
        expr_options.SetLanguage (lldb.eLanguageTypeObjC_plus_plus)
        expr_options.SetCoerceResultToId(True)
        expr_options.SetGenerateDebugInfo(True)
        return expr_options
    
  3. 遍历调用栈帧地址数组,通过当前地址 - 函数起始地址获取当前地址在函数中的偏移量

    1.addr = target.ResolveLoadAddress(frameAddr)  ///根据地址10或16进制数 获取lldb.SBAddress
    2.addr.GetLoadAddress(target) - addr.symbol.addr.GetLoadAddress(target) /// 获取偏移量
    3.name = addr.symbol.name /// 获取符号地址对应的函数名 (如果这里获取不到,可以借助 sbt 脚本中的获取方式,即使没有DWARF也可以还原OC的函数名)
    
  4. 最终获取的结果如 脚本支持的功能下图所示