编写一个C++程序main.cpp
#include<iostream>
#define c 3
typedef int Demo_int;
int main(){
int a = 1;
Demo_int b = 2;
printf("%d\n",a+b+c);
std::cout << a+b+c << std::endl;
return 0;
}

通过命令可以打印源码的编译流程
# clang -ccc-print-phases main.cpp

0、输入文件是 main.cpp,是 C++ 源代码文件,使用的是 C++ 编译流程。
+- 0: input, "main.cpp", c++
1、预处理阶段:对文件进行预处理,即展开宏、处理 #include 指令和条件编译等。
此阶段的输出是一个纯粹的 C++ 源代码文件,所有宏和头文件都已展开。
+- 1: preprocessor, {0}, c++-cpp-output
2、编译阶段:使用预处理后的源代码(阶段 1 的输出)来生成 LLVM 中间表示(IR)。
输出是 ir,表示 LLVM IR 文件,通常有 .bc 或 .ll 扩展名。
+- 2: compiler, {1}, ir
3、后端阶段:将 LLVM IR 代码(阶段 2 的输出)转换为汇编代码。
输出是汇编文件,通常具有 .s 扩展名。
+- 3: backend, {2}, assembler
4、汇编阶段:使用汇编代码(阶段 3 的输出)将其转换为 目标文件(Object File)。
目标文件包含机器码,但尚未链接成最终的可执行文件。
输出是 .o 或 .obj 文件,这是二进制机器码文件。
+- 4: assembler, {3}, object
5、链接阶段:Clang 使用目标文件(阶段 4 的输出)来生成最终的可执行文件或库。
在链接过程中,Clang 会将目标文件与所需的库文件连接起来,最终生成可执行映像文件。
输出是链接后的可执行文件或库文件,通常是 .out(在 Unix 系统中)或 .exe(在 Windows 系统中)文件。
5: linker, {4}, image
这些阶段描述了整个编译过程的每个步骤。具体过程如下:
- 输入:C++ 源文件
main.cpp。 - 预处理:展开宏和头文件。
- 编译:生成 LLVM 中间表示(IR)。
- 后端:将 IR 转换为汇编代码。
- 汇编:将汇编代码转换为目标文件(
.o)。 - 链接:将目标文件链接为最终的可执行文件或库。
每个阶段都是对前一阶段输出的进一步处理,最终生成可执行程序。
1、预处理阶段
该阶段是对文件进行预处理,即展开宏、处理 #include 指令和条件编译等。此阶段的输出是一个纯粹的 C++ 源代码文件,所有宏和头文件都已展开。
预处理主要是处理:
- 头文件导入
-
- 包括引入的头文件中的头文件
- 宏定义,替换宏
-
- 比如上方定义的
#define c 3,预处理后,不会看到c,而是直接会用3替代。
- 比如上方定义的
// 在终端直接查看预处理的结果
$ clang -E main.cpp
// 把预处理的结果输出到main2.cpp文件中
$ clang -E main.m >> main2.cpp
- typedef 处理类型别名时,在预处理阶段
不会被替换掉。 - #define 在预处理阶段
会被替换掉。在逆向工程中,通常会被用来进行代码混淆,将核心方法等使用系统相似的名称,来达到代码混淆的目的,使代码更安全。

2、编译阶段
由词法分析、语法分析、生成中级代码IR等组成。
使用预处理后的源代码(阶段 1 的输出)来生成 LLVM 中间表示(IR)。
输出是 ir,表示 LLVM IR 文件,通常有 .bc 或 .ll 扩展名。
2.1 词法分析
预处理完成后就会进行词法分析,这里会把代码切成一个个token,比如大小括号、等号、字符串、关键词等。
$ clang -fmodules -fsyntax-only -Xclang -dump-tokens main.cpp

Clang 编译器输出的 词法分析(Lexical Analysis)结果,描述了 C++ 代码中每个 词法单元(token) 的类型和位置信息。
输出解释:
identifier 'std' [StartOfLine] [LeadingSpace] Loc=<main.cpp:9:2>
-
- 类型:
identifier(标识符) - 内容:
std - 位置信息:位于
main.cpp文件的第 9 行,第 2 列。 - 含义:这是 C++ 标准库的命名空间
std。编译器在这个位置识别到了std这个标识符,表明接下来会有std::命名空间的成员。
- 类型:
coloncolon '::' Loc=<main.cpp:9:5>
-
- 类型:
coloncolon(双冒号) - 内容:
:: - 位置信息:位于
main.cpp文件的第 9 行,第 5 列。 - 含义:这表示命名空间的作用域运算符(
::)。它用于指定std命名空间中的成员(比如std::cout)。
- 类型:
lessless '<<' [LeadingSpace] Loc=<main.cpp:9:12>
-
- 类型:
lessless(双左移运算符) - 内容:
<< - 位置信息:位于
main.cpp文件的第 9 行,第 12 列。 - 含义:这是输出运算符,用于将数据流传递给
std::cout。在std::cout << ...结构中,<<用于输出后面的内容。
- 类型:
plus '+' Loc=<main.cpp:9:16>
-
- 类型:
plus(加号运算符) - 内容:
+ - 位置信息:位于
main.cpp文件的第 9 行,第 16 列。 - 含义:这是加号运算符,用于计算表达式中的和。
- 类型:
numeric_constant '3' Loc=<main.cpp:9:19 <Spelling=main.cpp:2:11>>
-
- 类型:
numeric_constant(数字常量) - 内容:
3 - 位置信息:位于
main.cpp文件的第 9 行,第 19 列。 - 含义:这是数字常量
3,对应于宏定义#define c 3中的c,此常量在编译时会被展开为3。
- 类型:
lessless '<<' [LeadingSpace] Loc=<main.cpp:9:21>
-
- 类型:
lessless(双左移运算符) - 内容:
<< - 位置信息:位于
main.cpp文件的第 9 行,第 21 列。 - 含义:再次出现的输出运算符,继续将数据输出到
std::cout。
- 类型:
semi ';' Loc=<main.cpp:9:33>
-
- 类型:
semi(分号) - 内容:
; - 位置信息:位于
main.cpp文件的第 9 行,第 33 列。 - 含义:语句结束符,表示当前语句的结束。
- 类型:
这些输出表示 Clang 对 C++ 代码的词法分析结果,每一行都描述了源代码中的一个词法单元(token),并标明了其在源代码中的位置和类型。通过这些信息,可以看到 std::cout 和 std::endl 的每一部分是如何被 Clang 识别和处理的,以及如何生成最终的输出。
输出解释:
coloncolon '::' Loc=</usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/iostream:74:18>
- 类型:
coloncolon(双冒号) - 内容:
:: - 位置:该 token 出现于
iostream文件的第 74 行,第 18 列。 - 含义:这表示作用域解析运算符
::,通常用于访问命名空间或类的成员。例如std::cout或std::endl,::表示std命名空间的作用域。
identifier 'Init' Loc=</usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/iostream:74:20>
- 类型:
identifier(标识符) - 内容:
Init - 位置:该标识符出现在
iostream文件的第 74 行,第 20 列。 - 含义:这是一个标识符,名为
Init,可能是std::Init或其他命名空间中的一个函数、变量或类型。具体上下文需要查看代码。
identifier '__ioinit' [LeadingSpace] Loc=</usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/iostream:74:25>
- 类型:
identifier(标识符) - 内容:
__ioinit - 位置:该标识符出现在
iostream文件的第 74 行,第 25 列。 - 含义:这是另一个标识符,名为
__ioinit。通常以__开头的标识符是实现(implementation)级别的标识符,表示它是一个内部或私有函数,通常不建议在用户代码中使用。
semi ';' Loc=</usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/iostream:74:33>
- 类型:
semi(分号) - 内容:
; - 位置:该分号出现在
iostream文件的第 74 行,第 33 列。 - 含义:这是语句结束符,表示当前语句的结束。
r_brace '}' [StartOfLine] Loc=</usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/iostream:77:1>
- 类型:
r_brace(右大括号) - 内容:
} - 位置:该右大括号出现在
iostream文件的第 77 行,第 1 列。 - 含义:这是一个右大括号,表示代码块的结束。
这些词法单元描述了 <iostream> 头文件中的一段代码。具体来说,这段代码看起来可能是定义了某个命名空间或类的成员,或者是执行某些初始化工作。
::运算符用于表示作用域。Init和__ioinit是两个标识符,可能是std命名空间或其他地方定义的函数、类型或变量。- 分号
;是语句的结束符。 - 右大括号
}表示代码块的结束。
这些词法单元可能来自于某个与流初始化相关的代码,例如 std::Init 或 __ioinit 可能是负责初始化输入输出流的函数或操作。
2.2 语法分析
词法分析完成后就是语法分析,它的任务是验证语法是否正确,在词法分析的基础上将单词序列组合成各类此法短语,如程序、语句、表达式 等等,然后将所有节点组成抽象语法树(Abstract Syntax Tree = AST),语法分析判断程序在结构上是否正确。
$ clang -fmodules -fsyntax-only -Xclang -ast-dump main.cpp
如果导入头文件找不到,可以指定SDK
$ clang -isysroot (自己Xcode下对应SDK路径) -fmodules -fsyntax-only -Xclang -ast-dump main.cpp
$ clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.3.sdk/ -fmodules -fsyntax-only -Xclang -ast-dump main.cpp
截取语法分析中的一部分:

这是严格的按照语法进行处理的,从代码的对齐上可以看出点东西来。
使用GPT解释其中的一段代码:
这段输出来自 Clang 的 抽象语法树(AST),它显示了代码中某个表达式的树状结构。我们可以根据这些信息,逐步分析出该表达式的含义。
结构分析
BinaryOperator 0x5568c03c1fa8 <col:15, line:2:11> 'int' '+'
-
- 类型:
BinaryOperator(二元运算符) - 位置:位于第 2 行,第 11 列
- 操作符:
+(加法运算符) - 结果类型:
int
- 类型:
这是一个二元运算符节点,表示加法操作。它将两个操作数相加,并返回 int 类型的结果。
|-BinaryOperator 0x5568c03c1f68 <line:9:15, col:17> 'int' '+'
-
- 类型:
BinaryOperator(二元运算符) - 位置:位于第 9 行,第 15 到第 17 列
- 操作符:
+ - 结果类型:
int
- 类型:
这是另一个二元加法运算符节点,表示 a + b 或类似的表达式。
|-ImplicitCastExpr 0x5568c03c1f38 <col:15> 'int' <LValueToRValue>
-
- 类型:
ImplicitCastExpr(隐式类型转换) - 位置:位于第 9 行,第 15 列
- 结果类型:
int - 转换类型:
LValueToRValue(左值转右值)
- 类型:
这表示从左值(a)到右值的隐式类型转换。在 C++ 中,左值是指可以被修改的对象(例如变量),而右值是临时对象或常量。在加法操作中,需要将左值 a 转换为右值,以便与其他右值相加。
| |-DeclRefExpr 0x5568c03c1ef8 col:15 'int' lvalue Var 0x5568c03c1a68 'a' 'int'`
-
- 类型:
DeclRefExpr(声明引用表达式) - 位置:位于第 9 行,第 15 列
- 结果类型:
int - 含义:这表示
a变量的引用,它的类型是int,这是一个左值(可以修改的变量)。
- 类型:
| |-ImplicitCastExpr 0x5568c03c1f50 col:17 'Demo_int':'int' <LValueToRValue>`
-
- 类型:
ImplicitCastExpr(隐式类型转换) - 位置:位于第 9 行,第 17 列
- 结果类型:
Demo_int(实际上是int类型的别名) - 转换类型:
LValueToRValue(左值转右值)
- 类型:
这表示从 b 到右值的隐式类型转换。虽然 b 是 Demo_int 类型(typedef int Demo_int;),在加法操作中,它会被隐式转换为 int 类型。
| |-DeclRefExpr 0x5568c03c1f18 col:17 'Demo_int':'int' lvalue Var 0x5568c03c1b90 'b' 'Demo_int':'int'`
-
- 类型:
DeclRefExpr(声明引用表达式) - 位置:位于第 9 行,第 17 列
- 结果类型:
Demo_int - 含义:这是对变量
b的引用。由于Demo_int是int的别名,实际上它就是一个int类型的变量。
- 类型:
|-IntegerLiteral 0x5568c03c1f88 line:2:11 'int' 3`
-
- 类型:
IntegerLiteral(整数字面量) - 位置:位于第 2 行,第 11 列
- 值:
3 - 结果类型:
int
- 类型:
这表示常量 3,它是表达式中的一个字面量值。
总结:
这段 AST 表示的是表达式 a + b + 3 的结构:
- 外层
BinaryOperator:表示加法运算符+。
-
- 左侧操作数:是另一个
BinaryOperator,表示a + b。 - 右侧操作数:是字面量
3。
- 左侧操作数:是另一个
- 内部的
BinaryOperator:表示a + b的计算。
-
- 左侧操作数:变量
a,它通过隐式类型转换被转换为右值。 - 右侧操作数:变量
b,它是Demo_int类型(实际是int的别名),同样通过隐式类型转换转换为右值。
- 左侧操作数:变量
- 常量
3:作为加法的常量部分,直接参与加法运算。
最终,a + b + 3 将被计算,结果为一个 int 类型的值。
CompoundStmt:函数的作用域,大括号的开始与结束{}DeclStmt:局部变量声明CallExpr:函数调用BinaryOperator:运算表达式
如果当我们写的代码有问题时,在编译阶段就会出现问题,比如我们在上面的代码中删除一个分号,再运行一下命令。


在语法分析阶段就会把错误清晰的暴露出来。
2.3 生成中级代码IR
完成以上步骤后,就开始生成中间代码IR了,代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM IR。
可以通过下面命令生成xx.ll的文本文件,也就是IR代码。
// 默认不优化
$ clang -S -fobjc-arc -emit-llvm main.m
// IR文件的优化,在Xcode中target - build setting -optimization level可以设置。
// LLVM的优化登记分别为 -O0、 -O1 、-O2、-O3、-Os(第一个字母为大写O)
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll


以下是IR的基本语法:
@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32bit,4个字节
store 写入内存
load 读取数据
call 调用函数
ret 返回

; ModuleID = 'main.cpp'
source_filename = "main.cpp"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
%"class.std::ios_base::Init" = type { i8 }
%"class.std::basic_ostream" = type { ptr, %"class.std::basic_ios" }
%"class.std::basic_ios" = type { %"class.std::ios_base", ptr, i8, i8, ptr, ptr, ptr, ptr }
%"class.std::ios_base" = type { ptr, i64, i64, i32, i32, i32, ptr, %"struct.std::ios_base::_Words", [8 x %"struct.std::ios_base::_Words"], i32, ptr, %"class.std::locale" }
%"struct.std::ios_base::_Words" = type { ptr, i64 }
%"class.std::locale" = type { ptr }
@_ZStL8__ioinit = internal global %"class.std::ios_base::Init" zeroinitializer, align 1
@__dso_handle = external hidden global i8
@.str = private unnamed_addr constant [4 x i8] c"%d\0A\00", align 1
@_ZSt4cout = external global %"class.std::basic_ostream", align 8
@llvm.global_ctors = appending global [1 x { i32, ptr, ptr }] [{ i32, ptr, ptr } { i32 65535, ptr @_GLOBAL__sub_I_main.cpp, ptr null }]
; Function Attrs: noinline uwtable
define internal void @__cxx_global_var_init() #0 section ".text.startup" {
call void @_ZNSt8ios_base4InitC1Ev(ptr noundef nonnull align 1 dereferenceable(1) @_ZStL8__ioinit)
%1 = call i32 @__cxa_atexit(ptr @_ZNSt8ios_base4InitD1Ev, ptr @_ZStL8__ioinit, ptr @__dso_handle) #3
ret void
}
declare void @_ZNSt8ios_base4InitC1Ev(ptr noundef nonnull align 1 dereferenceable(1)) unnamed_addr #1
; Function Attrs: nounwind
declare void @_ZNSt8ios_base4InitD1Ev(ptr noundef nonnull align 1 dereferenceable(1)) unnamed_addr #2
; Function Attrs: nounwind
declare i32 @__cxa_atexit(ptr, ptr, ptr) #3
; Function Attrs: mustprogress noinline norecurse optnone uwtable
define dso_local noundef i32 @main() #4 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
%3 = alloca i32, align 4
store i32 0, ptr %1, align 4
store i32 1, ptr %2, align 4
store i32 2, ptr %3, align 4
%4 = load i32, ptr %2, align 4
%5 = load i32, ptr %3, align 4
%6 = add nsw i32 %4, %5
%7 = add nsw i32 %6, 3
%8 = call i32 (ptr, ...) @printf(ptr noundef @.str, i32 noundef %7)
%9 = load i32, ptr %2, align 4
%10 = load i32, ptr %3, align 4
%11 = add nsw i32 %9, %10
%12 = add nsw i32 %11, 3
%13 = call noundef nonnull align 8 dereferenceable(8) ptr @_ZNSolsEi(ptr noundef nonnull align 8 dereferenceable(8) @_ZSt4cout, i32 noundef %12)
%14 = call noundef nonnull align 8 dereferenceable(8) ptr @_ZNSolsEPFRSoS_E(ptr noundef nonnull align 8 dereferenceable(8) %13, ptr noundef @_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_)
ret i32 0
}
declare i32 @printf(ptr noundef, ...) #1
declare noundef nonnull align 8 dereferenceable(8) ptr @_ZNSolsEi(ptr noundef nonnull align 8 dereferenceable(8), i32 noundef) #1
declare noundef nonnull align 8 dereferenceable(8) ptr @_ZNSolsEPFRSoS_E(ptr noundef nonnull align 8 dereferenceable(8), ptr noundef) #1
declare noundef nonnull align 8 dereferenceable(8) ptr @_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(ptr noundef nonnull align 8 dereferenceable(8)) #1
; Function Attrs: noinline uwtable
define internal void @_GLOBAL__sub_I_main.cpp() #0 section ".text.startup" {
call void @__cxx_global_var_init()
ret void
}
attributes #0 = { noinline uwtable "frame-pointer"="all" "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #1 = { "frame-pointer"="all" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #2 = { nounwind "frame-pointer"="all" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #3 = { nounwind }
attributes #4 = { mustprogress noinline norecurse optnone uwtable "frame-pointer"="all" "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
!llvm.module.flags = !{!0, !1, !2, !3, !4}
!llvm.ident = !{!5}
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 8, !"PIC Level", i32 2}
!2 = !{i32 7, !"PIE Level", i32 2}
!3 = !{i32 7, !"uwtable", i32 2}
!4 = !{i32 7, !"frame-pointer", i32 2}
!5 = !{!"clang version 19.1.5"}
这段代码是 Clang 编译器生成的 LLVM IR(中间表示) 文件,表示了一个简单的 C++ 或 Objective-C 程序的编译结果。LLVM IR 是一种平台无关的中间表示,通常用于优化和代码生成。在这里,编译器生成的 IR 代码主要包括类型定义、全局变量、函数声明、以及程序的逻辑部分。
让我们逐步解释这段代码中的各个部分:
1. ModuleID & Metadata
; ModuleID = 'main.cpp'
source_filename = "main.cpp"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
- ModuleID:
main.cpp表示此模块(或 IR 文件)对应的源文件名。 - source_filename:指定源文件的路径。
- target datalayout:指定数据布局,这有助于编译器正确理解机器架构相关的内存布局信息。
- target triple:指定目标平台的三元组,这里是
x86_64-unknown-linux-gnu,表示 64 位的 Linux 系统。
2. 类型定义
这些类型定义是 std::ios_base、std::basic_ostream 等 C++ 标准库的内部类型。
%"class.std::ios_base::Init" = type { i8 }
%"class.std::basic_ostream" = type { ptr, %"class.std::basic_ios" }
%"class.std::basic_ios" = type { %"class.std::ios_base", ptr, i8, i8, ptr, ptr, ptr, ptr }
%"class.std::ios_base" = type { ptr, i64, i64, i32, i32, i32, ptr, %"struct.std::ios_base::_Words", [8 x %"struct.std::ios_base::_Words"], i32, ptr, %"class.std::locale" }
%"struct.std::ios_base::_Words" = type { ptr, i64 }
%"class.std::locale" = type { ptr }
这些行定义了 C++ 标准库中一些类的结构体。例如,std::ios_base 和 std::basic_ostream 都是 C++ 标准库用于流操作的关键类。它们在 LLVM IR 中用 type 关键字定义了结构体类型。
3. 全局变量
@_ZStL8__ioinit = internal global %"class.std::ios_base::Init" zeroinitializer, align 1
@__dso_handle = external hidden global i8
@.str = private unnamed_addr constant [4 x i8] c"%d\0A\00", align 1
@_ZSt4cout = external global %"class.std::basic_ostream", align 8
@llvm.global_ctors = appending global [1 x { i32, ptr, ptr }] [{ i32, ptr, ptr } { i32 65535, ptr @_GLOBAL__sub_I_main.cpp, ptr null }]
@_ZStL8__ioinit:初始化std::ios_base::Init类的全局变量,它是内部变量。@__dso_handle:外部隐藏的全局变量,用于支持动态共享对象(DSO)处理。@.str:定义了一个常量字符串"%d\n",用于格式化输出。@_ZSt4cout:外部全局变量std::cout,用于标准输出流。@llvm.global_ctors:定义了全局构造函数列表。这里的条目会在程序启动时调用,在 C++ 中用于调用构造函数(如初始化std::ios_base::Init)。
4. 函数声明
declare void @_ZNSt8ios_base4InitC1Ev(ptr noundef nonnull align 1 dereferenceable(1)) unnamed_addr #1
declare void @_ZNSt8ios_base4InitD1Ev(ptr noundef nonnull align 1 dereferenceable(1)) unnamed_addr #2
declare i32 @__cxa_atexit(ptr, ptr, ptr) #3
declare i32 @printf(ptr noundef, ...) #1
declare noundef nonnull align 8 dereferenceable(8) ptr @_ZNSolsEi(ptr noundef nonnull align 8 dereferenceable(8), i32 noundef) #1
declare noundef nonnull align 8 dereferenceable(8) ptr @_ZNSolsEPFRSoS_E(ptr noundef nonnull align 8 dereferenceable(8), ptr noundef) #1
declare noundef nonnull align 8 dereferenceable(8) ptr @_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(ptr noundef nonnull align 8 dereferenceable(8)) #1
- 这些
declare语句声明了外部函数,如printf、std::cout的输出操作函数等。 @__cxa_atexit用于注册退出时调用的函数,用于 C++ 的全局析构函数。
5. 全局初始化函数 (@__cxx_global_var_init)
define internal void @__cxx_global_var_init() #0 section ".text.startup" {
call void @_ZNSt8ios_base4InitC1Ev(ptr noundef nonnull align 1 dereferenceable(1) @_ZStL8__ioinit)
%1 = call i32 @__cxa_atexit(ptr @_ZNSt8ios_base4InitD1Ev, ptr @_ZStL8__ioinit, ptr @__dso_handle) #3
ret void
}
- 这是 C++ 程序中的全局变量初始化函数,它会在程序启动时自动调用。它通过调用
std::ios_base::Init构造函数来初始化输入输出流,确保流的正常工作。 @__cxa_atexit用于注册析构函数,当程序退出时,析构函数会被调用。
6. main 函数
define dso_local noundef i32 @main() #4 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
%3 = alloca i32, align 4
store i32 0, ptr %1, align 4
store i32 1, ptr %2, align 4
store i32 2, ptr %3, align 4
%4 = load i32, ptr %2, align 4
%5 = load i32, ptr %3, align 4
%6 = add nsw i32 %4, %5
%7 = add nsw i32 %6, 3
%8 = call i32 (ptr, ...) @printf(ptr noundef @.str, i32 noundef %7)
%9 = load i32, ptr %2, align 4
%10 = load i32, ptr %3, align 4
%11 = add nsw i32 %9, %10
%12 = add nsw i32 %11, 3
%13 = call noundef nonnull align 8 dereferenceable(8) ptr @_ZNSolsEi(ptr noundef nonnull align 8 dereferenceable(8) @_ZSt4cout, i32 noundef %12)
%14 = call noundef nonnull align 8 dereferenceable(8) ptr @_ZNSolsEPFRSoS_E(ptr noundef nonnull align 8 dereferenceable(8) %13, ptr noundef @_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_)
ret i32 0
}
- 这是程序的
main函数,定义了程序的执行逻辑。 - 它首先定义了三个整数变量
a = 0, b = 1, c = 2。 - 计算
a + b + c和a + b + c的两次结果。 - 使用
printf打印第一个结果,使用std::cout打印第二个结果,并且在末尾输出换行符。
7. 全局析构函数 (@_GLOBAL__sub_I_main.cpp)
define internal void @_GLOBAL__sub_I_main.cpp() #0 section ".text.startup" {
call void @__cxx_global_var_init()
ret void
}
- 这是 C++ 的全局析构函数,负责清理全局变量。它在程序结束时调用。
bitcode
在Xcode7以后,开启了bitcode,苹果会做进一步的优化,生成.bc的中间代码。
$ clang -emit-llvm -c main.ll -o main.bc
|
特性 |
文件 |
文件 |
|
表示方式 |
文本格式(可读) |
二进制格式(不可读) |
|
用途 |
调试、查看、编辑 IR |
高效存储和传输 IR |
|
可转换工具 |
|
|
|
效率 |
人类友好但效率低 |
更紧凑,处理效率高 |
两者之间可以相互转换,通常在开发和调试阶段使用 .ll 文件,而在优化和生成目标代码时使用 .bc 文件。
3、后端
LLVM在后端主要是会通过一个个的Pass去优化,每个Pass做一些事情,最终生成汇编代码。
按照整个llvm的流程,是通过.ll文件生成汇编文件:
$ clang -S -fobjc-arc main.ll -o main.s
我们也可以直接使用源文件生成汇编代码:
$ clang -Os -S -fobjc-arc main.m -o main.s
这里需要注意的是,在上一步中优化后得到的汇编代码与直接使用优化得到的汇编代码是一样的。
4、生成目标文件
目标文件的生成,是汇编器以汇编代码作为插入,将汇编代码转换为机器代码,最后输出目标文件(object file),.o结尾。
clang -fmodules -c main.s -o main.o
接下来我们看看.o文件中有哪些内容(main.o的符号):
$ nm -nm main.o

假设输出的结果为:
$ nm -nm main.o
(undefined) external _printf
0000000000000000 (__TEXT,__text) external _test
000000000000000a (__TEXT,__text) external _main
_printf函数是一个是undefined、external类型的:
undefined:表示在当前文件暂时找不到符号_printf。external:表示这个符号是外部可以访问的。__TEXT,__text表示符号位于代码段中。
之所以找不到,是因为没有运行,有一些动态库、静态库是需要在运行时才被链接进来的。一堆堆的.o文件,链接起来,最后生成我们的可以执行文件。
5、链接
连接器把编译生成的.o文件和.dyld、·.a·文件链接,生成一个mach-o文件。
其中,静态库和可执行文件合并,动态库是独立的(系统的动态库可以让所有mach-o访问的)。
clang++ main.o -o main

这个错误表明链接器找不到 std::ios_base::Init 类的构造函数和析构函数(Init() 和 ~Init())。这些函数通常是由 C++ 标准库提供的,且与流(如 std::cout, std::cin 等)相关的初始化和销毁过程有关。
原因:可能是你没有正确链接 C++ 标准库,或者有其他链接问题。std::ios_base::Init 是 C++ 标准库的一部分,用于在程序启动时初始化和销毁 I/O 流。因此,如果没有正确链接到标准库,链接器就无法找到这些函数的实现。



接下来看一下生成的可执行文件main的符号:
$ nm -nm main
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100003f6d (__TEXT,__text) external _test
0000000100003f77 (__TEXT,__text) external _main
0000000100008008 (__DATA,__data) non-external __dyld_private
可以看到,把dyld相关的库已经链接到可执行文件中了。

这个时候可以直接运行这个可执行文件:
$ ./main
6
直接可以输出结果。
6、绑定
通过不同的架构,生成对应的mach-o格式的可执行文件。
我们可以直接通过file命令查看可执行文件的类型:
$ file main
main: Mach-O 64-bit executable x86_64
![]()
# file main
main: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped
总结

2559

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



