GCC 编译链接产物详解:.out、.o、.a、.so 与静态动态库选型
2026/9/17 19:22:58 网站建设 项目流程

在Linux上写C/C++,没人能绕开gcc。就算你平时用的是CMake、Meson、Bazel这类构建系统,剥掉那层封装,底下跑的仍然是gcc或者它的近亲。很多人第一次敲下gcc hello.c的时候,终端里安安静静,当前目录却多出一个a.out,直接./a.out就能跑,于是一堆疑问跟着来了:为什么不叫hello.o是什么?.a.so又差在哪?这些问题在面试里被反复问,在实际项目里也反复踩坑。我把gcc这套产出物的逻辑从头到尾梳理一遍——.out.o.a.so这四种文件各自的角色、生成方式、链接行为、排查技巧,全部说清楚。看完你应该能独立写出一个库,判断什么时候该用静态、什么时候该用动态,也能在看到undefined reference to这类报错时心里有底,而不是到处复制粘贴搜答案。无论你是刚装完Ubuntu、敲着apt install gcc -y的新手,还是接手了别人一堆遗留Makefile的老手,这篇都能对得上号。

1. 四种文件到底在编译链的哪一环

1.1 从源码到可执行文件,gcc内部走了四步

很多人把"编译"当成一个动作,其实gcc一次调用背后藏着四个阶段,而我们要讨论的四种文件,正是这些阶段的中间产物和最终产物。理解这条流水线,后面所有疑惑都会自动消解。

第一步是预处理(preprocessing)。gcc会把#include的头文件原地展开,把#define的宏替换掉,处理#ifdef这类条件编译,产出.i文件(C++是.ii)。这一步不涉及任何语法分析,纯粹是文本层面的替换。你可以用gcc -E hello.c -o hello.i单独停下看结果,会发现文件膨胀得厉害,一个几十行的源文件展开后常常上千行。

第二步是编译(compilation),这里的"编译"是狭义上的,指把预处理后的代码翻译成汇编。gcc -S hello.i -o hello.s能拿到汇编文件,看懂汇编不是必须,但偶尔排查性能问题或者内联失败时会用到。

第三步是汇编(assembly),把汇编翻译成机器码,产出目标文件,也就是.ogcc -c hello.s -o hello.o就是这个动作。注意.o里面是真正的机器指令,但它还不能直接运行。

第四步是链接(linking),把所有.o.a.so拼装起来,解析符号引用,最终生成可执行文件。gcc hello.o -o hello完成这一步。默认不指定-o时,输出文件叫a.out

这四步里,前三步都是"每个源文件各自为战",只有链接是"全局统筹"。这个分工是理解.o.a.so的关键——编译阶段不需要知道其他文件的存在,链接阶段才需要

1.2 用一张对照表看清四类文件的身份

文件类型生成阶段典型命令能否直接执行主要用途
.out链接后gcc main.o -o app可以最终交付的可执行程序
.o汇编后gcc -c main.c -o main.o不可以编译单元,供后续链接
.a打包.oar rcs libx.a a.o b.o不可以静态库,链接期整体嵌入
.so链接共享gcc -shared -fPIC -o libx.so a.c不可以动态库,运行时按需加载

这张表里有个容易误解的点:.out并不是一种格式,它只是个文件名的历史遗留。真正的格式是ELF(在Linux上),你把它改名成myappservertest都能正常跑。同理,.so也不是靠后缀被识别的,系统靠的是文件头的ELF标识和PT_DYNAMIC段来判断它是不是共享对象。

.o.a都是可重定位文件家族的,区别在于.o是单个编译单元,.a是多个.o的归档包,本质上就是个"没有压缩的tar"。链接器从.a里按需抽取.o,没被用到的成员根本不会进入最终产物——这是静态库能减小体积的原理。

.so则完全不同,它是共享对象,链接时只在可执行文件里记一笔"我需要 libx.so 里的某个符号",真正的内容在运行时才加载进内存。

2..o目标文件:编译的最小交付单元

2.1-c参数究竟做了什么

gcc -c main.c -o main.o这条命令里,-c的含义是 "compile and assemble, but do not link"。它把前面说的前三步全跑了,唯独跳过链接。产出物就是.o

为什么要有这一步?因为分离编译是C/C++工程化的基石。假设你有100个源文件,改动了其中一个,如果没有分离编译,你得把100个文件全部重新编译一遍;有了.o,你只需要重新编译改动的那一个,然后重新链接所有.o。链接虽然也要遍历全部目标文件,但速度比编译快一到两个数量级,这就是增量构建的威力。

拿一个最小的例子验证:

# add.c int add(int a, int b) { return a + b; } # main.c #include <stdio.h> int add(int a, int b); int main(void) { printf("%d\n", add(3, 4)); return 0; }
gcc -c add.c -o add.o gcc -c main.c -o main.o gcc main.o add.o -o app ./app # 输出 7

这里main.o在编译时根本不知道add函数在哪,它只在符号表里留了一条"我要用add,地址待定"的记录。这个"待定"就是未解析符号,由链接器在最后一步填上。

2.2 符号表:.o里最值得看的东西

.o文件里除了机器码,还有一个至关重要的部分叫符号表(symbol table)。它记录了三种信息:

  • 全局符号(global):本文件定义、可以被外部引用的函数和变量,比如add
  • 外部符号(external / undefined):本文件引用、但定义在别处的符号,比如main.o里的addprintf
  • 局部符号(local):static修饰的函数和变量,只在本编译单元可见

nm命令可以直观看到:

nm main.o # 输出类似: # U add # U printf # 0000000000000000 T main

U表示 undefined,T表示定义在代码段(text)。add.onm输出则是T add。链接器的工作就是拿所有U去找对应的T,找到了就填地址,找不到就报那个让所有人头疼的undefined reference to 'xxx'

提示:nm看到的TtDdBb是有差别的。大写表示全局可见,小写表示局部(static)。这个区别在做符号冲突排查时特别有用——两个库都定义了同名函数,如果两者都是T,链接时就会报 multiple definition。

2.3 实操中容易翻车的几点

新手在这一步最常见的三个坑,我给排一下。

第一个坑:.o不能直接运行。经常有人./main.o然后报Permission denied或者cannot execute binary file,就开始怀疑编译出错。其实不是,.o里没有入口点的完整信息,也没有链接C运行时启动代码(crt1.o那套),它就是块"毛坯",必须经过链接才能住人。

第二个坑:忘了加-cgcc main.c -o main.o不会报错,但生成的main.o实际上是一个完整的可执行文件,只是名字骗了你。等你后面想链接它,会发现符号重定义或者行为诡异。养成习惯:产出.o一定带-c

第三个坑:头文件和源文件路径不匹配导致的隐式声明。如果main.c里忘了#include对应头文件,gcc 在老标准下会默认函数返回int,链接时可能因为参数类型不一致导致运行时崩溃。现代gcc(C99之后)会警告,但不会默认报错。建议编译时加-Wall -Wextra,把警告都逼出来。

gcc -c -Wall -Wextra -O2 -g main.c -o main.o

-g带上调试信息,-O2开优化,这两个一起用不冲突,调试器仍然能对应到源码行。生产环境如果不需要调试信息,去掉-g能显著减小.o体积。

3..a静态库:一堆.o的归档包

3.1ar是怎么把.o装进去的

静态库的生成工具不是gcc,而是ar(archiver)。这是Unix时代留下来的老工具,语法和tar有点像但更简陋:

gcc -c math_util.c -o math_util.o gcc -c string_util.c -o string_util.o ar rcs libutil.a math_util.o string_util.o

三个参数的含义:r是 replace(插入或替换成员),c是 create(不存在就创建),s是写索引(相当于给归档包建一个符号目录,加快链接时的查找)。现代实践里s千万别省,省了之后链接器要找符号得顺序扫描整个.a,大库会明显变慢,有些链接器甚至会直接报错。

生成之后可以用ar t libutil.a列出成员:

ar t libutil.a # math_util.o # string_util.o

注意,.a里装的是.o,不是源码。所以如果.o是带调试信息-g编的,.a也会带着;如果.o用了-fPIC.a也能被塞进.so。这一点后面讲动态库时还会提到。

3.2 链接静态库的顺序是有讲究的

这是静态库最经典的坑,几乎每个人都踩过。看这条命令:

gcc main.o -lutil -lm -o app

链接器处理库的顺序是从左到右,遇到-lutil时会去libutil.a里找当前所有未解析的符号,把能解决的.o拉进来。问题是,如果libutil.a里的某个.o引用了libm.a里的符号,而-lm排在它后面,那没问题;反过来,如果-lm在前、-lutil在后,libm.a在扫描时还不知道后面会需要它,符号就被漏掉了,最终报undefined reference

注意:记忆口诀是"被依赖的库放在右边"。基础库(libc、libm)永远放最后,上层库放前面。

如果依赖关系复杂到无法用线性顺序表达,可以用--start-group--end-group把一组库包起来,让链接器反复扫描:

gcc main.o -Wl,--start-group -lA -lB -lC -Wl,--end-group -o app

代价是链接变慢,所以只在循环依赖时用。

3.3 静态库的取舍:什么时候值得用

静态库的好处很直接:部署简单,不依赖运行环境。可执行文件里已经把用到的代码全嵌进去了,拷到任何同架构的机器上都能跑,不会出现"目标机器上没装这个.so"的尴尬。

代价是体积和更新。假如同一个库有10个程序在用,静态链接意味着每个程序里都有一份库代码的副本,磁盘和内存都被重复占用。更麻烦的是,库出了安全漏洞,你得把所有链接过它的程序全部重新编译发布,一个都不能漏。

我的经验是:底层基础库、算法库、工具函数库,用静态(比如自己写的一堆字符串处理、日志、配置解析),因为它们变更少、复用广,静态链接省心。业务模块、插件、需要热更新的部分,用动态

4..so动态库:运行时才露面的角色

4.1-fPIC不是可选项,是必选项

编译动态库时,-fPIC这个参数几乎是必须的:

gcc -fPIC -c util.c -o util.o gcc -shared -o libutil.so util.o

-fPIC全称 Position Independent Code,位置无关代码。为什么动态库非要它不可?

动态库被加载到进程地址空间的哪个位置,编译时是不知道的,每次运行都可能不同(尤其是开了ASLR之后)。如果代码里用了绝对地址跳转,加载位置一变,所有地址全错。PIC的做法是通过全局偏移表(GOT)和过程链接表(PLT)做一层间接寻址:代码里跳转的是GOT表项,GOT表项在加载时才被动态链接器填上真实地址。

代价是每次跨模块调用多一次内存访问,性能有轻微损失。现代CPU的分支预测和缓存让这个损失通常小于1%,基本可以忽略。但反过来,如果把动态库用非PIC方式编译,在x86-64上链接会直接报错:

relocation R_X86_64_32 against `.rodata' can not be used when making a shared object; recompile with -fPIC

这个报错信息其实很友好,直接告诉你解法了。看到就加-fPIC重新编译所有相关的.o,注意是全部,漏一个都不行。

4.2-shared-Wl,-soname的分工

-shared告诉gcc:"这次不生成可执行文件,生成共享对象"。它会跳过链接C运行时启动代码,也不要求有main函数。

但一个成熟的.so还得处理版本问题,这就是soname。真实项目里的.so通常长这样:

libfoo.so -> libfoo.so.1 -> libfoo.so.1.2.3

三个文件:libfoo.so是开发时链接用的符号链接,libfoo.so.1是运行时识别的soname,libfoo.so.1.2.3是真实文件。生成时这样写:

gcc -shared -fPIC -Wl,-soname,libfoo.so.1 -o libfoo.so.1.2.3 foo.o ln -sf libfoo.so.1.2.3 libfoo.so.1 ln -sf libfoo.so.1 libfoo.so

为什么要这么绕?因为二进制兼容性。如果只是修了个bug、没动接口,你更新.2.3.2.4,soname 仍然是.so.1,用它的程序不需要重新编译,加载时自动用新版。如果改了不兼容的接口,就把 soname 升到.so.2,新老版本可以共存,老程序继续用.so.1,互不干扰。

这个机制在大型系统里非常重要,很多发行版的包管理就靠它来判断依赖能不能升级。

4.3 运行时找不到.so怎么办

动态库最经典的报错就是:

error while loading shared libraries: libfoo.so.1: cannot open shared object file

编译时明明过了,一运行就翻车。原因是编译期和运行期的库搜索路径不是同一套

编译时,链接器看的是-L指定的路径和默认系统路径;运行时,动态加载器ld.so看的是另一套规则,顺序大致是:

  1. 可执行文件里DT_RPATH(除非有DT_RUNPATH
  2. 环境变量LD_LIBRARY_PATH
  3. 可执行文件里DT_RUNPATH
  4. /etc/ld.so.cache缓存
  5. 默认路径/lib/usr/lib/lib64

要查清楚自己程序到底在找什么,用ldd

ldd ./app # linux-vdso.so.1 (0x...) # libfoo.so.1 => not found # libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x...)

not found就是问题所在。三种解法,从临时到正规:

临时应急,设置环境变量:

export LD_LIBRARY_PATH=/opt/myapp/lib:$LD_LIBRARY_PATH ./app

只对当前shell有效,适合测试,别写进生产脚本。

规范做法,在/etc/ld.so.conf.d/下加一个配置文件:

echo "/opt/myapp/lib" | sudo tee /etc/ld.so.conf.d/myapp.conf sudo ldconfig

ldconfig会重新生成/etc/ld.so.cache,之后所有程序都能找到这个目录下的库。注意ldconfig之后,原来设的LD_LIBRARY_PATH就不再起作用了(缓存优先级更高),排查时要心里有数。

另一种思路,在链接时就把路径写死进可执行文件,用-Wl,-rpath

gcc main.o -L/opt/myapp/lib -lfoo -Wl,-rpath,/opt/myapp/lib -o app

这样库路径就嵌在ELF里了,部署时不需要额外配置。缺点是路径被固定,搬迁目录后需要重新编译。用在打包发布、定制设备上很合适。

4.4 用readelf看清一个.so的底细

排查动态库问题的核心工具是readelf。几组最常用的命令:

# 看动态段信息,包括 soname 和依赖 readelf -d libfoo.so.1.2.3 | head -20 # 看导出的符号 readelf --dyn-syms libfoo.so.1 | grep FUNC # 看可执行文件的 rpath / runpath readelf -d ./app | grep -i path # 看文件是32位还是64位、什么架构 readelf -h libfoo.so

readelf -d输出里,SONAME那一行就是运行时被识别的名字,NEEDED是依赖的其他库。这两个值是排查"库到底要找谁"的关键证据。

如果你想让自己的动态库只导出必要的符号,减少命名冲突和攻击面,编译时加-fvisibility=hidden,然后对需要导出的函数用__attribute__((visibility("default")))标注。这套做法在大型C++项目里几乎是标配,能显著减少符号表体积,也能让链接更快。

5..out可执行文件与它的命名习惯

5.1a.out这个名字的来历

a.out是 "assembler output" 的缩写,来自上世纪七十年代的Unix。当时编译器的输出直接就是可执行文件,名字固定叫a.out。这个习惯被gcc继承下来,成了今天的默认行为。

很多人以为.out是一种特殊格式,其实不是。Linux下所有可执行文件,不管叫什么名字,格式都是ELF。file命令能证实:

gcc hello.c file a.out # a.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, ...

注意dynamically linked这几个字,说明默认生成的是动态链接的可执行文件,依赖系统的libc.so.6。如果你想要一个完全自包含的,得加-static

gcc -static hello.c -o hello_static

生成的体积会从几KB涨到接近1MB,因为把整个libc的静态版本都塞进去了。除非要在没有标准库的最小系统上跑,否则不建议这么干。

5.2-o参数的正确姿势

实际项目里几乎没人用默认的a.out,都用-o指定名字:

gcc main.o util.o -o myapp

有一点值得说一下,-o的位置不严格,但习惯放在最后,因为gcc按顺序处理参数,放最后能让前面的输入文件先被识别。在Makefile里通常写成:

myapp: main.o util.o $(CC) $^ -o $@

$^是所有依赖(即所有.o),$@是目标名。这样写出来的规则通用,改文件名不用动命令。

另外,链接顺序在.o之间通常不重要(不像静态库),因为链接器会把所有.o都加载进来,然后统一解析符号。但如果两个.o里有同名符号,会报 multiple definition,这时候要考虑把其中一个改成static或者重命名。

6. 静态库和动态库,到底怎么选

6.1 关键维度对照

对比维度.a静态库.so动态库
链接时机编译链接期运行时加载
可执行文件体积大(代码内嵌)小(只存引用)
部署依赖需要目标机器有对应库
内存占用每个进程独立一份多进程共享一份物理内存
更新方式重新编译所有使用者替换.so文件即可
启动速度略快(无加载开销)略慢(需要动态链接)
编译要求普通编译即可需要-fPIC
版本管理不涉及靠 soname 区分
适用场景底层工具库、嵌入式业务模块、插件、系统库

表格里最容易被忽略的是内存占用那一行。动态库的一大优势是,同一个.so被10个进程加载时,物理内存里只有一份代码段(只读部分),10个进程共享。静态链接的话,每份可执行文件各带一份,加起来就是10份。在跑几百个进程的服务器上,这个差别非常可观。

6.2 一个"半静态"的折中方案

有时候你既想要静态库的部署省心,又想要动态库的更新灵活。可以混着来:核心库静态链接,可选功能动态加载。

举个例子,主程序用静态链接把日志、配置这些基础模块打进去,而业务插件通过dlopen在运行时按需加载:

#include <dlfcn.h> #include <stdio.h> int main(void) { void *handle = dlopen("./libplugin.so", RTLD_LAZY); if (!handle) { fprintf(stderr, "load failed: %s\n", dlerror()); return 1; } typedef int (*fn_t)(int, int); fn_t compute = (fn_t)dlsym(handle, "compute"); if (!compute) { fprintf(stderr, "symbol not found: %s\n", dlerror()); dlclose(handle); return 1; } printf("%d\n", compute(5, 3)); dlclose(handle); return 0; }

编译时要注意加-ldl

gcc main.c -ldl -o app

dlopen这套玩法是很多插件化架构的基础。它让主程序不需要在编译期知道插件的存在,插件可以独立编译、独立升级,甚至由第三方提供。代价是符号查找是运行时行为,拼写错了要等到运行才发现,测试覆盖必须跟上。

提示:dlsym返回void*,直接赋给函数指针在严格标准下是未定义行为,某些编译器会警告。规范写法是中间用memcpy或者union转换,或者直接开-Wno-pedantic忽略。实践里大多数人直接强转,跑起来没问题,但心里得知道这里有个标准上的瑕疵。

7. 常见报错与排查实录

7.1 高频报错速查

报错信息根本原因解决方向
undefined reference to 'xxx'符号找不到定义检查是否漏了.o/.a/.so,检查链接顺序
multiple definition of 'xxx'符号被重复定义检查全局变量是否在头文件里定义,改用extern
cannot open shared object file运行时找不到.soLD_LIBRARY_PATH或写rpathldconfig
relocation R_X86_64_32 ... -fPIC.o未用-fPIC编译重新编译所有相关.o
cannot execute binary file试图执行.o或架构不匹配确认是链接后的可执行文件、确认架构
file not recognized: File format not recognized文件损坏或格式不对file命令确认文件类型
undefined reference to 'dlopen'漏了-ldl-ldl
relocation truncated to fit链接地址超出范围检查是否误用-mcmodel或代码过大

这张表覆盖了八成以上的链接类问题。真正难的不是知道这些,而是快速定位是哪一个符号、在哪一层出的问题

7.2 定位undefined reference的一套流程

遇到未定义符号,我的固定流程是这样的:

第一步,用nm或者c++filt看清符号的真实名字。C++ 有名称修饰(name mangling),报错里的_ZSt4cout得用c++filt翻译:

c++filt _ZSt4cout # std::cout

第二步,确认符号确实应该在某个库里:

nm -A *.o *.a 2>/dev/null | grep " T xxx"

-A会把文件名一起打出来,一眼就能看到是哪个文件定义的。

第三步,如果符号在.so里,用readelf --dyn-syms或者nm -D查:

nm -D libfoo.so | grep xxx

注意nm -D只能看动态符号表,.so里未导出的符号它看不到。如果-fvisibility=hidden把符号藏起来了,这里就查不到,需要回到源码层面确认导出声明。

第四步,检查链接顺序。前面讲过静态库的顺序敏感,把被依赖的库往后放试试。

第五步,如果实在找不到,用-Wl,--trace-symbol=xxx让链接器打印它在哪里找过这个符号:

gcc main.o -L. -lfoo -Wl,--trace-symbol=xxx -o app

输出会告诉你链接器在哪些库里翻过、有没有翻到,比盲猜高效得多。

7.3 关于"反编译"和架构迁移的两点经验

nm -Dobjdump -dreadelf这一套工具不仅能排查链接问题,也能用来做逆向分析。objdump -d libfoo.so能反汇编出机器码,配合nm -D的符号表,能把一个.so的导出接口大致还原出来。调试符号没被剥离的话(-g编的),甚至能隐约看到变量名和行号。发布正式版本时记得strip libfoo.so,能大幅减小体积,也能避免泄露内部实现细节。

跨架构迁移是另一个高频需求。x86-64 上编好的.so不能直接拿到 ARM 设备上用,必须用交叉工具链重新编译。流程是:把gcc换成aarch64-linux-gnu-gcc(具体名字看你的工具链),其他参数基本不变:

aarch64-linux-gnu-gcc -fPIC -c util.c -o util.o aarch64-linux-gnu-gcc -shared -o libutil.so util.o

编出来的.sofile一看就是ARM aarch64。如果项目大、依赖多,最好用 CMake 配toolchain file,避免手敲每条命令时漏掉参数。记住:源文件不需要改,但每一个.o都要用目标架构的编译器重新生成,混用不同架构的.o链接会报格式不匹配。

8. 参数速查与实操心得

8.1 常用参数速查表

参数作用常见搭配
-c只编译不链接,产出.ogcc -c a.c -o a.o
-o指定输出文件名所有场景
-S产出汇编调试、教学
-E只做预处理查宏展开
-fPIC位置无关代码.so必备
-shared生成共享对象.so必备
-static静态链接生成自包含可执行文件
-L指定库搜索路径链接自定义库
-l指定库名(去掉 lib 前缀)-lfoo对应libfoo.a/so
-I指定头文件搜索路径编译时找.h
-Wl,-rpath,<path>嵌入运行时库路径部署固定路径
-Wall -Wextra打开大部分警告建议始终开启
-O0~-O3优化等级调试用-O0,发布用-O2
-g带调试信息配合 gdb
-D定义宏-DDEBUG

-lfoo的搜索顺序需要特别说明:链接器优先找libfoo.so,找不到才找libfoo.a。如果两个都存在而你只想用静态的,有两个办法:一是明确写全路径./libfoo.a,二是加-Wl,-Bstatic -lfoo -Wl,-Bdynamic,把这一段切成静态搜索。

8.2 我踩过的几个坑和总结出的习惯

第一个习惯,编译和链接的参数分阶段写。在Makefile里,我会用CFLAGS管编译参数,LDFLAGS管链接参数,LDLIBS管具体库名。这样出问题时能快速判断是编译阶段还是链接阶段的问题,改参数也不会互相干扰。

第二个习惯,永远带-Wall -Wextra。这两条开启的警告,几乎每一次都能帮你省下几个小时的debug时间。特别是隐式函数声明、未使用变量、有符号无符号比较这几类,在跨平台移植时坑特别多。如果接手的老代码警告太多,先用-Wno-xxx压掉一部分,逐步清理,别一次性全开然后被淹没。

第三个习惯,.o文件保留-g。很多团队为了减小体积,发布版本不加-g。我的做法是编译.o时加-g,最后链接完之后再strip或者用objcopy --strip-debug。这样即使线上出问题,也能拿带调试信息的中间产物来分析。毕竟出问题的时候,你手上有一份带符号的版本和没有,排查效率差出好几倍。

第四个习惯,Makefile里显式声明库的顺序和依赖。别依赖"碰巧能过"的顺序。把依赖关系写清楚,改一个库的顺序就重新跑一遍完整构建,别偷懒。静态库的顺序问题一旦上线才暴露,修起来要多花好几倍的时间。

第五个习惯,ldd做发布前检查。程序编完,先ldd ./app看一眼所有依赖是不是都能解析到。有not found就立刻处理,别等部署到目标机器才发现。这一条在两台机器环境不一致的团队里尤其重要——开发机的/usr/lib什么都全,生产机可能是最小化安装,少一个库就起不来。

说到底,gcc这套产物体系本身并不复杂,难的是在具体项目里把链接顺序、运行时路径、架构匹配这些细节都照顾到。-c产出.oar打包成.a-fPIC -shared生成.so,最后链接成可执行的.out(或者叫任何你想要的名字),这条链路摸透了,再看别人写的构建脚本会轻松很多。碰到报错别急着搜,先用nmreadelfldd这三个工具把现场看清楚,答案通常就在输出里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询