Xcode version 13.2 & 13.2.1 在iOS12上 由libswift_Concurrency.dylib 引起的crash修复

2,401 阅读6分钟

最近很多朋友都遇到了在iOS12系统上关于libswift_Concurrency的crash, 现象是通过xcode 13.2 & 13.2.1 构建的项目直接或间接使用了Concurrency 的功能, 在iOS12上稳定crash

原因分析

xcode 13.2 release notes中提到该Crash是Clang 编译器的bug, 13.2.1 release notes说已经修复, 实际测试并没有, 13.3 beta应该是修复了;

xcode编译器在低版本(12)上没有将libswift_Concurrency.dylib剔除, 反而是通过内嵌的方式将libswift_Concurrency.dylib放到了ipa的Frameworks路径下, 导致动态链接时, libswift_Concurrency被链接导致crash

error分析过程:

  1. 通过报错信息Library not loaded: /usr/lib/swift/libswiftCore.dylib 分析是动态库没有加载, 提示是libswift_Concurrency.dylib引用了该库, 但是libswift_Concurrency只有在iOS15系统上才会存在, 考虑为什么iOS12会启动加载呢,

    iOS12的系统是不存在这个库的, 能加载只有一种情况即类似iOS12以下版本中swift核心库的处理方式,内嵌在了ipa中;

    校验方式也很简答, 通过iOS12真机run一下, 崩溃后通过image list查看加载的镜像文件会找到libswift_Concurrency的路径是ipa/Frameworks下的, 通过解包ipa也证实了这一点

    WechatIMG20.png

  2. 在按照xcode 13.2 release notes提供的方案, 将libswiftCore设置为weak并指定rpath后, crash信息变更, 此时error原因是___chkstk_darwin 符号找不到;

    symbol not found.png 根据error Referenced from 发现还是libswift_Concurrency引用的, 可以通过nm -u xxxAppPath/Frameworks/libswift_Concurrency.dylib查看所有类型为U的符号, 其中确实包含了___chkstk_darwin, 到这里说明官方提供的解决方案并没有生效(至少是对于我的crash来说)

  3. error 提示期望该符号应该在libSystem.B.dylib中, 但是通过找到libSystem.B.dylib 并打印导出符号nm -gAUj libSystem.B.dylib 发现即使是高版本的中libSystem.B.dylib也并没有该符号,

    那么如何确定该符号存在于哪个库呢, 这里用了一个取巧的方式, run iOS15以上真机确保不会crash, 并设置symbol符号___chkstk_darwin, xcode会标记所有存在该导出符号的库, 经过第1&2步骤思考, 认为是在查找libswiftCore核心库的符号可能性更大

    libSystem.B.dylib路径在~/Library/Developer/Xcode/iOS DeviceSupport/xxversion/Symbols/usr/lib)

    ___chkstk_darwin.png

  4. 如果校验呢, 通过xcode上iOS12 && iOS13两个不同版本的libswiftCore.dylib查看导出符号,可以发现, 12上的Core库不存在, 对比组15上是存在的, 所以基本可以断定symbol not found是这个原因造成的; 当然你也可以把其他几个库也采用相同的方式验证

通过在 ~/Library/Developer/Xcode/iOS DeviceSupport/xxversion/Symbols/usr/lib/swift/libswiftCore.dylib 不同的version路径下找到不同系统对应的libswiftCore.dylib库, 然后用nm -gUAj libswiftCore.dylib可以查看过滤后的全局符号验证, 本人实验iOS 12.5.4版本不存在, 15版本存在

库的路径,可以通过linkmap或者运行一个demo打个断点, 然后通过image list查找

分析总结: 无论是xcode提供的解决方案还是分析过程, 发现根源还是因为在iOS12上链接了libswift_Concurrency造成的,既然问题出在异步库, 解决方案也很明了,移除项目中的libswift_Concurrency.dylib库即可

解决方案

使用其他xcode版本构建

使用xcode13.1构建 或者xcode13.3 Beta, 但是beta版无法构建app store ipa, xcode13.1不存在该bug

构建后移除libswift_Concurrency.dylib

添加脚本,每次移除嵌入的libswift_Concurrency.dylib, 移除xcode iOS12上未剔除的异步SDK

Edit Scheme... -> Build -> Post-actions -> Click '+' to add New Run Script

rm "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/libswift_Concurrency.dylib" || echo "libswift_Concurrency.dylib not exists"

WechatIMG87.jpeg

暂不升级使用了concurrency的三方库版本

降低查找到使用concurrency的三方库版本, 不使用该库就不会引入,后续等xcode修复后再升级; 一般项目都是通过cocoapods管理的, 可以直接指定之前的版本

查找项目中使用了libswift_Concurrency.dylib的库

该如何查找哪些三方库中有用到concurrency呢,

如果是源码, 全局搜索相关的await async可以找到部分SDK, 但是如果是SDK或者是间接使用的, 则只能通过符号查找

思路:

  1. 首先明确动态库的链接是依赖导出符号, 即xx库引用了target_xxx 动态库是通过调用target_xxx的导出符号(全局符号)实现的, 全局符号的标识是大写的类型, U表示当前库中未定义的符号, 即需要查找其他库动态链接的符号
  2. 如何查看项目中引用了指定动态库target_xxx的哪些符号呢, 可以通过linkmap文件查找, 但是由于libswift_Concurrency有可能是间接依赖的, linkmap中不存在对这个库的直接依赖, 所以没办法获取使用的符号, 这里通过获取libswift_Concurrency的所有符号匹配, libswift_Concurrency的路径可以通过上文提到的image list获取, 一般都是用的/usr/lib/swift下的

MachO中记录的动态库都是直接引用的, 所以otool -L xxx或者MachOView查看的方式, 无法查找到间接依赖的库

  1. 遍历项目中所有的库, 查找里面用到的未定义符号, (U)标识是外部符号, 匹配的则标识有调用关系

使用的脚本代码如下:

#!/bin/bash
SHELL_BASE_PATH=`pwd`.chomp
SYSTEM_LIB_BASE_PATH="$HOME/Library/Developer/Xcode/iOS DeviceSupport"
LIBSIWFTCONCURRENCY_SYMBOLSFILE="libswiftConcurrencySymbols.txt"

FRAMEWORK_FILE=""
FRAMEWORK_PATH=""
TMP_RESULT_PATH="$HOME/Desktop/iOS12 Crash Result"

# -f 指定单独的Framework/.a文件
# -p 指定包含Framework/.a的路径, 支持相对路径或者绝对路径
# -o 指定结果输出路径, 默认是~/Desktop/Desktop/iOS12 Crash Result
while getopts ":f:p:o:" optname
do
    case "$optname" in
      "f")
        FRAMEWORK_FILE="$OPTARG"
        ;;
      "p")
        FRAMEWORK_PATH="$OPTARG"
        ;;
      "o")
        TMP_RESULT_PATH="$OPTARG"
        ;;
      ":")
        echo "No argument value for option $OPTARG"
        ;;
      "?")
        echo "Unknown option $OPTARG"
        ;;
      *)
        echo "Unknown error while processing options"
        ;;
    esac
    #echo "option index is $OPTIND"
done

# 获取当前xcode中libswift_Concurrency.dylib的符号, 并排序去重

function log_success() {
    echo -e "\033[32m $1 \033[0m" 
}

function log_warning() {
    echo -e "\033[33m $1 \033[0m"
}

function log_error() {
    echo -e "\033[31m $1 \033[0m" 
    exit 1
}

function log_msg() {
    echo -e "\033[34m $1 \033[0m"
}

function findConcurrencyPath() {
    local pathslist=()
    while IFS= read -r -d $'\0' file
    do
        pathslist+=("$file")
    done < <(find "${SYSTEM_LIB_BASE_PATH}"  -name "libswift_Concurrency.dylib" -print0)
    # echo ${pathslist[-1]}
    echo ${pathslist[${#pathslist[@]}-1]}
}

function checkExistAndClear() {
    local path=$1
    :> "${path}"
}

# 判断FRAMEWORK_PATH文件夹不存在
if [[ ! -d "${FRAMEWORK_PATH}" && ! -f "$FRAMEWORK_FILE" ]]; then
    log_error "SDK路径不存在,请检查..."
fi 

SALVEIFS=$IFS
IFS=$(echo -en "\n\b")

# 如果不存在该目录, 则新建
if [[ ! -d "${TMP_RESULT_PATH}" ]]; then 
    mkdir "${TMP_RESULT_PATH}" || log_error "${TMP_RESULT_PATH}创建失败..."
fi

# 获取当前xcode中libswift_Concurrency的库
_concurrencyPath=$(findConcurrencyPath)

# 如果为空, 表示未查询到异步库

# mktemp 产生的临时文件名一般以 tmp 为前缀,随机产生后缀,同时文件会放在 $TMPDIR 目录下。
# 但是我们可以通过 -t 前缀.XXXXXX 来设置临时文件名的模板,通过 -p 目录 指定在哪个目录中产生临时文件:
# TMPFILE=$(mktemp -p $HOME -t TEST.XXXXXXX)

tmp_symbols_path=$(mktemp -d -t tmp_symbols) || log_error "临时目录创建失败, 检查权限后重试..."
log_msg "临时目录${tmp_symbols_path}创建成功..."
# 设置退出清空临时文件
trap 'rm -rf "$tmp_symbols_path"' EXIT

# 判断文件存在, 提取符号文件, 否则退出
if [[ -f "${_concurrencyPath}" ]]; then 
    nm -gAUj "${_concurrencyPath}" > "${tmp_symbols_path}/${LIBSIWFTCONCURRENCY_SYMBOLSFILE}"
else
    log_error "${SYSTEM_LIB_BASE_PATH} 路径下未找到libswift_Concurrency.dylib库, 请手动处理符号后进行去重排序命名为${LIBSIWFTCONCURRENCY_SYMBOLSFILE}存储到${TMP_RESULT_PATH}路径..."
fi

# 创建 result文件, 如果已经存在, 清空
result_libs=""${TMP_RESULT_PATH}"/result_libs.txt"
result_linksymbols=""${TMP_RESULT_PATH}"/result_linksymbols.txt"
$(checkExistAndClear ${result_linksymbols})
$(checkExistAndClear ${result_libs})
$(checkExistAndClear "${TMP_RESULT_PATH}/${LIBSIWFTCONCURRENCY_SYMBOLSFILE}")
$(checkExistAndClear "${TMP_RESULT_PATH}/targetlibs.txt")

# 处理符号, 模式匹配
while read line
do
    line=${line##* }
    echo "${line}" >> "${tmp_symbols_path}/libswift_Concurrency_tmp_symbols.txt"
done < "${tmp_symbols_path}/${LIBSIWFTCONCURRENCY_SYMBOLSFILE}"

# 排序去重
cat "${tmp_symbols_path}/libswift_Concurrency_tmp_symbols.txt" | sort | uniq > ""${TMP_RESULT_PATH}"/"${LIBSIWFTCONCURRENCY_SYMBOLSFILE}""

# 如果是FRAMEWORK_PATH遍历, 否则直接存储

file=$FRAMEWORK_FILE
if [ "${file##*.}"x = "framework"x ]||[ "${file##*.}"x = "a"x ];then
    echo "$FRAMEWORK_FILE" >  ""${TMP_RESULT_PATH}"/targetlibs.txt"
fi

# 遍历指定路径下的所有framework 和 .a库
find $FRAMEWORK_PATH -name "*.framework" -o -name "*.a" >> ""${TMP_RESULT_PATH}"/targetlibs.txt"

# 遍历每一个库, 获取所有未定义的符号(U), 即外部符号
while read line
do
    # 优化打印函数, 
    log_msg "检测SDK $line..."
    path="$line"

    if [[ $line =~ ".framework" ]]; then
        path=${line##*/}
        path=${path%%.*}
        path=$line/$path

        # 输出到临时文件夹
        nm -u "$path" > "${tmp_symbols_path}/symbols.txt"
    else
        # 输出到临时文件夹
        nm -u "$path" > "${tmp_symbols_path}/symbols.txt"
    fi

    # 对比符号 排序
    cat "${tmp_symbols_path}/symbols.txt" | sort | uniq  > "${tmp_symbols_path}/libs_symbols.txt"
    comm -12  "${tmp_symbols_path}/libs_symbols.txt"  "${TMP_RESULT_PATH}/${LIBSIWFTCONCURRENCY_SYMBOLSFILE}" > "${tmp_symbols_path}/common_symbols.txt"

    # 判断是否为空, 空表示未调用, 否则common_symbols.txt中存储的是引用的符号
    if [[ -s "${tmp_symbols_path}/common_symbols.txt" ]]; then 
        # 不为空, 插入空行分割
        echo -e >> "$result_linksymbols"
        echo "================================$line" >> "$result_linksymbols"
        echo "$path" >> "${result_libs}"

        # 处理符号
        while read symbol
        do
            # 记录 SDK Path + symbol 到引用文件夹
            echo $symbol >> "${result_linksymbols}"
        done < "${tmp_symbols_path}/common_symbols.txt"
    fi

    log_msg "SDK ${line} 处理完成..."

done < "${TMP_RESULT_PATH}/targetlibs.txt"
IFS=$(echo ${SALVEIFS})
log_success "查看${TMP_RESULT_PATH}目录下的result_linksymbols.txt获取所有链接libswift_Concurrency.dylib的符号"
log_success "查看${TMP_RESULT_PATH}目录下的result_libs.txt获取所有使用libswift_Concurrency.dylib的SDK名称"

使用方法:
-f: 指定单个framework/.a静态库进行检查
-p: 指定目录,检查目录下的所有framework/.a
-o 输出目录, 默认是~/Desktop/iOS12 Crash Result

由于本人对shell熟练度较低, 脚本可能存在诸多bug, 如果有任何使用问题可以直接联系本人, 欢迎一起交流学习

引用

如何检测哪些三方库用了libstdc++
使用 otool 命令查看 App 所使用的动态库
After upgrading to Xcode 13.2.1, debugging with a lower version of the iOS device still crashes at launching
Xcode 13​.2 Release Notes