文章目录
Clang
Clang(发音为/ˈklæŋ/类似英文单字clang) 是一个C、C++、Objective-C和Objective-C++编程语言的编译器前端。它采用了LLVM作为其后端,由LLVM2.6开始,一起发布新版本。它的目标是提供一个GNU编译器套装(GCC)的替代品,支持了GNU编译器大多数的编译设置以及非官方语言的扩展。作者是克里斯·拉特纳(Chris Lattner),在苹果公司的赞助支持下进行开发,而源代码许可是使用类BSD的伊利诺伊大学厄巴纳-香槟分校开源码许可。
Clang项目包括Clang前端和Clang静态分析器等。
之前我非常好奇,编程中从编写源代码到可执行文件的过程,很多集成开发环 (IDE)(比如Code::Blocks ,Microsoft Virtual Studio,Dev-Cpp等)都是先build一下,再run就完事。详细一点的涉及到编译器(Compiler) 和链接器(Linker),虽然又看了很多更为详细的文章,但始终还是缺乏实践操作,在捣鼓clang这玩意的时候,发现了竟然还有分步编译,这使得我能上手操作,真正直观感受一下这一过程:源代码(source code)→ 预处理器(preprocessor)→ 编译器(compiler)→ 汇编程序(assembler)→ 目标代码(object code)→ 链接器(linker)→ 可执行文件(executables),最后打包好的文件就可以给电脑去判读执行了。
下面看看Clang的分步编译是怎么搞得
# 预处理(Preprocessing)
clang++ -E hello.cpp -o hello.ii
# 编译为汇编代码(Compilation)
clang++ -S hello.ii -o hello.s
# 汇编为目标文件(Assembly)
clang++ -c hello.s -o hello.o
# 链接为可执行文件(Linking)
clang++ hello.o -o hello
先温习一下几个术语:
源代码(英语:source code),是指一系列人类可读的计算机语言指令
目标代码(英语:Object code)指计算机科学中编译器或汇编器处理源代码后所生成的代码,它一般由机器代码或接近于机器语言的代码组成。[1]目标文件(英语:Object file)即存放目标代码的计算机文件,它常被称作二进制文件(Binaries)。
字节码(英语:Bytecode)通常指的是已经经过编译,但与特定机器代码无关,需要解释器转译后才能成为机器代码的中间代码。字节码通常不像源码一样可以让人阅读,而是编码后的数值常量、引用、指令等构成的序列。
机器语言(machine language)是一种指令集的体系。这种指令集称为机器代码(machine code),是计算机的CPU或GPU可直接解读的资料。
预处理器(Preprocessor)
在计算机科学中,预处理器是程序中处理输入数据,产生能用来输入到其他程序的数据的程序。继承于C语言的C++的C预处理器,是将采用以’#'为行首的指示,将相关的代码包含进来。
main.cpp
// 将 #include <iostream> 替换为iostream头文件的全部内容
// 可能从几行代码变成几千行代码
#include <iostream>
void a()
{
std::cout << " a here " << '\n';
}
void b()
{
a();
}
void c()
{
a();
b();
}
void d()
{
a();
b();
c();
}
int main()
{
d();
return 0;
}
使用
clang++ -E main.cpp -o main.ii
调用预处理器处理源文件main.cpp之后,我打开处理之后的main.ii看了看,加上原来的总共得有37040行代码,下面截取了开头和末尾一部分代码,应该是#include 告诉编译器把C++标准库iostream里面的内容包含进来了。

main.ii
# 1 "main.cpp"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 468 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.cpp" 2
# 1 "/usr/lib/gcc/x86_64-linux-gnu/14/../../../../include/c++/14/iostream" 1 3
# 37 "/usr/lib/gcc/x86_64-linux-gnu/14/../../../../include/c++/14/iostream" 3
# 1 "/usr/lib/gcc/x86_64-linux-gnu/14/../../../../include/c++/14/bits/requires_hosted.h" 1 3
# 31 "/usr/lib/gcc/x86_64-linux-gnu/14/../../../../include/c++/14/bits/requires_hosted.h" 3
# 1 "/usr/lib/gcc/x86_64-linux-gnu/14/../../../../include/x86_64-linux-gnu/c++/14/bits/c++config.h" 1 3
# 34 "/usr/lib/gcc/x86_64-linux-gnu/14/../../../../include/x86_64-linux-gnu/c++/14/bits/c++config.h" 3
# 308 "/usr/lib/gcc/x86_64-linux-gnu/14/../../../../include/x86_64-linux-gnu/c++/14/bits/c++config.h" 3
......
void a()
{
std::cout << " a here " << '\n';
}
void b()
{
a();
}
void c()
{
a();
b();
}
void d()
{
a();
b();
c();
}
int main()
{
d();
return 0;
}
我注意到 iostream 头文件的实际路径是 “/usr/lib/gcc/x86_64-linux-gnu/14/…/…/…/…/include/c++/14/iostream”。然后我尝试切换工作目录(cd)到了 “/usr/lib/gcc/x86_64-linux-gnu/14/include/”,但我发现这个路径下并没有我要找的东西,或者这个路径本身不存在。我感到困惑。经过查询原来中间的…也代表当前目录的父目录,于是我算了一下,有4组…,从/14推到了/usr下,然后输入后面的信息,终于找到了iostream

iostream (only read) 共计88行
1 // Standard iostream objects -*- C++ -*-
2
3 // Copyright (C) 1997-2024 Free Software Foundation, Inc.
4 //
5 // This file is part of the GNU ISO C++ Library. This library is free
6 // software; you can redistribute it and/or modify it under the
7 // terms of the GNU General Public License as published by the
8 // Free Software Foundation; either version 3, or (at your option)
......
33 #ifndef _GLIBCXX_IOSTREAM
34 #define _GLIBCXX_IOSTREAM 1
35
36 #pragma GCC system_header
37
38 #include <bits/requires_hosted.h> // iostreams
39
40 #include <bits/c++config.h>
41 #include <ostream>
42 #include <istream>
......
67 #ifdef _GLIBCXX_USE_WCHAR_T
68 extern wistream wcin; ///< Linked to standard input
69 extern wostream wcout; ///< Linked to standard output
70 extern wostream wcerr; ///< Linked to standard error (unbuffered)
71 extern wostream wclog; ///< Linked to standard error (buffered)
72 #endif
73 ///@}
74
75 // For construction of filebuffers for cout, cin, cerr, clog et. al.
76 // When the init_priority attribute is usable, we do this initialization
77 // in the compiled library instead (src/c++98/globals_io.cc).
78 #if !(_GLIBCXX_USE_INIT_PRIORITY_ATTRIBUTE \
79 && __has_attribute(__init_priority__))
80 static ios_base::Init __ioinit;
81 #elif defined(_GLIBCXX_SYMVER_GNU) && defined(__ELF__)
82 __extension__ __asm (".globl _ZSt21ios_base_library_initv");
83 #endif
84
85 _GLIBCXX_END_NAMESPACE_VERSION
86 } // namespace
87
88 #endif /* _GLIBCXX_IOSTREAM */
....
从38-42,iostream文件还有其他的依赖库,或许闲了可以探索一下这些库之间的依赖关系。接下来看看编译器又将main.ii带向何方?
编译器(compiler)
编译器(compiler)是一种计算机程序,它会将某种编程语言写成的源代码(原始语言)转换成另一种编程语言(目标语言)。
它主要的目的是将便于人编写、阅读、维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序,也就是可执行文件。编译器将原始程序(source program)作为输入,翻译产生使用目标语言(target language)的等价程序。源代码一般为高级语言(High-level language),如Pascal、C、C++、C# 、Java等,而目标语言则是汇编语言或目标机器的目标代码(Object code),有时也称作机器代码(Machine code)。
使用
clang++ -S main.ii -o main.s
将预处理的main.ii文件编译成汇编代码main.s,在shell上打开main.s看看汇编是怎么样的,它是一种符号语言。
main.s
1 .text
2 .file "main.cpp"
3 # Start of file scope inline assembly
4 .globl _ZSt21ios_base_library_initv
5
6 # End of file scope inline assembly
7 .globl _Z1av # -- Begin function _Z1av
8 .p2align 4, 0x90
9 .type _Z1av,@function
10 _Z1av: # @_Z1av
11 .cfi_startproc
12 # %bb.0:
13 pushq %rbp
14 .cfi_def_cfa_offset 16
15 .cfi_offset %rbp, -16
16 movq %rsp, %rbp
17 .cfi_def_cfa_register %rbp
18 movq _ZSt4cout@GOTPCREL(%rip), %rdi
19 leaq .L.str(%rip), %rsi
20 callq _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
21 movq %rax, %rdi
22 movl $10, %esi
23 callq _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_c@PLT
24 popq %rbp
25 .cfi_def_cfa %rsp, 8
26 retq
27 .Lfunc_end0:
28 .size _Z1av, .Lfunc_end0-_Z1av
29 .cfi_endproc
30 # -- End function
31 .globl _Z1bv # -- Begin function _Z1bv
32 .p2align 4, 0x90
33 .type _Z1bv,@function
34 _Z1bv: # @_Z1bv
35 .cfi_startproc
36 # %bb.0:
37 pushq %rbp
38 .cfi_def_cfa_offset 16
39 .cfi_offset %rbp, -16
40 movq %rsp, %rbp
41 .cfi_def_cfa_register %rbp
42 callq _Z1av
43 popq %rbp
44 .cfi_def_cfa %rsp, 8
45 retq
46 .Lfunc_end1:
47 .size _Z1bv, .Lfunc_end1-_Z1bv
48 .cfi_endproc
49 # -- End function
50 .globl _Z1cv # -- Begin function _Z1cv
51 .p2align 4, 0x90
52 .type _Z1cv,@function
53 _Z1cv: # @_Z1cv
54 .cfi_startproc
55 # %bb.0:
56 pushq %rbp
57 .cfi_def_cfa_offset 16
58 .cfi_offset %rbp, -16
59 movq %rsp, %rbp
60 .cfi_def_cfa_register %rbp
61 callq _Z1av
62 callq _Z1bv
63 popq %rbp
64 .cfi_def_cfa %rsp, 8
65 retq
66 .Lfunc_end2:
67 .size _Z1cv, .Lfunc_end2-_Z1cv
68 .cfi_endproc
69 # -- End function
70 .globl _Z1dv # -- Begin function _Z1dv
71 .p2align 4, 0x90
72 .type _Z1dv,@function
73 _Z1dv: # @_Z1dv
74 .cfi_startproc
75 # %bb.0:
76 pushq %rbp
77 .cfi_def_cfa_offset 16
78 .cfi_offset %rbp, -16
79 movq %rsp, %rbp
80 .cfi_def_cfa_register %rbp
81 callq _Z1av
82 callq _Z1bv
83 callq _Z1cv
84 popq %rbp
85 .cfi_def_cfa %rsp, 8
86 retq
87 .Lfunc_end3:
88 .size _Z1dv, .Lfunc_end3-_Z1dv
89 .cfi_endproc
90 # -- End function
91 .globl main # -- Begin function main
92 .p2align 4, 0x90
93 .type main,@function
94 main: # @main
95 .cfi_startproc
96 # %bb.0:
97 pushq %rbp
98 .cfi_def_cfa_offset 16
99 .cfi_offset %rbp, -16
100 movq %rsp, %rbp
101 .cfi_def_cfa_register %rbp
102 subq $16, %rsp
103 movl $0, -4(%rbp)
104 callq _Z1dv
105 xorl %eax, %eax
106 addq $16, %rsp
107 popq %rbp
108 .cfi_def_cfa %rsp, 8
109 retq
110 .Lfunc_end4:
111 .size main, .Lfunc_end4-main
112 .cfi_endproc
113 # -- End function
114 .type .L.str,@object # @.str
115 .section .rodata.str1.1,"aMS",@progbits,1
116 .L.str:
117 .asciz " a here "
118 .size .L.str, 9
119
120 .ident "Debian clang version 19.1.7 (3+b1)"
121 .section ".note.GNU-stack","",@progbits
122 .addrsig
123 .addrsig_sym _Z1av
124 .addrsig_sym _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_c
125 .addrsig_sym _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
126 .addrsig_sym _Z1bv
127 .addrsig_sym _Z1cv
128 .addrsig_sym _Z1dv
129 .addrsig_sym _ZSt4cout
上面有很多代码,但是基本都是重复的,提取出来就是下面的代码,根据ai和自己的理解查了查了意思。
1 .text # 将后续代码放入可执行代码段(text section)
2 .file "main.cpp" # 指明源文件名,用于调试信息
3 # Start of file scope inline assembly
4 .globl _ZSt21ios_base_library_initv # 声明全局符号(通常是C++运行时库初始化例程)
5
6 # End of file scope inline assembly
7 .globl _Z1av # 声明函数a()为全局符号,允许其他文件调用
8 .p2align 4, 0x90 # 16字节对齐(2^4=16),用0x90(NOP指令)填充空隙
9 .type _Z1av,@function # 声明符号_Z1av的类型是函数
10 _Z1av: # 函数a()的入口点(标签)
11 .cfi_startproc # 开始生成调用帧信息(CFI),用于调试和异常处理
12 # %bb.0: # 基本块0的标签(由编译器生成的控制流标记)
13 pushq %rbp # 保存调用者的栈帧指针到栈上
14 .cfi_def_cfa_offset 16 # CFI: 规范帧地址(CFA)现在在原始SP+16的位置
15 .cfi_offset %rbp, -16 # CFI: 旧的RBP值保存在CFA-16的位置
16 movq %rsp, %rbp # 建立新的栈帧:将当前栈指针复制到RBP
17 .cfi_def_cfa_register %rbp # CFI: 现在使用RBP作为计算CFA的基准寄存器
18 movq _ZSt4cout@GOTPCREL(%rip), %rdi # 获取std::cout地址放入RDI(第一个参数)
19 leaq .L.str(%rip), %rsi # 计算字符串" a here "的地址放入RSI(第二个参数)
20 callq _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT # 调用operator<<输出字符串
21 movq %rax, %rdi # 将返回值(cout引用)作为下一次调用的第一个参数
22 movl $10, %esi # 将数字10(换行符'\n'的ASCII码)放入ESI(第二个参数)
23 callq _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_c@PLT # 调用operator<<输出字符
24 popq %rbp # 从栈中恢复调用者的栈帧指针
25 .cfi_def_cfa %rsp, 8 # CFI: 现在CFA在RSP+8的位置(指向返回地址之前)
26 retq # 从函数返回
27 .Lfunc_end0: # 函数结束的标签
28 .size _Z1av, .Lfunc_end0_Z1av # 定义函数大小(结束地址-开始地址)
29 .cfi_endproc # 结束调用帧信息(CFI)记录
7行.globl是全局可见的意思,作用类似于c++中的全局变量,就是什么地方都可以访问它,_Z1av 就是指代标识符a,这就是为什么同一个作用域中函数名,变量名要唯一,目的是方便编译器识别。
9行 .type _Z1av,@function是关键字.type进一步告诉编译器_Z1av(a)是一个函数。
10行和27行是a函数开始和结束的地方,对应着a函数的左边花括号‘{’ 和右花括号‘}’的位置。
11行和27行是调用栈开始和结束的地方。我们听说一个函数就是一个栈,能够自动创建和销毁。而new出来的就是堆上面,需要手动创建和销毁,因此常常有跟指针相关的问题。
//todo 这里对汇编代码有点问题,稍后解决。
汇编器(assembler)
汇编语言(英语:assembly language或assembler language)是任何一种用于电子计算机、微处理器、微控制器,或其他可编程器件的低级语言。在不同的设备中,汇编语言对应着不同的机器语言指令集。
使用汇编语言编写的源代码,然后通过相应的汇编程序将它们转换成可执行的机器代码(或者称为目标代码)。这一过程被称为汇编过程。
典型的现代汇编器(assembler)建造目标代码,由解译组语指令集的助记符(Mnemonics)到操作码,并解析符号名称(Symbolic names)成为存储器地址以及其它的实体。
目标代码(英语:Object code)指计算机科学中编译器或汇编器处理源代码后所生成的代码,它一般由机器代码或接近于机器语言的代码组成。[1]目标文件(英语:Object file)即存放目标代码的计算机文件,它常被称作二进制文件(Binaries)。
在shell上使用命令行
clang++ -c main.s -o main.o
汇编语言main.s 就会编译成目标代码main.o,下面打开main.o,我也懵逼了,这是啥东西,我用vim显示行数为7行,但是我连它怎么分行的都不清楚。
main.o
1 ^?ELF^B^A^A^@^@^@^@^@^@^@^@^@^A^@>^@^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@ ^E^@^@^@^@^@^@^@^@^@^@@^@^@^@^@^@@^@^K^ @^A^@UH<89>åH<8b>=^@^@^@^@H<8d>5^@^@^@^@è^@^@^@^@H<89>Ǿ
2 ^@^@^@è^@^@^@^@]Ãf.^O^_<84>^@^@^@^@^@UH<89>åè^@^@^@^@]Ã^O^_D^@^@UH<89>åè^@^@^@^@è^@^@^@^@]ÃUH<89>åè^@^@^@^@è^@^@^@^@ è^@^@^@^@]Ãf.^O^_<84>^@^@^@^@^@<90>UH<89>åH<83>ì^PÇEü^@^@^@^@è^@^@^@^@1ÀH<83>Ä^P]Ã a here ^@^@Debian clang version 1 9.1.7 (3+b1)^@^@^@^@^@^@^@^@^T^@^@^@^@^@^@^@^AzR^@^Ax^P^A^[^L^G^H<90>^A^@^@^\^@^@^@^\^@^@^@^@^@^@^@&^@^@^@^@A^N^P<86 >^BC^M^Fa^L^G^H^@^@^@^\^@^@^@<^@^@^@^@^@^@^@^K^@^@^@^@A^N^P<86>^BC^M^FF^L^G^H^@^@^@^\^@^@^@\^@^@^@^@^@^@^@^P^@^@^@^@ A^N^P<86>^BC^M^FK^L^G^H^@^@^@^\^@^@^@|^@^@^@^@^@^@^@^U^@^@^@^@A^N^P<86>^BC^M^FP^L^G^H^@^@^@^\^@^@^@<9c>^@^@^@^@^@^@^ @^\^@^@^@^@A^N^P<86>^BC^M^FW^L^G^H^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@[^@^@^@^D^@ñÿ^@^@^@^@^@^@^@^ @^@^@^@^@^@^@^@^@^@^@^@^@^C^@^B^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@T^@^@^@^A^@^D^@^@^@^@^@^@^@^@^@ ^@^@^@^@^@^@^@^A ^@^@^@^P^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@0^@^@^@^R^@^B^@^@^@^@^@^@^@^@^@&^@^@^@^@^@^@^@A^@^@^@^P^@^@^@^@^@^@^@^ @^@^@^@^@^@^@^@^@^@^@^@Ì^@^@^@^P^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@<96>^@^@^@^P^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@ ^@^@*^@^@^@^R^@^B^@0^@^@^@^@^@^@^@^K^@^@^@^@^@^@^@$^@^@^@^R^@^B^@@^@^@^@^@^@^@^@^P^@^@^@^@^@^@^@^^^@^@^@^R^@^B^@P^@^ @^@^@^@^@^@^U^@^@^@^@^@^@^@d^@^@^@^R^@^B^@p^@^@^@^@^@^@^@^\^@^@^@^@^@^@^@^G^@^@^@^@^@^@^@*^@^@^@^F^@^@^@üÿÿÿÿÿÿÿ^N^@ ^@^@^@^@^@^@^B^@^@^@^C^@^@^@üÿÿÿÿÿÿÿ^S^@^@^@^@^@^@^@^D^@^@^@^G^@^@^@üÿÿÿÿÿÿÿ ^@^@^@^@^@^@^@^D^@^@^@^H^@^@^@üÿÿÿÿÿÿÿ5 ^@^@^@^@^@^@^@^D^@^@^@^E^@^@^@üÿÿÿÿÿÿÿE^@^@^@^@^@^@^@^D^@^@^@^E^@^@^@üÿÿÿÿÿÿÿJ^@^@^@^@^@^@^@^D^@^@^@ ^@^@^@üÿÿÿÿÿ ÿÿU^@^@^@^@^@^@^@^D^@^@^@^E^@^@^@üÿÿÿÿÿÿÿZ^@^@^@^@^@^@^@^D^@^@^@ ^@^@^@üÿÿÿÿÿÿÿ_^@^@^@^@^@^@^@^D^@^@^@
3 ^@^@^@üÿÿÿÿÿÿÿ<80>^@^@^@^@^@^@^@^D^@^@^@^K^@^@^@üÿÿÿÿÿÿÿ ^@^@^@^@^@^@^@^B^@^@^@^B^@^@^@^@^@^@^@^@^@^@^@@^@^@^@^@^@^@ ^@^B^@^@^@^B^@^@^@0^@^@^@^@^@^@^@`^@^@^@^@^@^@^@^B^@^@^@^B^@^@^@@^@^@^@^@^@^@^@<80>^@^@^@^@^@^@^@^B^@^@^@^B^@^@^@P^@ ^@^@^@^@^@^@ ^@^@^@^@^@^@^@^B^@^@^@^B^@^@^@p^@^@^@^@^@^@^@^E^H^G
4 ^K^F^@_ZSt21ios_base_library_initv^@_Z1dv^@_Z1cv^@_Z1bv^@_Z1av^@.rela.text^@_ZSt4cout^@.comment^@.L.str^@main.cpp^@m ain^@.note.GNU-stack^@.llvm_addrsig^@.rela.eh_frame^@_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_c^@_ZStlsIS t11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc^@.strtab^@.symtab^@.rodata.str1.1^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^ @^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^D^A^@^@^ C^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@w^D^@^@^@^@^@^@#^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^ @;^@^@^@^A^@^@^@^F^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@@^@^@^@^@^@^@^@<8c>^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^P^@^@^@^@^@^@^@^@^ @^@^@^@^@^@^@6^@^@^@^D^@^@^@@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@ð^B^@^@^@^@^@^@^H^A^@^@^@^@^@^@
@ @ @
5 ^@^@^@^B^@^@^@^H^@^@^@^@^@^@^@^X^@^@^@^@^@^@^@^T^A^@^@^A^@^@^@2^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@Ì^@^@^@^@^@^@^@ ^@^@ ^@^@^@^@^@^@^@^@^@^@^@^@^@^A^@^@^@^@^@^@^@^A^@^@^@^@^@^@^@K^@^@^@^A^@^@^@0^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@Õ^@^@^@^@^@^ @^@$^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^A^@^@^@^@^@^@^@^A^@^@^@^@^@^@^@i^@^@^@^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@ù^@ ^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@<8c>^@^@^@^A^@^@p^B^@^@^@^@^@^@^@^@^@^@^ @^@^@^@^@^@^A^@^@^@^@^@^@¸^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^H^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@<87>^@^@^@^D^@^@^@@^@^@^@^@^ @^@^@^@^@^@^@^@^@^@^@ø^C^@^@^@^@^@^@x^@^@^@^@^@^@^@
6 ^@^@^@^G^@^@^@^H^@^@^@^@^@^@^@^X^@^@^@^@^@^@^@y^@^@^@^CLÿo^@^@^@<80>^@^@^@^@^@^@^@^@^@^@^@^@p^D^@^@^@^@^@^@^G^@^@^@^ @^@^@^@
7 ^@^@^@^@^@^@^@^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^L^A^@^@^B^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@¸^A^@^@^@^@^@^@8^A^@^@ ^@^@^@^@^A^@^@^@^D^@^@^@^H^@^@^@^@^@^@^@^X^@^@^@^@^@^@^@
最后一步就是链接器了,它做完最后的处理就会生成可执行文件,放到windows上就是exe文件,当点击exe文件运行时候出现的错误,我们叫做运行错误。
链接器(Linker)
链接器的作用是合并所有目标文件,并生成所需的输出文件(例如,可以运行的可执行文件)。这个过程称为链接。如果链接过程中的任何步骤失败,链接器都会生成一条描述该问题的错误消息,然后中止运行。
首先,链接器读取编译器生成的(经过前面三个步骤)每个目标文件并确保它们有效。
其次,链接器确保所有跨文件依赖关系都得到正确解析。例如,如果您在一个 .cpp 文件中定义了某个内容,然后在另一个 .cpp 文件中使用它,链接器会将这两个内容连接起来。如果链接器无法将对某个内容的引用与其定义连接起来,则会收到链接器错误,并且链接过程将中止。
第三,链接器通常链接一个或多个库文件,这些库文件是已“打包”以供其他程序重用的预编译代码的集合。
最后,链接器输出所需的输出文件。通常,这是一个可以启动的可执行文件(但如果您的项目设置了库文件,也可以是一个库文件)。
在shell上使用命令行
clang++ main.o -o main
链接器就会将目标代码main.o处理成可执行文件main,同样也晦涩难懂。
main
^?ELF^B^A^A^@^@^@^@^@^@^@^@^@^C^@>^@^A^@^@^@`^P^@^@^@^@^@^@@^@^@^@^@^@^@^@89^@^@^@^@^@^@^@^@^@^@@^@8^@^N^@@^@^_^@^^^ @^F^@^@^@^D^@^@^@@^@^@^@^@^@^@^@@^@^@^@^@^@^@^@@^@^@^@^@^@^@^@^P^C^@^@^@^@^@^@^P^C^@^@^@^@^@^@^H^@^@^@^@^@^@^@^C^@^@ ^@^D^@^@^@<94>^C^@^@^@^@^@^@<94>^C^@^@^@^@^@^@<94>^C^@^@^@^@^@^@^\^@^@^@^@^@^@^@^\^@^@^@^@^@^@^@^A^@^@^@^@^@^@^@^A^@ ^@^@^D^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@¨^G^@^@^@^@^@^@¨^G^@^@^@^@^@^@^@^P^@^@^@^@^@^@^A^@^@^@^E ^@^@^@^@^P^@^@^@^@^@^@^@^P^@^@^@^@^@^@^@^P^@^@^@^@^@^@å^A^@^@^@^@^@^@å^A^@^@^@^@^@^@^@^P^@^@^@^@^@^@^A^@^@^@^D^@^@^@ ^@ ^@^@^@^@^@^@^@ ^@^@^@^@^@^@^@ ^@^@^@^@^@^@¬^A^@^@^@^@^@^@¬^A^@^@^@^@^@^@^@^P^@^@^@^@^@^@^A^@^@^@^F^@^@^@<98>-^@^@ ^@^@^@^@<98>=^@^@^@^@^@^@<98>=^@^@^@^@^@^@<88>^B^@^@^@^@^@^@<90>^B^@^@^@^@^@^@^@^P^@^@^@^@^@^@^B^@^@^@^F^@^@^@¨-^@^@ ^@^@^@^@¨=^@^@^@^@^@^@¨=^@^@^@^@^@^@^P^B^@^@^@^@^@^@^P^B^@^@^@^@^@^@^H^@^@^@^@^@^@^@^D^@^@^@^D^@^@^@P^C^@^@^@^@^@^@P ^C^@^@^@^@^@^@P^C^@^@^@^@^@^@ ^@^@^@^@^@^@^@ ^@^@^@^@^@^@^@^H^@^@^@^@^@^@^@^D^@^@^@^D^@^@^@p^C^@^@^@^@^@^@p^C^@^@^@^ @^@^@p^C^@^@^@^@^@^@$^@^@^@^@^@^@^@$^@^@^@^@^@^@^@^D^@^@^@^@^@^@^@^D^@^@^@^D^@^@^@<8c>!^@^@^@^@^@^@<8c>!^@^@^@^@^@^@ <8c>!^@^@^@^@^@^@ ^@^@^@^@^@^@^@ ^@^@^@^@^@^@^@^D^@^@^@^@^@^@^@Såtd^D^@^@^@P^C^@^@^@^@^@^@P^C^@^@^@^@^@^@P^C^@^@^@^@ ^@^@ ^@^@^@^@^@^@^@ ^@^@^@^@^@^@^@^H^@^@^@^@^@^@^@Påtd^D^@^@^@^P ^@^@^@^@^@^@^P ^@^@^@^@^@^@^P ^@^@^@^@^@^@L^@^@^@^@ ^@^@^@L^@^@^@^@^@^@^@^D^@^@^@^@^@^@^@Qåtd^F^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^ @^@^@^@^@^@^@^P^@^@^@^@^@^@^@Råtd^D^@^@^@<98>-^@^@^@^@^@^@<98>=^@^@^@^@^@^@<98>=^@^@^@^@^@^@h^B^@^@^@^@^@^@h^B^@^@^@ ^@^@^@^A^@^@^@^@^@^@^@^D^@^@^@^P^@^@^@^E^@^@^@GNU^@^B<80>^@À^D^@^@^@^A^@^@^@^@^@^@^@^D^@^@^@^T^@^@^@^C^@^@^@GNU^@^X^ h´_qõ×u°^O1Û6PNÓl[G/lib64/ld-linux-x86-64.so.2^@^B^@^@^@ ^@^@^@^A^@^@^@^F^@^@^@^@^@<81>^@^@^@^@^@ ^@^@^@^@^@^@ ^@ÑeÎm^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@Û^@^@^@^R^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@<99>^@^ @^@^R^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@F^@^@^@^R^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@Ñ^@^@^@^Q^@^@^@^@^@^@^@^@^ @^@^@^@^@^@^@^@^@^@^@|^@^@^@^R^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^P^@^@^@ ^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^ A^@^@^@ ^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@,^@^@^@ ^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@í^@^@^@"^@^@^@^@^@^@^@^@ ^@^@^@^@^@^@^@^@^@^@^@^@__gmon_start__^@_ITM_deregisterTMCloneTable^@_ITM_registerTMCloneTable^@_ZStlsISt11char_trai tsIcEERSt13basic_ostreamIcT_ES5_c^@_ZSt21ios_base_library_initv^@_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5 _PKc^@_ZSt4cout^@__libc_start_main^@__cxa_finalize^@libstdc++.so.6^@libm.so.6^@libgcc_s.so.1^@libc.so.6^@GLIBCXX_3.4 .32^@GLIBCXX_3.4^@GLIBC_2.34^@GLIBC_2.2.5^@^@^@^@^C^@^D^@^D^@^D^@^E^@^A^@^A^@^A^@^B^@^@^@^@^@^A^@^B^@ü^@^@^@^P^@^@^@ 0^@^@^@Bø<97>^B^@^@^E^@-^A^@^@^P^@^@^@t)<92>^H^@^@^D^@<^A^@^@^@^@^@^@^A^@^B^@#^A^@^@^P^@^@^@^@^@^@^@´<91><96>^F^@^@^ C^@H^A^@^@^P^@^@^@u^Zi ^@^@^B^@S^A^@^@^@^@^@^@<98>=^@^@^@^@^@^@^H^@^@^@^@^@^@^@@^Q^@^@^@^@^@^@ =^@^@^@^@^@^@^H^@^@^ @^@^@^@^@^@^Q^@^@^@^@^@^@^X@^@^@^@^@^@^@^H^@^@^@^@^@^@^@^X@^@^@^@^@^@^@¸?^@^@^@^@^@^@^F^@^@^@ ^@^@^@^@^@^@^@^@^@^@ ^@À?^@^@^@^@^@^@^F^@^@^@^A^@^@^@^@^@^@^@^@^@^@^@È?^@^@^@^@^@^@^F^@^@^@^D^@^@^@^@^@^@^@^@^@^@^@Ð?^@^@^@^@^@^@^F^@^@^@ ^F^@^@^@^@^@^@^@^@^@^@^@Ø?^@^@^@^@^@^@^F^@^@^@^G^@^@^@^@^@^@^@^@^@^@^@à?^@^@^@^@^@^@^F^@^@^@^H^@^@^@^@^@^@^@^@^@^@^@
在shell上我们来执行一下 main
./main
会输出
a here
a here
a here
a here
使用LLVM流:(Arch Linux)
流程的详细说明
Clang (前端):
source.cpp -> source.ii: 这一步叫做预处理 (Preprocessing)。clang -E 会处理所有以 # 开头的指令(如 #include, #define),将头文件内容插入、展开宏,生成一个“纯净”的、准备被编译的单个文件(.ii 是预处理后的 C++ 文件扩展名)。
更常见的中间步: 实际上,Clang 更主要的工作是直接将 C++ 代码编译成 LLVM IR(一种中间表示,通常保存为 .ll 或 .bc 文件),而不是汇编代码。优化器优化的是 IR。
LLVM Optimizer (中端):
它接收的是 LLVM IR(而不是直接的 .ii 文件),并进行各种优化。
LLVM CodeGen (后端):
它将优化后的 LLVM IR 转换为特定平台(如 x86_64)的汇编代码 (.s 文件)。
汇编器 (Assembler):
这里有一个隐含的步骤:汇编器 (as)。它接收 .s 汇编文件,将其转换为机器码的目标文件 (.o 文件)。clang 通常会自动调用汇编器,所以用户通常看不到这一步。
LLD (链接器):
LLD 将一个或多个 .o 目标文件和你指定的库(如 libstdc++.a 或 libc++.a)链接在一起,生成最终的可执行文件 (a.out 或你指定的名字)。
LLDB (调试器):
当 a.out 运行时出现错误(如崩溃、逻辑错误),你就使用 LLDB 来加载它、设置断点、单步执行、查看变量状态,从而找到错误的根源。
安装建议
在 Arch Linux 上,你通常会安装以下包组:
安装 Clang 和 LLVM 工具链(这是编译的核心):
sudo pacman -S clang
Packages (4) compiler-rt-20.1.8-1 libedit-20250104_3.1-1 llvm-libs-20.1.8-1 clang-20.1.8-1
(这通常会同时安装 llvm,因为 clang 依赖它)
强烈推荐安装 LLDB(用于调试):
sudo pacman -S lldb
Packages (3) mpdecimal-4.0.1-1 python-3.13.7-1 lldb-20.1.8-1
安装 C++ 标准库(通常也需要):
sudo pacman -S libc++ # LLVM 的C++标准库实现
Packages (2) libc++abi-20.1.6-2 libc++-20.1.6-2

下载中不小心终端了,又重新下载了一遍。


#如何使用
用vim写一个main.cpp
main.cpp
#include <iostream>
int main()
{
std::cout << "Hello, archlinux!\n";
return 0;
}

分步手动编译与调试
第 1 步:预处理 (Preprocessing) - 由 clang 完成
作用:处理所有 # 开头的指令(如 #include, #define),将头文件展开。
clang++ -E main.cpp -o main.ii
# 生成:main.ii (预处理后的输出)
第 2 步:编译为 LLVM IR (Compilation to IR) - 由 clang 完成
作用:将 C++ 代码编译成与硬件无关的 LLVM 中间表示(IR),这是优化器能理解的语言。
clang++ -S -emit-llvm main.cpp -o main.ll
# 生成:main.ll (可读的LLVM IR文本文件)
# 注:你也可以从上一步的 main.ii 文件开始:clang++ -S -emit-llvm main.ii -o main.ll
第 3 步:优化 IR (Optimization) - 由 opt 完成
作用:在 IR 层面进行各种优化。
opt -O2 -S main.ll -o main_optimized.ll
# 生成:main_optimized.ll (优化后的LLVM IR)
# -O2 是标准的优化级别
第 4 步:生成汇编代码 (Code Generation) - 由 llc 完成
作用:将优化后的 LLVM IR 转换为特定目标架构(如 x86_64)的汇编代码。
llc -O2 main_optimized.ll -o main.s
# 生成:main.s (汇编代码文件)
第 5 步:汇编 (Assembling) - 由 as 完成
作用:将汇编代码转换为机器码的目标文件(.o 文件)。
as main.s -o main.o
# 生成:main.o (目标文件)<----猜测这会生成绝对地址的程序,后面会报错,为了符合现在操作系统的安全设计,采用相对地址
第 6 步:链接 (Linking) - 由 lld 完成
作用:将目标文件与 C++ 标准库等链接在一起,生成最终的可执行文件。
# 使用 lld 进行链接。你需要告诉它链接哪些库。
# 首先,找到你的 C++ 标准库路径。通常如下:
#ld.lld main.o -o main /usr/lib/libc++.so.1 /usr/lib/libc++abi.so.1 -lc -dynamic-linker /usr/lib/ld-linux-x86-64.so.2
# 生成:main (最终的可执行文件)
# 上述命令很复杂。更简单的方法是让 clang 帮你调用 lld:
# avoid error: relocation R_X86_64_64 cannot be used against local symbol;
clang++ -fPIC -c main.cpp -o main.o
clang++ -fuse-ld=lld main.o -o main
第 7 步:调试 (Debugging) - 由 lldb 完成
作用:运行和调试生成的可执行文件。为了有效调试,你需要在编译时添加 -g 选项。
重新编译以包含调试信息(最好从第 2 步开始):
# 更简单的方法:直接让clang生成带调试信息的目标文件
clang++ -g -c main.cpp -o main_debug.o
clang++ -fuse-ld=lld -g main.o -o main_debug
#使用 LLDB 进行调试:
lldb ./main_debug
在 LLDB 提示符下,你可以使用以下命令:
text
(lldb) breakpoint set --name main # 在main函数设置断点
(lldb) run # 运行程序
(lldb) step # 单步执行
(lldb) print variable_name # 查看变量值
(lldb) continue # 继续运行
(lldb) quit # 退出LLDB


这里总是升级不彻底,导致部分更新,部分还是旧的,当要求安装的某个包的版本,与系统已经安装的另一个包所要求的版本发生了冲突。所以一定要多刷几遍把100多个包全部更新完毕。
第一步(Clang前端):(不重要,其实用LLVM流的话根本不会生成预处理后的ii文件,而是生成LLVM另外接受的一种.ll文件,就是第二步)

第二步:

第三步(中端):又有点毛病 llvm没有安装好,识别不了opt命令,



第四步LLVM CodeGen(后端):

第五步(汇编器(Assembler):

第六步lld Linker :

虽然我链接了 libc++ (/usr/lib/libc++.so.1) 和 libc++abi,但仍然缺少一些东西。在 Arch Linux 上,完整的 C++ 运行时库 可能还需要其他组件。我决定还是clang 自己驱动链接
clang++ -fuse-ld=lld main.o -o main

查询之后说是R_X86_64_64: 这是一种重定位类型,用的是绝对地址,而现代计算机为了安全考虑,执行主程序时要用相对地址。-fPIC: 链接器识别出问题,并给出了完美的解决方案,让用相对地址。重新删除目标文件后,用-fPIC生成相对地址的目标文件main.o,然后就可以安全地生成可执行文件main了。

第七步lldb调试:
生成可执行文件main 就可以./main运行了,但是如果要调试,就需要-g生成可调试文件,这次记为
这么复杂,肯定手动肯定累死个人,其实不用担心,我们可以使用脚本可以创建一个 Shell 函数来自动化这个流程。将以下内容添加到你的 ~/.bashrc 或 ~/.zshrc:



删除了重新写一个
总结一下:我们在第六步的时候遇到了一个R_X86_64_64错误,R_X86_64_32 是一种要求生成 32位绝对地址 的重定位类型。在现代操作系统中,这几乎总是不被允许的,因为它破坏了位置无关性(PIC/PIE),而这是安全特性(如ASLR)的基础。我们当时的解决办法是删除main.o,借助参数-fPIC由源文件main.cpp生成main.o文件往后面走的,这不可取。
为了解决这个问题,这次我们一开始就生成相对位置的文件。
备注:命名上会体现出经过了“器”的处理,便将它作为目标文件的名字。(比如经过汇编器处理后的文件就是as.o)
main.cpp
1 #include <iostream>
2
3 int getValue()
4 {
5 std::cout << "Enter an integer: ";
6 int integer {};
7 std::cin >> integer;
8
9 return integer;
10 }
11
12 void printValue(int in)
13 {
14 std::cout << in << '\n';
15 }
16
17 int main()
18 {
19 int in { getValue() };
20 printValue(in);
21
22 return 0;
23 }
~ ~
如何使用LLVM流


看到上面,我们会发现as其实不是LLVM流,这里可以用clang替代,具体过程我试了一下,我删除了a.out,as.o,重新走了一下流程,也可以实现纯LLVM.当然LLVM 还提供了一个更底层的工具 called llvm-mc (LLVM Machine Code Toolkit)。这个工具功能强大,既可以做汇编器(Assembler),也可以做反汇编器(Disassembler)。

所以这里用llvm-mc。

LLVM 编译工具链完整流程示例 (使用 llvm-mc)
这是一个完全基于LLVM生态工具的编译流程示例,从C++源代码开始,最终生成可执行文件,避免了使用GNU工具链。
#如何手动一步步编译:
clang++ -S -emit-llvm -fPIC main.cpp -o ir.ll
opt -O2 -S ir.ll -o opt.ll
llc -O2 -relocation-model=pic opt.ll -o llc.s
llvm-mc -filetype=obj -triple=x86_64-pc-linux-gnu llc.s -o mc.o
(clang -c -fPIC llc.s -o mc.o)
clang++ -fuse-ld=lld mc.o -o a.out
! ./a.out
#Clang 编译器一次性完成从源代码到可执行文件的全部过程(包括编译、汇编、链接)
clang++ -Wall -Wextra -g -o main main.cpp
#如果手动调试:带-g
clang++ -g -fuse-ld=lld mc.o -o program
lldb ./program
(lldb) b main #打断点
(lldb) list #查看附近的行
(lldb)run # 启动程序
(lldb) gui # 进入gui模式
总结:LLVM 工具链组成
| 阶段 | 工具名称 | 分类与描述 | 所属生态 | 示例命令 |
|---|---|---|---|---|
| 前端 | clang++ | 编译器驱动程序 (Compiler Driver) / 前端 (Frontend)。负责预处理、编译,将源代码转换为LLVM IR | LLVM | clang++ -S -emit-llvm -fPIC main.cpp -o ir.ll |
| 中端 | opt | 优化器 (Optimizer)。对LLVM IR进行机器无关的优化 | LLVM | opt -O2 -S ir.ll -o opt.ll |
| 后端 | llc | 静态编译器 (Static Compiler) / 后端 (Backend)。将LLVM IR编译到特定架构的汇编代码 | LLVM | llc -O2 -relocation-model=pic opt.ll -o llc.s |
| 汇编1 | llvm-mc | 机器码工具 (Machine Code Toolkit)。作为汇编器 (Assembler)。将汇编代码转换为目标文件 | LLVM | llvm-mc -filetype=obj -triple=x86_64-pc-linux-gnu llc.s -o mc.o |
| 汇编2(推荐) | (or)clang | 编译器驱动程序 (Compiler Driver)。此处利用其集成汇编器功能,将汇编代码转换为目标文件,实现纯LLVM流。 | LLVM | clang -c -fPIC llc.s -o mc.o |
| 链接(可执行文件) | clang++ | 编译器驱动程序(调用链接器)。协调链接过程 | LLVM | clang++ -fuse-ld=lld mc.o -o a.out |
| lld | 链接器(Linker)。将目标文件和库链接为可执行文件 | LLVM | (通过clang调用,见此链接(可执行文件)) | |
| 运行 | ./a.out | |||
| 链接(可调试文件) | clang++ | 编译器驱动程序(调用链接器)。协调链接过程 | LLVM | clang++ -g -fuse-ld=lld mc.o -o program |
| 调试 | lldb | 调试器(Debugger)。用于调试可执行程序 | LLVM | lldb ./program |
虽然开启了调试,但是看不到源代码,我再尝试着学一下。


我们可以将上述命令保存到一个Shell脚本中(例如 debug.sh),实现完全纯净的LLVM工具链编译:
build_debug() {
# 使用 clang 直接完成所有步骤,并包含调试信息,使用 lld 链接
clang++ -g -fuse-ld=lld "$1" -o "${1%.*}_debug"
echo "Built debug executable: ${1%.*}_debug"
echo "Run it with: lldb ./${1%.*}_debug"
}
source debug.sh #编译器脚步
build_debug main.cpp #调用脚本函数


1万+

被折叠的 条评论
为什么被折叠?



