1. 从源码到可执行文件,编译器到底在忙什么
很多初学者第一次接触"编译和链接"这个概念时,最容易产生的误解是:编译器就是把源代码翻译成机器码。这句话不能算错,但实在太粗糙了。实际上一段源代码变成可以运行的程序,中间要经历预处理、编译、汇编、链接四个阶段,而其中最容易让人栽跟头的,恰恰是大多数人不太关注的最后一步——链接。
我见过太多人在 Windows 上用 Visual Studio 写得好好的项目,一旦搬到 Linux 下用 gcc 编译,立刻冒出一堆莫名其妙的报错。有些是头文件路径不对,有些是库文件找不到,还有些是符号未定义。这些问题十有八九不是语法错误,而是链接阶段出了问题。所以说,搞清楚编译和链接的底层逻辑,不是学院派的无病呻吟,是每个写代码的人都绕不开的必修课。
这篇文章我想用尽量通俗的方式,把预处理、编译、汇编、链接这条完整链路拆开揉碎了讲清楚。重点放在链接上面,因为静态链接和动态链接的选择、动态链接器的搜索路径、跨平台编译时的库依赖问题,这些才是实际开发中最常踩坑的地方。无论你是刚学 C/C++ 的学生,还是需要在 Linux 下部署服务的工程师,这篇内容应该都能帮到你。
需要说明的是,文中涉及的很多细节是我在多年实战中总结出来的经验,不一定每个都写在教科书里,但确实能解决实际问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 编译过程的四个阶段,每一步都有它的道理
2.1 预处理:展开宏和头文件,代码看起来"变多"了
预处理是编译器的第一个动作。在这个阶段,编译器会处理所有以井号开头的指令,包括宏定义的展开、头文件的包含、条件编译的判断等等。
你可以用 gcc 的 -E 参数单独查看预处理结果。比如有一个简单的 hello.c:
c复制#include <stdio.h>
#define GREETING "Hello, World!"
int main() {
printf("%s\n", GREETING);
return 0;
}
执行 gcc -E hello.c -o hello.i 之后,打开生成的 hello.i 文件,你会发现里面有几千行代码,因为 stdio.h 的内容被完整地展开了。这就是预处理阶段的作用:把源代码"充实"成编译器后端真正需要处理的完整翻译单元。
这里有一个容易被忽略的细节:头文件的展开顺序和搜索路径。gcc 默认会在系统目录下搜索头文件,比如 /usr/include、/usr/local/include,但你也可以用 -I 参数指定额外的搜索路径。很多跨平台编译报错说找不到某个头文件,通常就是没有把对应的 -I 路径加进去。
2.2 编译:把 C 代码变成汇编语言
预处理完成后,编译器开始真正的"翻译"工作。这个阶段会把预处理后的代码转换成汇编语言,也就是针对特定处理器架构的指令助记符。
用 gcc -S hello.c -o hello.s 可以生成汇编文件。打开看一下,你会看到类似 movl、pushq、call 这样的指令,这就是 x86-64 架构的汇编代码。这个阶段做的事情包括词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成等一系列复杂的操作。
编译原理课程里讲的词法分析、语法分析,就是在这个阶段发挥作用的。比如你写了一个 int 1a = 5;,词法分析器会发现 1a 不是一个合法的标识符,直接报错。如果你写的是 int a = "hello"; 这种类型不匹配的代码,语义分析阶段就会提示错误。
很多人觉得编译原理是门枯燥的理论课,实际上理解了编译阶段做的事情,你再回头看那些文法、自动机、LR 分析表,就会觉得这些东西其实都是为实际工程服务的。
2.3 汇编:从汇编语言到机器指令
汇编阶段相对简单,就是把汇编代码转换成机器指令,生成目标文件。在 Linux 下,目标文件通常是 .o 文件,Windows 下是 .obj 文件。
用 gcc -c hello.c -o hello.o 可以生成目标文件。这时候如果用 file hello.o 查看文件类型,你会看到类似 ELF 64-bit LSB relocatable 的说明。注意这个 relocatable 关键词,它表示这个文件还不能直接运行,因为里面还有一些符号的地址是"悬空"的,需要等链接阶段来修正。
目标文件里面其实包含了多个段(section),比如代码段、数据段、符号表等等。你可以用 nm hello.o 查看目标文件里的符号表,会看到 main、printf、puts 等符号。其中 main 是已定义的,而 printf 是未定义的,它引用自外部的 C 标准库。这就是链接阶段需要解决的问题。
2.4 链接:把碎片拼成完整的程序
链接是最后一个阶段,也是我认为最值得深入理解的一个阶段。链接器的主要工作包括:把多个目标文件合并成一个可执行文件,解析各个目标文件之间的符号引用,重定位符号的地址。
当你写一个项目,里面有 main.c、utils.c、network.c 三个源文件,每个源文件会分别编译成 .o 文件。main.o 里调用了一个函数 utils_parse_config(),但这个函数的实际代码在 utils.o 里。链接器要做的就是找到 utils_parse_config() 的定义,把 main.o 里的调用指令和 utils.o 里的函数实现"对上号",然后把所有目标文件合并成一个完整的可执行文件。
这个阶段最常见的错误就是 undefined reference to 'xxx',意思是链接器在整个输入文件集合中都找不到某个符号的定义。这个错误的常见原因包括:忘记链接对应的库、库的链接顺序不对、函数声明和定义不匹配(比如 C 和 C++ 混编时没有加 extern "C")等。
3. 静态链接与动态链接,选错了一辈子难受
3.1 静态链接:把库代码"复制"进可执行文件
静态链接是指在链接阶段,把静态库中的代码完整地复制到最终的可执行文件里。这样生成的可执行文件不依赖外部的库文件,可以独立运行。
在 Linux 下,静态库通常以 .a 为后缀,Windows 下通常以 .lib 为后缀。创建静态库的命令是 ar,比如:
bash复制gcc -c utils.c -o utils.o
ar rcs libutils.a utils.o
链接时使用 gcc main.c -L. -lutils -o app,其中 -L. 指定库文件搜索路径为当前目录,-lutils 表示寻找 libutils.a 这个库文件。
静态链接的优点是部署简单,直接把一个二进制文件拷到目标机器上就能跑,不需要考虑目标机器上是否装了对应的库。缺点是生成的体积较大,而且如果多个程序都静态链接了同一个库,内存和磁盘上会有多份重复的代码。
3.2 动态链接:只在运行时才加载库
动态链接是指在链接阶段只记录库的引用信息,实际运行时才去加载库文件。在 Linux 下,动态库以 .so 为后缀,Windows 下是 .dll。当你在网上搜"动态链接器搜索路径"这类问题时,搜的就是这个机制的核心环节。
使用动态链接生成可执行文件的方式:
bash复制gcc -fPIC -shared utils.c -o libutils.so
gcc main.c -L. -lutils -o app
这里 -fPIC 表示生成位置无关代码,-shared 表示生成动态库。运行 app 时,操作系统会通过动态链接器(在 Linux 下是 ld.so)找到 libutils.so 并加载到内存中。
动态链接器的搜索路径是按一定顺序查找的,通常是:
- 环境变量
LD_LIBRARY_PATH指定的路径 /etc/ld.so.cache中缓存的路径(由 ldconfig 生成)- 默认的系统目录,一般是
/lib和/usr/lib
这就解释了为什么你在自己机器上编译好的动态链接程序,拷到另一台机器上运行时会报 error while loading shared libraries: libxxx.so: cannot open shared object file。大概率就是目标机器上找不到这个动态库。
3.3 静态库和动态库混用的情况
实际项目里经常会出现静态库和动态库混用的情况。比如你自己的代码编译成静态库给团队使用,但依赖的第三方库用动态库方式部署。这种情况下最需要小心的是链接顺序问题。
在 gcc 命令中,库的链接顺序是敏感的。如果 a.o 引用了 libb.a 中的符号,那么 libb.a 必须放在 a.o 的后面。因为链接器是从左到右扫描输入文件的,如果先扫描到 libb.a 时发现里面的符号还没有被任何目标文件引用,它就不会把这个符号"收进来",等扫描到 a.o 时发现缺少这个符号,再回头去找已经晚了。
避免这个问题的一个简单方式,是在编译命令中把同一个库写多遍,或者使用 -Wl,--start-group 和 -Wl,--end-group 把一组库包起来,让链接器循环扫描。
4. 动态链接器搜索路径,这条链路一定要捋清楚
4.1 ld.so 的工作机制
在 Linux 系统中,动态链接器本身就是一个可执行文件,通常在 /lib64/ld-linux-x86-64.so.2。当你运行一个动态链接的可执行文件时,内核首先加载动态链接器,然后由动态链接器负责加载程序依赖的所有共享库。
程序的动态库依赖关系可以用 ldd 命令查看。比如执行 ldd app,你会看到类似这样的输出:
text复制linux-vdso.so.1 (0x00007ffe3a3f3000)
libutils.so => /home/user/lib/libutils.so (0x00007f0a1b123000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0a1af41000)
如果某项显示 not found,说明动态链接器没能在搜索路径中找到对应的库。
4.2 三种设置搜索路径的方法
第一种是临时设置环境变量:
bash复制export LD_LIBRARY_PATH=/home/user/my_libs:$LD_LIBRARY_PATH
./app
这种方法只对当前 shell 会话有效,适合开发调试阶段临时使用。
第二种是修改系统缓存:
bash复制sudo nano /etc/ld.so.conf.d/myapp.conf
# 在文件中写入 /home/user/my_libs
sudo ldconfig
ldconfig 命令会更新 /etc/ld.so.cache,这样所有用户、所有程序的动态链接器都能找到这个路径下的库。这种方法适合正式部署。
第三种是在编译时通过 rpath 嵌入搜索路径:
bash复制gcc main.c -L. -lutils -Wl,-rpath,/home/user/my_libs -o app
-Wl,-rpath 后面指定的是运行时库搜索路径,这个路径会被硬编码到可执行文件里。这样即使不设置 LD_LIBRARY_PATH,程序启动时也会先去这个路径下找库。
我个人在实际项目中更推荐第三种方式,因为它是自我描述的,可执行文件本身就包含了库的路径信息,不容易出现"在我机器上好好的,到别人机器上就跑不了"的问题。但要注意 rpath 的路径不能写死到 /home/某某 这种个人目录,部署时应该使用相对路径或统一安装路径。
4.3 一个典型的部署事故复盘
有一次我把一个编译好的程序放到生产服务器上,执行时一直报找不到一个动态库。我用了 ldd 一查,发现缺少的是 libssl.so.1.1。但是系统里有 libssl.so.3,版本对不上。
这里就需要解释一下动态库的版本命名规则。动态库的完整文件名通常包含主版本号和次版本号,比如 libssl.so.1.1 表示 OpenSSL 1.1 系列。程序在链接时就记录了它需要的 SONAME(即 libssl.so.1.1),运行时动态链接器会精确查找这个文件名,而不是模糊匹配。所以即使系统里有一个更新版本的 libssl.so.3,程序也无法使用它。
解决办法是安装对应版本的库包,或者用兼容层把旧版本库放进去。这种问题在老旧项目移植到新系统时特别常见,理解了 SONAME 机制,排查起来就快很多了。
5. 跨平台编译的经典场景:VS 工程转 Linux 编译
5.1 为什么 Windows 上好好的,到 Linux 就编译失败
把 Visual Studio 工程迁移到 Linux 下编译,是很多人都会遇到的需求。最常见的问题包括:Windows 特有的头文件和 API 在 Linux 下不存在、路径分隔符不同、库依赖不同、编译选项差异等等。
比如 Windows 下有 <windows.h>,里面定义了 Sleep、CreateFile 等函数,这些在 Linux 下都不存在。Linux 下需要用的是 <unistd.h> 里的 usleep、open 等函数。一个正经项目的跨平台改造,第一步就是要做平台抽象,把系统相关的调用隔离到单独的模块里。
再比如 __declspec(dllexport) 和 __declspec(dllimport) 是 Windows 下导出和导入符号的语法,在 Linux 下需要改成 __attribute__((visibility("default"))) 或者干脆不用。这些细节在 VS 工程转 Linux 编译时一定会碰到。
5.2 CMake 是跨平台编译的利器
遇到这种情况,我一般建议直接用 CMake 重写构建脚本。CMake 可以生成多种平台的原生构建系统文件,在 Windows 下可以生成 VS 工程,在 Linux 下可以生成 Makefile,同时可以方便地处理平台差异。
一个简单的 CMakeLists.txt 示例:
cmake复制cmake_minimum_required(VERSION 3.10)
project(myapp C)
set(CMAKE_C_STANDARD 11)
if(WIN32)
add_definitions(-DWINDOWS_PLATFORM)
else()
add_definitions(-DLINUX_PLATFORM)
endif()
add_executable(app main.c utils.c)
target_link_libraries(app PRIVATE m pthread)
在 Linux 下执行:
bash复制mkdir build && cd build
cmake ..
make
5.3 常见编译期报错和解决办法
跨平台编译时常见的编译期异常包括:
- 找不到头文件:检查
-I路径和系统中是否安装了对应开发包。Linux 下头文件和库通常是分离的,需要额外安装-dev或-devel包。 - 函数未定义:检查代码是否使用了平台特有的 API,或者是否需要额外的库。
- 类型定义冲突:Windows 下的
BOOL、DWORD等类型在 Linux 下不存在,需要使用标准类型或者自己定义。
很多项目迁移到 Linux 下编译报错,原因往往不是代码本身的问题,而是开发环境没有装全依赖的库。比如 Ubuntu 系统默认不带 C/C++ 开发环境,你至少需要安装 build-essential 这个包。
6. 两个高频实战案例:源码编译安装与第三方库集成
6.1 Ubuntu 下源码编译安装 Redis 的完整流程
源码编译安装是 Linux 工程师的基本功。以 Redis 为例,源码包下载解压后,进入目录,执行:
bash复制make
make install
Redis 使用 Makefile 构建,正常情况下直接 make 就能编译出 redis-server 和 redis-cli 两个可执行文件。但在某些精简系统上,可能会遇到缺少 gcc 或 make 工具的问题,此时需要先安装:
bash复制sudo apt update
sudo apt install build-essential
如果 Redis 版本较新,编译器版本太旧,也可能会出现编译期异常。比如老版本 gcc 不支持某些 C11 语法特性,这时候可以考虑升级编译器或者使用较旧的 Redis 版本。
安装完成后,可以用 redis-server --version 验证安装结果。源码编译安装的好处是可以自定义编译参数,比如指定安装路径、启用某些扩展模块等。但代价是如果你不熟悉构建过程的细节,遇到问题排查起来可能比较折腾。
6.2 Linux 下编译 cpprestsdk 的步骤拆解
cpprestsdk 是微软开源的 C++ REST 库,在 Linux 下编译它需要依赖 Boost、OpenSSL、libcurl 等库。这正好是一个典型的"依赖地狱"案例。
编译 cpprestsdk 的主要步骤是:
bash复制git clone https://github.com/microsoft/cpprestsdk.git
cd cpprestsdk
mkdir build && cd build
cmake .. -DCPPREST_EXCLUDE_WEBSOCKETS=ON
make -j$(nproc)
sudo make install
需要注意的几个点:
- 如果 Boost 库版本不兼容,编译会报各种奇奇怪怪的错误。可以指定
-DBOOST_ROOT指向特定的 Boost 安装路径。 - OpenSSL 版本太新也可能导致兼容性问题。cpprestsdk 对 OpenSSL 的版本比较敏感,1.1 和 3.x 的 API 有一些差异。
-j$(nproc)表示用所有 CPU 核心并行编译,能显著加快速度,但要是内存不够,建议把并行数调小一些,否则可能 OOM。
6.3 已经编译好的库能不能直接用
网上经常有人问"有没有已经编译好的 PDFium 库可以直接用"。PDFium 是 Google 的 PDF 渲染引擎,自己从源码编译确实比较费劲,因为依赖多、编译时间长、构建系统也比较复杂。
如果你使用的是比较常见的平台和架构,确实可以找到社区预编译的二进制包。比如 vcpkg 和 conan 这两个 C/C++ 包管理器就提供了大量预编译库。但"开箱即用"的前提是你的编译器和这些预编译库的 ABI 兼容。如果编译器的版本、标准库实现、平台架构不匹配,链接时依然会报错。
这里面最典型的例子是 C++ 的 ABI 问题。GCC 8 前后发生过一次 ABI 变化,导致用旧版 GCC 编译的库和用新版 GCC 编译的代码无法链接在一起。如果你要使用别人预编译的 C++ 库,最好确认对方使用的编译器版本范围和你的开发环境兼容。
7. 链接期报错排查手册:从报错信息反推根因
7.1 undefined reference 的六种常见原因
undefined reference 是最经典的链接期报错。按照我排查问题的经验,常见原因和解决办法可以整理成下面这个表格:
| 报错特征 | 可能原因 | 排查方向 |
|---|---|---|
| 引用了标准库函数(如 printf、malloc)但报错 | 缺少某个系统库的链接 | 检查是否加了 -lm、-lpthread 等参数 |
| 引用了自定义函数但报错 | 对应的源文件没有参与编译,或者目标文件没被链入 | 检查 Makefile 或 CMake 里是否漏了文件 |
| 引用了第三方库函数但报错 | 库本身没有被链接 | 检查 -l 参数是否正确,库文件是否存在于路径中 |
| 库存在但顺序不对 | 库依赖了后续库的符号 | 调整链接顺序,或使用 --start-group 包裹 |
| C++ 代码引用了 C 库函数 | 缺少 extern "C" 声明 |
在 C++ 中声明 C 函数时需要加 extern "C" |
| 编译器架构不匹配 | 库是 32 位的,而程序编译成 64 位 | 检查编译目标和库的架构是否一致 |
7.2 实战排查:一个混合编译场景的排错记录
有一次我帮同事排查一个问题:他在 Linux 下编译自己的项目,链接一个第三方提供的静态库,总是报一堆 undefined reference。这个第三方库本身是用 C 写的,但提供的头文件里却没有加 extern "C" 保护。
这就导致了一个很隐蔽的问题:C++ 编译器在编译阶段会对函数名做 name mangling(名称修饰),比如 foo 会变成 _Z3foov。而 C 编译器不会做这个处理,库里面的符号名就叫 foo。链接时,C++ 编译的目标文件引用的是 _Z3foov,但库里只有 foo,自然找不到。
解决办法是在引用该库的头文件时加一层包装:
cpp复制extern "C" {
#include "third_party_lib.h"
}
或者在自己的代码里用 extern "C" 显式声明需要链接的 C 函数。这个坑如果不知道背后的机制,靠猜是很难定位的。
7.3 使用 nm 和 objdump 精准定位符号问题
排查这类问题,nm、objdump、readelf 这三个工具是主力。比如你怀疑某个库里面没有你想要的符号,可以直接:
bash复制nm libfoo.a | grep foo_function
如果输出里没有这个符号,说明库里面确实没有,可能是库版本不对,或者你链接错了库。如果输出里显示 T foo_function,说明该函数是已定义的全局符号,那问题就出在调用方的符号修饰上。
再比如你想查看可执行文件依赖了哪些动态库:
bash复制objdump -x app | grep NEEDED
输出会列出所有 NEEDED 项,也就是程序运行时需要的动态库名称。这个信息可以帮助你判断程序需要哪些库、有没有多余的依赖。
8. 环境搭建相关:Android Framework 编译环境与 Qt 安卓交叉编译
8.1 在 Ubuntu 上搭建 Android Framework 编译环境
Android Framework 的编译是一个庞大而复杂的任务。在 Ubuntu 上搭建编译环境,首先要注意的是 JDK 版本和系统版本的匹配。不同 Android 版本要求不同的 JDK 版本,比如 Android 12 及以后版本需要 JDK 11 或更高。
编译 Android Framework 还需要安装一堆依赖包,包括 git、gnupg、flex、bison、build-essential 等。Google 官方建议使用 Ubuntu 18.04 及以上版本,但具体还要看你想编译的 Android 版本。
编译时使用:
bash复制source build/envsetup.sh
lunch aosp_arm64-eng
make -j8
这个过程中最常见的两类问题:一是磁盘空间不够(源码加编译产物动辄几百 GB),二是内存不够(建议 16GB 起步)。编译器版本也会影响编译结果,Ubuntu 自带的 gcc 版本如果太新,可能会和 Android 源码中预定义的编译规则冲突。
8.2 Qt 编译安卓平台 APK 的简易思路
经常有人问 "Qt 想编译一个安卓平台的 APK 该如何简单操作"。用 Qt 做安卓开发,本质上就是用 Qt 的 Android 工具链做交叉编译。
关键步骤是:
- 安装 Android SDK、NDK、JDK
- 在 Qt Creator 中配置 Android 工具链路径
- 创建一个 Qt Widgets 或 Qt Quick 项目
- 选择 Android 套件作为构建目标
- 点击构建,Qt Creator 会自动调用 Gradle 和 NDK 生成 APK
这里最容易出问题的是 NDK 版本和 Qt 版本的匹配。比如 Qt 6.5 可能需要 NDK r25 或更新的版本,如果 NDK 过老或过新,编译时会出现各种奇怪的 C++ 标准库相关错误。
如果你不是必须用 Qt 的跨平台 UI 能力,只是想要一个简单的 Android 应用,我更推荐直接用 Android Studio 加 Kotlin 开发。Qt 在 Android 上的调试体验和原生开发相比还是有一些差距的。
8.3 交叉编译时常见错误:qml 编译错误等
Qt Quick 项目编译成 APK 时,QML 编译错误是常见的拦路虎。QML 是解释执行的,但 Qt 6 的 qmlcachegen 会尝试把 QML 编译成 C++ 代码来提升性能。如果你的 QML 语法有问题,编译器可能不会给出很直观的报错,而是一个长长的 C++ 编译错误。
排查思路是:先单独用 qmlscene 或 qmlscene 的 debug 模式运行 QML 文件,看有没有语法错误提示。也可以打开 Qt Creator 的 QML 语法检查器,它通常能在编码阶段就标出问题位置。
还有一个比较隐蔽的问题:QML 中用到的资源文件没有被正确打包进 APK。在 .pro 文件或 CMakeLists.txt 里,需要把 QML 文件和相关的资源文件都加到资源列表里,否则运行时会提示找不到文件。
9. 身边常见的编译工具链问题速查
把平时排查问题积累的一些点整理成一个清单,方便大家遇到类似问题时对照使用:
- gcc 版本太低导致编译失败:升级 gcc,或者使用
-std=c++11等参数调整标准。新版 Ubuntu 上可以用sudo apt install gcc-12 g++-12安装特定版本,然后用update-alternatives切换默认版本。 - 源里没有对应的开发包:Ubuntu 上很多库需要额外安装
-dev包才有头文件。比如编译用到 libcurl,需要先sudo apt install libcurl4-openssl-dev。 - make -j 并行编译 OOM:
-j参数不跟数字时,make 会使用所有 CPU 核心,如果源码体积大,内存不够很容易 OOM。建议先nproc看下 CPU 核心数,然后取一半作为并行数。 - 链接时提示无法找到 -lxxx:说明链接器在搜索路径中找不到名为
libxxx.so或libxxx.a的库文件。先用find /usr -name "libxxx*"确认库文件是否安装,如果已存在但路径特殊,用-L指定路径。 - 动态库版本不兼容:程序提示需要
libxxx.so.1,但系统只有libxxx.so.2。检查是否安装了对应的旧版本兼容包。 - CMake 找不到依赖库:大部分 CMake 的 find_package 会查找系统默认路径,如果库安装到非标准路径,需要设置
CMAKE_PREFIX_PATH。
10. 关于编译器选型和构建工具的一点个人经验分享
聊到最后,分享一些我踩过多次坑之后总结出的体会。编译器这个东西,能用稳定版本就别追新。特别是接手老项目的时候,先把系统默认的 gcc 版本查清楚,再决定项目用什么编译参数。很多"编译期异常"其实不是代码的问题,是编译器版本和代码产生了兼容性冲突。
另外,构建系统的选择要趁早。小项目用 Makefile 足够,项目稍微变大一点,模块一多,手写 Makefile 就是灾难。我见过太多团队在 Makefile 里补丁摞补丁,最后没人敢动。有条件的话,新项目直接用 CMake,它对第三方库的探测和管理能力比 Makefile 强太多了。
还有一个非常实际的建议:项目的编译和链接命令一定要固化下来,不要靠记忆在命令行敲。不管是用 CMake 还是 Makefile,或者简单的编译脚本,把完整命令和依赖关系记录下来。这样即使过了半年,换一台机器,也能快速把编译环境重建出来。
编译和链接的坑,说多不多,说少不少。理解了整个过程,大部分问题都可以通过工具链的报错信息和合理的排查手段解决。希望这篇文章能帮你少走一些弯路。
