C++基础:clang的分步编译-了解build细节

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.ll3 步:优化 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 IRLLVMclang++ -S -emit-llvm -fPIC main.cpp -o ir.ll
中端opt优化器 (Optimizer)。对LLVM IR进行机器无关的优化LLVMopt -O2 -S ir.ll -o opt.ll
后端llc静态编译器 (Static Compiler) / 后端 (Backend)。将LLVM IR编译到特定架构的汇编代码LLVMllc -O2 -relocation-model=pic opt.ll -o llc.s
汇编1llvm-mc机器码工具 (Machine Code Toolkit)。作为汇编器 (Assembler)。将汇编代码转换为目标文件LLVMllvm-mc -filetype=obj -triple=x86_64-pc-linux-gnu llc.s -o mc.o
汇编2(推荐)(or)clang编译器驱动程序 (Compiler Driver)。此处利用其集成汇编器功能,将汇编代码转换为目标文件,实现纯LLVM流。LLVMclang -c -fPIC llc.s -o mc.o
链接(可执行文件)clang++编译器驱动程序(调用链接器)。协调链接过程LLVMclang++ -fuse-ld=lld mc.o -o a.out
lld链接器(Linker)。将目标文件和库链接为可执行文件LLVM(通过clang调用,见此链接(可执行文件))
运行./a.out
链接(可调试文件)clang++编译器驱动程序(调用链接器)。协调链接过程LLVMclang++ -g -fuse-ld=lld mc.o -o program
调试lldb调试器(Debugger)。用于调试可执行程序LLVMlldb ./program

虽然开启了调试,但是看不到源代码,我再尝试着学一下。
在这里插入图片描述

在这里插入图片描述

点击跳转常用lldb命令

我们可以将上述命令保存到一个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 #调用脚本函数

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值