两个文件都以.o结尾,一个能反汇编出机器指令,另一个却能还原成LLVM IR。它们为什么都可能是合法产物?在上一篇观察构建阶段以后,这次我们用同一份源码生成几种文件,逐个核对它们保存了什么、下一步交给谁。
先分清两个维度:文件在构建中承担什么角色,以及它用什么格式保存内容。.o通常表示目标文件的用途;ELF是一种文件格式。可重定位目标文件可以是ELF,最终可执行程序也可以是ELF。因此,不能把“.ll → .bc → .s → .o → ELF”画成五个必经步骤。
先把几种产物放到一张图上
| 名称 | 本文中的内容 | 先用什么检查 |
|---|---|---|
| .ll | LLVM IR的文本表示 | 文本编辑器;llvm-as检查并转为bitcode |
| .bc | LLVM IR的bitcode表示 | llvm-dis;不能按普通文本阅读 |
| .s | 目标相关的汇编文本 | 文本编辑器;交给相应目标的汇编器 |
| .o | 通常是待链接的目标文件;本例普通编译产物为ELF可重定位文件 | 先用file识别;确认为ELF后用readelf、objdump |
| ELF | 组织头、节、符号、程序段等信息的一种格式 | readelf;结合文件类型与具体内容判断用途 |
twice.cpp
├─ 输出 LLVM IR:twice.ll 或 twice.bc
│ .ll ⇄ .bc
└─ 生成目标代码:twice.s → twice.o
+ main.o → demo
本例中的 twice.o、main.o、demo 都使用 ELF 格式。这张图展示观察点,不要求每个文件都在一次普通构建中落盘。集成汇编器可以直接生成目标文件;IR也常留在编译器内存里。只有要求输出或保存中间结果,才会得到相应文件。
生成一套可以检查的文件
实验环境:Ubuntu 24.04.4(WSL2,x86-64),Clang/LLVM 18.1.3,GNU binutils 2.42。
这里把计算和入口拆成两个文件,目的是在后面看到跨文件符号,而不是引入复杂程序。保存为twice.cpp:
extern "C" unsigned twice(unsigned x) {
return x * 2u + 0u;
}extern "C"给这个函数指定C语言链接,便于在符号表中找到名字twice;函数本身仍由C++编译器处理。再保存main.cpp:
extern "C" unsigned twice(unsigned);
int main() {
return twice(21u) == 42u ? 0 : 1;
}先记录工具版本,再生成各类产物:
clang++ --version
clang++ -dumpmachine
readelf --version
clang++ -std=c++17 -O0 -S -emit-llvm twice.cpp -o twice.ll
clang++ -std=c++17 -O0 -c -emit-llvm twice.cpp -o twice.bc
clang++ -std=c++17 -O0 -S twice.cpp -o twice.s
clang++ -std=c++17 -O0 -c twice.cpp -o twice.o
clang++ -std=c++17 -O0 -c main.cpp -o main.o
clang++ twice.o main.o -o demo
./demo
printf 'exit=%s\n' "$?"本次demo没有终端输出,退出码为0,表示twice(21u)确实得到42。它不是性能测试。前四条编译命令分别从源码生成观察对象,不是让文件依次穿过四次编译。
file twice.ll twice.bc twice.s twice.o demo本次识别结果的关键字段如下;路径、Build ID和其他附加字段未列出:
| 文件 | file识别的关键结果 |
|---|---|
| twice.ll | ASCII text |
| twice.bc | LLVM IR bitcode |
| twice.s | assembler source, ASCII text |
| twice.o | ELF 64-bit LSB relocatable, x86-64 |
| demo | ELF 64-bit LSB pie executable, x86-64 |
怎样查看.ll和.bc
.ll适合读和比较。你可以看到函数定义、指令、类型,以及target triple、target datalayout等目标信息。.bc则把IR编码成bitcode,适合LLVM工具传递和保存。它不是“已经编译成CPU机器码的.ll”。
用与生成文件相匹配的LLVM工具转换。本文安装路径为/usr/lib/llvm-18/bin;其他安装方式请换成实际路径:
/usr/lib/llvm-18/bin/llvm-dis twice.bc -o twice.from-bc.ll
/usr/lib/llvm-18/bin/llvm-as twice.ll -o twice.from-ll.bc这两条命令本次均成功。它们转换表示形式,不执行程序,也不是把IR转换成汇编。文本往返后注释、编号或打印形式可能变化,不能把文本逐字相同当成唯一正确性标准。
LLVM IR保留的主要是编译器所需的语义,不是原C++文件的备份。注释通常不会保留,源级结构也可能被降低或改写。版本间读取能力还受bitcode兼容规则限制;分析陌生文件前,先查生成它的LLVM版本。
.o里除了指令还有什么
本例普通编译生成的twice.o包含机器指令,但整个文件并不是一串可以从头执行到尾的指令。ELF头和节表告诉工具怎样找到代码、符号以及其他信息。
readelf -h twice.o
readelf -S twice.o
objdump -d twice.o本次readelf -h显示类型REL (Relocatable file);节表中能看到.text和.symtab;objdump -d则把代码节中的指令解码成汇编。.text是节名,.s是汇编文本文件的常用扩展名,两者不要混用。
再看调用方main.o:
readelf -s main.o
readelf -r main.o符号表里,main有定义,twice的节索引是UND,表示这个目标文件还没有给出它的定义。本次还看到一条指向twice的R_X86_64_PLT32重定位记录。它告诉后续链接过程需要怎样修补相应位置;记录的具体类型与目标架构、调用方式和选项有关。
因此,main.o编译成功完全可以与twice尚未解析同时成立。等两个目标文件交给链接器,跨文件的定义和引用才能结合起来处理。这也是目标文件通常不能直接当作主程序运行的原因之一。
ELF不等于可执行程序
ELF是格式,文件头里的类型才进一步说明角色。常见的ET_REL用于可重定位文件,ET_EXEC用于可执行文件,ET_DYN用于共享对象;现代Linux的位置无关可执行程序(PIE)也采用ET_DYN。
readelf -h twice.o
readelf -h demo这次twice.o显示REL,而已经成功运行的demo显示DYN (Position-Independent Executable file)。所以,看到DYN就断言“这只能是普通共享库”也不成立。需要结合程序头、入口、解释器及装载方式判断。
ELF也不是所有平台的统一目标文件格式。Windows原生工具链常用COFF/PE,macOS常用Mach-O。本机虽运行Windows,但上述命令在WSL2的Linux环境执行,所选目标是x86_64-pc-linux-gnu。决定输出不能只看宿主系统的名字,还要看工具链的目标配置。
同样是.o,LTO会改变什么
启用Clang的链接时优化后,再生成一个文件:
clang++ -std=c++17 -O0 -flto -c twice.cpp -o twice.lto.o
file twice.lto.o
/usr/lib/llvm-18/bin/llvm-dis twice.lto.o -o twice.lto.ll本次file识别为LLVM IR bitcode,llvm-dis也成功读出IR。它名为.o,却不是前面的原生ELF可重定位文件。对它运行readelf -h得到:
readelf: Error: This is a LLVM bitcode file - try using llvm-bcanalyzer这是工具与格式不匹配的预期结果,不足以说明文件损坏。链接时还需要能处理LLVM bitcode的工具链。本次使用现有GNU链接器及可用的LLVM LTO插件完成了以下链接:
clang++ -flto twice.lto.o main.o -o demo-lto
./demo-lto
printf 'exit=%s\n' "$?"退出码同样为0。别的环境若缺少相应插件或使用不匹配的链接器,可能在这里失败。本文不把这一条Clang路径外推为所有编译器、所有LTO模式的统一文件格式。
实际排查时,可以按这个顺序做:找生成命令和版本 → 用file识别 → 选择对应格式的工具 → 检查符号与链接要求。重命名文件只改名字,不会把bitcode变成ELF,也不会把可重定位文件变成可执行程序。
符号与重定位:为什么“已经有指令”仍不够
构造一个PC相对引用的概念例子:目标符号最终地址为S,重定位位置为P,记录给定的加数为A。某类重定位要求写入与S+A−P相关的值。对象文件生成时,S或最终布局尚未确定,因此保留一项待后续处理的请求。
这个式子帮助区分三件事:符号表描述名字与定义,重定位记录描述哪个位置按何种规则处理,指令字节是被编码或待修补的对象。它们协作,却不是同一个表。具体位宽、溢出检查、PLT/GOT规则由架构ABI确定,不能把概念式视为所有重定位的统一公式。
ELF通用ABI在Relocation章节给出记录的通用结构。本文x86-64的R_X86_64_PLT32观察应按对应架构语义理解,不外推为其他平台的调用编码。
这也解释了为何反汇编器显示的调用目标可能只是暂定形式,真正含义需要与重定位一起读。只截取objdump中的一行,忽略对象类型和重定位,就可能把尚未完成的地址解释成错误机器码。
格式能读取,与产物能互用,中间还有哪些条件
llvm-dis能够读取bitcode,只证明该读取器理解其编码与对应IR;它不保证目标后端支持模块里所有操作。readelf能够列出ELF节,也不保证当前机器具备目标ISA或运行库。每一种工具都只检查它负责的边界。
位码还可能带目标数据布局、地址空间与intrinsic依赖。把扩展名从.o改成.bc,最多帮助某些工具选择入口,不会清除这些约束;把bitcode包进原生对象容器,也不会自动让普通链接器完成其中的代码生成。
实际交付可附一个最小产物清单:生成工具与参数、识别格式、目标、预期消费者、必需插件或运行时、可复现检查。它比只有文件名更能帮助另一台机器恢复正确路径。本文普通编译与LTO的既有运行结果保持;本轮只补充对产物契约的解释。
2026-10-18内容修订:深化重定位及消费者契约,避免后缀/可读/可运行之间跳跃。本次为文献与语义审校,未新增实验;首次发布日期、原网址及历史运行版本保持。
来源、验证范围与阅读路线
本文实测包括普通编译产物、IR/bitcode转换、ELF头/节/符号/重定位观察、LTO文件识别,以及普通和LTO两种链接结果运行。保存了源码、命令、完整日志与产物哈希。没有测量性能,也没有在Windows原生目标或其他架构上运行本例。
版本:1.1 · 2026-09-29首次发布,2026-10-07以两种.o产物补清开头问题;实验命令和结论不变。
独立问题:为什么 .o 文件不一定是机器码?(知乎,围绕普通编译与LTO展开)。
写下你的评论
要发表评论,您必须先登录。