(十五)SB示例 Malloc日志

1. Malloc日志

下面我们将了解在创建对象时,MallocStackLogging如何获取堆栈跟踪。

我们将创建一个自定义的LLDB命令。该命令为我们提供对象何时在内存中分配或释放的堆栈跟踪,即使堆栈跟踪早已从调试器中消失也可以。

了解对象在程序中创建位置的堆栈跟踪不仅对逆向工程有用,而且在日常调试中也有很好的用例。当一个进程崩溃时,了解该内存的历史记录以及崩溃前发生的任何分配或解除分配事件,是非常有帮助的。

1.1 脚本配置

我们有两个脚本要实现。让我们逐一了解它们以及如何使用它们:

  • msl.py:这是命令(MallocStackLogging的缩写)是我们将在这里使用的脚本。里面有一个基本的逻辑框架。
  • lookup.py: 在之前的版本上添加了一些附加选项。将使用其中一个选项把搜索限制在进程中的特定模块。
  • sbt.py:使用非符号化符号进行回溯,并对其进行符号化。
  • search.py:遍历堆中的所有对象并搜索特定子类。用于快速获取对特定类实例的引用。

1.2 MallocStackLogging解释

如果不熟悉MallocStackLogging环境变量,下面将对其进行描述,并展示它的典型用法。

MallocStackLogging环境变量被传递到进程中并设置为true时,它将监视堆上内存的分配和释放。

Malloc Stack

启用这个选项后,运行项目,就可以看到一些打印信息。

ShadesOfRay(62232,0x105de6dc0) malloc: stack logs being written into /tmp/stack-logs.62232.1020d0000.ShadesOfRay.EO2R9T.index
ShadesOfRay(62232,0x105de6dc0) malloc: recording malloc and VM allocation stacks to disk using standard recorder
ShadesOfRay(62232,0x105de6dc0) malloc: process 62192 no longer exists, stack logs deleted from /tmp/stack-logs.62192.1043e3000.ShadesOfRay.KeDSdc.index

点击Generate a Ray按钮。

点击按钮

点击按钮后,创建了一个图片视图。请执行以下步骤:

  1. 选择位于Xcode中LLDB控制台顶部的Debug Memory Graph
  2. 选择在左侧面板中Show the Debug navigator
  3. 在左侧面板的底部,选择Show only content from workspace
  4. 选择RayView的引用。
  5. 在Xcode的右侧面板中,确保选中了Show the Memory Inspector
    步骤

在做完了上面的操作之后,我们就可以通过Xcode中的Backtrace部分得到RayView实例是在哪里被创建的精确栈记录。

在getenv中追踪

我们可以跟踪一个实例化的对象的调用栈,但是我们要比苹果做得更好。

我们的命令将能够通过LLDB随意打开MallocStackLogging功能。这意味着我们不必依赖环境变量。这还有一个额外的好处,如果在调试会话期间忘记打开进程,则无需重新启动进程。

MallocStackLogging是传递到进程中的环境变量。这意味着C getenv函数可能用于检查是否提供了此参数。如果提供了,则执行相应的逻辑。

当进程启动时,需要使用getenv查看的所有配置项。创建一个符号断点,以便在调用getenv时查看char*参数。

符号断点

这个会输出很多环境变量,我们进行下一步。限制断点条件为((int)strcmp("MallocStackLogging", $arg1) == 0),并用bt打印调用栈。

符号断点

 * frame #0: 0x0000000112b4da26 libsystem_c.dylib`getenv
    frame #1: 0x0000000112c7dd53
libsystem_malloc.dylib`_malloc_initialize + 466
    frame #2: 0x0000000112ddcac1 libsystem_platform.dylib`_os_once + 36
    frame #3: 0x0000000112c7d849
libsystem_malloc.dylib`default_zone_malloc + 77
    frame #4: 0x0000000112c7d259
libsystem_malloc.dylib`malloc_zone_malloc + 103
    frame #5: 0x0000000112c7f44a libsystem_malloc.dylib`malloc + 24
frame #6: 0x0000000112aa2947 libdyld.dylib`tlv_load_notification +
286
    frame #7: 0x000000010e0f68a9 dyld_sim`dyld::registerAddCallback(void
(*)(mach_header const*, long)) + 134
    frame #8: 0x0000000112aa1a0d
libdyld.dylib`_dyld_register_func_for_add_image + 61
    frame #9: 0x0000000112aa1be7 libdyld.dylib`_dyld_initializer + 47

libsystem_malloc.dylib这个模块感觉和MallocStackLogging有很大关系。使用lookup命令查看libsystem_malloc.dylib模块实现的所有方法。在iOS 13上, 命中了461条。实在太多了,我们只关心log相关的方法:

(lldb) lookup . -m libsystem_malloc.dylib
(lldb) lookup (?i)log -m libsystem_malloc.dylib
****************************************************
25 hits in: libsystem_malloc.dylib
****************************************************
purgeable_log
szone_log
nanov2_log
default_zone_log
nano_log
__mach_stack_logging_copy_uniquing_table
__mach_stack_logging_enumerate_records
__mach_stack_logging_frames_for_uniqued_stack
__mach_stack_logging_get_frames
__mach_stack_logging_get_frames_for_stackid
__mach_stack_logging_set_file_path
__mach_stack_logging_stackid_for_vm_region
__mach_stack_logging_start_reading
__mach_stack_logging_stop_reading
__mach_stack_logging_uniquing_table_copy_from_serialized
__mach_stack_logging_uniquing_table_read_stack
__mach_stack_logging_uniquing_table_release
__mach_stack_logging_uniquing_table_retain
__mach_stack_logging_uniquing_table_serialize
__mach_stack_logging_uniquing_table_sizeof
__stack_logging_early_finished
malloc_register_stack_logger
malloc_zone_log
turn_off_stack_logging
turn_on_stack_logging

现在只有25条了,其中有3条比较有意思的函数:

__mach_stack_logging_get_frames
turn_off_stack_logging
turn_on_stack_logging

看名字就知道turn_on_stack_logging__mach_stack_logging_get_frames值得我们研究。

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

turn_on_stack_logging函数期望传入一个intstack_logging_mode_type的参数。其中stack_logging_mode_all选项的是1

我们再看看另外一个函数__mach_stack_logging_get_framesstack_logging.h也包含了这个函数的声明。

extern kern_return_t 
__mach_stack_logging_get_frames(
                                task_t task,
                  mach_vm_address_t address,
     mach_vm_address_t *stack_frames_buffer,
                  uint32_t max_stack_frames,
                            uint32_t *count );
    /* Gets the last allocation record (malloc, realloc, or free) about
address */

那这些参数,比如task_t task是什么意思,又是怎么获得的呢?在Google中搜索之后,从heap_find.cpp找到了相关的信息。

task_t task = mach_task_self();
/* Omitted code.... */
    stack_entry->address = addr;
    stack_entry->type_flags = stack_logging_type_alloc;
    stack_entry->argument = 0;
    stack_entry->num_frames = 0;
    stack_entry->frames[0] = 0;
    err = __mach_stack_logging_get_frames(task,
                       (mach_vm_address_t)addr,
                           stack_entry->frames,
                                      MAX_FRAMES,
                      &stack_entry->num_frames);
    if (err == 0 && stack_entry->num_frames > 0) {
      // Terminate the frames with zero if there is room
      if (stack_entry->num_frames < MAX_FRAMES)
        stack_entry->frames[stack_entry->num_frames] = 0;
    } else {
      g_malloc_stack_history.clear();
    }
} }

这是一个指明你想要让函数在哪个进程上执行的最基本的参数。当前进程使用位于libsystem_kernel.dylib库中的mach_task_self函数,可以很容易获取到task_t参数。

1.4 测试一下这些函数

在Xcode中, 找到stack_logger.cpp文件。因为__mach_stack_logging_get_frames是用C++写的,因我们你需要用C++的代码来执行它。这个文件中只有一个函数trace_address

void trace_address(mach_vm_address_t addr) {
  
  typedef struct LLDBStackAddress {
    mach_vm_address_t *addresses;
    uint32_t count = 0;
  } LLDBStackAddress;  // 1
  
  LLDBStackAddress stackaddress;  // 2
  mach_vm_address_t address = (mach_vm_address_t)addr;
  task_t task = mach_task_self_;  // 3
  stackaddress.addresses = (mach_vm_address_t *)calloc(100, sizeof(mach_vm_address_t));  // 4
  __mach_stack_logging_get_frames(task, address, stackaddress.addresses, 100, &stackaddress.count);  // 5
  // 6
  for (int i = 0; i < stackaddress.count; i++) {
    printf("[%d] %llu\n", i, stackaddress.addresses[i]);
  }
  free(stackaddress.addresses);  // 7
}
  1. LLDB在执行的时候允许返回一个对象。这里我们创建了一个C结构体。
  2. 声明一个LLDBStackAddress结构体的实例。
  3. 全局变量mach_task_self_是调用mach_task_self时的返回值。
  4. 分配100个mach_vm_address_t
  5. 执行__mach_stack_logging_get_frames。如果有任何可用的栈记录信息,LLDBStackAddress结构体addresses数组中会有很多地址信息。
  6. 打印出__mach_stack_logging_get_frames返回的所有地址。
  7. 最后, 谁申请谁释放。刚刚申请的100个mach_vm_address_t,需要被释放掉。
LLDB测试

点击一下**Generate a Ray! **按钮,然后暂停运行:

(lldb) search RayView -b
RayView * [0x00007fba7540e570]

(lldb) po trace_address(0x00007fba7540e570)
[0] 140734573540504
[1] 140734573542215
[2] 140734556578141
[3] 4366603222
[4] 140734401953791
[5] 140734391624723
[6] 140734391624321
...

(lldb) image lookup -a 140734573540504
      Address: libsystem_malloc.dylib[0x000000000000f498] (libsystem_malloc.dylib.__TEXT.__text + 60164)
      Summary: libsystem_malloc.dylib`malloc_zone_calloc + 139

还有一种解析内存地址的办法。复制第三帧的地址,用SBAddress方法来获取地址对应的信息。

(lldb) script print(lldb.SBAddress(4366603222, lldb.target))
ShadesOfRay`-[ViewController generateRayViewTapped:] + 54 at ViewController.m:41:18
用lldb.value浏览C数组

trace_address函数结束的地方设置一个断点。

(lldb) e -lobjc++ -O -i0 -- trace_address(0x00007fba7540e570)
(lldb) script print(lldb.frame.FindVariable('stackaddress'))
(LLDBStackAddress) stackaddress = {
  addresses = 0x00007fba754143c0
  count = 27
}
(lldb) script a = lldb.value(lldb.frame.FindVariable('stackaddress'))
(lldb) script print(a)
(LLDBStackAddress) stackaddress = {
  addresses = 0x00007fba754143c0
  count = 27
}
(lldb) script print(a.count)
(uint32_t) count = 27
(lldb) script print(a.addresses[0])
(mach_vm_address_t) [0] = 140734573540504
(lldb) script print(a.addresses[3])
(mach_vm_address_t) [3] = 4366603222
(lldb) script print(lldb.SBAddress(4366603222, lldb.target))
ShadesOfRay`-[ViewController generateRayViewTapped:] + 54 at ViewController.m:41:18
将数字传入到栈帧中

打开~/lldb/msl.py。找到handle_command命令并添加下面的代码:

command_args = shlex.split(command)
parser = generateOptionParser()
try:
    (options, args) = parser.parse_args(command_args)
except:
    result.SetError(parser.usage)
    return
cleanCommand = args[0]
process = debugger.GetSelectedTarget().GetProcess()
frame = process.GetSelectedThread().GetSelectedFrame()
target = debugger.GetSelectedTarget()

shlex.split中这里没有使用posix=False。因为这个命令不会处理任何反斜杠和破折号字符。

#1
script = generateScript(cleanCommand, options)
#2
sbval = frame.EvaluateExpression(script, generateOptions())
#3
if sbval.error.fail:
    result.AppendMessage(str(sbval.error))
    return
val = lldb.value(sbval)
addresses = []
#4
for i in range(val.count.sbvalue.unsigned):
    address = val.addresses[i].sbvalue.unsigned
    sbaddr = target.ResolveLoadAddress(address)
    loadAddr = sbaddr.GetLoadAddress(target)
    addresses.append(loadAddr)
#5
retString = processStackTraceStringFromAddresses(addresses, target)
#6
freeExpr = 'free('+str(val.addresses.sbvalue.unsigned)+')'
frame.EvaluateExpression(freeExpr, generateOptions())
result.AppendMessage(retString)
  1. 使用generateScript函数返回一个跟trace_address函数一样的字符串脚本。
  2. 执行脚本返回一个SBValue
  3. 检查EvaluateExpression是否出错。 如果出错了,提取出错误信息并退出。
  4. for循环会遍历val对象中的内存地址放入addresses列表。
  5. 通过addresses列表来对应函数名,返回将要输出的栈记录字符串。
  6. 脚本里面申请了内存,最后用完后需要我们手动释放这部分内存。
(lldb) reload_script
(lldb) search RayView -b
RayView * [0x00007fba7540e570]
RayView * [0x00007fba7540f590]

(lldb) msl 0x00007fba7540e570
frame #0 : 0x7fff52437498 libsystem_malloc.dylib`malloc_zone_calloc + 139
frame #1 : 0x7fff52437b47 libsystem_malloc.dylib`calloc + 24
frame #2 : 0x7fff5140a15d libobjc.A.dylib`_objc_rootAllocWithZone + 37
frame #3 : 0x1044513d6 ShadesOfRay`-[ViewController generateRayViewTapped:] + 54
frame #4 : 0x7fff48093fff UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83
...

1.6 Swift对象的栈记录

50 Shades of Ray里面包含有一个名字叫做SomeSwiftModule的Swift模块。模块中,有一个SomeSwiftCode的类,声明了一个单例。

public final class SomeSwiftCode {
    private init() {}
    static let shared = SomeSwiftCode()
}
(lldb) e -lswift -O -- import SomeSwiftModule
(lldb) e -lswift -O -- SomeSwiftCode.shared
<SomeSwiftCode: 0x600003267c40>

(lldb) search SomeSwiftModule.SomeSwiftCode
<__NSArrayM 0x600003ca8210>(
SomeSwiftModule.SomeSwiftCode
)

(lldb) search SomeSwiftModule.SomeSwiftCode -b
_TtC15SomeSwiftModule13SomeSwiftCode * [0x0000600003267c40]

Swifth会试图在description中隐藏指针。在search命令中使用--brief(-b)选项使用我们的方法获取实例信息。之后使用msl查看调用栈:

(lldb) msl 0x0000600003267c40
frame #0 : 0x7fff52437339 libsystem_malloc.dylib`malloc_zone_malloc + 140
frame #1 : 0x7fff52437ac4 libsystem_malloc.dylib`malloc + 21
frame #2 : 0x104a47af9 libswiftCore.dylib`swift_slowAlloc + 25
frame #3 : 0x104a47b74 libswiftCore.dylib`_swift_allocObject_(swift::TargetHeapMetadata<swift::InProcess> const*, unsigned long, unsigned long) + 20
frame #4 : 0x10476e9a4 SomeSwiftModule`SomeSwiftModule.SomeSwiftCode.__allocating_init() -> SomeSwiftModule.SomeSwiftCode + 52
...

1.7 只运行Python代码

我们现在使用Stripped 50 Shades of Ray这个scheme,确保了取消Malloc Stack选项,重新运行。

scheme

我们来尝试一下turn_on_stack_logging函数。运行程序,点击一下**Generate a Ray! **按钮,然后暂停运行:

(lldb) search RayView -b
RayView * [0x00007fc413d0cd80]
(lldb) msl 0x00007fc413d0cd80

什么都没有。因为MallocStackLogging环境变量没有被应用到这个进程上。在LLDB中输入:

(lldb) po turn_on_stack_logging(1)
ShadesOfRay(64227,0x115dcddc0) malloc: stack logs being written into /tmp/stack-logs.64227.1122eb000.ShadesOfRay.egknZC.index
ShadesOfRay(64227,0x115dcddc0) malloc: recording malloc and VM allocation stacks to disk using standard recorder
0x0000000000000001

我们得到了一些与将MallocStackLogging环境变量开启时类似的输出。继续运行,点击一下**Generate a Ray! **按钮,之后暂停。

(lldb) search RayView -b
RayView * [0x00007fc413d0cd80]
RayView * [0x00007fc413d15570]
(lldb) msl 0x00007fc413d15570
frame #0 : 0x7fff52437498 libsystem_malloc.dylib`malloc_zone_calloc + 139
frame #1 : 0x7fff52437b47 libsystem_malloc.dylib`calloc + 24
frame #2 : 0x7fff5140a15d libobjc.A.dylib`_objc_rootAllocWithZone + 37
frame #3 : 0x10b7753d6 ShadesOfRay`___lldb_unnamed_symbol4$$ShadesOfRay + 54
frame #4 : 0x7fff48093fff UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83
...

注意到我们的第三帧是stripped的!

frame #3 : 0x10b7753d6 ShadesOfRay`___lldb_unnamed_symbol4$$ShadesOfRay + 54

但是我们的sbt中的processStackTraceStringFromAddresses是有解析它的能力的。在msl.py中先import sbt,并在handle_command函数中替换retString = processStackTraceStringFromAddresses(addresses, target)为:

if options.resymbolicate:
    retString = sbt.processStackTraceStringFromAddresses(addresses, target)
else:
    retString = processStackTraceStringFromAddresses(addresses, target)

在测试代码之前,我们还需要创建一个快捷命令来启用turn_on_stack_logging。找到文件中的__lldb_init_module函数,然后添加下面的代码:

debugger.HandleCommand('command alias enable_logging expression -lobjc -O -- extern void turn_on_stack_logging(int); turn_on_stack_logging(1);')

这这个命令声明了一个快捷命令enable_logging来打开malloc栈日志。

实验一下。🎉🎉🎉

lldb) reload_script
(lldb) msl 0x00007fbe7e430840 -r
frame #0 : 0x7fff52437498 libsystem_malloc.dylib`malloc_zone_calloc + 139
frame #1 : 0x7fff52437b47 libsystem_malloc.dylib`calloc + 24
frame #2 : 0x7fff5140a15d libobjc.A.dylib`_objc_rootAllocWithZone + 37
frame #3 : 0x10b7753d6 ShadesOfRay`-[ViewController generateRayViewTapped:] + 54
frame #4 : 0x7fff48093fff UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341

推荐阅读更多精彩内容