【问题标题】:Stack overflow: Thread 1: EXC_BAD_ACCESS (code=2, address=0x16d09aa00)堆栈溢出:线程 1:EXC_BAD_ACCESS(代码=2,地址=0x16d09aa00)
【发布时间】:2020-05-14 09:24:43
【问题描述】:

崩溃描述

最近,我在我的一个 iOS/Swift 项目中遇到了非常奇怪的内存问题。我真的不确定发生了什么,感觉也不太容易描述,但无论如何我会尽力而为。

它的行为基本上如下:

  • 在某个代码库中,崩溃总是发生在同一个地方(100% 可重现)
  • 对代码库的更改可能会解决问题,但也可能只是在其他地方弹出
  • 崩溃只发生在真实设备上,绝不会在模拟器中发生

当前应用程序崩溃并出现以下错误(3 次不同运行的结果):

线程 1:EXC_BAD_ACCESS(代码=2,地址=0x16d09aa00)

线程 1:EXC_BAD_ACCESS(代码=2,地址=0x16af46a00)

线程 1:EXC_BAD_ACCESS(代码=2,地址=0x16d526a00)


内存地址推理

WWDC 会议

我在 WWDC 2018 上发现了一个有趣的会话 (Understanding Crashes and Crash Logs),其中一个人指出,有时可以从特定内存地址中获取更多信息,然后发生崩溃。

不幸的是,它在我的应用程序中崩溃的地址有些完全不同,但也许我们可以从他们那里得到线索?至少有趣的是,它们都非常相似,不是吗?

因启用诊断选项而发生的变化

进一步调查显示,前 2 个字节 (16) 始终保持不变,随后是 4 个随机字节,然后是 3 个字节 (a00)。激活诊断(例如 ASan 或 Scribble)时,最后 3 个字节会发生变化(例如 3a0 或 9e0)。但也许这只是由于添加了更多“调试内容”而导致的一种转变?我真的不是那种“记忆力强”,只是想提供我注意到的任何东西。


尝试“诊断选项”

我尝试了不同的诊断选项(来自方案),但它们都没有真正改变崩溃,也没有提供更多信息。

1。涂鸦

崩溃不引用 0xAA 或 0x55,所以使用 Scribble 没有什么可以捕获的? (Xcode - scribble, guard edges and guard malloc)

2。 Malloc 保护边缘

使用这个也没有发现任何区别。

3。僵尸

使用这个guide

malloc_info --type 0x16b15e9c0

错误:错误:试图将堆栈放入不可读的内存中:0x16b15e920。

4。阿桑

使用 ASan 只是将以下条目放在堆栈跟踪的顶部。不幸的是,我没有发现任何与此相关的有用信息。

#0 0x0000000109efbf60 in __asan_alloca_poison ()

5。 TSan

在真实设备上不可用(崩溃只发生在那里)


递归/BOF?

可能是递归太长,还是其他类型的堆栈/堆缓冲区溢出? 但似乎真实设备和模拟器上的堆栈大小与524288 字节(来自Thread.main.stackSize)完全相同。

那么,由于它不会在模拟器中崩溃,所以它不是 BOF?还是架构差异太大,无法在这里得出这样的结论?


拆解

我也试过“拆机”。

disassemble -a 0x16d09aa00

错误:找不到地址 0x16d09aa00 的函数边界

disassemble -frame

但是我的汇编技能确实落后,所以目前我无法从这些信息中得到任何信息。


需要帮助

如您所见,我的想法真的不多了。要么崩溃真的很奇怪,要么我没有足够的知识/技能来使用上述工具,让我更接近这些问题的原因。

无论哪种方式...任何帮助、提示、想法或任何可以为我指明正确方向的东西都非常感谢!

提前谢谢各位。


2020 年 5 月 19 日更新

我完全忘了提及,我们在应用程序中大量使用了ReSwift,我猜崩溃似乎与我们在那里使用中间件的方式有关。

我也已经与那里的开发人员联系:github.com/ReSwift/ReSwift/issues/271

最后是一些代码。不幸的是,我不能分享所有的应用程序代码(这可能是必要的!?),也不想让你用太多的代码来超载。

当前问题

线程 1:EXC_BAD_ACCESS(代码=1,地址=0x16ed82da0)

UserAccountMiddleware.swift

注意:使用这些DispatchQueue.main.async 实际上会使崩溃消失。他们确实打破了当前的循环,所以可能发生了某种递归或时间问题?

func userAccountMiddleware() -> Middleware<AppState> {
    return { dispatch, getState in
        return { next in
            return { action in
                switch action {
                case _ as ReSwiftInit:
//                    DispatchQueue.main.async {
                        dispatch(UserAccountSetAuthToken(authToken: Defaults.customerAuthToken))
                        dispatch(UserAccountSetAvatar(index: Defaults.avatarIndex))
//                    }
                    if let data = Defaults.customer,
                        let customer = try? JSONDecoder().decode(Customer.self, from: data) {
//                        DispatchQueue.main.async {
                            dispatch(UserAccountSetCustomerLoggedIn(customer: customer))
//                        }
                    }

                // [...]

                default:
                    break
                }

                next(action)
            }
        }
    }
}

ReSwift Store.swift

    // [...]
    open func _defaultDispatch(action: Action) {
        guard !isDispatching else {
            raiseFatalError(
                "ReSwift:ConcurrentMutationError- Action has been dispatched while" +
                " a previous action is action is being processed. A reducer" +
                " is dispatching an action, or ReSwift is used in a concurrent context" +
                " (e.g. from multiple threads)."
            )
        }

        isDispatching = true
        let newState = reducer(action, state) // Thread 1: EXC_BAD_ACCESS (code=1, address=0x16ed82da0)
        isDispatching = false

        state = newState
    }
    // [...]

Xcode 控制台:

(lldb) po state
error: warning: couldn't get required object pointer (substituting NULL): Couldn't load 'self' because its value couldn't be evaluated

error: Trying to put the stack in unreadable memory at: 0x16d95ad00.

汇编程序(崩溃的最后一步):

myapp`type metadata accessor for GlobalState:
    0x101f6ac10 <+0>:  sub    sp, sp, #0x30             ; =0x30 
->  0x101f6ac14 <+4>:  stp    x29, x30, [sp, #0x20] // Thread 1: EXC_BAD_ACCESS (code=1, address=0x16ed82da0)
    0x101f6ac18 <+8>:  adrp   x8, 3620
    0x101f6ac1c <+12>: add    x8, x8, #0x148            ; =0x148 
    0x101f6ac20 <+16>: ldr    x8, [x8]
    0x101f6ac24 <+20>: mov    x9, #0x0
    0x101f6ac28 <+24>: mov    x1, x8
    0x101f6ac2c <+28>: str    x0, [sp, #0x18]
    0x101f6ac30 <+32>: str    x1, [sp, #0x10]
    0x101f6ac34 <+36>: str    x9, [sp, #0x8]
    0x101f6ac38 <+40>: cbnz   x8, 0x101f6ac54           ; <+68> at <compiler-generated>
    0x101f6ac3c <+44>: adrp   x1, 2122
    0x101f6ac40 <+48>: add    x1, x1, #0x1dc            ; =0x1dc 
    0x101f6ac44 <+52>: ldr    x0, [sp, #0x18]
    0x101f6ac48 <+56>: bl     0x102775358               ; symbol stub for: swift_getSingletonMetadata
    0x101f6ac4c <+60>: str    x0, [sp, #0x10]
    0x101f6ac50 <+64>: str    x1, [sp, #0x8]
    0x101f6ac54 <+68>: ldr    x0, [sp, #0x8]
    0x101f6ac58 <+72>: ldr    x1, [sp, #0x10]
    0x101f6ac5c <+76>: str    x0, [sp]
    0x101f6ac60 <+80>: mov    x0, x1
    0x101f6ac64 <+84>: ldr    x1, [sp]
    0x101f6ac68 <+88>: ldp    x29, x30, [sp, #0x20]
    0x101f6ac6c <+92>: add    sp, sp, #0x30             ; =0x30 
    0x101f6ac70 <+96>: ret     

【问题讨论】:

  • 崩溃代码几乎总是毫无意义的。您能提供完整的崩溃日志吗?
  • 好的,我稍后会尝试提供崩溃报告。
  • 请输入代码。
  • 调试模式与分发在设备上构建有什么区别?是在扩展崩溃?应用程序是在前台还是后台进程?
  • 好的,我很确定我明白了。除了那些 ReSwift 中间件“递归”之外,我们还有一个相当大的数据结构(根据MemoryLayout.size(ofValue: getState()),状态约为 11kb)。大型结构和更深层次的递归可能会很快填满这个 512kb 的堆栈 -> 堆栈溢出(或者你怎么称呼它)。我也可以在一个完全精简的演示项目中重现这个问题。因此,如果有人感兴趣,我可以稍后在此处提供更多详细信息。

标签: ios swift xcode exc-bad-access reswift


【解决方案1】:

TL;DR

只需将巨大的结构移动到堆中,将它们包装在数组中即可。使用@propertyWrappers,这至少是部分优雅的解决方案。

@propertyWrapper
struct StoredOnHeap<T> {
    private var value: [T]

    init(wrappedValue: T) {
        self.value = [wrappedValue]
    }

    var wrappedValue: T {
        get {
            return self.value[0]
        }

        set {
            self.value[0] = newValue
        }
    }
}

// Usage:
@StoredOnHeap var hugeStruct: HugeStruct

https://gist.github.com/d4rkd3v1l/ab582a7cafd3a8b8c164c8541a3eef96


加长版

我现在几乎 100% 确定这是堆栈溢出,因为我(终于)设法在一个小演示项目中重现了这一点:https://github.com/d4rkd3v1l/ReSwift-StackOverflowDemo

现在我将为其他可能遇到此问题或类似问题的人提供更多详细信息和解决方案。

iOS 上的堆栈大小(从 iOS 13 开始)为 512kb,应该适用于设备和模拟器。为什么我说“应该”?因为它几乎可以肯定在模拟器上有所不同,因为我没有看到那些崩溃。所以也许Thread.main.stackSize 只是告诉 512kb 但实际上更大? IDK?‍♂️


这里有一些指标,你可能会遇到同样的问题:

  • 您收到EXC_BAD_ACCESS 崩溃,代码为 1 或 2**。 并且崩溃发生在高内存地址中,或者至少完全不在您的应用程序/堆栈的其余部分通常“存在”的位置。就我而言,类似于0x16d95ad00
  • 减少您放入堆栈的内容(值类型,例如非常非常大的结构)或将调用堆栈分解成更小的部分(例如调度异步)以给堆栈一些“喘息时间”来防止这种情况发生崩溃。

在后者中,我们已经处于该问题的解决方案的中间。由于堆栈大小不能(甚至可能不应该)增加,因此您必须减少放在那里的负载,如第二点所述。

至少这是我们可能会采用的解决方案。 ?


*至少对于主线程是这样,其他线程可能不同。

**我认为代码 0 有点空指针异常,因此不适用于此处。如果我对此有误,请纠正我。

【讨论】:

  • 谢谢!当使用结构类型参数为大结构实现初始化程序时,我遇到了类似的崩溃。
  • 感谢@d4Rk 的详细解释,我在使用 RxSwift + 一种减速器时遇到了类似的问题。你对我帮助很大!
  • 伙计,你的问题和你的回答都恰到好处!非常感谢分享@d4Rk。刚刚遇到了同样的麻烦,并且和你一样怀疑 SO,并且已经进入 DispatchQueue.async 方法......同样有趣的是,对我们来说,崩溃只发生在 Swift 编译器优化 -Onone 时,当转向 -O - 崩溃消失了。这基本上使我们免于在发布版本中出现大量崩溃...
猜你喜欢
  • 2019-01-04
  • 2015-10-07
  • 2020-11-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-12
  • 1970-01-01
  • 2018-03-13
相关资源
最近更新 更多