文章

找不到 __darwin_check_fd_set_overflow 符号

找不到 __darwin_check_fd_set_overflow 符号

背景

上篇文章记录了找不到 libswiftCore.dylib Swift 运行时库导致的无法启动问题,今天又遇到了一个找不到 __darwin_check_fd_set_overflow 符号导致无法启动的有趣问题:

1
2
3
4
Dyld Error Message: 
  Symbol not found: ___darwin_check_fd_set_overflow
  Referenced from: /Applications/Player.app/Contents/MacOS/../Frameworks/FSPlayer.framework/Versions/A/FSPlayer
  Expected in: /usr/lib/libSystem.B.dylib in /Applications/SHPlayer.app/Contents/MacOS/../Frameworks/FSPlayer.framework/Versions/A/FSPlayer

这个问题很离奇,线上版本不会崩溃,更换了私有网络库之后,就发现小于 11 系统的 intel Mac 无法启动问题。实际上在更换网络库之前,还接入了一个新的 SDK,实现了 drm 功能。

误判 OpenSSL

由于最近更换了网络库,所以就在私有网络库里查找相关的符号,结果瞎猫逮到了死耗子,虽然没直接搜到,但是搜到了一个比较接近的符号,查看库的符号使用 nm 命令:

1
2
[matt@matt lib (master)]$ nm libssl.a | grep set_fd
000000000000212c T _SSL_set_fd

之前 OpenSSL 没有打开多线程支持出现过崩溃,所以这次出于惯性思维,事情到此就反馈给了私有网络库团队来解决,对方判断使用的 llvm 太高了,并且在 Xcode 里发现了 __darwin_check_fd_set_overflow 的最低可用版本是 11 系统,所以这个锅他们背了。

1
int __darwin_check_fd_set_overflow(int, const void *, int) __API_AVAILABLE(macosx(11.0), ios(14.0), tvos(14.0), watchos(7.0));

牛逼的他们第二天就给了我新包验证,他们的修改方案是 x86 用 Xcode 9.4.1 编译,arm64 用Xcode12.5.1编译,从而避免在 x86 上找不到 __darwin_check_fd_set_overflow 符号。

我这边更新库后换包验证,可以正常启动了。后来了解到他们知道网络库用到了 select,并且知道是 libcurl 用的,而我当时是不知道的。以为是 openssl 对 fd_set 做了封装,改名字叫 set_fd 呢。

卷土重来

5 天后,测试过程中发现 smb 功能不可用了,排查到由于脚本改动导致了 libsmb2 库没有链接到 FFmpeg 库里,导致 smb 协议缺失,脚本问题很快得到了修复,本机 M1 芯片 Mac 验证没有问题后开始打包,然而在 10.14 的 intel 系统上又出现了找不到 __darwin_check_fd_set_overflow 符号导致无法启动的问题。

什么鬼?链接的 libsmb2 库已经用了好几个月了,没有任何变更,感觉不应该是这个库的问题。虽然我在 libsmb2 库的源码里找到了 FD_SET,符号也找到了:

1
2
[matt@matt lib (master)]$ nm libsmb2.a | grep fd_set 
                 U ___darwin_check_fd_set_overflow

查看最低支持版本:

1
2
3
4
      cmd LC_VERSION_MIN_MACOSX
 cmdsize 16
  version 10.11
       sdk 14.5

这时我想起来,在早些时候接入了实现 drm 功能的库,于是发现也包含 ___darwin_check_fd_set_overflow 符号,接着使用 otool 命令检查最低支持版本发现了问题:

1
2
3
4
5
6
7
8
9
10
11
cmd LC_VERSION_MIN_MACOSX
  cmdsize 16
  version 11
      sdk 12.3
      
# 顺便提下,ARM架构
cmd LC_BUILD_VERSION
  cmdsize 24
 platform 1
    minos 11.0
      sdk 12.3

可以看出最低支持版本有问题,于是要求乙方修改最低支持版本解决了,很奇怪为什么之前没发现?

回过头再次检查私有网络库,并检查 openssl 的源码,没有找到 ___darwin_check_fd_set_overflow 符号,但是在 libcurl 这个库里却找到了,说明 openssl 被冤枉的,问题出在 libcurl 上,查看最低支持版本发现了点异常:

修改后的:

1
2
3
4
5
6
7
8
9
10
11
12
 # x86_64
      cmd LC_VERSION_MIN_MACOSX
  cmdsize 16
  version 10.12
      sdk n/a
 
 # arm64
      cmd LC_BUILD_VERSION
  cmdsize 24
 platform 1
    minos 11.0
      sdk 11.3

修改前的:

1
2
3
4
5
6
7
8
9
10
11
12
 # x86_64
      cmd LC_VERSION_MIN_MACOSX
  cmdsize 16
  version 10.12
      sdk n/a
 
 # arm64
      cmd LC_BUILD_VERSION
  cmdsize 24
 platform 1
    minos 11.0
      sdk 14.4

虽然最低支持版本是 10.12,但是 sdk 没找到。由于私有网络库的是在 Linux 上交叉编译的,环境有些复杂,至今这个问题还像个谜一样,不过我在 macOS 上使用 Clang 编译的 libsmb2 库倒是没有问题。

FD_SET 的实现

FD_SET 是一个宏定义,源码在 <sys/_types/_fd_set.h>

1
2
3
4
#ifndef FD_SET
#include <sys/_types/_fd_def.h>
#define FD_SET(n, p)    __DARWIN_FD_SET(n, p)
#endif /* FD_SET */

__DARWIN_FD_SET 也是个宏定义,它在 <sys/_types/_fd_def.h>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
__BEGIN_DECLS
typedef struct fd_set {
	__int32_t       fds_bits[__DARWIN_howmany(__DARWIN_FD_SETSIZE, __DARWIN_NFDBITS)];
} fd_set;

int __darwin_check_fd_set_overflow(int, const void *, int) __API_AVAILABLE(macosx(11.0), ios(14.0), tvos(14.0), watchos(7.0));
__END_DECLS

__header_always_inline int
__darwin_check_fd_set(int _a, const void *_b)
{
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability-new"
#endif
	if ((uintptr_t)&__darwin_check_fd_set_overflow != (uintptr_t) 0) {
#if defined(_DARWIN_UNLIMITED_SELECT) || defined(_DARWIN_C_SOURCE)
		return __darwin_check_fd_set_overflow(_a, _b, 1);
#else
		return __darwin_check_fd_set_overflow(_a, _b, 0);
#endif
	} else {
		return 1;
	}
#ifdef __clang__
#pragma clang diagnostic pop
#endif
}

/* This inline avoids argument side-effect issues with FD_ISSET() */
__header_always_inline int
__darwin_fd_isset(int _fd, const struct fd_set *_p)
{
	if (__darwin_check_fd_set(_fd, (const void *) _p)) {
		return _p->fds_bits[(unsigned long)_fd / __DARWIN_NFDBITS] & ((__int32_t)(((unsigned long)1) << ((unsigned long)_fd % __DARWIN_NFDBITS)));
	}

	return 0;
}

__header_always_inline void
__darwin_fd_set(int _fd, struct fd_set *const _p)
{
	if (__darwin_check_fd_set(_fd, (const void *) _p)) {
		(_p->fds_bits[(unsigned long)_fd / __DARWIN_NFDBITS] |= ((__int32_t)(((unsigned long)1) << ((unsigned long)_fd % __DARWIN_NFDBITS))));
	}
}

__header_always_inline void
__darwin_fd_clr(int _fd, struct fd_set *const _p)
{
	if (__darwin_check_fd_set(_fd, (const void *) _p)) {
		(_p->fds_bits[(unsigned long)_fd / __DARWIN_NFDBITS] &= ~((__int32_t)(((unsigned long)1) << ((unsigned long)_fd % __DARWIN_NFDBITS))));
	}
}


#define __DARWIN_FD_SET(n, p)   __darwin_fd_set((n), (p))
#define __DARWIN_FD_CLR(n, p)   __darwin_fd_clr((n), (p))
#define __DARWIN_FD_ISSET(n, p) __darwin_fd_isset((n), (p))

在 Xcode 16 里输入这些宏之后,不能直接跳转到定义的地方,需要从头文件进去看,很不方便。

看了 __darwin_check_fd_set 的实现之后,发现里面会先检查 __darwin_check_fd_set_overflow 符号是否存在,然后决定是否调用。

在 Xcode 9.4 版本,是这么实现的:

1
#define      __DARWIN_FD_SET(n, p)   do { int __fd = (n); ((p)->fds_bits[(unsigned long)__fd/__DARWIN_NFDBITS] |= ((__int32_t)(((unsigned long)1)<<((unsigned long)__fd % __DARWIN_NFDBITS)))); } while(0)

多路复用 I/O(I/O Multiplexing)

程序在单个线程中同时监控多个文件描述符(如套接字、文件、管道等),并在其中任何一个或多个变为可读、可写或异常状态时获得通知,从而避免阻塞在单个 I/O 操作上。

多路复用的关键在于使用特定系统调用(如 select、poll、epoll)来监控多个文件描述符的状态,当某个描述符就绪时,程序才进行相应的 I/O 操作。

其中 select 方式,需要通过 FD_SET 将多个描述符加入到一个集合里,然后调用 select() 阻塞等待,返回后再检查哪些描述符发生了变化,做出相应的操作。这种方式的缺点是受 FD_SETSIZE 的限制,可监控的文件描述符数量有限,每次调用需重新设置集合,且需遍历所有描述符检查状态,内核与用户空间频繁复制数据,效率低。但无论怎样,单线程多路复用技术都比多线程(多进程)+ 传统阻塞 I/O 实现并发连接要高效的多。

其中 poll 模式,使用链表存储描述符,摆脱了最大数量限制的问题。

而 epoll 模式,则通过事件驱动机制,仅返回就绪的描述符,无需遍历。epoll的效率得到了极大的提升,可高效处理大量连接(十万级以上),采用边缘触发(Edge Triggered)或水平触发(Level Triggered)模式,内核与用户空间使用共享内存,减少数据复制。

Linux 上有 epoll 技术,Windows 有 IOCP,macOS 有 kqueue 技术,而 libuv 库做了跨平台封装,实现了不同操作系统的 I/O 多路复用机制,Node.js 就是单线程、非阻塞 I/O 的 JavaScript 运行时,其核心是 事件驱动架构 和 libuv 库。

Nginx 是高性能的 Web 服务器和反向代理,采用多进程 + 事件驱动的架构,其中主进程负责管理工作进程。每个工作进程都是单线程运行,并且通过 epoll 同时处理多个连接。默认情况下,工作进程数与 CPU 核心数相同,为了充分利用多核 CPU 的性能。

本文由作者按照 CC BY 4.0 进行授权

© debugly. 保留部分权利。

本站采用 Jekyll 主题 Chirpy