ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从g++到CMake:理清C++编译工具链的关键概念

从g++到CMake:理清C++编译工具链的关键概念 很多人搜C编译器、g、cmake的时候其实还不太清楚这三个东西各自是干什么的。我见过不少同学在VSCode里折腾一下午只为了把第一段C代码跑起来最后却卡在编译器未包含main类型这种莫名其妙的报错上。这篇文章把我这些年实际使用 C 工具链的经验整理出来讲清楚源代码到可执行文件之间到底发生了什么以及 g 和 cmake 分别扮演什么角色。如果你刚开始学 C刚写完第一个小游戏或者刷完几道算法题打算认真组织一个多文件工程这篇文章正好能帮你把这些概念一次理顺。1. 编译器和编辑器为什么总被搞混1.1 编辑器只负责写字编译器负责翻译先说一个非常基础但特别容易被忽略的区别编辑器和编译器是完全不同的两个程序。VSCode、Sublime、Vim、甚至记事本都叫编辑器。编辑器的任务很单纯——让你能打字、改字、保存文本。它根本不在乎你写的是 C 还是 Python也不关心你的代码能不能运行。编译器是另一回事。它的工作是把 C 这种人看得懂的高级语言翻译成 CPU 能执行的机器指令。g 就是一个编译器cl.exeMSVC也是一个编译器clang 也是。为什么很多人会把它们混在一起因为 IDE集成开发环境把编辑器、编译器、调试器打包到了一起比如 Visual Studio、Qt Creator。你按一下 F5 就把所有事情都办了所以根本分不清哪一步是哪一步干的。但一旦脱离 IDE回到 VSCode 加命令行这种轻量组合问题就暴露了你可以装一百个编辑器插件但机器上没装编译器代码永远跑不起来。我经常用做菜打比方编辑器是你的菜刀和砧板g 是那个负责炒菜的厨师。菜刀再好没有厨师食材永远不会变成菜。1.2 一条命令背后的四个阶段你可能见过别人编译 C 程序就一条命令g hello.cpp -o hello看起来简单但这条命令背后其实做了四件完全不同的事。理解这四件事后面排查编译错误能省一半时间。预处理Preprocessing处理所有#include、#define宏替换、条件编译。简单说把要引用的头文件内容复制粘贴进来把宏展开。这一步对应g -E。编译Compilation把预处理后的 C 代码翻译成汇编语言。这一步做语法检查、类型检查绝大多数编译错误都是在这里报出来的。对应g -S。汇编Assembly把汇编语言翻译成机器码生成目标文件.o或.obj。对应g -c。链接Linking把多个目标文件、库文件拼在一起解决函数调用地址最终生成可执行文件。初学者最常见的undefined reference to main就是在最后一步——链接阶段——报出来的因为整个程序里找不到入口函数main。这个后面我会专门展开。这四步也可以看成流水线预处理是洗菜切菜编译是炒菜汇编是装盘链接是把一桌菜摆好。每道工序都可能出问题但症状和报错的位置完全不同。1.3 g、cl.exe、clang 之间怎么选既然编译器不止一种新手常问的另一个问题就是我用哪个gGCC 编译器家族的 C 前端Linux 系统默认带的就是它。免费、开源、教程最多、跨平台Windows 上通过 MinGW-w64 也能用。入门首选没有之一。cl.exe微软的 MSVC 编译器Visual Studio 内置。Windows 原生生态调用 Windows API、写 Windows 桌面程序最方便。但它的命令行工具依赖一堆环境变量直接用比较麻烦一般通过 VS 或者开发人员命令提示符来调用。clangLLVM 家族的 C 编译器macOS 自带的那个clang就是它。报错信息比 g 友好编译速度也快但讲真新手阶段用 g 和用 clang 差异不大。提一句不同编译器对同一份代码可能有不同的警告和错误行为但标准 C 代码基本都能编译通过。真正重要的是同一个项目里尽量统一工具链不要 g 写一点、MSVC 写一点因为链接阶段可能因为 ABI 差异出现各种奇怪问题。我实际踩过这种坑后文会提。2. g 的常用姿势从单个文件到多文件工程2.1 一条命令跑通第一个程序先创建一个最简单的文件// hello.cpp #include iostream int main() { std::cout Hello, World! std::endl; return 0; }编译g hello.cpp -o hello-o的意思是 output指定输出文件名。如果不写-o hellog 在 Linux/macOS 上默认生成一个叫a.out的文件在 Windows 上生成a.exe。我见过不少人编译完找不到自己的程序就是因为没注意默认输出名。运行./hello在 Linux/macOS 上运行当前目录的可执行文件需要加./前缀这是 shell 的路径搜索规则决定的。Windows 上直接hello.exe也行。这一步跑通了恭喜你已经完成了编辑器写代码 编译器翻译 执行的完整闭环。2.2 把四个阶段拆开看如果你好奇每步到底产出了什么可以用-E、-S、-c三个参数手动拆开看# 预处理生成 hello.i体积会比原文件大很多因为头文件内容被展开了 g -E hello.cpp -o hello.i # 编译生成汇编文件 hello.s g -S hello.i -o hello.s # 汇编生成目标文件 hello.o机器码不能直接运行 g -c hello.s -o hello.o # 链接从目标文件生成可执行文件 g hello.o -o hello我建议新手用这种方式从头到尾跑几次看看.i、.s、.o文件的形态。尤其是.i文件你能直观地看到#include iostream到底带来了多少代码这样就能理解为什么 C 编译比很多语言慢——因为头文件展开的工作量大。另外一个实用技巧如果你在.i文件里看到某个宏展开的结果跟自己预期不符说明宏写错了。这类 bug 找起来很痛苦用-E展开排查是最快的方法。2.3 多文件工程怎么编译实际项目不可能只有一个文件。假设你的工程结构是project/ ├── main.cpp ├── utils.h └── utils.cpp其中utils.h里声明函数utils.cpp里定义函数main.cpp里调用。编译命令是g main.cpp utils.cpp -o app注意只需要传入.cpp文件不需要传.h文件。因为头文件的内容通过#include已经复制到了每个.cpp文件里。这也是 C 编译的一个重要特点编译单元是.cpp文件。那为什么要把声明写在头文件、定义写在源文件里最直接的原因是避免重复定义。main.cpp和utils.cpp如果各自定义一遍同一个函数链接时就会报重复定义错误而只把声明放在头文件里两边#include后只是知道有这样一个函数存在具体实现只在utils.cpp里有一份链接时大家共用这一份。如果是稍大一点的项目比如 10 个.cpp文件手写g a.cpp b.cpp c.cpp ...还能忍。到 50 个文件、还牵扯第三方库的时候手写命令就不现实了。这正是需要 cmake 出场的时机后面专门讲。2.4 参数速查表和优化选项g 的参数非常多但日常高频使用的其实就十来个。我整理了一个速查表参数作用备注-o file指定输出文件名不带就默认a.out-Wall -Wextra开启常见警告强烈建议常开能帮你发现很多潜在问题-g生成调试信息配合 gdb 调试必需-O0 -O1 -O2 -O3优化级别-O0不优化-O2常用发布级别-Og优化但保留调试体验调试时推荐-stdc17指定 C 标准常见还有c11、c14、c20-Idir添加头文件搜索路径相当于#include的额外查找目录-Ldir添加库文件搜索路径告诉链接器去哪找库-lname链接某个库比如-lpthread链接 pthread 库-static静态链接生成的可执行文件体积更大但部署更省心这里重点说下优化选项和调试的关系这是我见过很多人踩坑的地方。默认情况下 g 不优化也就是-O0。但你如果手一抖用了-O2去编译然后开 gdb 调试会发现一个诡异的现象断点打在某一行程序就是不在这里停某个变量的值调试器里怎么看都是optimized out。原因很简单-O2会做常量折叠、内联、重排指令等优化源代码和机器码的对应关系变得非常稀薄。调试器没法准确地把机器码这一刻的状态映射回源代码这一刻的状态。我的建议是调试阶段用-O0 -g或者-Og -g发布版本再用-O2。不要嫌麻烦这种切换在 cmake 里设置一个CMAKE_BUILD_TYPE就完成了后面会讲。3. cmake把 g 的复杂调用封装成一份可维护的配置3.1 命令长成什么样时需要 cmake先说结论cmake 不是编译器它不直接编译代码而是生成一套构建系统的配置文件之后还是由 make 或 ninja 调用 g 完成实际编译。它的核心价值是跨平台和可维护。想象这种场景你的项目突然要链接一个第三方库比如 OpenCV。手写编译命令就变成g main.cpp utils.cpp -o app \ -I/usr/include/opencv4 \ -L/usr/local/lib \ -lopencv_core -lopencv_imgcodecs -lopencv_highgui到了 Windows 上路径全变了库名可能也变了甚至编译器换成 MSVC这套命令全部作废。这就需要一个更高层的工具只写一份配置让它在每个平台上自动生成对应的构建规则。cmake 干的就是这个活。cmake 的配置流程非常经典你写一个CMakeLists.txt文件描述项目里有哪些源文件、要生成什么目标、需要哪些库。运行cmake命令它检测当前系统的编译器、库、环境变量然后生成一份本地的构建文件通常是 Makefile 或者 Ninja。运行make或ninja真正的编译就此开始。很多新人第一次看到CMakeLists.txt会懵其实把它理解成一份构建说明书就可以了cmake 读说明书然后指挥真正干活的工具把代码变成程序。3.2 最小的 CMakeLists.txt新建一个目录里面有两个文件一个CMakeLists.txt一个main.cpp。CMakeLists.txt内容如下cmake_minimum_required(VERSION 3.10) project(MyApp) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(myapp main.cpp)逐行解释cmake_minimum_required(VERSION 3.10)声明需要的最低 cmake 版本。如果你的 cmake 版本低于这个数字会在配置阶段直接报错。这就是热词里cmake 3.13 or higher is required这个报错的来源。project(MyApp)给项目起个名字。这个名字也会作为变量PROJECT_NAME供后续引用。set(CMAKE_CXX_STANDARD 17)告诉编译器使用 C17 标准等价于给 g 传-stdc17。add_executable(myapp main.cpp)声明要生成一个可执行文件myapp源文件是main.cpp。多个源文件就往后加add_executable(myapp main.cpp utils.cpp)。然后执行cmake -S . -B build cmake --build build-S .指定 CMakeLists.txt 所在目录-B build指定构建目录。这两条是 cmake 推荐的标准用法。重点说下构建目录的概念。直接在源码目录里运行cmake .也可以但会在源码目录里生成一堆CMakeCache.txt、CMakeFiles/之类的中间产物把源码目录搞得一片混乱。用-B build把所有中间产物隔离到一个build子目录源码目录永远是干净的。想重新构建时把build目录整个删掉重来成本为零。这个习惯非常值得养成。编译完成后可执行文件在build/目录下。Linux/macOS 下叫myappWindows 下叫myapp.exe。3.3 常见变量的设置CMakeLists.txt本质上是在设置各种变量cmake 根据这些变量生成构建规则。前面说的CMAKE_CXX_STANDARD就是其中之一。另一个非常常用的是CMAKE_BUILD_TYPE它决定优化和调试信息级别取值作用Debug相当于-O0 -g适合调试Release相当于-O3 -DNDEBUG适合发布RelWithDebInfo-O2 -g带调试信息的发布版MinSizeRel-Os追求最小体积在CMakeLists.txt里写死一种类型不太灵活我推荐在命令行指定cmake -S . -B build -DCMAKE_BUILD_TYPEDebug-D表示定义一个变量。每次重新配置时传不同的CMAKE_BUILD_TYPE就可以切换 Debug/Release而不用改配置文件。这也是对应 2.4 节说的调试优化关系的最方便解法。另一个可能用到的变量是CMAKE_PREFIX_PATH它负责告诉 cmake去哪里找第三方库的安装位置。比如你在/opt/my-libs下装了一个库cmake -S . -B build -DCMAKE_PREFIX_PATH/opt/my-libs这样后续find_package时就有更大的概率找到这个库。3.4 链接第三方库的基本思路稍微正经一点的项目一定会用到第三方库cmake 提供了相当优雅的写法。以 OpenCV 为例cmake_minimum_required(VERSION 3.10) project(MyCVProject) set(CMAKE_CXX_STANDARD 17) find_package(OpenCV REQUIRED) add_executable(mycv main.cpp) target_include_directories(mycv PRIVATE ${OpenCV_INCLUDE_DIRS}) target_link_libraries(mycv PRIVATE ${OpenCV_LIBS})find_package(OpenCV REQUIRED)的意思是在系统中查找 OpenCV。找到了cmake 会填充OpenCV_INCLUDE_DIRS、OpenCV_LIBS等变量找不到REQUIRED会让配置阶段直接报错不浪费时间往下走。target_include_directories和target_link_libraries是给目标mycv指定头文件搜索路径和要链接的库。注意这里有个很重要的设计理念所有属性都挂到 target 上而不是全局设置。这样在一个项目里有多个可执行文件或库时各自的依赖可以隔离不会互相污染。我还想提一点find_package机制之所以好用是因为很多库在安装时会带上一个XXXConfig.cmake文件cmake 借此知道这个库的头文件在哪、库文件在哪、链接时叫什么名字。如果你用 vcpkg 管理第三方库它也是通过 cmake 的 toolchain 机制来让find_package找到库的。理解了这套逻辑vcpkg 那套配置也就不神秘了。另外嵌入式场景下交叉编译也是一样的逻辑。比如你要给 ARM 芯片编译程序只需要把默认的 g 换成arm-none-eabi-gcmake 通过 toolchain 文件指定交叉编译器路径其余工程组织逻辑完全相同。所以我才说cmake 这一个工具能覆盖桌面开发、嵌入式开发、跨平台项目值得认真学一次。3.5 一个容易踩的坑Windows 中文乱码和编码方式这个话题在 Windows 上做 C 开发时几乎一定会遇到热词里专门有人搜cmake如何指定编码方式。先讲现象代码文件是 UTF-8 编码里面有一句中文输出。在 Linux 上用 g 编译运行正常。换个环境在 Windows 上用 MSVC 编译控制台可能输出乱码甚至编译时出现 C4819 警告——该文件包含不能在当前代码页中表示的字符。原因在于 MSVC 默认按系统的本地代码页比如 GBK来读取源文件而你的文件是 UTF-8两边对不上中文就解释错了。解决方案一般有两个在源文件顶部加一行#pragma execution_character_set(utf-8)但可移植性不行不推荐。在 CMakeLists.txt 里对 MSVC 编译器加上/utf-8参数if(MSVC) add_compile_options($$CXX_COMPILER_ID:MSVC:/utf-8) endif()add_compile_options是给所有编译命令追加参数后面那段$$CXX_COMPILER_ID:MSVC:...是 cmake 的生成器表达式意思是只有当前编译器是 MSVC 时才加这个参数。这样写的好处是当你在 Linux 上用 g 时该参数不会生效因为 g 本来就默认 UTF-8。这个问题就是典型的代码逻辑没错但跨平台工具链差异导致的坑。提前在 CMakeLists.txt 里处理好后面能省下大量排查时间。4. 编辑器、编译器、构建系统怎么串起来VSCode/VISUAL Studio/Qt Creator4.1 VSCode 配置 C/C 环境的正确姿势VSCode 本身也是编辑器它不会编译 C。所以你在 VSCode 里的配置工作本质上就是让编辑器能找到编译器并且帮你把编译命令组织好。很多人在 VSCode 里装完 Microsoft C/C 扩展就以为万事大吉结果打开任何 C 文件都报无法解析包含文件。原因很简单这个扩展只是编辑器的一部分它不是编译器。它负责的功能是代码补全、语法高亮、IntelliSense 跳转想让代码真正编译运行你还需要一个独立的编译器。在 Windows 上最常见的两条路线路线 AMinGW-w64 g去 MinGW-w64 的可靠来源下载工具链解压到一个没有中文和空格的路径然后把其中的bin目录加到系统环境变量PATH里。验证方式是在新开的终端里执行g --version能打印出版本信息就成了。之后在 VSCode 里写代码按 CtrlShiftB 配置tasks.json把编译命令写成g -g main.cpp -o main.exe再建一个launch.json用于调试这套就走通了。路线 BMSVC cl.exe如果你想在 VSCode 里用微软的 MSVC 编译器有一个关键细节必须知道cl.exe不像g那样配个 PATH 就能用它依赖 INCLUDE、LIB 等一堆环境变量。直接开一个普通终端跑cl几乎都会报不是内部或外部命令。正确做法是从开始菜单找到Developer Command Prompt for VS开发人员命令提示符在那个已经设置好环境变量的终端里输入code启动 VSCode。这样 VSCode 内部集成终端就继承了 MSVC 所需的所有环境然后你才能在tasks.json里正常调用cl.exe。这也是热词里VSCode 如何配置使用 MSVC 编译器(cl.exe)这个问题背后的完整答案。核心不是配置文件怎么改而是启动环境必须正确。4.2 Visual Studio 打开 CMake 工程其实不用装额外东西Visual Studio 2019 和 2022 对 CMake 的支持是内置的不需要装插件。操作方式文件 → 打开 → CMake选择项目的CMakeLists.txt即可。打开之后VS 会自动做一次 CMake 配置默认用的是它自带的 MSVC 工具链。你可以在工具栏的配置下拉框里切换 Debug/Release也可以手动添加新的 CMake 配置。如果你打开后报错如下CMake project configuration failed. No CMake configuration for ...通常是因为项目里没找到合适的 CMakeLists.txt或者当前配置需要的某个依赖库没装全。VS 的输出窗口会有更详细的信息先看输出窗口再动手不要盲目重配。VS 的 CMake 模式最适合那种从别人仓库 clone 下来的、已经按 CMake 组织好的项目。你几乎不用配置什么打开就能构建非常省心。4.3 Qt Creator 编译器列表是空的Qt Creator 是另一个自带编辑器 调试器界面的 IDE但它同样需要发现你系统里的编译器。如果你打开选项 → Kits → 编译器发现列表是空的基本可以断定这台机器上安装了 Qt Creator但没有安装任何可被它识别的编译器工具链或者装了但没有被自动检测到。处理方法在编译器选项卡里点击添加 → GCC在编译器路径里手动指向你的g.exe或gcc.exe然后应用。之后在Kits选项卡里新建一个 Kit把编译器和 Qt 版本对应起来。如果用的是 MSVC需要先安装 Visual Studio Build Tools并且最好用开发人员命令提示符环境启动 Qt Creator它才能自动发现 MSVC 工具链。这个问题的根因跟 VSCode 那部分是同一个编译器是独立于 IDE 的实体IDE 不会自带编译器。4.4 一套顺手的工作流建议工具链串联起来之后我个人的习惯是VSCode 里安装 C/C 扩展和 CMake Tools 扩展然后完全不手动写tasks.json而是直接用 CMake Tools 的界面操作。这样做的理由是工程结构由 CMakeLists.txt 统一定义换 IDE 不用重配。Debug/Release 切换只是一个下拉菜单。交叉编译、链接第三方库、指定 C 标准全在同一个配置里管理。IDE 的tasks.json式配置适合临时跑一个小文件而只要项目达到了要长期维护、要交付、要跨人协作的程度CMake 化是更稳妥的选择。5. 实战中高频出现的编译报错排查清单5.1 undefined reference to main这个报错我见过的次数在所有 C 编译问题里能排进前二。它的出现时机是链接阶段含义是链接器在所有目标文件里都没找到程序的入口main函数。原因一般有三个按频率排列源文件里真的没写 main 函数只写了别的函数或类。有些教程会让初学者练习写一个函数库再单独写一个带 main 的测试文件如果只用g utils.cpp把它编译成可执行文件就会报这个错。解决办法是g -c utils.cpp只生成目标文件或者写一个 main 再整体链接。main 拼写错了比如写成mian、mAIN。main是一个有特殊意义的函数名拼错它没有任何语法错误编译器不会拦你直到链接阶段才发现找不到入口。编译命令没包含正确的文件。比如你明明在utils.cpp里写了 main但命令是g main.cpp utils.cpp而真正的 main 在test.cpp里。链接器只找得到你传给它的那些目标文件文件都没传进来自然找不到。排查思路很简单先确认文件里有没有int main()再确认拼写最后检查编译命令里到底传了哪些.cpp文件。我记得有一次在三方项目里折腾这个报错最后发现是 Makefile 里的源文件列表漏了一项属于典型的编译脚本没人维护就腐化。如果你用的是 CMake对应的检查点就是add_executable那一行。5.2 cmake 3.13 or higher is required. You are running version 3.10.2这个报错在热词里单独出现不是没有原因的。Ubuntu 18.04 系统默认的 cmake 版本恰好是 3.10.2而很多现代第三方库把cmake_minimum_required写成了 3.13 甚至 3.16于是 configure 阶段直接罢工。解决办法不是去改库的CMakeLists.txt而是升级本机 cmake。Linux 上比较省心的路径是用pip install --upgrade cmake装一个较新的版本。如果你已经在用 Python 环境这会非常方便。用 Kitware 官方 APT 源仓库里提供更新版本。下载官方预编译的.tar.gz包解压后把bin加进 PATH。顺便提一句如果你在 conda 环境里装 cmake然后跑cmake --version看到版本和系统不是同一个那是 PATH 顺序的经典问题。用which cmake看看实际调的是哪个再决定是调整 PATH 还是卸载其中一个。这种报错的本质是版本约束检查在工程里非常常见。养成 read 一下报错文本最前面那几行的习惯通常那才是根因。5.3 编译器未包含main类型 / IntelliSense 报错编译器未包含main类型这句话非常误导人我第一次看到也愣了一下以为代码有问题。后来才明白这类提示通常来自 VSCode 的 C/C 扩展它的实际含义是IntelliSense 没有找到合适的编译器或者源码解析方式并不是说你代码逻辑有编译错误。出现这种情况排查顺序是确认g --version能正常输出。如果命令都找不到说明编译器没装或者没配 PATH。在 VSCode 里打开命令面板执行C/C: Edit Configurations (JSON)看C_Cpp.default.compilerPath是否指向了正确的编译器路径。如果配置没问题但还报错可能是没有重新加载窗口执行Developer: Reload Window。还有一类类似的问题是你装了 msys2 的编译器但 VSCode 里配置的路径写到了 Cygwin 的目录下两个工具链的文件混着用IntelliSense 就懵了。给编译器的路径一定要精确到具体的可执行文件不要填一个目录上去。另外一个注意点如果你在tasks.json里写编译命令时用了绝对路径换了一台机器这个路径就不存在了。所以工程级配置里尽量用相对路径或者环境变量这才是跨机器协作的地基。5.4 还有一些容易忽略的坑编译器的堆空间不足这类错误你的代码没写错但编译器在编译某个超大源文件时内存占用过高。常见诱发因素是并行编译任务太多比如make -j12在内存只有 8GB 的机器上同时起 12 个编译进程。这种时候先看系统剩余内存把-j参数调小或者关掉其他占用内存的程序再试。另一个相关场景是嵌入式交叉编译工具链本身就吃内存建议优先减少并行度。运行程序时提示缺少 VCRUNTIME140.dll 之类的运行库这是运行环境缺了 Visual C Redistributable跟你自己用没用 MSVC 没关系。任何一个用 VS 2015~2022 编译的程序目标机器上都需要对应版本的运行库。注意这跟编译器是两回事不要混为一谈。CMake 还在用老编译器你刚刚用某个方式换了套工具链但 cmake build 目录里的缓存还是旧的。这时最省心的做法是删除build目录重新执行cmake -S . -B build。CMake 的缓存机制很聪明但也正因为聪明偶尔会有旧配置残留导致的诡异问题。删build重来是万能钥匙成本极低。在线编译器和本地编译器的差异像 HxD 这类在线编译器、以及各种网页版编译环境用来快速试语法、验证算法思路非常方便。但真实项目迟早要回到本地工具链因为只有本地才能调试、才能链接第三方库、才能跑构建系统。尽早把g和cmake用起来比临时依赖在线环境要省事得多。整个 C 工具链的脉络梳理下来其实就这么几条先搞清楚编译器和编辑器的区别再学会用 g 处理单文件和简单多文件工程接着当项目复杂度上来后引入 cmake 做工程组织最后把 VSCode/VS/Qt Creator 这些前端工具和编译器/构建系统正确对接。每一层都有它必须存在的原因理解了原因遇到问题时就不会觉得是在跟一堆毫无逻辑的配置搏斗。我个人的体会是一开始不必把 g 的所有参数都背下来只要会跑通上面的流程后面每遇到一个实际问题自然就会去查对应的参数。真正值得早点养成的习惯是保持源码目录干净让构建产物都在build目录里工程一旦有多个文件就尽早用 CMake 管理起来编译报错时先看是发生在编译阶段还是链接阶段这决定了你排查的方向。顺着这个思路C 这套工具链会慢慢变成你手里相当顺手的工具。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进