iOS - 自定义LLDB命令

命令的作用

在console中打印出内存地址对象的创建栈,效果如下:

(lldb) mallocstack 0x115a43740
frame #0 : 0x1a8a07368 libsystem_malloc.dylib`calloc + 40
frame #1 : 0x1a7b4445c libobjc.A.dylib`class_createInstance + 76
frame #2 : 0x1a7b55bb8 libobjc.A.dylib`_objc_rootAlloc + 52
frame #3 : 0x10183dd90 TestApp`-[HTYBottomView buttonImage] + 80
frame #4 : 0x10183a348 TestApp`-[HTYBottomView setupView] + 232
frame #5 : 0x10183a1ec TestApp`-[HTYBottomView initWithCoder:] + 164
frame #6 : 0x18c5d3fb4 UIKit`-[UIClassSwapper initWithCoder:] + 248
frame #7 : 0x18c771d24 UIKit`UINibDecoderDecodeObjectForValue + 680
frame #8 : 0x18c771a64 UIKit`-[UINibDecoder decodeObjectForKey:] + 104

例子适用场景

有时候在console中会看到一些直接用内存地址表示的对象。举个例子,像这些layout warnings

2017-07-22 09:50:16.832586+0800 TestApp[20583:2520440] [LayoutConstraints] Unable to simultaneously satisfy constraints.
2017-07-22 09:55:45.391041+0800 TestApp[20813:2526791] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<MASLayoutConstraint:0x1c44b0da0 UIImageView:0x115a43740.width == 44>",
    "<MASLayoutConstraint:0x1c02a90c0 UIButton:0x115b4ed90.left == HTYBottomView:0x115b4dd80.left + 130>",
    "<MASLayoutConstraint:0x1c02a8fa0 UIButton:0x115b4ed90.right == HTYBottomView:0x115b4dd80.right>",
    "<MASLayoutConstraint:0x1c44ace40 UIImageView:0x115a43740.left == HTYBottomView:0x115b4dd80.left + 16>",
    "<MASLayoutConstraint:0x1c44b0ec0 UIImageView:0x115a43740.centerX == HTYBottomView:0x115b4dd80.centerX>"
)

Will attempt to recover by breaking constraint 
<MASLayoutConstraint:0x1c44b0da0 UIImageView:0x115a43740.width == 44>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

如果想知道某个内存地址对应的Class和它的创建栈,可以这样做:

Xcode的方式

1. 开启Logging Malloc Stack
2. 开启Xcode的Debug Memory Graph,通过搜索UI控件的内存地址定位到UI Debugger里的UI控件,然后在Backtrace查看到UI控件的创建栈,定位到是哪个UI控件,再哪里创建的。

通过上面的定位过程,可以解决问题,但是效率不高,步骤比较多。如果可以在console里就直接把内存对象的创建栈打印出来,不就方便多了吗?

LLDB的方式 - Xcode功能对应的代码实现

1. 新建Symbolick Breakpoint,把控制Logging Malloc Stack开关的变量打印出来
Symbol:getenv
Action:po (char*)$arg1
Automatically continue after evaluating actions: √
...
"MallocStackLogging"
...

这样,可以找到MallocStackLogging变量

2. 新建Symbolick Breakpoint,定位MallocStackLogging变量的相关库
Symbol:getenv
Condition:((int)strcmp("MallocStackLogging",$arg1)==0) 
Action:bt
Automatically continue after evaluating actions: X
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
  * frame #0: 0x0000000117368a26 libsystem_c.dylib`getenv
    frame #1: 0x00000001165e96f3 libobjc.A.dylib`environ_init + 464
    frame #2: 0x00000001165ecb0c libobjc.A.dylib`_objc_init + 32
    frame #3: 0x000000011727287b libdispatch.dylib`_os_object_init + 13

这样,可以找到对应的动态库是libsystem_malloc.dylib

3. 打印出libsystem_malloc.dylib的所有方法
(lldb) image dump symtab libsystem_malloc.dylib
...
[  254]    254   X Code            0x0000000000015187 0x00000001174a0187 0x00000000000001f7 0x000f0000 __mach_stack_logging_get_frames
...

这个,很可疑

关于__mach_stack_logging_get_frames

贴到Google,官网的heap.py例子就出来了。

def dump_stack_history_entries(options, result, addr, history):
    # malloc_stack_entry *get_stack_history_for_address (const void * addr)
    expr_prefix = '''
typedef int kern_return_t;
typedef struct $malloc_stack_entry {
    uint64_t address;
    uint64_t argument;
    uint32_t type_flags;
    uint32_t num_frames;
    uint64_t frames[512];
    kern_return_t err;
} $malloc_stack_entry;
'''
    single_expr = '''
#define MAX_FRAMES %u
typedef unsigned task_t;
$malloc_stack_entry stack;
stack.address = 0x%x;
stack.type_flags = 2;
stack.num_frames = 0;
stack.frames[0] = 0;
uint32_t max_stack_frames = MAX_FRAMES;
stack.err = (kern_return_t)__mach_stack_logging_get_frames (
    (task_t)mach_task_self(),
    stack.address,
    &stack.frames[0],
    max_stack_frames,
    &stack.num_frames);
if (stack.num_frames < MAX_FRAMES)
    stack.frames[stack.num_frames] = 0;
else
    stack.frames[MAX_FRAMES-1] = 0;
stack''' % (options.max_frames, addr)

其中,这段代码正是我们想要的 - 打印对象的创建栈。

材料都全了,拼拼凑凑,debug->改->debug->改...,Python脚本就出来啦。把它的path放到~/.lldbinit

command script import ~/Documents/source_code/lldb/mallocstack.py

然后就可以开始用了。

(lldb) mallocstack 0x115a43740
frame #0 : 0x1a8a07368 libsystem_malloc.dylib`calloc + 40
frame #1 : 0x1a7b4445c libobjc.A.dylib`class_createInstance + 76
frame #2 : 0x1a7b55bb8 libobjc.A.dylib`_objc_rootAlloc + 52
frame #3 : 0x10183dd90 TestApp`-[HTYBottomView buttonImage] + 80
frame #4 : 0x10183a348 TestApp`-[HTYBottomView setupView] + 232
frame #5 : 0x10183a1ec TestApp`-[HTYBottomView initWithCoder:] + 164
frame #6 : 0x18c5d3fb4 UIKit`-[UIClassSwapper initWithCoder:] + 248
frame #7 : 0x18c771d24 UIKit`UINibDecoderDecodeObjectForValue + 680
frame #8 : 0x18c771a64 UIKit`-[UINibDecoder decodeObjectForKey:] + 104
frame #9 : 0x18c5d3c58 UIKit`-[UIRuntimeConnection initWithCoder:] + 188
frame #10: 0x18c771d24 UIKit`UINibDecoderDecodeObjectForValue + 680
frame #11: 0x18c771e9c UIKit`UINibDecoderDecodeObjectForValue + 1056
frame #12: 0x18c771a64 UIKit`-[UINibDecoder decodeObjectForKey:] + 104
frame #13: 0x18c5d2f98 UIKit`-[UINib instantiateWithOwner:options:] + 1168
frame #14: 0x18c3d32cc UIKit`-[UIViewController _loadViewFromNibNamed:bundle:] + 372
frame #15: 0x18c195dd4 UIKit`-[UIViewController loadView] + 176
frame #16: 0x18c076dd8 UIKit`-[UIViewController loadViewIfRequired] + 184
frame #17: 0x18c076d08 UIKit`-[UIViewController view] + 28
frame #18: 0x18c1fb6a0 UIKit`-[UINavigationController _startCustomTransition:] + 892
frame #19: 0x18c11e634 UIKit`-[UINavigationController _startDeferredTransitionIfNeeded:] + 696
frame #20: 0x18c11e254 UIKit`-[UINavigationController __viewWillLayoutSubviews] + 156
frame #21: 0x18c11e15c UIKit`-[UILayoutContainerView layoutSubviews] + 188
frame #22: 0x18c0744f0 UIKit`-[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1224
frame #23: 0x18b590a84 QuartzCore`-[CALayer layoutSublayers] + 148
frame #24: 0x18b584d70 QuartzCore`CA::Layer::layout_if_needed(CA::Transaction*) + 296
frame #25: 0x18b584c2c QuartzCore`CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 32
frame #26: 0x18b4fb88c QuartzCore`CA::Context::commit_transaction(CA::Transaction*) + 276
frame #27: 0x18b522ae4 QuartzCore`CA::Transaction::commit() + 520
frame #28: 0x18c069d78 UIKit`_afterCACommitHandler + 256
frame #29: 0x1851fc6e4 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
frame #30: 0x1851fa18c CoreFoundation`__CFRunLoopDoObservers + 412
frame #31: 0x1851fa670 CoreFoundation`__CFRunLoopRun + 1076
frame #32: 0x18511503c CoreFoundation`CFRunLoopRunSpecific + 436
frame #33: 0x19596af94 GraphicsServices`GSEventRunModal + 100
frame #34: 0x18c0da86c UIKit`UIApplicationMain + 208
frame #35: 0x101b07aa4 TestApp`main + 124
frame #36: 0x1a88a7d1c libdyld.dylib`start + 4
frame #37: 0x0 None`None 
frame #38: 0x1b2336981 libsystem_pthread.dylib`_thread + 1

能不能更方便些?

Xcode开启Loggin Malloc Stack的功能,只能在程序运行前进行设置。如果当程序已经在运行中时,也可以动态开关,那不就更方便了。
libsystem_malloc.dylib里再捞一捞,

...
[  359]    359   X Code            0x000000000000e887 0x0000000117499887 0x00000000000000f9 0x000f0000 turn_off_stack_logging
[  360]    360   X Code            0x000000000000ca2c 0x0000000117497a2c 0x00000000000001d4 0x000f0000 turn_on_stack_logging
...

这2个,就是吧

幸运的是,libmalloc是开源的,dive到stack_logging.h,可以发现

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();

这样,通过在console中调用turn_on_stack_logging(2)turn_off_stack_logging,就可以动态开关Logging Malloc Stack功能了。

附带:完整的Python脚本

import lldb
import os
import shlex
import optparse


def handle_command(debugger, command, result, dict):
    '''
    mallocstack for po malloc stack  
    '''

    command_args = shlex.split(command)
    parser = get_options()
    try:
        (options, args) = parser.parse_args(command_args)
    except:
        result.SetError(parser.usage)
        return

    implementation(debugger, result, options, args)


def implementation(debugger, result, options, args):
    cleanCommand = args[0]
    frame = debugger.GetSelectedTarget().GetProcess().GetSelectedThread().GetSelectedFrame()
    target = debugger.GetSelectedTarget()
    script = get_script(cleanCommand, options)
    sbval = frame.EvaluateExpression(script, get_expression_options())

    if sbval.error.fail:
        result.AppendMessage(str(sbval.error))
        return

    val = lldb.value(sbval)
    addresses = []
    for i in range(val.count.sbvalue.unsigned):
        address = val.addresses[i].sbvalue.unsigned
        loadAddr = target.ResolveLoadAddress(address).GetLoadAddress(target)
        addresses.append(loadAddr)

    retString = get_stack_trace_from_addresses(addresses, target)

    frame.EvaluateExpression('free(' + str(val.addresses.sbvalue.unsigned) + ')', get_expression_options())
    result.AppendMessage(retString)


def get_stack_trace_from_addresses(frameAddresses, target):

    frame_string = ''
    for index, frameAddr in enumerate(frameAddresses):
        addr = target.ResolveLoadAddress(frameAddr)
        symbol = addr.symbol
        name = symbol.name
        offset_str = ''
        offset = addr.GetLoadAddress(target) - addr.symbol.addr.GetLoadAddress(target)
        if offset > 0:
            offset_str = '+ {}'.format(offset)

        frame_string += 'frame #{:<2}: {} {}`{} {}\n'.format(index, hex(addr.GetLoadAddress(target)), addr.module.file.basename, name, offset_str)

    return frame_string


def get_expression_options():
    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


def get_script(addr, options):
    script = '''  
typedef int kern_return_t;
typedef struct $malloc_stack_entry {
    uint64_t address;
    uint64_t argument;
    uint32_t type_flags;
    uint32_t num_frames;
    uint64_t frames[512];
    kern_return_t err;
} $malloc_stack_entry;

#define MAX_FRAMES %u
typedef unsigned task_t;
$malloc_stack_entry stack;
stack.address = 0x%x;
stack.type_flags = 2;
stack.num_frames = 0;
stack.frames[0] = 0;
uint32_t max_stack_frames = MAX_FRAMES;
stack.err = (kern_return_t)__mach_stack_logging_get_frames (
    (task_t)mach_task_self(),
    stack.address,
    &stack.frames[0],
    max_stack_frames,
    &stack.num_frames);
if (stack.num_frames < MAX_FRAMES)
    stack.frames[stack.num_frames] = 0;
else
    stack.frames[MAX_FRAMES-1] = 0;
stack''' % (100, addr)
    return script


def get_options():
    usage = "usage: %prog [options] <address>"
    description = '''mallocstack for po malloc stack.'''
    parser = optparse.OptionParser(
        description=description,
        prog="mallocstack",
        usage=usage
    )
    return parser


if __name__ == '__main__':
    lldb.debugger = lldb.SBDebugger.Create()

handle_command.__doc__ = get_options().format_help()
lldb.debugger.HandleCommand(
    'command script add -f %s.handle_command mallocstack' %
    __name__)

print '"mallocstack" commands have been installed, use the "--help" options for detailed help.'
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 作为这一部分的最后一章, 你将会经过同样的步骤, 我自己理解当一个对象被创建的到时候MallocStackLogg...
    股金杂谈阅读 1,504评论 0 0
  • 首先如果遇到应用卡顿或者因为内存占用过多时一般使用Instruments里的来进行检测。但对于复杂情况可能就需要用...
    攻克乃还_阅读 2,237评论 0 7
  • 近日在 Archives of Pathology & Laboratory Medicine 杂志上发表了一篇题...
    谭灏文阅读 3,642评论 4 6
  • 喜欢一个名为“kitchen”的公众号,文章图文相宜。精美的早餐、温馨的小故事,动听的音乐,很长一段时间都让我感觉...
    豆子121阅读 236评论 6 4

友情链接更多精彩内容