llvm-编译流程(C++语言)

编写一个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

这些阶段描述了整个编译过程的每个步骤。具体过程如下:

  1. 输入:C++ 源文件 main.cpp
  2. 预处理:展开宏和头文件。
  3. 编译:生成 LLVM 中间表示(IR)。
  4. 后端:将 IR 转换为汇编代码。
  5. 汇编:将汇编代码转换为目标文件(.o)。
  6. 链接:将目标文件链接为最终的可执行文件或库。

每个阶段都是对前一阶段输出的进一步处理,最终生成可执行程序。

1、预处理阶段

该阶段是对文件进行预处理,即展开宏、处理 #include 指令和条件编译等。此阶段的输出是一个纯粹的 C++ 源代码文件,所有宏和头文件都已展开。

预处理主要是处理:

  1. 头文件导入
    • 包括引入的头文件中的头文件
  1. 宏定义,替换宏
    • 比如上方定义的#define c 3,预处理后,不会看到c,而是直接会用3替代。
// 在终端直接查看预处理的结果
$ clang -E main.cpp

// 把预处理的结果输出到main2.cpp文件中
$ clang -E main.m >> main2.cpp
  1. typedef 处理类型别名时,在预处理阶段不会被替换掉
  2. #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) 的类型和位置信息。

输出解释:

  1. identifier 'std' [StartOfLine] [LeadingSpace] Loc=<main.cpp:9:2>
    • 类型identifier(标识符)
    • 内容std
    • 位置信息:位于 main.cpp 文件的第 9 行,第 2 列。
    • 含义:这是 C++ 标准库的命名空间 std。编译器在这个位置识别到了 std 这个标识符,表明接下来会有 std:: 命名空间的成员。
  1. coloncolon '::' Loc=<main.cpp:9:5>
    • 类型coloncolon(双冒号)
    • 内容::
    • 位置信息:位于 main.cpp 文件的第 9 行,第 5 列。
    • 含义:这表示命名空间的作用域运算符(::)。它用于指定 std 命名空间中的成员(比如 std::cout)。
  1. lessless '<<' [LeadingSpace] Loc=<main.cpp:9:12>
    • 类型lessless(双左移运算符)
    • 内容<<
    • 位置信息:位于 main.cpp 文件的第 9 行,第 12 列。
    • 含义:这是输出运算符,用于将数据流传递给 std::cout。在 std::cout << ... 结构中,<< 用于输出后面的内容。
  1. plus '+' Loc=<main.cpp:9:16>
    • 类型plus(加号运算符)
    • 内容+
    • 位置信息:位于 main.cpp 文件的第 9 行,第 16 列。
    • 含义:这是加号运算符,用于计算表达式中的和。
  1. 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
  1. lessless '<<' [LeadingSpace] Loc=<main.cpp:9:21>
    • 类型lessless(双左移运算符)
    • 内容<<
    • 位置信息:位于 main.cpp 文件的第 9 行,第 21 列。
    • 含义:再次出现的输出运算符,继续将数据输出到 std::cout
  1. semi ';' Loc=<main.cpp:9:33>
    • 类型semi(分号)
    • 内容;
    • 位置信息:位于 main.cpp 文件的第 9 行,第 33 列。
    • 含义:语句结束符,表示当前语句的结束。

这些输出表示 Clang 对 C++ 代码的词法分析结果,每一行都描述了源代码中的一个词法单元(token),并标明了其在源代码中的位置和类型。通过这些信息,可以看到 std::coutstd::endl 的每一部分是如何被 Clang 识别和处理的,以及如何生成最终的输出。

输出解释:

  1. coloncolon '::' Loc=</usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/iostream:74:18>
  • 类型coloncolon(双冒号)
  • 内容::
  • 位置:该 token 出现于 iostream 文件的第 74 行,第 18 列。
  • 含义:这表示作用域解析运算符 ::,通常用于访问命名空间或类的成员。例如 std::coutstd::endl:: 表示 std 命名空间的作用域。
  1. 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 或其他命名空间中的一个函数、变量或类型。具体上下文需要查看代码。
  1. 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)级别的标识符,表示它是一个内部或私有函数,通常不建议在用户代码中使用。
  1. semi ';' Loc=</usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/iostream:74:33>
  • 类型semi(分号)
  • 内容;
  • 位置:该分号出现在 iostream 文件的第 74 行,第 33 列。
  • 含义:这是语句结束符,表示当前语句的结束。
  1. 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),它显示了代码中某个表达式的树状结构。我们可以根据这些信息,逐步分析出该表达式的含义。

结构分析

  1. BinaryOperator 0x5568c03c1fa8 <col:15, line:2:11> 'int' '+'
    • 类型BinaryOperator(二元运算符)
    • 位置:位于第 2 行,第 11 列
    • 操作符+(加法运算符)
    • 结果类型int

这是一个二元运算符节点,表示加法操作。它将两个操作数相加,并返回 int 类型的结果。

  1. |-BinaryOperator 0x5568c03c1f68 <line:9:15, col:17> 'int' '+'
    • 类型BinaryOperator(二元运算符)
    • 位置:位于第 9 行,第 15 到第 17 列
    • 操作符+
    • 结果类型int

这是另一个二元加法运算符节点,表示 a + b 或类似的表达式。

  1. |-ImplicitCastExpr 0x5568c03c1f38 <col:15> 'int' <LValueToRValue>
    • 类型ImplicitCastExpr(隐式类型转换)
    • 位置:位于第 9 行,第 15 列
    • 结果类型int
    • 转换类型LValueToRValue(左值转右值)

这表示从左值(a)到右值的隐式类型转换。在 C++ 中,左值是指可以被修改的对象(例如变量),而右值是临时对象或常量。在加法操作中,需要将左值 a 转换为右值,以便与其他右值相加。

  1. | | -DeclRefExpr 0x5568c03c1ef8 col:15 'int' lvalue Var 0x5568c03c1a68 'a' 'int'`
    • 类型DeclRefExpr(声明引用表达式)
    • 位置:位于第 9 行,第 15 列
    • 结果类型int
    • 含义:这表示 a 变量的引用,它的类型是 int,这是一个左值(可以修改的变量)。
  1. | | -ImplicitCastExpr 0x5568c03c1f50 col:17 'Demo_int':'int' <LValueToRValue>`
    • 类型ImplicitCastExpr(隐式类型转换)
    • 位置:位于第 9 行,第 17 列
    • 结果类型Demo_int(实际上是 int 类型的别名)
    • 转换类型LValueToRValue(左值转右值)

这表示从 b 到右值的隐式类型转换。虽然 bDemo_int 类型(typedef int Demo_int;),在加法操作中,它会被隐式转换为 int 类型。

  1. | | -DeclRefExpr 0x5568c03c1f18 col:17 'Demo_int':'int' lvalue Var 0x5568c03c1b90 'b' 'Demo_int':'int'`
    • 类型DeclRefExpr(声明引用表达式)
    • 位置:位于第 9 行,第 17 列
    • 结果类型Demo_int
    • 含义:这是对变量 b 的引用。由于 Demo_intint 的别名,实际上它就是一个 int 类型的变量。
  1. | -IntegerLiteral 0x5568c03c1f88 line:2:11 'int' 3`
    • 类型IntegerLiteral(整数字面量)
    • 位置:位于第 2 行,第 11 列
    • 3
    • 结果类型int

这表示常量 3,它是表达式中的一个字面量值。

总结:

这段 AST 表示的是表达式 a + b + 3 的结构:

  1. 外层 BinaryOperator:表示加法运算符 +
    • 左侧操作数:是另一个 BinaryOperator,表示 a + b
    • 右侧操作数:是字面量 3
  1. 内部的 BinaryOperator:表示 a + b 的计算。
    • 左侧操作数:变量 a,它通过隐式类型转换被转换为右值。
    • 右侧操作数:变量 b,它是 Demo_int 类型(实际是 int 的别名),同样通过隐式类型转换转换为右值。
  1. 常量 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"
  • ModuleIDmain.cpp 表示此模块(或 IR 文件)对应的源文件名。
  • source_filename:指定源文件的路径。
  • target datalayout:指定数据布局,这有助于编译器正确理解机器架构相关的内存布局信息。
  • target triple:指定目标平台的三元组,这里是 x86_64-unknown-linux-gnu,表示 64 位的 Linux 系统。
2. 类型定义

这些类型定义是 std::ios_basestd::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_basestd::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 语句声明了外部函数,如 printfstd::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 + ca + 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

特性

.ll

文件

.bc

文件

表示方式

文本格式(可读)

二进制格式(不可读)

用途

调试、查看、编辑 IR

高效存储和传输 IR

可转换工具

llvm-as.bc

llvm-dis.ll

效率

人类友好但效率低

更紧凑,处理效率高

两者之间可以相互转换,通常在开发和调试阶段使用 .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

总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

魏大橙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值