IDA 9.4 beta 和 iOS 27 的 dyld_shared_cache 的一些变化

· 2026-06-12 09:41 · 0 阅读

原创 0xcc 2026-06-12 09:41 瑞士

IDA Pro 9.4 beta 测试初体验;iOS 27 给链接器又上了什么黑魔法?

6 月 11 日 IDA Pro 9.4 放出了 beta 测试版。这一版相比上一版加入了非常多的功能,然而我就不丢给 AI 自动洗稿了,请参阅原文。

https://docs.hex-rays.com/release-notes/9_4beta

在这里提两个新功能的痛点。

一个是 PathFinder,顾名思义就是找调用链。可以给出 source 和 sink,然后帮你遍历路径,也就是之前需要外接一个图数据库才能做的。想象一下接上 llm,简直了。

在一个就是查找二进制序列。之前只能做逐字节匹配,例如:AA BB CC;现在可以用通配符了:AA B? C? DD。


值得一提的是这一版的 dyld_shared_cache 工作流改善后非常之好用,从从交互到速度和各种 bug 修复都让人印象深刻。

https://docs.hex-rays.com/9.4/core/disassembler/concepts/dsc-workflow

我上个月写过一篇文章,由于现有的反编译器 loader 对 DSC 支持仅仅聊胜于无,我干脆写了个提取单个 dylib 的工具:2026 年了,还在折腾 dyld_shared_cache

才过去一个月这篇文章就过时了,现在的方案是:闭着眼用 9.4 就好。

和之前每次载入 dylib 都要点开几层菜单,然后在对话框里选择路径不同,IDA Pro 把 DSC 支持提升到了一等公民,直接在工作区右边添加了一个新的侧边栏。

体验很顺滑,载入模块的体感快了很多。当然我没有严格对比掐过秒表,也可能是心理作用。毕竟现在数码评测要资质,还是别测了。

在二进制视图中的未知地址不再显示红色数字,而是会提示其所在的库,可以选择性载入内容。

一个显著修复的 bug 就是 __objc_methlist 节的解析。9.3 下红字(错误的偏移量):

9.4 正确识别:

当然这个例子有点鸡蛋里挑骨头,大多数人可能都不会注意到。

这个节的信息对 class-dump 很有用,用以关联 Objective-C 方法名的 selector 和具体函数,以及方法类型签名。

但说回来直接在 dyld_shared_cache 上做 class-dump,静态的有 ipsw 工具内置支持,动态的话则只需要调用 objc/runtime.h 里的 API 就可以,完全不需要管具体的结构。

目前试用下来个人给出绝对好评。哪怕是周一刚出炉的 iOS 27 beta 的 dyld_shared_cache 也做到了开箱即用。

下面是博主被打脸时间。

前面提到并链接了我之前解包单个链接库的思路,里面用到了一些作弊的技巧。

此前版本的 dyld 优化会讲一些跨文件的函数调用(__auth_stub)从模块中提取出来,统一合并成全局共享 stub island 和 global offset table(GOT)。这一过程处理完之后,原本 dylib 里的 __auth_stub 和 __auth_got 节实质上就已经没有用了,只是二进制优化这一步没有去掉。

类似的还有一些 Objective-C runtime 需要的数据,例如 selector 字符串池(__objc_methname 和 __objc_classname 等),会统一合并到 libobjc.A.dylib 的 __OBJC_RO 段。

原始库当中的副本实质上没有交叉引用,依旧保留到最终的 DSC 文件。我的工具利用这个特点省去了很多符号恢复和区段重建的工作。

但是!来看周一 WWDC 刚发的 KeyNote:

iOS 27 大刀阔斧地开始性能优化。

具体怎么做没说,但分析 DSC 就能看到一些端倪。

前文提到这些 section 在 iOS 26 还保留副本在 dylib,但是没有被实际用到。这次直接被优化掉了,可能是到了 35 岁了吧。

现在如果闲得慌还想实现单个文件提取,就得从零重建好几个 section,而且这些 section 都是第一个 segment (__TEXT),这就导致整个二进制文件需要重排。

于是我的工具炸了,一时半会修不好。笔者奉行实用主义,既然 IDA 已经足够好用,就不必炫技了——反正也是丢给 AI 写的。太不给面子了,公众号才发出来一个月呐。


下面再节选几个点分析 iOS 27 二进制层面做的一些优化。

在 iOS 26 上,大部分调用 objc_msgSend 的代码会被 outline 处理,放进 __objc_stubs 当中,一共有 6 行指令实现(觉得我数错的请面壁并回去复习 ARM 伪指令):

    Foundation:__objc_stubs:00000001815DD580
    _objc_msgSend$HTTPBody                  ; CODE XREFURLRequest.httpBody.getter+24↑p
    ADRP            X1#selRef_HTTPBody@PAGE
    LDR             X1, [X1,#selRef_HTTPBody@PAGEOFF] ; "HTTPBody" ...
    ADRL            X17, _objc_msgSend_ptr
    LDR             X16, [X17] ; _objc_msgSend
    BRAA            X16X17 ; _objc_msgSend

    到了 iOS 27 这个 __objc_stubs 不见了,和 stub island 一样被移出了框架,到了一个不属于任何一个 dylib 的区域,而且缩减成了 3 行指令:

      ADRL            X1, selRef_HTTPBody
      B               objc_msgSend ; /usr/lib/objc/libobjcMsgSend.dylib

      这时候有些读者就要问了,branch 指令的跳转范围是有限制的,这一整个 cache 肯定超了。怎么做到所有的 objc_msgSend 调用都直接用标签而不用 GOT?

      解析这个 DSC 的文件列表会发现 iOS 专门为 Objective-C 消息转发生成了一堆同样的 dylib:

        /usr/lib/objc/libobjcMsgSend.dylib
        /usr/lib/objc/libobjcMsgSend1.dylib
        /usr/lib/objc/libobjcMsgSend2.dylib
        ...
        /usr/lib/objc/libobjcMsgSend33.dylib

        多机智……每隔一段距离就插入一份代码副本,你就说要不要担心跳转长度吧。

        而对于跨 dylib 的函数调用,在 iOS 26 上 stub island 是这样的:

          ADRL            X17, _dlsym_ptr ; GOT 里的函数指针
          LDR             X16, [X17]
          BRAA            X16, X17

          到了 iOS 27 仍然可以看到这样的代码,但是多出了另外两种类型。

          一种少了一次内存 load,直接跳转过去:

            ADRL            X16, _open
            BR              X16

            另一种让人第一眼非常困惑:

              dyld_shared_cache_arm64e.02:__stubs:0000000188138000
              _objc_retainBlock_0                     ; CODE XREF: _objc_retainBlock↑j
              ADR             X16, loc_18803CC2C
              MOV             X17, #0x9B7
              ADD             X16, X16, X17,LSL#21
              BR              X16

              其实就是用一个加法移位计算出 x16 的值,也就是目标函数的地址,然后跳转过去。

                  X16 = 0x18803CC2C + (0x9B7 << 21)
                      = 0x18803CC2C + 0x136E00000
                      = 0x2BEE3CC2C ; libsystem_block.dylib!__Block_copy

                第一个 ADR 用的是 PC 相对地址,这是合法的位置无关代码。

                但乍一看这个写法指令数也没少啊?

                之前的代码需要从 GOT 里载入一个目标函数的指针。这个指针在程序启动(或者页面第一次被访问)的时候进行重定位,arm64e 下还要处理 PAC 加签。

                现在少了 GOT,首先省去了启动时的指针 rebase 操作,第二执行时也不需要 load 内存,连 cache 都不用考虑。而且 PAC 指令的跳转还要校验指针合法性,相比无条件的 BR 也有额外开销。

                我去,这也行。


                虽然对 IDA 一阵猛夸,还是看到了一处可以优化的点。

                这堆代码有点莫名其妙。因为 IDA 暂时不认识新版 __objc_stub 的跳转逻辑,BL 的标签没有一个有效的名字:

                但这个问题可以手动修复。如下例子:

                  Foundation:__text:0000000180C8657C
                  __NSBundle_load_
                  MOV             X2, #0
                  B               unk_1880035F0

                    BOOL __cdecl -[NSBundle load](NSBundle *self, SEL a2)
                    {
                      BOOL result; // w0


                      unk_1880035F0();
                      return result;
                    }

                    双击 unk_1880035F0 跳转到目标地址,按一下 c 标记成代码:

                      dyld_shared_cache_arm64e.01:GAP:00000001880035F0
                      loc_1880035F0
                      ADRL            X1, sel_loadAndReturnError_ ; "loadAndReturnError:"
                      B               ⇗ /usr/lib/objc/libobjcMsgSend.dylib:_objc_msgSend

                      这时候真正的 selector 就出来了。在 ADRL 上按 n 重命名地址,根据具体的 selector,如上的例子为 _objc_msgSend$loadAndReturnError:

                      IDA Pro 允许函数命名包含冒号,提交之后会显示成下划线。

                      如果 IDA 没有自动识别出参数个数,最好按 y 手动定义一下。再返回调用方,F5 就好多了:

                        BOOL __cdecl -[NSBundle load](NSBundle *self, SEL a2)
                        {
                          BOOL result; // w0


                          objc_msgSend_loadAndReturnError_(a1: self, (void *)a2, a3: nullptr);
                          return result;
                        }

                        已经反馈给官方了 

                        觉得这篇笔记有意思的话给个打赏呗 

                        跳转微信打开