ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Ubuntu下OpenOCD与GDB联合调试STM32:从命令行到断点的完整实战

Ubuntu下OpenOCD与GDB联合调试STM32:从命令行到断点的完整实战 “一键调试”按钮的背后Ubuntu下OpenOCD与GDB的联合调试如果你用过STM32CubeIDE、Keil或者IAR你大概率从来没有直接碰过OpenOCD和GDB。你看到的只是一个“Debug”按钮点了之后程序就跑起来了断点也停了一切顺理成章。我第一次在Ubuntu下尝试脱离IDE、用OpenOCD和GDB纯命令行调试一块STM32开发板时内心是相当抗拒的。没有按钮没有一目了然的寄存器窗口连“烧录”这个动作都要自己敲命令来完成。但真正把所有环节跑通了之后我才意识到一个关键事实IDE里的那一套调试交互底层干活的从来都是OpenOCD和GDBIDE只是给它们套了一层图形壳。理解了这条调试链路的底层逻辑你再回头去用任何IDE都会有完全不同的掌控感。这篇文章我会完整记录在Ubuntu系统下从零搞定OpenOCD和GDB的全过程包括为什么需要它们两个配合、怎么编译一个可用的OpenOCD、怎么配置调试器与目标芯片、GDB怎么连上OpenOCD以及调试过程中那些高频出现的报错和对应的排查思路。1. 为什么要用OpenOCDGDB而不是直接用IDE很多人问的第一个问题是我有IDE可以用命令行调试不是给自己找罪受吗这个问题的答案取决于你处于什么阶段。如果你只是跟着教程点按钮那IDE确实够用但一旦你需要定制烧录流程、调试非主流芯片、跑自动化测试脚本或者单纯想知道调试器到底是怎么跟芯片对话的你就离不开OpenOCD和GDB这套组合。1.1 OpenOCD和GDB各自扮演什么角色OpenOCD的全称是Open On-Chip Debugger它负责的是硬件层的通信。你的电脑通过USB连接一个调试器比如ST-Link、J-Link、CMSIS-DAP调试器再通过SWD或JTAG协议接到目标芯片上。OpenOCD就是中间这个翻译官它把电脑这边的指令翻译成SWD/JTAG时序再把芯片返回的数据翻译回电脑能理解的格式。GDB负责的是软件层的调试。它做的事情是设置断点、读写内存、查看寄存器、单步执行这些。但GDB本身并不知道怎么跟你的芯片说话它只管发出“在0x08000100这个地址设断点”这样的命令至于这条命令怎么到达芯片GDB不关心。OpenOCD在这里做了一件很巧妙的事它内置了一个GDB Server功能对外开启一个端口默认3333对GDB来说这个端口就是一个“远程目标”。于是GDB只需要按照远程调试协议把命令发给这个端口OpenOCD收到之后再转发给芯片。这就是这套调试链路的核心模型。1.2 命令行调试带来的实际收益用命令行调试最大的感受变化就是一切都在你的掌控之中。IDE把很多细节隐藏了你根本不知道点击Debug之后它到底执行了什么操作。而在命令行下你会清楚地看到OpenOCD初始化的每一步、GDB连接的每一次握手、Flash擦写的每一个扇区。举个例子我遇到过一次程序跑飞的问题程序总是复位后卡死在某个外设初始化。IDE下我按了好几次暂停都只能看到当前的PC指针看不出什么问题。后来用GDB脚本自动化地复位、设置断点、执行、再读取几个关键寄存器的值一遍跑完就把问题定位到了——是某个外设时钟没开。这种自动化调试能力是IDE的图形界面很难给你的。另外OpenOCD和GDB都是跨平台的自由软件你在这套环境里总结出来的一套调试流程将来无论换到Windows、macOS还是各种Linux发行版思路都是一样的。2. Ubuntu环境准备与依赖安装在动手编译之前先把系统环境说清楚。2.1 建议的Ubuntu版本和基础工具链我在多台机器上跑过这套流程从Ubuntu 18.04到24.04都验证过基本上都能顺利编译。如果你用的是LTS版本20.04、22.04、24.04直接照着做就行。首先要确认系统里有完整的编译工具链sudo apt update sudo apt install build-essential pkg-config autoconf automake libtool texinfo之所以要装pkg-config和autoconf这一堆是因为OpenOCD从源码编译时它的构建系统会依赖这些工具来检测当前环境中存在哪些依赖库并生成对应的Makefile。如果你的系统里缺了它们configure脚本会直接报错而且报错信息往往不太直观不如一次装齐。2.2 编译OpenOCD前期需要确认的依赖库OpenOCD的核心功能本身不依赖太多外部库但如果你想用到某些特定功能那就得提前装好相应的依赖。以下是我实际用到的几个功能需求需要的依赖库安装命令基础USB调试器支持ST-Link、CMSIS-DAP等libusb-1.0sudo apt install libusb-1.0-0-dev高性能并行编程接口libftdisudo apt install libftdi1-dev libusb-1.0-0-devCapstone反汇编引擎用于一些高级调试功能libcapstone-devsudo apt install libcapstone-dev通过HID协议通信的调试器libhidapi-devsudo apt install libhidapi-dev我自己在编译Jessie版本的OpenOCD时只装了libusb和libftdi就足矣驱动ST-Link和CMSIS-DAP。如果你是J-Link用户OpenOCD是调用J-Link官方驱动库的不需要额外装libftdi但需要你在configure的时候确保SDL用于图形化显示的依赖和HIDAPI被正确检测到。有个小技巧在跑configure之前先执行一次sudo apt build-dep openocd。这条命令会把编译OpenOCD所需的全部依赖一次性装好省得一个一个去排查。3. 从源码编译OpenOCDconfigure参数和编译细节Ubuntu的软件源里其实是有OpenOCD的直接sudo apt install openocd就可以装。但我不推荐直接用系统源里的版本因为发行版软件源里的OpenOCD一般都比较旧而OpenOCD对新型号芯片和调试器的支持更新非常快。比如某些最新发布的STM32系列可能要最新的OpenOCD源码才认识。3.1 获取最新源码OpenOCD官方仓库在SourceForge上但GitHub镜像更新得也很及时。我用的是官方Git仓库直接拉取最新代码git clone https://git.code.sf.net/p/openocd/code openocd cd openocd想切到某个稳定版本的话可以查看tag列表git tag -l git checkout v0.12.0 # 以v0.12.0为例3.2 构建引导和configure配置OpenOCD的源码包不像很多项目那样拿到就能直接./configure它需要先执行bootstrap来生成configure脚本./bootstrap这个过程会调用autoconf、automake和libtool生成一系列构建所需的文件。如果你在前面的依赖安装步骤中漏掉了texinfo这一步会报警告但通常不影响继续。接下来是关键的一步——configure参数配置./configure --enable-stlink --enable-jlink --enable-cmsis-dap --enable-ftdi --enable-vendor --enable-rtt我来逐个解释这些参数的含义--enable-stlink启用ST-Link调试器支持这是国内嵌入式开发最常用的一类调试器--enable-jlinkJ-Link支持如果你用J-Link就加上但要确保系统里有libusb--enable-cmsis-dapCMSIS-DAP调试器支持很多国产开发板自带的调试器就是走的CMSIS-DAP协议--enable-ftdiFTDI芯片方案的调试器DIY党很喜欢用的那种--enable-vendor启用其他厂商的调试器支持--enable-rttSEGGER RTT实时传输支持做嵌入式日志输出很有用这些参数不是越多越好。每启用一个功能都会在编译时引入对应的依赖如果依赖没装全configure就会报错。如果你只想驱动ST-Link就只加--enable-stlink这样能最大化降低编译失败的概率。configure成功后你会看到OpenOCD会输出一份配置摘要。仔细看一下Debug adapter和Interface drivers这两栏确认你需要的调试器驱动在yes的状态。3.3 编译和安装配置没问题之后直接编译make -j$(nproc)-j$(nproc)是利用多核并行编译可以大幅缩短编译时间。OpenOCD的源码不算特别庞大四核以上的机器一般几分钟就编译完了。编译完成后安装到系统目录sudo make install安装完成后验证一下openocd --version如果输出类似Open On-Chip Debugger 0.12.0的信息说明安装成功。如果提示找不到命令多半是安装路径不在PATH环境变量里。默认安装路径是/usr/local/bin在Ubuntu上一般都在PATH里。3.4 配置udev规则解决权限问题这一步非常关键而且特别容易被忽略。编译安装完OpenOCD后如果你直接插上ST-Link然后运行OpenOCD很可能看到这样的错误Error: libusb_open() failed with LIBUSB_ERROR_ACCESS Error: open failed这个报错的本质是Linux的USB权限管理问题。默认情况下普通用户没有权限直接访问USB设备需要root权限。你有两个选择第一每次运行OpenOCD都加sudo。这是最简单粗暴的但副作用是后面对接GDB的时候需要GDB也以root运行一旦操作失误很难排查清楚。第二配置udev规则把调试器的USB设备权限放开给普通用户。OpenOCD的源码里自带了一个参考规则文件sudo cp contrib/udev/99-openocd.rules /etc/udev/rules.d/ sudo udevadm control --reload-rules sudo udevadm trigger这里要说明一个坑OpenOCD源码头文件和发行版之间的udev规则可能不完全匹配。如果你插上调试器后运行lsusb能识别到设备但OpenOCD还是报权限错误可以检查一下99-openocd.rules文件中调试器的VID/PID是否跟你的设备一致。比如常见的ST-Link/V2的VID是0483PID是374b或3748如果你发现文件里没有对应条目手动加一行就行SUBSYSTEMusb, ATTR{idVendor}0483, ATTR{idProduct}374b, MODE0666, GROUPplugdev配置完后把当前用户加入plugdev组sudo usermod -aG plugdev $USER重新登录一次让组权限生效。4. 准备OpenOCD的配置文件接口和目标的实际写法OpenOCD启动时需要指定配置文件告诉它三件事用什么调试器、调试什么芯片、以什么方式连接。配置文件分为两部分interface接口和target目标。4.1 选择合适的interface配置文件OpenOCD的配置模板位于tcl/interface目录下我们可以直接引用官方提供的模板。常用的几类真正的意法半导体ST-Linktcl/interface/stlink.cfg各种兼容ST-Link的国产调试器通常也用stlink.cfg或者根据芯片方案选择对应的驱动配置J-Linktcl/interface/jlink.cfgCMSIS-DAPtcl/interface/cmsis-dap.cfg以ST-Link为例你还需要在接口配置文件里指定具体的适配器速度。建议把速度设置在合理的范围比如SWD模式下的4MHzsource [find interface/stlink.cfg] transport select hla_swd adapter speed 4000transport select hla_swd这一步很关键它把传输方式指定为High-Low-Adapter的SWD模式。ST-Link既可以走SWD协议也可以走JTAG协议这个命令是用来告诉OpenOCD你的物理连接方式。4.2 target配置与芯片家族的坑目标配置文件位于tcl/target目录以STM32F103系列为例source [find target/stm32f1x.cfg]如果只是调试裸机程序这样指定就够了。但如果你调试的是有Bootloader和App分区的情况或者需要在复位后暂停、等待调试器接管就需要先了解target配置文件内部的结构。打开stm32f1x.cfg看一下你会发现里面定义了一个叫stm32f1x.cpu的target对象设置了一些关键的属性。最需要注意的参数是_WORKAREA_SIZE和_FLASH_SIZE。如果你的芯片是512KB的Flash、64KB的RAM但配置文件里写的是256KB和32KB后续烧录的时候就会出问题。正确的做法是在自己的配置文件中覆盖这两处设置。比如我这里定义了一个针对GD32F303的配置source [find interface/stlink.cfg] transport select hla_swd adapter speed 4000 source [find target/stm32f1x.cfg] # 适配GD32F303 set _WORKAREA_SIZE 0x5000 set _FLASH_SIZE 0x80000这里覆盖的理由是GD32F303内部标称Flash 512KB但它的实际SRAM是96KB而OpenOCD默认的工作区大小work area是从RAM里划分出来的一块临时缓冲区如果你不把它调到合理大小烧录时OpenOCD会提示工作区不足。不过说实话如果你完全不清楚芯片的内部架构最简单的方式还是直接写一个最小的自定义配置而不是去改官方target文件里的变量。因为OpenOCD的变量作用域在整个配置阶段都是全局的你在外部覆盖set _WORKAREA_SIZE之后官方target文件里的stm32f1x.cpu configure -work-area-phys $_WORKAREA_SIZE就会用到你的值不会产生冲突。4.3 验证配置文件是否有效配置写完之后可以先让OpenOCD以只读模式跑一下看能不能正确识别芯片openocd -f my_board.cfg启动日志末尾如果能输出类似Info : STLINK V2J29S7 (API v2) VID:PID 0483:374B Info : Target voltage: 3.3V Info : clock speed 4000 kHz Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints说明你已经成功了一大半。6 breakpoints, 4 watchpoints这一行尤其重要——这说明OpenOCD已经和芯片建立了连接并且读到了内核调试单元的信息。如果启动时报错Error: target not halted通常是因为芯片还在运行之前的程序OpenOCD在初始化时需要先把芯片halt住但某些低功耗模式或者异常状态会导致halt操作失败。一个简单的应对方法是在配置文件中加上reset_config srst_nrst或者在OpenOCD启动后在telnet终端里手动执行reset halt。5. GDB的多架构支持与初始化配置OpenOCD那边跑通之后再来看GDB。5.1 安装gdb-multiarch还是arm-none-eabi-gdbUbuntu的软件源里有现成的gdb针对x86_64架构但这个版本不能用来调试ARM芯片——它不懂ARM指令集。你有两种解决方式第一种安装gdb-multiarchsudo apt install gdb-multiarch这个版本号称“多架构”可以同时支持x86_64和多种嵌入式架构包括ARM。用的时候命令是gdb-multiarch。第二种如果你使用的是ARM官方GCC工具链它自带一个配套的GDBsudo apt install gcc-arm-none-eabi装好后命令是arm-none-eabi-gdb专门针对ARM Cortex-M系列做了优化。我个人的建议是两种都装但日常调试优先用arm-none-eabi-gdb。因为它在某些细节上更贴合嵌入式场景——比如对Cortex-M内核的特殊寄存器xPSR、PRIMASK等的显示格式更友好。不过gdb-multiarch的好处是它在架构之间切换很方便如果你同时玩RISC-V和ARM用它就不用装一堆GDB版本了。验证安装arm-none-eabi-gdb --version5.2 编写.gdbinit配置文件GDB启动时会先读取当前用户主目录下的.gdbinit文件。我们可以把跟OpenOCD连接的初始化命令写在这里target remote localhost:3333 monitor reset halt load monitor reset halt continue逐条解释一下target remote localhost:3333这是告诉GDB连接本机的3333端口也就是OpenOCD的GDB Servermonitor reset haltmonitor前缀表示这条命令不是由GDB处理的而是直接透传给OpenOCD执行。reset halt的意思是复位芯片并立即挂起让芯片停在复位向量处等待调试load把当前正在调试的程序由file命令加载进来的elf文件烧录到芯片的Flash中第二次monitor reset halt烧录完成后再次复位确保程序从入口处开始跑continue让程序全速运行这里要注意一个细节load命令烧录的是ELF文件OpenOCD会解析ELF中的段信息自动把代码段和数据段放到正确的位置。不需要你手动指定起始地址。5.3 路径与符号加载的坑在GDB里file命令加载ELF文件之后符号信息就自动有了。但如果你编译程序的时候是用相对路径很多Makefile就是这么干的GDB显示的源码路径可能是错的。解决办法有两个。第一是在GDB里手动设置源码路径directory /home/user/project/src第二是编译时如果用的是GCC建议加-gdwarf-2 -g3参数。-g3会生成最多的调试信息包括宏定义。如果只用-g调试时看不到宏展开有时候会很不方便arm-none-eabi-gcc -mcpucortex-m3 -mthumb -g -gdwarf-2 -g3 -O0 -c main.c -o main.o注意-O0也很重要——关闭优化不然GDB单步调试时行号会对不上变量值也可能不符合直觉。6. 一次完整的命令行调试实战记录准备工作都做完之后我来手把手跑一遍完整流程同时展示几个高频操作。6.1 启动OpenOCD和GDB先启动OpenOCDopenocd -f my_board.cfg终端会停留在前台运行日志。然后另开一个终端arm-none-eabi-gdb build/test.elf进入GDB界面后如果你在.gdbinit里写好了配置输入target remote localhost:3333GDB会输出连接到调试器的确认信息。如果连接失败最常见的原因是OpenOCD还没启动完成或者端口被占用。6.2 核心调试命令和实际操作连接成功后我一般先做三件事查看反汇编、设断点、单步观察寄存器。查看当前所在位置info registers这条命令会把所有核心寄存器打印出来包括r0-r15、pc、sp、xPSR等。调试的时候我最常看的是pc程序计数器和sp堆栈指针。设置断点break main或者指定地址break *0x0800012A查看断点列表和删除断点info breakpoints delete 1单步执行step # 进入函数内部单步 next # 不进入函数跳过整个函数调用查看内存内容x/8xw 0x20000000这条命令的含义是从地址0x20000000开始以32位4字节为单位打印8个word的内容。x/8xw里的x是examine8是数量xw表示十六进制word。修改变量值set variable count 10查看函数调用栈backtrace这套命令用熟了之后你会发现命令行调试的效率其实比IDE更高因为你可以把一连串操作写成一个GDB脚本直接执行。6.3 编写自动化GDB脚本这是命令行调试强大的另一个核心点。假设你要复现一个bug每次都需要在特定位置设断点、读取一组寄存器和内存的值、打印之后继续跑。你不需要每次都敲十几条命令写个脚本一次性执行就行# debug_script.gdb target remote localhost:3333 monitor reset halt break HardFault_Handler commands printf HARD FAULT HIT\n info registers x/8xw $sp bt continue end continue在GDB里执行source debug_script.gdb这段脚本在OpenOCD配合下每次程序触发HardFault中断都会自动打印出寄存器现场。做嵌入式开发的人都知道HardFault是调试时最头疼的问题有自动打印现场信息的脚本能省很多事。7. 高频报错的完整排查链路与避坑清单这一节整理我在实际调试中遇到的高频问题。如果你能照着我说的思路走一遍大多数问题都能自己解决。7.1 Error: open failed | libusb_open() failed这个报错前面说过是USB权限问题。排查链路如下第一步确认系统识别到了调试器lsusb第二步检查是否设置了udev规则以及当前用户是否在plugdev组groups第三步检查调试器是否被其他进程占用。尤其是Windows和Linux双系统用户在Windows下装了SEGGER的J-Link驱动后那个驱动可能会在重启后以某种服务的形式驻留。在Linux下就表现为端口或USB设备被占用。sudo lsof /dev/bus/usb/001/004如果有进程占用了这个设备先关停它。7.2 Info : clock speed 4000 kHz 之后卡住没有芯片ID输出OpenOCD启动卡在clock speed这一步通常意味着SWD通信没有建立起来。排查链路首先看接线。SWDIO、SWCLK、GND三根线必须接对这是最基本的。如果用了杜邦线连接线长超过20cm就容易出现信号完整性问题——对于SWD这种高速协议来说导线过长导致波形畸变非常常见。我遇到过不下三次最后发现是杜邦线接触不良。其次排查复位电路。某些板子的复位脚被外围电路拉着导致芯片始终处于复位状态。你可以用示波器看NRST引脚的电平如果一直是低电平问题就出在复位电路。最后试一下降低SWD频率adapter speed 100如果1500kHz下能连上4000kHz下连不上说明信号质量不够好。此时优先处理物理连接而不是硬调频率。7.3 Cannot access memory at 0xXXXXXXXX这个报错出现在GDB尝试读取某个地址的内存时芯片没有给出有效响应。导致这个问题的原因通常是访问了芯片不存在的地址空间或者芯片当前处于低功耗模式外设总线时钟没有开启。排查链路先用info registers看一下当前PC值在哪里。如果PC值异常比如0xffffffff说明程序已经跑飞了如果PC值停在一个正常地址再用monitor reset halt把芯片复位重置一下试试。7.4 烧录时提示cannot perform JTAG flash因为OpenOCD server没有运行这个报错几乎是每个新手都会碰到的你在GDB里敲了load结果GDB回你一句Cannot perform JTAG flash, because OpenOCD server is not running!。这个报错的根本原因其实很简单GDB没有连上OpenOCD。可能的情况有三种OpenOCD根本没启动或者启动失败了OpenOCD启动了但GDB用了target remote连的不是同一个端口你在GDB里先执行了monitor reset haltOpenOCD在复位过程中把GDB连接断掉了排查方式也很直接先确认OpenOCD终端里有没有Info : accepting gdb connection on tcp/3333这样的日志。如果没有这行日志说明GDB和OpenOCD之间根本没有建立连接那load命令自然没有可用的服务器来响应。还有一个我踩过的具体坑是我同时开了一个IDE的调试会话IDE里的调试器占用了ST-Link。这时候OpenOCD虽然能启动但初始化时芯片就已经被另外一个调试会话控制住了导致GDB这边一执行load就报错。解决方式是先把IDE的调试会话完全关闭。7.5 调试Cortex-M7系列时断点不生效如果你在调试M7内核的芯片比如STM32H7可能会发现硬件断点正常情况下能用但一旦断点数量超过6个就会失败。这不是bug而是Cortex-M7的硬件断点数量上限就是6个FPB单元。GDB的break命令默认使用的是硬件断点因为Flash断点不像RAM断点那么方便超过硬件限制后GDB会尝试用软件断点但在只读的Flash上软件断点是不支持的。在GDB里查看当前用了多少断点info breakpoints如果想手动指定用硬件断点hbreak main调试跑飞的程序时我喜欢写一个GDB脚本把程序里可能的几个关键函数全部打上硬件断点最多不超过6个然后一键运行、看哪个断点先命中就能快速缩小问题范围。8. 进阶玩法在GDB里驱动OpenOCD的内置命令最后这部分算是附赠的经验。OpenOCD的GDB Server不只是接收GDB的标准调试协议命令它还允许你通过GDB的monitor命令直接调用OpenOCD自身的命令集。8.1 用monitor命令操作芯片状态比如你可以这样monitor reset halt monitor flash write_image erase main.hex monitor reset run这三条命令组合起来的效果是复位并暂停芯片、擦除并烧录一个hex文件、复位并全速运行。这意味着你可以完全不依赖任何烧录工具纯用GDB命令行来完成烧录动作。8.2 OpenOCD的RTOS线程感知调试如果你在裸机开发中用了FreeRTOSOpenOCD还支持RTOS线程感知。前提是启动OpenOCD时启用对应的RTOS支持openocd -f my_board.cfg -c rtos create FreeRTOS -target stm32f1x.cpu启用之后GDB的info threads就能看到当前FreeRTOS的任务列表。这对调试多任务程序的价值非常大——你能直观地看到当前哪个任务在运行、哪个任务被阻塞、就绪队列里有哪些任务。不过这个功能需要OpenOCD正确识别FreeRTOS的内核数据结构。如果你的FreeRTOS版本比较新或者使用了裁剪配置OpenOCD可能识别不到任务列表表现为info threads只能看到空列表。这时候通常需要检查你的FreeRTOS是否保留了内核调试信息尤其是configUSE_TRACE_FACILITY这个配置项需要置1。这是一个经常被忽略的细节值得留意。8.3 用OpenOCD做Flash保护位的设置和解除很多芯片支持Flash读出保护RDP。一旦设置了RDP你的调试器就再也不能通过SWD口读出Flash内容了。OpenOCD提供了直接操作选项字节的接口比如STM32monitor stm32f1x option_read 0 monitor stm32f1x lock 0 monitor stm32f1x unlock 0这组命令分别对应读取选项字节、锁定芯片防止调试和读Flash、解锁芯片。要特别提醒的是unlock操作会导致Flash内容被全部擦除。如果你是为了找回一个设置了读保护、自己又忘了内容或者想帮别人“解密”的芯片不要再折腾了——解锁的同时数据就没了不存在绕过读保护读出内容的办法。说回正题。整个调试环境跑通之后我最直观的体会是OpenOCD和GDB这套组合并不比IDE难用它只是把IDE里被隐藏起来的细节全部暴露给你了。你花在理解这套底层原理上的时间会在之后的每一次疑难问题排查中成倍回报回来。如果你从一开始就在命令行下调试久而久之你会形成一种“调试思维”——看到报错先想协议栈的哪一层出了问题而不是盲目去点GUI上的某个按钮。这种思维方式的转变可能才是这套工具链能给你最大的收获。根据我个人经验第一次搭建这个环境预留一个下午的时间是比较稳妥的。不要急着马上调试具体项目先用一个简单的LED闪烁程序把从编译、烧录到断点的完整链路跑通再逐步增加复杂度。环境这不复杂复杂的是排查环境问题的耐心。这套环境一旦跑起来之后真的就是“一次配置长期受益”了。
返回列表