ARTICLE DETAIL

资讯详情

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

C语言重新开始:编译通过了,为什么还会链接失败?

C语言重新开始:编译通过了,为什么还会链接失败? gcc -c main.c没有报错不代表已经能得到一个可执行程序。如果main.c调用了一个只有声明、没有提供定义的函数问题可能直到链接时才出现。这篇从一个小程序开始亲手把每个中间文件生成出来。原文曾以“C语言重拾”为开头却残留了不相关的默认模板这里重新整理为独立的 C 入门实验不把模板当作学习经历。实验使用 Linux 下的 GCC已在 WSL 的 Arch Linux、GCC 16.1.1 上验证。Windows 可以在已安装 GCC 的 WSL 中运行下面的文件都放在一个单独的实验目录不需要管理员权限也不改系统配置。1. 先准备三个文件建立并进入一个空目录例如mkdir c-build-demo后执行cd c-build-demo。将以下代码分别保存为对应文件。greet.h告诉调用者函数怎么调用。#ifndefGREET_H#defineGREET_H#defineAPP_NAMEC build demovoidgreet(constchar*name);#endifmain.c使用头文件中的宏和函数声明。#includegreet.hintmain(void){greet(APP_NAME);return7;}greet.c提供函数真正执行的代码也就是定义。#includestdio.h#includegreet.hvoidgreet(constchar*name){printf(Hello, %s\n,name);}先把完整程序跑通gcc-stdc11-Wall-Wextra-Werrormain.c greet.c-odemo ./demostatus$?printfexit status: %s\n$status屏幕会出现Hello, C build demo exit status: 7Hello是程序打印的文本7是main返回给运行环境的退出状态。它们不是一回事。本例故意返回7用于观察程序执行到了最后但在 shell 的约定里非零退出状态仍表示非成功状态不应把它用于“成功”的自动化判定。2. 一条 gcc 命令背后有四个阶段可以把过程画成这条链main.c greet.h | | 预处理处理 #include、宏和条件编译 v main.i | | 编译产生汇编代码 v main.s | | 汇编产生目标文件 v main.o ----- greet.o \ / \ / 链接结合目标文件与所需库 demo这里“编译”有两种常见说法日常说“编译程序”往往泛指整条构建链讨论具体阶段时则指从预处理后的 C 代码到汇编代码的转换。先分清语境才不会把“编译没错”和“整个构建成功”混在一起。GCC 的-E、-S、-c分别控制停在哪个阶段。注意-S main.c仍会先预处理-c main.c仍会先预处理、编译、汇编不是跳过前面的工作。GCC 阶段控制选项命令停止位置产物还不能说明什么gcc -E main.c -o main.i预处理后展开后的文本C 语义正确、能链接gcc -S main.c -o main.s编译后汇编文本所需函数都能找到定义gcc -c main.c -o main.o汇编后目标文件已经是可直接运行的程序gcc main.o greet.o -o demo链接后可执行文件运行结果符合业务预期目标文件不是“装着 C 源码的文本”。它包含机器代码以及链接所需的符号、重定位等信息通常不能像./demo那样直接运行。3. 分阶段生成亲眼看中间结果gcc-stdc11-Emain.c-omain.i gcc-stdc11-Smain.c-omain.s gcc-stdc11-Wall-Wextra-Werror-cmain.c-omain.o gcc-stdc11-Wall-Wextra-Werror-cgreet.c-ogreet.o打开main.i能看到函数声明和展开后的调用greet(C build demo);APP_NAME已替换成字符串但greet的函数体并不会因为包含greet.h自动出现头文件里只有声明。main.s的具体指令会因 CPU 架构、编译器版本及优化选项不同而变化。不需要背这段汇编先确认它是编译器生成的汇编文本而不是最终可执行文件即可。还可以用nm看目标文件里的符号nm main.o nm greet.o在本例的普通目标文件中main.o会把greet标为U表示它在这个文件中尚未定义greet.o则提供该函数。具体地址不必相同。U不等于程序一定有错它可能由其他目标文件或库在后续步骤中提供。GNU nm 符号说明4. 故意漏掉一个目标文件复现链接失败仅链接main.ogcc main.o-omissing-demostatus$?printflink status: %s\n$status这一步会失败错误信息中包含对greet的未定义引用常见措辞是undefined reference to greet。不同工具链的标点、路径、诊断格式可能不同因此自动验证不能要求整段错误文本逐字相等。为什么之前gcc -c main.c能通过编译器看到了void greet(const char *name);知道函数接收什么参数、返回什么类型可以生成调用代码。链接时才需要把调用和实际实现接起来而当前命令没有提供greet.o。修复不是删除函数声明也不是关闭警告而是把定义所在的目标文件交给链接器gcc main.o greet.o-odemo ./demostatus$?printfexit status: %s\n$status如果出现类似问题可以先问三件事函数有没有定义定义所在的文件有没有参与构建声明与定义的名称、签名是否一致不要一看到错误就去改#include“找不到头文件”和“链接找不到函数”不是同一个阶段。5. 验证时不能只看有没有生成文件这个实验应检查的是一组事实而不是“命令似乎运行过”检查点期望分阶段命令全部返回0生成非空.i、.s、.o预处理结果宏展开为字符串仅链接main.o非零退出诊断提及greet链接两个目标文件返回0运行demo输出准确同时退出状态为7其中“故意失败”的链接命令返回非零才算这个测试点成立不能给整段脚本简单加上set -e后就不管它否则会在期望的失败处提前结束。运行返回7的程序也一样需要单独捕获状态。再做一步变化把APP_NAME换成另一段文本重新构建后观察输出。头文件没有独立变成可执行文件却通过预处理影响了使用它的源码。这也说明真实项目里改了头文件为什么相关源文件常常需要重新编译。这次真正要记住的不是三个扩展名而是声明让调用可以被编译定义让调用在链接时有着落链接成功之后还要验证运行结果。排查错误先定位阶段会比盲目修改源码更有方向。
返回列表