
搞过线上 C/C 服务的人应该都有这种经历程序半夜崩了早上起来拿到 core 文件满怀期待地打开 GDB 想定位崩溃行结果bt出来只有一串十六进制地址函数名没有、源码行号没有当场血压就上来了。这通常是因为发布时把调试信息一起 strip 掉了。但真要把所有调试信息保留在二进制里发布包又会膨胀好几倍而且会把你电脑上的源码路径、内部符号全部暴露给用户。后来我改用objcopy把调试信息从可执行文件里分离出来单独归档发布出去的只是一个不带调试信息的精简二进制一旦线上崩溃再把归档的调试文件交给 GDB照样能直接定位到崩溃堆栈和源码行号。这套做法既保证了发布包体积又保留了事后分析线上问题的能力。今天把这套完整流程拆开来讲包括核心命令、GDB 的配置方式、崩溃现场的实战分析以及我在实际项目中踩过的坑和总结出来的排查技巧覆盖从编译、分离、发布到 GDB 定位崩溃行的完整链路。1. 为什么要把调试信息和可执行文件拆开1.1 调试信息到底是什么GDB 为什么依赖它先理清一个基础概念。用gcc -g编译出来的可执行文件里除了机器码以外还包含一套 DWARF 格式的调试信息。这套信息里至少有三个层面的内容源代码行号表告诉你哪个地址对应源码第几行、符号表函数名、变量名、结构体定义、类型信息变量的类型和内存布局。GDB 拿到一个地址之后就是靠这些信息把“地址”翻译成人能看懂的“文件名:行号”和“函数名:参数列表”。没有调试信息的纯二进制不是不能调试只是非常痛苦。bt只能看到0x5555555561a4这样的地址你根本不知道这个地址属于哪个函数更别说定位到具体源码行了。如果有符号但没行号还能看到函数名但看不到具体是函数内哪一行崩的。所以对“崩溃行”级定位来说行号表是不可或缺的。这也解释了为什么发布时绝对不能无脑strip完就完事。strip --strip-all会把.symtab、调试分段全干掉后面拿着 core 文件也基本只能靠反汇编硬猜效率极低。正确思路是调试信息不删除而是和二进制分开存放。1.2 分离方案解决的核心痛点我最早遇到这个需求是在做一个对外分发的大型服务端程序时出现的。当时面对三个现实问题。第一体积问题。实测过同一个程序开启-g后体积能膨胀 3 到 5 倍甚至更多。我们当时的发布包要送到大量服务器上多出来的几十 MB 乘以服务器台数成本增加不少传输时间也变长。第二信息暴露问题。调试信息带完整编译路径比如/home/user/project/src/main.cpp而且公开了所有未加static的内部函数名。这类信息发到外部等于把项目的目录结构、代码组织方式都向用户展示了一遍有些客户确实会在意这一点。第三事后可排查问题。完全 strip 又不可接受因为线上必然会出现偶发崩溃没有调试信息就只能让现场工程师去翻汇编定位周期非常长。用objcopy --only-keep-debug和objcopy --strip-debug这两个命令组合能做到发布的是精简版二进制调试信息单独抽出来放私有归档目录。线上崩溃后把调试文件拷到 GDB 能搜索到的位置直接加载 core 文件定位能力和本机开发调试时几乎没差别。1.3 这套方案适合什么场景服务端程序有稳定发布流程可以配备私有符号服务器或归档服务器。嵌入式设备产物交付Flash 空间有限调试信息全塞进去不现实。商业软件对外发布既想压缩体积又想保留内部问题定位能力。自研的插件或动态库在宿主进程里崩溃需要靠 GDB 事后分析。反过来如果只是在本机调试、代码不发布出去那完全没必要分离直接-g编出来用就行别增加多余步骤。分离调试信息是为了解决“发布”和“事后定位”之间的冲突本地开发阶段不需要提前引入这套工作流。2. 分离调试信息前的准备工作与整体工作流2.1 工具链与环境依赖整套方案依赖的软件不多一个二进制工具objcopy一个调试器 GDB。objcopy属于 binutils 包GDB 一般跟随发行版自带。我用的是 Ubuntu 24.04 环境GDB 版本到了 13.2其他主流发行版也没问题GDB 版本低一些的 8.x、9.x 也支持这套机制。有一点值得注意GDB 从很早的版本4.x 时期就支持.gnu_debuglink所以只要你没在配置里恶意限制路径搜索规则老版本 GDB 照样能找到分离出去的调试文件。GDB 13.2 对我真正有用的新特性是debuginfod客户端的完善和更友好的自动下载提示它和我们的私有归档不冲突可以共存。编译工具建议尽量用同一套。如果程序是用 GCC/G 编译的objcopy、strip、readelf、addr2line这些都推荐用同一体系内的 binutils 版本避免出现 ABI 识别上的小概率问题。Clang 编译的程序通常也能用 GNU objcopy 分离但个别目标平台下会遇到格式兼容细节的坑实际操作前先用readelf -S看一眼分段布局确认没有异常再说。2.2 完整工作流从编译到崩溃分析我先用文字把这套流程完整串一遍后面每个环节再展开细节。第一步编译时带调试信息。源文件用-g或-g2编译并链接生成带调试信息的原始可执行文件比如myapp.debug的“源版”myapp_full。第二步抽取调试信息。用objcopy --only-keep-debug myapp_full myapp.debug把调试信息单独抽到一个二进制文件里。第三步剥离原始二进制。objcopy --strip-debug myapp_full或者strip --strip-debug把原文件里的调试分段删掉得到干净的发布版myapp。第四步建立调试文件和发布版的关联。用objcopy --add-gnu-debuglinkmyapp.debug myapp往发布版里写一个.gnu_debuglink分段这样 GDB 就能自动按名字去找调试文件。第五步发布并归档。把精简版myapp部署到目标机把myapp.debug按固定规则归档起来。归档时除了.debug文件本身最好保留构建号、git commit、时间戳确保版本对得上。第六步出现崩溃后收集 core 文件用 GDB 指向归档调试文件加载 corebt定位崩溃行。第七步可选的增强把addr2line等工具纳入排查工具箱。有时没有 core只有日志里记录的崩溃地址用addr2line配合调试文件也能快速出结果。整体上看核心就是一次编译产出“两个文件”一个是发布给用户的精简版一个是只给自己用的调试版。它们用.gnu_debuglink或 build-id 绑在一起。2.3 归档策略调试文件本身也是资产很多人做完objcopy分离把.debug文件存进了一个不算太起眼的文件夹然后过两个版本回头看完全分不清哪个 debug 文件对应哪个版本号。这个问题一定要在流程设计阶段想清楚。调试文件是排障资产不是临时产物。我后来养成的一个习惯是每次发布构建时把.debug文件连同构建产物一起按“dists/版本号/构建号/myapp.debug”路径归档。同时记录下构建时的 git commit、编译选项、内核版本、GCC 版本这些信息在排查时经常能派上用场。别等半年后线上出问题再去找匹配的 debug 文件那时候连你自己都不知道当时用的哪个编译环境。3. 核心实操用 objcopy 拆分调试信息3.1 编译带调试信息的程序为了让整个流程更直观我用一个小例子演示。写一个简单但能复现崩溃的 C 程序crash_demo.c#include stdio.h #include stdlib.h void inner_func(int *p) { *p 42; // 这里会段错误 } void outer_func(int flag) { int *p NULL; if (flag) { inner_func(p); } } int main() { printf(start crash demo\n); outer_func(1); printf(should not reach here\n); return 0; }编译命令gcc -g -O0 -o crash_demo_full crash_demo.c注意这里我用-O0是为了演示时行号能精确对应到源码。实际生产为了性能会开-O2-O2下行号定位整体仍可靠但个别行会因为指令重排、内联出现偏差后面排查部分会细讲。编译完成后看一下基本信息file crash_demo_full ls -lh crash_demo_full readelf -S crash_demo_full | grep debug正常会看到.debug_info、.debug_line、.debug_abbrev、.debug_str等分段文件大小明显比不带-g编译的大很多。这些分段就是调试信息接下来要单独抽走的正是它们。3.2 objcopy 三条核心命令解读第一条命令objcopy --only-keep-debug crash_demo_full crash_demo.debug--only-keep-debug的含义是只保留调试信息相关分段把代码段、数据段等运行时分段的内容清空会保留结构输出为crash_demo.debug。这个文件不能直接运行但包含完整的 DWARF 调试信息、符号表和可选的 build-id。它负责给 GDB 提供所有“翻译”素材。第二条命令objcopy --strip-debug crash_demo_full这里我选择直接在原文件上执行把调试信息分段从crash_demo_full里删掉。执行后crash_demo_full变成精简的发布版。如果想保留一份完整带调试信息的原件建议先cp crash_demo_full crash_demo_full_with_dbg再操作或者干脆把第一步的“full”文件重命名成crash_demo_full_debug留底然后第二步在副本上做。工作流上我认为留底最稳妥万一后面要再抽别的信息不用重复编译。--strip-debug只删调试分段--strip-all则会把符号表也删掉发布版里连函数名都不剩。到底删多狠取决于你的策略。我建议通常只用--strip-debug保留.symtab里的全局函数名和变量名。符号名不算敏感信息而且保留后在 GDB 里bt至少能看到函数名对定位非常有帮助。他人反调试本来就拦不住动态分析没必要用删除所有符号的方式增加排查难度。第三条命令objcopy --add-gnu-debuglinkcrash_demo.debug crash_demo_full这条命令把crash_demo.debug的路径信息写进发布版的.gnu_debuglink分段。GDB 加载发布版时会主动读取这个分段获取调试文件名当然还有 build-id然后按搜索规则去定位.debug文件。这一步相当于在发布版和调试文件之间建立了一条自动通道。后面详细讲 GDB 的搜索机制。三条命令执行完后目录里有两个关键文件crash_demo_full发布版体积通常只有原来的三分之一甚至更小和crash_demo.debug调试信息专用。如果还想进一步减小发布版可以用strip --strip-unneeded替换--strip-debug它会把重定位处理中不需要的符号也删掉体积更小但全局符号也没了我一般不推荐在这个场景下用它。3.3 验证拆分结果拆完之后不要急着走下一步先验证一下三个关键点发布版还能不能正常跑、调试文件信息是否完整、.gnu_debuglink是否写进去了。# 运行发布版确认精简后功能不受影响 ./crash_demo_full # 查看发布版还有哪些分段 readelf -S crash_demo_full | grep -E debug|gnu_debuglink # 查看调试文件的分段 readelf -S crash_demo.debug | grep debug # 查看 .gnu_debuglink 内容 readelf -x .gnu_debuglink crash_demo_full正常情况如下发布版里.gnu_debuglink分段存在.debug_*分段消失crash_demo.debug里的.debug_*分段完整。readelf -x能看到 debug 文件名比如Hex dump of section .gnu_debuglink: 0x00000000 63726173 685f6465 6d6f2e64 65627567 crash_demo.debug同时建议执行file crash_demo_full确认文件类型仍然是 ELF 可执行文件而不是错误的 stripped 状态。这里有个常见误区有人看到file输出里带with debug_info以为是没剥离成功其实那是针对原文件的发布版应该显示stripped或只保留符号表如果只用了--strip-debug则不会完全标记为 stripped。验证通过后把crash_demo.debug按设计好的归档路径保存比如~/debugstore/crash_demo.debug。4. 让 GDB 顺利找到分离出去的调试信息4.1 .gnu_debuglink 的搜索机制做好分离后最关键的问题是 GDB 怎么知道去哪里找调试文件。GDB 加载一个程序时会依次尝试几类路径来定位调试信息。第一类原路径查找。如果发布版本身就带着在编译机上的绝对路径的调试信息未分离时就存在GDB 会去那个绝对路径找源文件。我们分离后发布版里已经没有 DWARF 信息了这条路走不通。第二类.gnu_debuglink记录的名字。GDB 读取.gnu_debuglink分段拿到纯文件名不带路径例如crash_demo.debug。然后它按固定顺序搜索程序所在目录、程序所在目录下的.debug子目录、GDB 内置的全局 debug 目录通常是/usr/lib/debug、以及用set debug-file-directory指定的目录。GDB 11 之前对debug-file-directory的处理比较笨新版支持在指定目录下再递归查找.debug子目录但依赖发行版是否打了补丁保险做法是把调试文件直接放在你显式设置的目录下形成确定性的规则。第三类build-id 路径。如果目标的文件里有.note.gnu.build-idGDB 会按debug-file-directory/.build-id/xx/yyyyyyyy.debug结构去找。这个机制更可靠因为 build-id 是内容哈希派生出来的跟文件名没关系能避免两个同名不同版本的调试文件冲突。我建议把两个机制都用起来既写.gnu_debuglink也保留 build-id。毕竟.gnu_debuglink在跨机器拷贝时经常因为文件名重复导致 gdb 加载到旧版本而 build-id 天然带版本指纹。4.2 手动指定调试文件目录在普通工作站上最直接的配置是启动 gdb 时指定gdb -ex set debug-file-directory /home/user/debugstore ./crash_demo_full core如果crash_demo.debug就放在/home/user/debugstore/crash_demo.debugGDB 会通过.gnu_debuglink找到它自动加载。加载成功时 GDB 通常会打印一行提示类似Reading symbols from /home/user/debugstore/crash_demo.debug...也可以进入 GDB 交互模式后再用命令设置(gdb) set debug-file-directory /home/user/debugstore (gdb) file ./crash_demo_full (gdb) bt注意优先级GDB 先尝试set debug-file-directory里的目录和它的子目录然后才轮到当前目录的.debug子目录。如果你的归档目录结构是/home/user/debugstore/.build-id/xxx/yyy.debug对应设置 debug-file-directory 为/home/user/debugstore就行GDB 会自动匹配 build-id 结构。如果不小心在 GDB 里看到 “no debugging symbols found” 的提示不用慌先用info sharedlibrary或info files检查当前加载的调试文件路径再看.gnu_debuglink里的文件名是否和实际对得上。4.3 生产环境里更省心的配置习惯在服务器上排查问题时我通常不依赖交互式 GDB 一条条敲命令而是先把配置写进.gdbinit文件里或者用批处理模式执行。比如这样一条命令一键跑完gdb -batch -ex set debug-file-directory /opt/debugstore -ex bt ./crash_demo_full core-batch模式执行完自动退出很适合在故障现场快速拿堆栈。如果bt输出不够详细可以换成bt full或者bt -full把每个栈帧的局部变量值打出来对定位崩溃原因这一步往往就是关键。还有一个小细节GDB 如果连不上调试文件会尝试走debuginfod从网络下载符号。如果你处在一个内网离线环境下载必然失败还会产生几秒到几十秒的等待超时。这种情况下建议启动时显式关掉gdb -ex set debuginfod enabled off ./crash_demo_full core不关也不会出错但体验很差每次启动都要卡在网络超时上。尤其是排查现场时间比什么都宝贵我一般在.gdbinit里直接统一关掉 debuginfod除非明确需要从内部符号服务器拉取。5. 实战演练GDB 定位崩溃行5.1 准备好 core 文件先用前面编译好的crash_demo_full剥离了调试信息但带了.gnu_debuglink制造一次崩溃。默认情况下系统可能不会产生 core 文件因为 core 的大小被设成 0 了。先检查并调整ulimit -c ulimit -c unlimitedulimit -c输出0就表示禁止生成 core改成unlimited可以恢复。这个设置只对当前 shell 会话有效如果用户在服务器环境里通过 systemd 启动服务由服务管理器启动的进程不受当前 shell 影响那要改服务配置文件里的LimitCOREinfinity。core 文件的存放路径由/proc/sys/kernel/core_pattern控制常见情况可能是core当前工作目录或者是core.%p这类带 pid 的命名。Ubuntu 24.04 上可能已经改成了apport接管core 会被/usr/share/apport/apport处理并放到系统别处。实在找不到就用下面的命令监控sysctl kernel.core_pattern journalctl -u systemd-coredump --since today如果系统配置了systemd-coredumpcore 文件还可以直接通过coredumpctl调出来coredumpctl -1 info-1表示最近的崩溃记录他会自动用 GDB 把对应的符号配上。这个命令在利用 systemd 的服务环境里非常好用省去了手动找 core 的麻烦。5.2 制造崩溃并拿到 core运行崩溃程序./crash_demo_full预期输出start crash demo后直接Segmentation fault (core dumped)。如果 terminal 显示没有 core dumped检查ulimit -c和 core_pattern 的路径。我把 core_pattern 设成了/tmp/cores/core.%p这样方便验证。拿到的 core 文件大小一般只有几 MB但里面记录了崩溃时进程的内存镜像和寄存器现场。别把 core 文件到处乱拷它包含当时内存中的所有敏感数据属于高敏感文件生产环境要控制访问权限。5.3 GDB 加载 core 并定位崩溃行接下来是重头戏在 GDB 里把发布版程序、core 文件、归档的 debug 文件三者组合起来gdb ./crash_demo_full /tmp/cores/core.12345如果 4.2 节里的debug-file-directory已经设好GDB 会自动加载crash_demo.debug。此时输入bt应该能看到类似这样的输出#0 0x0000555555555169 in inner_func (p0x0) at crash_demo.c:5 #1 0x00005555555551b2 in outer_func (flag1) at crash_demo.c:10 #2 0x00005555555551ea in main () at crash_demo.c:15看到这个崩溃行就定位到了crash_demo.c:5说明在*p 42这个空指针赋值处崩了。顺着堆栈往下一层一层看就能分析出触发路径是main - outer_func - inner_func而内层函数传入了一个p0x0的空指针。如果bt显示只有地址没有符号和行号多半是调试文件没加载退回去排查路径配置。拿到崩溃行的下一步是看现场变量。输入frame 0切换到最内层栈帧然后(gdb) info locals (gdb) p p (gdb) info args输出可以确认p是0x0。再切到外层(gdb) frame 1 (gdb) info args (gdb) p p这样整个崩溃的数据流就清楚了。如果觉得还不够用disassemble /m可以同时看到源码、汇编、行号三者的对应关系理解优化后的指令流水很有用。5.4 core 分析时的几个实用指令平时我用得最频繁的 GDB 命令就这么几个整理成速查表。命令作用适用场景bt/backtrace打印调用栈崩溃定位的第一步bt full打印调用栈加每个栈帧的局部变量变量值丢失时的定位关键frame N切换到第 N 层栈帧向上层函数追根溯源info locals/info args查看局部变量和参数值确认崩溃时数据状态p 变量名打印变量值直接查看具体内容可加/x看十六进制list显示当前行附近的源码需要源码文件可用时disassemble /m源码和汇编混合展示排查优化级别下的行号错位info registers查看寄存器现场分析非法地址的根源出处GDB 12 之后的bt输出会自动带上线程号多线程崩溃时注意看崩溃线程别在错误线程里白忙活。5.5 没有 core 文件时的备选方案一个现实问题是有些生产环境根本不允许生成 core或者 core 文件被各种安全策略处置掉了。这时候还有一个利器addr2line。如果在日志框架里记录了崩溃时的程序计数器地址比如通过backtrace()backtrace_symbols_fd或者从SEGV信号的浏览器堆栈里提取就可以用归档的.debug文件直接解析addr2line -e crash_demo.debug -f -C 0x555555555169-e指定带调试信息的文件-f显示函数名-C做 C 名字还原。输出直接给出行号。不过这个方法要求崩溃地址必须和存档的 debug 文件对应的二进制版本完全一致任何一次重新编译都会导致地址错位。所以版本线的维护特别重要这回到了 2.3 节说的归档策略。6. 常见问题与排查技巧实录6.1 高频问题速查表这套流程跑多了之后我把线上现场遇到过的典型问题整理成了一个速查表直接照着排。现象可能原因处理方式gdb 提示no debugging symbols founddebug 文件路径不对或.gnu_debuglink没写入用readelf -x .gnu_debuglink检查发布版用set debug-file-directory指向实际目录bt能显示函数名但看不出行号只保留了.symtabDWARF 行号信息缺失确认调试文件是--only-keep-debug产出的crash_demo.debug而不是二次 strip 过的版本bt行号整体偏移了几行编译优化开启后指令重排换-O1/-O2调试或结合disassemble /m人工核对生产环境尽量保留-g且不做二次处理加了-g后文件体积没变大多少编译时-g被后续 strip 或链接脚本清掉了检查编译、链接、二次处理各环节的命令日志core 文件根本没生成ulimit -c 0或 systemd 服务没配LimitCORE检查 ulimit 和服务配置用coredumpctl查看gdb 启动就卡在网络等待上debuginfod 在尝试外网下载符号启动加set debuginfod enabled offGDB 加载了错误的 debug 文件符号对不上归档目录里有同名不同版本的.debug换 build-id 路径管理调试文件确保版本匹配用addr2line解析线上地址失败地址来自不同编译版本的二进制核对二进制构建时间和日志时间换正确 debug 文件6.2 Dev-C 或集成环境场景下的临时补救用 Dev-C 这一类 IDE 的同学可能会遇到教科书里完全没写的情况项目编译时根本没开-g拿到手里的就是一个干净二进制什么调试信息都没有。这种情况下从objcopy分离调试信息这条路是走不通的因为你压根没有可以分离的素材。唯一的补救办法是重新用相同的编译器、相同的编译选项生成带-g的版本尽量让编译环境一致然后通过首条崩溃地址往.debug里映射。但说实话版本一不一致很难严格保证成功率有限。所以真正做事前留后手比事后补救重要得多。我个人的建议是如果是个人小项目从第一天开始就在 Makefile 或 CMake 里固定开-g发布时做 strip/分离不要裸编译。这个习惯一旦养成后面省的是大把排障时间。6.3 我在实际项目中总结的几个独家技巧第一发布版建议把-g信息和-O2分开决策。-g是调试信息-O2是优化级别两者完全可以同时开。优化开得太高会让丢失性栈追踪和行号映射失真但完全没有优化会导致性能下降线上服务不可接受。建议普通服务用-O2 -g容易出现诡异内存问题的模块单独用-O0 -g重编一版做对照两套.debug都归档。第二把.debug文件和二进制放进同一个.tar.gz之前先在包里执行一遍objcopy --add-gnu-debuglink确保包里的文件路径规则是一致的。很多团队喜欢搞个“一键打包脚本”结果不同环境下脚本里写死了不同的 debuglink 路径到了现场 GDB 找不到非常尴尬。统一在一个构建脚本里完成only-keep-debug、strip-debug、add-gnu-debuglink、归档四个动作就不会出这种问题。第三线上服务如果用了 systemd 管理尽量保留 systemd-coredump。coredumpctl查崩溃记录真的极其顺手哪儿都不用翻直接就是 GDB 接口。不要为了图省事把 core_pattern 指到 /var/crash 然后又不清理万一故障当天磁盘满了连带服务都受影响。第四调试文件也是敏感文件。debug 文件里有源码路径和完整符号从信息安全角度讲不该随便外发。我在公司内部对 debug 归档目录做了权限管控只有排障人员能读。外发时通常只发 addr2line 跑出来的结果不发.debug本体。6.4 与 VS Code、日志输出配合的提效方案在桌面开发或者远程开发场景下我习惯用 VS Code 的 GDB 调试扩展来加载 core。只要在launch.json里配置了program指向发布版、coreDump指向 core 文件并在miDebuggerPath指定 gdb 路径中间调试文件的查找依然是 GDB 的规则来定。VS Code 会自动把 GDB 的输出显示成变量面板、调用栈非常直观。缺点就是如果调试文件路径配错视觉效果没有命令行那么明显一堆红色报错反而容易误导。另外一个提效思路是把 GDB 输出直接同步到日志文档里GDB 本身支持-batch -ex bt -ex info locals errlog.txt 21把输出同时落盘和显示。我在重大故障后会把这条命令的产物直接粘进在线文档的故障记录里方便团队复盘也避免现场日志被刷新覆盖后找不到原始信息。6.5 再补充一个调试信息被剥离后的检查方法万一你接手一个已经发布的二进制不确定它有没有带调试信息先跑readelf -S binary | grep debug有.debug_info、.debug_line那就是完整的只有.gnu_debuglink说明外部有配套 debug 文件什么都没有那就是被 strip 干净了。这个探测手段能帮你快速判断还有没有必要尝试 GDB 定位崩溃行。如果只剩.gnu_debuglink按前面说的方法找配套 debug 文件如果连这个都没有那就只能祈祷程序里打了足够多的日志或者用反汇编硬啃了。最后再分享一个心得。这套 objcopy 分离调试信息的方案真正价值并不是省下那点体积而是让线上系统的“可诊断性”和“可控性”同时在线。发布物看上去干净、轻量但一旦出事你有后备钥匙能把现场完整还原出来。排障速度很多时候决定了一个严重 P0 故障的恢复时效而这个速度建立在平时的工程细节上。建议你把第一步构建命令里的-g、剥离命令、归档路径都写进团队的构建脚本和上线 checklist别只在个人电脑上玩通。一旦出了事你会发现这套组合拳比临时装调试包、求人拷符号快得多。