LLVM系列(二)Clang编译过程详解

我们通过终端中输入clang -ccc-print-phases main.m,得到如下打印:

                +- 0: input, "main.m", objective-c  //输入
            +- 1: preprocessor, {0}, objective-c-cpp-output //预处理
         +- 2: compiler, {1}, ir //编译
      +- 3: backend, {2}, assembler //后端
   +- 4: assembler, {3}, object //汇编
+- 5: linker, {4}, image //链接
6: bind-arch, "x86_64", {5}, image //绑定

得到了7步操作的简要说明,分别是:

  • 0:导入文件:是为了找到源文件。
  • 1:预处理阶段:这个过程处理包括宏的替换,头文件的导入,会生成 main.i 文件,其中 objective-c-cpp-output 中的 cpp 不是指 C++ 语言,而是 c preprocessor 的缩写
  • 2:编译阶段:进行词法分析、语法分析、检测语法是否正确,最终生成IR。
  • 3:后端:这里LLVM会通过一个一个的Pass去优化,每个Pass做一些事情,最终生成汇编代码,生成一个 main.s 文件
  • 4:汇编:生成目标文件,即为 main.o
  • 5:链接:链接需要的动态库和静态库,生成可执行文件,最终生成 image
  • 6:绑定:通过不同的架构,生成对应的可执行文件,默认生成x86_64平台。
  • 下面我们一步一步拆解:

1、预处理阶段

        是通过预处理器执行一系列的文本转换与文本处理。预处理器是在真正的编译开始之前由编译器调用的独立程序。

        在终端中输入:clang -E main.m 会生成预编译结果。

        下面我们来把预编译结果生成到文件中,输入命令:clang -E main.m -o main.mi ,会把预编译结果生成一个main.mi文件,里面的内容如下:

# 1 "main.m"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 380 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.m" 2


/*
 *在 main 函数中输入
int main(int argc, char * argv[]) {
    int a = 1;
    int b = 2;
    return a + b;
}
**/

int main(int argc, char * argv[]) {
    int a = 1;
    int b = 2;
    return a + b;
}

        当然,我们不同类型的文件要输出对应的文件类型,具体如下:

  • C :.i (clang -E main.c -o main.i)
  • C++ :.ii (clang -E main.c++ -o main.ii)
  • Objective-C :.mi (clang -E main.m -o main.mi)
  • Objective-C++ :.mii(clang -E main.m++ -o main.mii)

        细心的小伙伴肯定看到上面打印结果前几行里面包含 1,2,3,4,380这些数字,甚是奇怪。我们继续向下看就明白了

1.直接导入文件解析

        首先我们要知道,预处理中我们比较熟悉的就是处理我们的宏定义部分。举个例子:

#import <Foundation/Foundation.h>

        预处理器对这行代码的处理是用 Foundation.h 文件中的内容去替换这行代码,如果 Foundation.h 中也使用了类似的宏引入,则会按照同样的处理方式用各个宏对应的真正代码进行逐级替代。这也就是为什么我们主张头文件最好尽量少的去引入其他的类或库,因为引入的东西越多,编译器需要做的处理就越多。例如,我们经常在 .h 文件中写如下的代码:

用
@class QJDClass;
代替:
#import "QJDClass.h"

        这么写是告诉编译器 QJDClass 是一个类,并且在 .m 实现文件中可以通过 import "QJDClass.h" 的方式来使用它。

        下面我们来看看在 main.m 中引入头文件之后预处理之后的结果。在 main.m 中写上如下代码:

#include <stdio.h>

int main(int argc, char * argv[]) {
    int a = 1;
    int b = 2;
    printf("%d %d", a, b);

    return 0;
}

        再在终端敲入命令:clang -E main.m -o main.mi 

        查看生成的文件发现有500多行代码,其中95%都是头文件导入的代码,这还算少的,你再看看

#import <Foundation/Foundation.h>

        导入这个之后,能发现有9万多行代码,可见这个乱导头文件带来的危害。不过现在的预处理能够判断没有使用的话不会导入里面的的内容,这个还是很优秀的。

        在上面生成的文件中也会看到大量的1,2,3,4等等一系列的数字。下面我们来聊一聊这些数字代表的意思。我们先看看下面这个:

# 1 "main.m"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 380 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.m" 2

          我们摘抄一下官方要点:

# linenum filename flags

These are called linemarkers. They are inserted as needed into the output (but never within a string or character constant). They mean that the following line originated in file filename at line linenumfilename will never contain any non-printing characters; they are replaced with octal escape sequences.

  • 1:This indicates the start of a new file.
  • 2:This indicates returning to a file (after having included another file).
  • 3:This indicates that the following text comes from a system header file, so certain warnings should be suppressed.
  • 4:This indicates that the following text should be treated as being wrapped in an implicit extern "C" block.

翻译一下(百度翻译):

这里面表示:内容开始于源文件 main.m 的第几行。

        格式为:# linenum filename flags

        其中 linenum 为 filename 中的第几行,filename 为原文件名,flags 为零个或者多个,有1、2、3、4。如果有多个 flags 时,彼此使用空格隔开。详细地文档:点击查看🤖🤖🤖      

  • 1:表示一个新文件的开始
  • 2:表示返回文件(包含另一个文件后)
  • 3:表示以下文本来自系统头文件,因此应禁止某些警告
  • 4:表示应将以下文本视为包装在隐式extern "C" 块中。

举个栗子:比如# 1 "<built-in>" 1,表示一个新文件的在 main.m 文件的第1行。

还比如:# 64 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h" 3 4

        表示 stdio.h 导入到 main.m 文件中的文本来自系统头文件,因此应禁止某些警告,并且将 stdio.h 中的文本视为包装在隐式 extern "C" 块中,在 stdio.m 中第64行。

        我们发现文档还有这样一句: If multiple flags are given, they must be in ascending order.所以这个 flags 是按照定义顺序排序的。

2.宏的预编译

        有时候我们会定义这样的函数:

#define MAX(a,b) a > b ? a : b

int main() {
  printf("largest: %d\n", MAX(10,100));
  return 0;
}

        上面这样写是没有问题的,但是当我们有些参数变的时候,就出现了下面的写法

#define MAX(a,b) a > b ? a : b

int main() {
  int i = 200;
  printf("largest: %d\n", MAX(i++,100));
  printf("i: %d\n", i);
  return 0;
}

​        建个 main.c 文件,把上述代码输进去,然后执行命令:

% clang main.c 会生成 a.out文件。
% ./a.out 

largest: 201
i: 202

        why?下面我来看看预编译后形成的代码:

首先执行:

% clang -E mian.c

输出很长的内容,我们只看最后几行,如下:
int main() {
  int i = 200;
  printf("largest: %d\n", i++ > 100 ? i++ : 100);
  printf("i: %d\n", i);
  return 0;
}

        仔细分析上述代码:i++ > 100,显然是成立的,执行完以后 i = 201,然后去执行后面的 i++,在 i++ 执行之前会先打印第一个 printf 打印,结果是 201,然后执行 i++语句,得出 i = 202,所以第二个 printf 打印,i=202。这是典型的宏使用不当,而且通常这类问题非常隐蔽且难以发现 。        

        你肯定想知道这种问题的解决方式,这个地方最好推荐的就是 static inline ,static inline会以一种类似于宏定义的方式,将调用被 static inline 修饰的函数的语句替换为那个函数体对应的指令,但实际上只是inline的作用,static作用其实是维护代码的健壮性,inline是向编译器建议,将被inline修饰的函数以内联的方式嵌入到调用这个函数的地方。 所以:

  • 好处:减少调用函数时的开销减少,传参时可能引起的压栈出栈的开销 ,减少PC跳转时对流水线的破坏。
  • 坏处:代码所占体积会更大。
#include <stdio.h>
static const int MyConstant = 200;

static inline int max(int l, int r) {
   return l > r ? l : r;
}

int main() {
  int i = MyConstant;
  printf("largest: %d\n", max(i++,100));
  printf("i: %d\n", i);
  return 0;
}

        上面代码通过同样的预编译处理,发现代码并没有被替换。结果如下:

static const int MyConstant = 200;

static inline int max(int l, int r) {
   return l > r ? l : r;
}

int main() {
  int i = MyConstant;
  printf("largest: %d\n", max(i++,100));
  printf("i: %d\n", i);
  return 0;
}

        整理在调试的时候也很友好,可以设置断点、类型检查以及避免异常行为。基本上,宏的最佳使用场景是日志输出,可以使用 __FILE__ 和 __LINE__ 和 assert 宏。

这里解释一下:

        __LINE__:用以指示本行语句在源文件中的位置信息
        __FILE__:用以指示本行语句所在源文件的文件

        assert:OC中常见        

上面说明了预处理过程中的宏定义和导入头文件的处理过程。下一篇我们在来看一下预处理过程的其他的处理,比较经典的:词法分析、语法分析和语义分析

参考资料:

        1.https://zhuanlan.zhihu.com/p/132726037

        2.http://tigcc.ticalc.org/doc/cpp.html#SEC41

        3.https://llvm.org/docs/LangRef.html

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值