浅谈编译过程

760 阅读11分钟

前言

笔者前端时间在运行一个组内 Swift 项目的时候,发现编译时间比较长。所以查了部分优化项目编译时间的资料(当然还有部分原因是自己的电脑配置比较低)。并打算记录2篇文章。第一篇文章主要记录关于编译过程的内容,第二篇文章记录笔者在优化 Swift 项目编译时间的一点尝试。

本文是第一篇关于编译过程的文章。笔者将本文中介绍编译过程相关的内容,本文会按如下几个部分展开。

  1. 编译相关名词解释;
    1.1 编译器
    1.2 编译器架构
    1.3 GCC
    1.4 Clang
    1.5 LLVM

  2. 编译过程简单了解;
    2.1 词法分析;
    2.2 语法分析;
    2.3 语义分析;
    2.4 生成中间代码;
    2.5 优化中间代码;
    2.6 生成目标代码;

  3. 用具体命令代码简单分析编译过程; 首先名词解释部分,笔者会介绍编译器、GCC、LLVM相关内容。

一、 名词解释

1. 编译器

编译器不是硬件,是可以把源程序编译为目标程序的计算机程序。

编译器(compiler)是一种计算机程序,它会将某种编程语言写成的源代码(原始语言)转换成另一种编程语言(目标语言)。 引自维基百科编译器

编译器功能示意图

2. 编译器架构

编译器架构:
2.1 Frontend:前端
词法分析、语法分析、语义分析、生成中间代码(汇编指令)

2.2 Optimizer:优化器
中间代码优化(汇编器进一步优化代码生成目标文件)

2.3 Backend:后端
生成机器码(链接器把目标文件链接起来,生成可执行文件)

3. GCC

GCC 即 GNU 编译器套件(GNU Compiler Collection)是可以编译 C、Objective-C、C++等源程序的编译器。

GNU编译器套件(GNU Compiler Collection)包括C、C++Objective-CFortranJavaAdaGo语言的前端,也包括了这些语言的库(如libstdc++、libgcj等等)。GCC的初衷是为GNU操作系统专门编写的一款编译器。GNU系统是彻底的自由软件。此处,"自由"的含义是它尊重用户的自由。 引自360百科 GCC

4. Clang

Clang 是C、Objective-C、C++ 等语言的编译器前端。

Clang(发音为/ˈklæŋ/类似英文单字clang[1]) 是一个CC++Objective-CObjective-C++编程语言的编译器前端。它采用了LLVM作为其后端,而且由LLVM2.6开始,一起发布新版本。它的目标是提供一个GNU编译器套装(GCC)的替代品,支持了GNU编译器大多数的编译设置以及非官方语言的扩展。引自维基百科Clang

5. LLVM

LLVM(Low Level Virtual Machine 底层虚拟机),狭义的来说,LLVM 是 Clang 编译器的后端部分,用于把编译代码转化为目标文件。广义的来说 LLVM 是用来开发编译器前端和后端的组件和工具链。

LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。

Low Level Virtual Machine 底层虚拟机。 引自360百科LLVM

6. 项目的整个预编译、编译、链接过程

下图表明一个项目从源代码到可执行文件,会经历预处理、编译、装载、链接的过程。

预编译、编译、链接过程.png

下边笔者初步分享下 GCC 的编译过程。

二、GCC 编译过程

这部分,笔者分2部分初步分享查看 GCC 编译源代码到可执行文件的过程,及 GCC 编译源代码到可执行文件的命令。

1. 查看 GCC 编译过程

WYW:GCC wangyongwangyongwang$ GCC -ccc-print-phases main.c
0: input, "main.c", c
1: preprocessor, {0}, 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

2. 编译过程可能用到的命令

// 1: preprocessor 预编译
WYW:GCCProcess wangyongwangyongwang$ gcc -E main.c -o main.i
// 2: compiler生成汇编文件
WYW:GCCProcess wangyongwangyongwang$ gcc -S main.i -o main.s
// 3: backend、4: assembler生成目标文件
WYW:GCCProcess wangyongwangyongwang$ gcc -c main.s -o main.o
// 5: linker、6: bind-arch生成可执行文件
WYW:GCCProcess wangyongwangyongwang$ gcc main.o -o main
// 执行可执行文件
WYW:GCCProcess wangyongwangyongwang$ sudo ./main
Hello, World!

2.1. 前端过程
2.1.1 词法分析

计算机科学中将字符序列转换为标记(token)序列的过程。

2.1.2 语法分析

根据给定的形式文法对由单词序列构成的输入文本进行分析并确定其语法结构。

2.1.3 语义分析

检测程序的语义错误(如检查所需传入的参数类型)。

2.1.4 生成中间代码

得到易于产生并易于翻译成目标程序的抽象机程序。

2.2 中间过程

2.2.1 优化

2.3 后端过程

2.3.1 生成目标代码

3. 链接目标文件为可执行文件

./可执行文件名字:执行当前路径的可执行文件名字

下边笔者将分享下 Clang 编译过程的内容,笔者目前的理解是 Clang 可以看做是 LLVM 的前端。LLVM 广义上来说是包含 Clang 的。

三、Clang 编译过程

这部分,笔者分2部分初步分享查看 Clang 编译源代码到可以可执行文件的过程,及浅析 Clang 编译源代码到可执行文件的内容。

1. Clang 编译过程

笔者先查看了自己电脑的 Clang 版本。

Apple LLVM version 11.0.0 (clang-1100.0.33.17)

WYW:~ wangyongwangyongwang$ objdump --version
Apple LLVM version 11.0.0 (clang-1100.0.33.17)
  Optimized build.
  Default target: x86_64-apple-darwin19.2.0
  Host CPU: broadwell

  Registered Targets:
    aarch64    - AArch64 (little endian)
    aarch64_be - AArch64 (big endian)
    arm        - ARM
    arm64      - ARM64 (little endian)
    armeb      - ARM (big endian)
    thumb      - Thumb
    thumbeb    - Thumb (big endian)
    x86        - 32-bit X86: Pentium-Pro and above
    x86-64     - 64-bit X86: EM64T and AMD64
查看 Clang 编译源代码到可执行文件的过程
WYW:LLVM wangyongwangyongwang$ clang -ccc-print-phases clang_main.c
0: input, "clang_main.c", c
1: preprocessor, {0}, 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

2. 浅析 Clang 编译 main.m 到可执行文件的过程

笔者这里用的main.m 的文件内容如下:

// 一、预编译
// 引入头文件预编译  注释的内容不会显示在预编译后的文件中
#import <Foundation/Foundation.h>
#import <Foundation/NSObjCRuntime.h>

// 条件编译
#if __has_include(<UIKit/UIKit.h>)
        #import <UIKit/UIKit.h>
    #ifndef Qi_HAS_IMPORT_UIKit
        #define QUC_HAS_IMPORT_UIKit 1
    #endif
#else
    #ifndef Qi_HAS_IMPORT_UIKit
        #define Qi_HAS_IMPORT_UIKit 0
    #endif
#endif

// 宏预编译
#define QiShareAge 2
// 带参数的宏预编译
#define QiShareMember(Member, Name) Member#Name

int main(int argc, char * argv[]) {
    
    // NSString * appDelegateClassName;
     @autoreleasepool {
         
         NSLog(@"Hello Clang Compile");
         
         const char *me = QiShareMember("QiShare","WYW");
         printf("member:%s \n", me);
         NSString *member = [NSString stringWithCString:me encoding:NSUTF8StringEncoding];
         NSLog(@"member:%@", member);
         // Setup code that might create autoreleased objects goes here.
         // appDelegateClassName = NSStringFromClass([AppDelegate class]);
         /** 输出内容
          * Hello Clang Compile
          * member:QiShare"WYW"
          * member:QiShare"WYW"
          */
     }
    // return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

2.1 预编译
// 预编译命令
clang -E main.m -o main_precompile.i
2.1.1 预编译过程做的事情
1. #include #import 预编译指令,将被包含的文件插入到该预编译指令的位置
2. 删除所有的注释// 及 块注释 但是会保留注释所在行为空白行
3. 添加行号和文件标识
4. 因为编译器有需要,会保留#pragma 编译指令
2.1.2 预编译后生成的代码
# 1 "main.m"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 374 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.m" 2
# 11 "main.m"
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 1 3

typedef enum __attribute__((enum_extensibility(closed))) NSComparisonResult : NSInteger NSComparisonResult; enum NSComparisonResult : NSInteger {
    NSOrderedAscending = -1L,
    NSOrderedSame,
    NSOrderedDescending
};


typedef NSComparisonResult (^NSComparator)(id obj1, id obj2);


typedef enum __attribute__((flag_enum,enum_extensibility(open))) NSEnumerationOptions : NSUInteger NSEnumerationOptions; enum NSEnumerationOptions : NSUInteger {
    NSEnumerationConcurrent = (1UL << 0),
    NSEnumerationReverse = (1UL << 1),
};

# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3
# 193 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 12 "main.m" 2
# 31 "main.m"
int main(int argc, char * argv[]) {


     @autoreleasepool {

         NSLog(@"Hello Clang Compile");

         const char *me = "QiShare""\"WYW\"";
         printf("member:%s \n", me);
         NSString *member = [NSString stringWithCString:me encoding:NSUTF8StringEncoding];
         NSLog(@"member:%@", member);
     }

}

下边2张图左边是生成的预编译文件,右侧是 <Foundation/NSObjCRuntime.h> 中的内容。可以发现有一一对应关系。

precompile1

precompile2

2.1.3 预编译小结

可以发现预编译 .m 文件生成的 .i 文件会把头文件导入,导入方式会具体的指明头文件在电脑上的具体问题; 会去除注释,相应地会把注释换成空白行; 添加行号。如main.m 的int main函数的在main.m 文件中位置是第31行。

2.2 词法分析

clang -fmodules -E -Xclang -dump-tokens main.mclang -fmodules -E -Xclang -dump-tokens main.m > main_precompile.i

// 词法分析结果
annot_module_include '#import <Foundation/Foundation.h>
#import <Foundation/NSObjCRuntime.h>

// 条件编译
#if __has_include(<UIKit/UIKit.h>)
        #import <UIKit/UIKit.h>
    #ifndef Qi_HAS_IMPORT_UIKit
        #define QUC_HAS_IMPORT_UIKit 1
    #endif
#else
  '        Loc=<main.m:11:1>
annot_module_include '#import <Foundation/NSObjCRuntime.h>

// 条件编译
#if __has_include(<UIKit/UIKit.h>)
        #import <UIKit/UIKit.h>
    #ifndef Qi_HAS_IMPORT_UIKit
        #define QUC_HAS_IMPORT_UIKit 1
    #endif
#else
    #ifndef Qi_HAS_IMPORT_UIKit
        #define Qi_HAS_IMPORT_UIKit 0
'        Loc=<main.m:12:1>
int 'int'     [StartOfLine]    Loc=<main.m:31:1>
identifier 'main'     [LeadingSpace]    Loc=<main.m:31:5>
l_paren '('        Loc=<main.m:31:9>
int 'int'        Loc=<main.m:31:10>
identifier 'argc'     [LeadingSpace]    Loc=<main.m:31:14>
comma ','        Loc=<main.m:31:18>
char 'char'     [LeadingSpace]    Loc=<main.m:31:20>
star '*'     [LeadingSpace]    Loc=<main.m:31:25>
identifier 'argv'     [LeadingSpace]    Loc=<main.m:31:27>
l_square '['        Loc=<main.m:31:31>
r_square ']'        Loc=<main.m:31:32>
r_paren ')'        Loc=<main.m:31:33>
l_brace '{'     [LeadingSpace]    Loc=<main.m:31:35>
at '@'     [StartOfLine] [LeadingSpace]    Loc=<main.m:34:6>
identifier 'autoreleasepool'        Loc=<main.m:34:7>
l_brace '{'     [LeadingSpace]    Loc=<main.m:34:23>
identifier 'NSLog'     [StartOfLine] [LeadingSpace]    Loc=<main.m:36:10>
l_paren '('        Loc=<main.m:36:15>
at '@'        Loc=<main.m:36:16>
string_literal '"Hello Clang Compile"'        Loc=<main.m:36:17>
r_paren ')'        Loc=<main.m:36:38>
semi ';'        Loc=<main.m:36:39>
const 'const'     [StartOfLine] [LeadingSpace]    Loc=<main.m:38:10>
char 'char'     [LeadingSpace]    Loc=<main.m:38:16>
star '*'     [LeadingSpace]    Loc=<main.m:38:21>
identifier 'me'        Loc=<main.m:38:22>
equal '='     [LeadingSpace]    Loc=<main.m:38:25>
string_literal '"QiShare"'     [LeadingSpace]    Loc=<main.m:38:27 <Spelling=main.m:38:41>>
string_literal '"\"WYW\""'        Loc=<main.m:38:27 <Spelling=<scratch space>:3:1>>
semi ';'        Loc=<main.m:38:57>
identifier 'printf'     [StartOfLine] [LeadingSpace]    Loc=<main.m:39:10>
l_paren '('        Loc=<main.m:39:16>
string_literal '"member:%s \n"'        Loc=<main.m:39:17>
comma ','        Loc=<main.m:39:33>
identifier 'me'     [LeadingSpace]    Loc=<main.m:39:35>
r_paren ')'        Loc=<main.m:39:37>
semi ';'        Loc=<main.m:39:38>
identifier 'NSString'     [StartOfLine] [LeadingSpace]    Loc=<main.m:40:10>
star '*'     [LeadingSpace]    Loc=<main.m:40:19>
identifier 'member'        Loc=<main.m:40:20>
equal '='     [LeadingSpace]    Loc=<main.m:40:27>
l_square '['     [LeadingSpace]    Loc=<main.m:40:29>
identifier 'NSString'        Loc=<main.m:40:30>
identifier 'stringWithCString'     [LeadingSpace]    Loc=<main.m:40:39>
colon ':'        Loc=<main.m:40:56>
identifier 'me'        Loc=<main.m:40:57>
identifier 'encoding'     [LeadingSpace]    Loc=<main.m:40:60>
colon ':'        Loc=<main.m:40:68>
identifier 'NSUTF8StringEncoding'        Loc=<main.m:40:69>
r_square ']'        Loc=<main.m:40:89>
semi ';'        Loc=<main.m:40:90>
identifier 'NSLog'     [StartOfLine] [LeadingSpace]    Loc=<main.m:41:10>
l_paren '('        Loc=<main.m:41:15>
at '@'        Loc=<main.m:41:16>
string_literal '"member:%@"'        Loc=<main.m:41:17>
comma ','        Loc=<main.m:41:30>
identifier 'member'     [LeadingSpace]    Loc=<main.m:41:32>
r_paren ')'        Loc=<main.m:41:38>
semi ';'        Loc=<main.m:41:39>
r_brace '}'     [StartOfLine] [LeadingSpace]    Loc=<main.m:49:6>
r_brace '}'     [StartOfLine]    Loc=<main.m:51:1>
eof ''        Loc=<main.m:51:2>

词法分析

2.2.1 词法分析小结

根据上述截图,可以发现词法分析的过程中,会把关键字和方法名描述符,逗号,分号、括号的行号和列号都会记录下来。

2.3 语法分析

语法分析命令:clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

// WYW:clang wangyongwangyongwang$ clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
// 语法分析输出内容
TranslationUnitDecl 0x7fbeb702da08 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0x7fbeb702e2a0 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x7fbeb702dfa0 '__int128'
|-TypedefDecl 0x7fbeb702e308 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0x7fbeb702dfc0 'unsigned __int128'
|-TypedefDecl 0x7fbeb702e3a0 <<invalid sloc>> <invalid sloc> implicit SEL 'SEL *'
| `-PointerType 0x7fbeb702e360 'SEL *' imported
|   `-BuiltinType 0x7fbeb702e200 'SEL'
|-TypedefDecl 0x7fbeb702e478 <<invalid sloc>> <invalid sloc> implicit id 'id'
| `-ObjCObjectPointerType 0x7fbeb702e420 'id' imported
|   `-ObjCObjectType 0x7fbeb702e3f0 'id' imported
|-TypedefDecl 0x7fbeb702e558 <<invalid sloc>> <invalid sloc> implicit Class 'Class'
| `-ObjCObjectPointerType 0x7fbeb702e500 'Class' imported
|   `-ObjCObjectType 0x7fbeb702e4d0 'Class' imported
|-ObjCInterfaceDecl 0x7fbeb702e5a8 <<invalid sloc>> <invalid sloc> implicit Protocol
|-TypedefDecl 0x7fbeb702e8e8 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag'
| `-RecordType 0x7fbeb702e700 'struct __NSConstantString_tag'
|   `-Record 0x7fbeb702e670 '__NSConstantString_tag'
|-TypedefDecl 0x7fbeb702e980 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0x7fbeb702e940 'char *'
|   `-BuiltinType 0x7fbeb702daa0 'char'
|-TypedefDecl 0x7fbeb7834868 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x7fbeb7834810 'struct __va_list_tag [1]' 1
|   `-RecordType 0x7fbeb7834690 'struct __va_list_tag'
|     `-Record 0x7fbeb7834600 '__va_list_tag'
|-ImportDecl 0x7fbeb79ef2a8 <main.m:11:1> col:1 implicit Foundation
|-ImportDecl 0x7fbeb79ef2e0 <line:12:1> col:1 implicit Foundation.NSObjCRuntime
|-FunctionDecl 0x7fbeb79ef580 <line:31:1, line:51:1> line:31:5 main 'int (int, char **)'
| |-ParmVarDecl 0x7fbeb79ef330 <col:10, col:14> col:14 argc 'int'
| |-ParmVarDecl 0x7fbeb79ef440 <col:20, col:32> col:27 argv 'char **':'char **'
| `-CompoundStmt 0x7fbeb88158a8 <col:35, line:51:1>
|   `-ObjCAutoreleasePoolStmt 0x7fbeb8815890 <line:34:6, line:49:6>
|     `-CompoundStmt 0x7fbeb8815858 <line:34:23, line:49:6>
|       |-CallExpr 0x7fbeb7a000d0 <line:36:10, col:38> 'void'
|       | |-ImplicitCastExpr 0x7fbeb7a000b8 <col:10> 'void (*)(id, ...)' <FunctionToPointerDecay>
|       | | `-DeclRefExpr 0x7fbeb79fffa8 <col:10> 'void (id, ...)' Function 0x7fbeb79ef688 'NSLog' 'void (id, ...)'
|       | `-ImplicitCastExpr 0x7fbeb7a000f8 <col:16, col:17> 'id':'id' <BitCast>
|       |   `-ObjCStringLiteral 0x7fbeb7a00038 <col:16, col:17> 'NSString *'
|       |     `-StringLiteral 0x7fbeb7a00008 <col:17> 'char [20]' lvalue "Hello Clang Compile"
|       |-DeclStmt 0x7fbeb7a005d0 <line:38:10, col:57>
|       | `-VarDecl 0x7fbeb7a00128 <col:10, <scratch space>:3:1> main.m:38:22 used me 'const char *' cinit
|       |   `-ImplicitCastExpr 0x7fbeb7a00208 <col:41, <scratch space>:3:1> 'const char *' <NoOp>
|       |     `-ImplicitCastExpr 0x7fbeb7a001f0 <main.m:38:41, <scratch space>:3:1> 'char *' <ArrayToPointerDecay>
|       |       `-StringLiteral 0x7fbeb7a001c8 <main.m:38:41, <scratch space>:3:1> 'char [13]' lvalue "QiShare\"WYW\""
|       |-CallExpr 0x7fbeb7a006f0 <main.m:39:10, col:37> 'int'
|       | |-ImplicitCastExpr 0x7fbeb7a006d8 <col:10> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
|       | | `-DeclRefExpr 0x7fbeb7a005e8 <col:10> 'int (const char *, ...)' Function 0x7fbeb7a00228 'printf' 'int (const char *, ...)'
|       | |-ImplicitCastExpr 0x7fbeb7a00738 <col:17> 'const char *' <NoOp>
|       | | `-ImplicitCastExpr 0x7fbeb7a00720 <col:17> 'char *' <ArrayToPointerDecay>
|       | |   `-StringLiteral 0x7fbeb7a00648 <col:17> 'char [14]' lvalue "member\357\274\232%s \n"
|       | `-ImplicitCastExpr 0x7fbeb7a00750 <col:35> 'const char *' <LValueToRValue>
|       |   `-DeclRefExpr 0x7fbeb7a00670 <col:35> 'const char *' lvalue Var 0x7fbeb7a00128 'me' 'const char *'
|       |-DeclStmt 0x7fbeb88156c8 <line:40:10, col:90>
|       | `-VarDecl 0x7fbeb7a00780 <col:10, col:89> col:20 used member 'NSString *' cinit
|       |   `-ObjCMessageExpr 0x7fbeb8815688 <col:29, col:89> 'NSString * _Nullable':'NSString *' selector=stringWithCString:encoding: class='NSString'
|       |     |-ImplicitCastExpr 0x7fbeb8815670 <col:57> 'const char *' <LValueToRValue>
|       |     | `-DeclRefExpr 0x7fbeb8815000 <col:57> 'const char *' lvalue Var 0x7fbeb7a00128 'me' 'const char *'
|       |     `-DeclRefExpr 0x7fbeb8815330 <col:69> 'NSStringEncoding':'unsigned long' EnumConstant 0x7fbeb8815028 'NSUTF8StringEncoding' 'NSStringEncoding':'unsigned long'
|       `-CallExpr 0x7fbeb88157d0 <line:41:10, col:38> 'void'
|         |-ImplicitCastExpr 0x7fbeb88157b8 <col:10> 'void (*)(id, ...)' <FunctionToPointerDecay>
|         | `-DeclRefExpr 0x7fbeb88156e0 <col:10> 'void (id, ...)' Function 0x7fbeb79ef688 'NSLog' 'void (id, ...)'
|         |-ImplicitCastExpr 0x7fbeb8815800 <col:16, col:17> 'id':'id' <BitCast>
|         | `-ObjCStringLiteral 0x7fbeb8815760 <col:16, col:17> 'NSString *'
|         |   `-StringLiteral 0x7fbeb8815738 <col:17> 'char [12]' lvalue "member\357\274\232%@"
|         `-ImplicitCastExpr 0x7fbeb8815818 <col:32> 'NSString *' <LValueToRValue>
|           `-DeclRefExpr 0x7fbeb8815780 <col:32> 'NSString *' lvalue Var 0x7fbeb7a00780 'member' 'NSString *'
`-<undeserialized declarations>

语法分析

可以发现语法分析过程中会以语法树的结构说明待编译文件的目录,比如会说明main函数的开始结束行,会说明自动释放池参数、字符串变量、调用函数的行号等信息。

笔者查到语法分析器的用途有:

  • 反编译语法分析
  • 代码高亮
  • 关键字匹配
  • 作用域判断
  • 代码压缩

其实我们可以尝试制造语法错误。更具体地查看词法分析、语法分析部分的用处。

2.3.1 制造语法错误 1

如果是出现如下错误,词法分析的时候并不会有问题,语法分析的时候会报错。

int int main(int argc, char * argv[]) {
    
}

词法分析结果

'		Loc=<main.m:12:1>
int 'int'	 [StartOfLine]	Loc=<main.m:31:1>
int 'int'	 [LeadingSpace]	Loc=<main.m:31:5>
identifier 'main'	 [LeadingSpace]	Loc=<main.m:31:9>
l_paren '('		Loc=<main.m:31:13>

语法分析结果

main.m:31:5: error: cannot combine with previous 'int' declaration specifier
int int main(int argc, char * argv[]) {
    ^
TranslationUnitDecl 0x7ff14302c808 <<invalid sloc>> <invalid sloc>

...

|           `-DeclRefExpr 0x7ff143922b80 <col:32> 'NSString *' lvalue Var 0x7ff14391f780 'member' 'NSString *'
`-<undeserialized declarations>
1 error generated.
2.3.2 制造语法错误 2
void logSomething(NSString *str1, NSString *str2) {
    
    NSLog(@"str1:%@--str2:%@", str1, str2);
}

int main(int argc, char * argv[]) {
    
    // NSString * appDelegateClassName;
     @autoreleasepool {
     	logSomething(1, 2);
     }
}
main.m:41:23: warning: incompatible integer to pointer conversion passing 'int'
      to parameter of type 'NSString *' [-Wint-conversion]
         logSomething(1, 2);
                      ^
main.m:31:29: note: passing argument to parameter 'str1' here
void logSomething(NSString *str1, NSString *str2) {
                            ^
main.m:41:26: warning: incompatible integer to pointer conversion passing 'int'
      to parameter of type 'NSString *' [-Wint-conversion]
         logSomething(1, 2);
                         ^
main.m:31:45: note: passing argument to parameter 'str2' here
void logSomething(NSString *str1, NSString *str2) {
                                            ^

可以发现像两个 int 关键字连续出现的语法错误,在词法分析的过程中并不会报错,在语法分析的时候,会有相应报错。

2.4 生成中间代码

生成中间代码可用命令:clang -S -fobjc-arc -emit-llvm main.m -o main.ll 可以用 cat 命令查看中间代码内容。

2.4.1 中间代码缩减

下列代码是中间代码,中间代码中会有文件名和系统名的标识。 中间代码中会有常量字符串,缓存、实例变量、消息列表、属性列表等信息说明。 笔者不怎么了解中间代码,不做分析。

WYW:clang wangyongwangyongwang$ clang -S -fobjc-arc -emit-llvm main.m -o main.ll
WYW:clang wangyongwangyongwang$ cat main.ll
; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"
    
// 常量字符串,缓存、实例变量、消息列表、属性列表等信息
%0 = type opaque
%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }
%struct._class_t = type { %struct._class_t*, %struct._class_t*, %struct._objc_cache*, i8* (i8*, i8*)**, %struct._class_ro_t* }
%struct._objc_cache = type opaque
%struct._class_ro_t = type { i32, i32, i32, i8*, i8*, %struct.__method_list_t*, %struct._objc_protocol_list*, %struct._ivar_list_t*, i8*, %struct._prop_list_t* }
%struct.__method_list_t = type { i32, i32, [0 x %struct._objc_method] }
%struct._objc_method = type { i8*, i8*, i8* }
%struct._objc_protocol_list = type { i64, [0 x %struct._protocol_t*] }
%struct._protocol_t = type { i8*, i8*, %struct._objc_protocol_list*, %struct.__method_list_t*, %struct.__method_list_t*, %struct.__method_list_t*, %struct.__method_list_t*, %struct._prop_list_t*, i32, i32, i8**, i8*, %struct._prop_list_t* }
%struct._ivar_list_t = type { i32, i32, [0 x %struct._ivar_t] }
%struct._ivar_t = type { i64*, i8*, i8*, i32, i32 }
%struct._prop_list_t = type { i32, i32, [0 x %struct._prop_t] }
%struct._prop_t = type { i8*, i8* }


@__CFConstantStringClassReference = external global [0 x i32]
@.str = private unnamed_addr constant [20 x i8] c"Hello Clang Compile\00", section "__TEXT,__cstring,cstring_literals", align 1
@_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([20 x i8], [20 x i8]* @.str, i32 0, i32 0), i64 19 }, section "__DATA,__cfstring", align 8
@.str.1 = private unnamed_addr constant [13 x i8] c"QiShare\22WYW\22\00", align 1
@.str.2 = private unnamed_addr constant [14 x i8] c"member\EF\BC\9A%s \0A\00", align 1

; Function Attrs: nounwind
declare i8* @llvm.objc.autoreleasePoolPush() #1

declare void @NSLog(i8*, ...) #2

declare i32 @printf(i8*, ...) #2

; Function Attrs: nonlazybind
declare i8* @objc_msgSend(i8*, i8*, ...) #3

; Function Attrs: nounwind
declare i8* @llvm.objc.retainAutoreleasedReturnValue(i8*) #1

; Function Attrs: nounwind
declare void @llvm.objc.storeStrong(i8**, i8*) #1

; Function Attrs: nounwind
declare void @llvm.objc.autoreleasePoolPop(i8*) #1

!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 10, i32 15]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
!2 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!3 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!4 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!5 = !{i32 1, !"Objective-C Class Properties", i32 64}
!6 = !{i32 1, !"wchar_size", i32 4}
!7 = !{i32 7, !"PIC Level", i32 2}
!8 = !{!"Apple clang version 11.0.0 (clang-1100.0.33.17)"}
!9 = !{}
生成汇编

生成汇编文件:clang -S main.m -o main_compile.s 并通过cat main_compile.s 查看文件内容。

删减版汇编文件

下述汇编文件是笔者参照网上资料做了部分注释后的内容。

// WYW:clang wangyongwangyongwang$ clang -S main.m -o main_compile.s
// WYW:clang wangyongwangyongwang$ cat main_compile.s
    
	
// 汇编指令
// .section 指令指定接下来会执行哪一个段。
    .section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 10, 15	sdk_version 10, 15
// .globl 指令说明 _main 是一个外部符号。这就是我们的 main() 函数。这个函数对于二进制文件外部来说是可见的,因为系统要调用它来运行可执行文件。
	.globl	_main                   ## -- Begin function main
// .align 指令指出了后面代码的对齐方式。在我们的代码中,后面的代码会按照 16(2^4) 字节对齐,如果需要的话,用 0x90 补齐。
	.p2align	4, 0x90


// main 函数的头部:
_main:                                  ## @main
	
// .cfi_startproc 指令通常用于函数的开始处。CFI 是调用帧信息 (Call Frame Information) 的缩写。这个调用 帧 以松散的方式对应着一个函数。它与后面的 .cfi_endproc 相匹配,以此标记出 main() 函数结束的地方。
    .cfi_startproc

## %bb.0:
// 在 OS X上,我们会有 X86_64 的代码,对于这种架构,有一个东西叫做 ABI ( 应用二进制接口 application binary interface),ABI 指定了函数调用是如何在汇编代码层面上工作的。在函数调用期间,ABI 会让 rbp 寄存器 (基础指针寄存器 base pointer register) 被保护起来。当函数调用返回时,确保 rbp 寄存器的值跟之前一样,这是属于 main 函数的职责。pushq %rbp 将 rbp 的值 push 到栈中,以便我们以后将其 pop 出来。
	pushq	%rbp

// 接下来是两个 CFI 指令:这将会输出一些关于生成调用堆栈展开和调试的信息。我们改变了堆栈和基础指针,而这两个指令可以告诉编译器它们都在哪儿,或者更确切的,它们可以确保之后调试器要使用这些信息时,能找到对应的东西。
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
// 接下来,movq %rsp, %rbp 将把局部变量放置到栈上。subq $32, %rsp 将栈指针移动 32 个字节,也就是函数会调用的位置。我们先将老的栈指针存储到 rbp 中,然后将此作为我们局部变量的基址,接着我们更新堆栈指针到我们将会使用的位置。
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	subq	$64, %rsp
	movl	%edi, -4(%rbp)
	movq	%rsi, -16(%rbp)
// 调用 autoreleasePoolPush
	callq	_objc_autoreleasePoolPush
	leaq	L__unnamed_cfstring_(%rip), %rsi
	movq	%rsi, %rdi
	movq	%rax, -40(%rbp)         ## 8-byte Spill
	movb	$0, %al
// 调用 NSLog
	callq	_NSLog
	leaq	L_.str.1(%rip), %rsi
	movq	%rsi, -24(%rbp)
	movq	-24(%rbp), %rsi
	leaq	L_.str.2(%rip), %rdi
	movb	$0, %al
// 调用 printf
	callq	_printf
	movq	L_OBJC_CLASSLIST_REFERENCES_$_(%rip), %rsi
	movq	-24(%rbp), %rdx
	movq	L_OBJC_SELECTOR_REFERENCES_(%rip), %rdi
	movq	%rdi, -48(%rbp)         ## 8-byte Spill
	movq	%rsi, %rdi
	movq	-48(%rbp), %rsi         ## 8-byte Reload
	movl	$4, %ecx
	movl	%eax, -52(%rbp)         ## 4-byte Spill
	
    callq	*_objc_msgSend@GOTPCREL(%rip)
	leaq	L__unnamed_cfstring_.4(%rip), %rcx
	movq	%rax, -32(%rbp)
	movq	-32(%rbp), %rsi
	movq	%rcx, %rdi
	movb	$0, %al

	callq	_NSLog
	movq	-40(%rbp), %rdi         ## 8-byte Reload
	callq	_objc_autoreleasePoolPop
	xorl	%eax, %eax
	addq	$64, %rsp
	popq	%rbp
	retq
// main() 函数结束的地方
	.cfi_endproc
                                        ## -- End function
	.section	__TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
	.asciz	"Hello Clang Compile"
2.5 生成目标文件

生成目标文件的命令为 clang -fmodules -c main.m -o main_obj.o。 可以通过 objdump 查看目标文件中的内容。 objdump -t main_obj.oobjdump -syms main_obj.o 用于查看目标文件符号表入口。 更多 objdump 命令可查看 14. objdump 二进制文件分析

// clang -fmodules -c main.m -o main_obj.o
// WYW:clang wangyongwangyongwang$ objdump -t main_obj.o

main_obj.o:	file format Mach-O 64-bit x86-64

SYMBOL TABLE:
0000000000000140 l     O __TEXT,__ustring	l_.str.3
0000000000000000 g     F __TEXT,__text	_main
0000000000000000         *UND*	_NSLog
0000000000000000         *UND*	_OBJC_CLASS_$_NSString
0000000000000000         *UND*	___CFConstantStringClassReference
0000000000000000         *UND*	_objc_autoreleasePoolPop
0000000000000000         *UND*	_objc_autoreleasePoolPush
0000000000000000         *UND*	_objc_msgSend
0000000000000000         *UND*	_printf

下列大部分内容引自:Mach-O 可执行文件

2.5.1 查看目标文件内容 section

一个可执行文件包含多个段(section)。可执行文件不同的部分将加载进不同的 section,并且每个 section 会转换进某个 segment 里。这个概念对于所有的可执行文件都是成立的。

我们来看看 a.out 二进制中的 section。我们可以使用 size 工具来观察

xcrun size -x -l -m main_obj.o

WYW:clang wangyongwangyongwang$ xcrun size -x -l -m main_obj.o
Segment : 0x1c0 (vmaddr 0x0 fileoff 1488)
	Section (__TEXT, __text): 0x9b (addr 0x0 offset 1488)
	Section (__TEXT, __cstring): 0x2f (addr 0x9b offset 1643)
	Section (__DATA, __cfstring): 0x40 (addr 0xd0 offset 1696)
	Section (__DATA, __objc_classrefs): 0x8 (addr 0x110 offset 1760)
	Section (__TEXT, __objc_methname): 0x1c (addr 0x118 offset 1768)
	Section (__DATA, __objc_selrefs): 0x8 (addr 0x138 offset 1800)
	Section (__TEXT, __ustring): 0x14 (addr 0x140 offset 1808)
	Section (__DATA, __objc_imageinfo): 0x8 (addr 0x154 offset 1828)
	Section (__LD, __compact_unwind): 0x20 (addr 0x160 offset 1840)
	Section (__TEXT, __eh_frame): 0x40 (addr 0x180 offset 1872)
	total 0x1b2
total 0x1c0

__TEXT segment 包含了被执行的代码。它被以只读和可执行的方式映射。进程被允许执行这些代码,但是不能修改。这些代码也不能对自己做出修改,因此这些被映射的页从来不会被改变。

__DATA segment 以可读写和不可执行的方式映射。它包含了将会被更改的数据。

otool(1) 来观察一个 section 中的内容:

xcrun otool -s __TEXT __text main_obj.o

WYW:clang wangyongwangyongwang$ xcrun otool -s __TEXT __text main_obj.o
main_obj.o:
Contents of (__TEXT,__text) section
0000000000000000	55 48 89 e5 48 83 ec 40 89 7d fc 48 89 75 f0 e8 
0000000000000010	00 00 00 00 48 8d 35 b5 00 00 00 48 89 f7 48 89 
0000000000000020	45 d8 b0 00 e8 00 00 00 00 48 8d 35 7f 00 00 00 
0000000000000030	48 89 75 e8 48 8b 75 e8 48 8d 3d 7d 00 00 00 b0 
0000000000000040	00 e8 00 00 00 00 48 8b 35 c3 00 00 00 48 8b 55 
0000000000000050	e8 48 8b 3d e0 00 00 00 48 89 7d d0 48 89 f7 48 
0000000000000060	8b 75 d0 b9 04 00 00 00 89 45 cc ff 15 00 00 00 
0000000000000070	00 48 8d 0d 78 00 00 00 48 89 45 e0 48 8b 75 e0 
0000000000000080	48 89 cf b0 00 e8 00 00 00 00 48 8b 7d d8 e8 00 
0000000000000090	00 00 00 31 c0 48 83 c4 40 5d c3 
2.5.2 反汇编

xcrun otool -v -t main_obj.o,通过添加 -v 来查看反汇编代码:

WYW:clang wangyongwangyongwang$ xcrun otool -v -t main_obj.o
main_obj.o:
(__TEXT,__text) section
_main:
0000000000000000	pushq	%rbp
0000000000000001	movq	%rsp, %rbp
0000000000000004	subq	$0x40, %rsp
0000000000000008	movl	%edi, -0x4(%rbp)
000000000000000b	movq	%rsi, -0x10(%rbp)
000000000000000f	callq	0x14
0000000000000014	leaq	0xb5(%rip), %rsi
000000000000001b	movq	%rsi, %rdi
000000000000001e	movq	%rax, -0x28(%rbp)
0000000000000022	movb	$0x0, %al
0000000000000024	callq	0x29
0000000000000029	leaq	0x7f(%rip), %rsi
0000000000000030	movq	%rsi, -0x18(%rbp)
0000000000000034	movq	-0x18(%rbp), %rsi
0000000000000038	leaq	0x7d(%rip), %rdi
000000000000003f	movb	$0x0, %al
0000000000000041	callq	0x46
0000000000000046	movq	0xc3(%rip), %rsi
000000000000004d	movq	-0x18(%rbp), %rdx
0000000000000051	movq	0xe0(%rip), %rdi
0000000000000058	movq	%rdi, -0x30(%rbp)
000000000000005c	movq	%rsi, %rdi
000000000000005f	movq	-0x30(%rbp), %rsi
0000000000000063	movl	$0x4, %ecx
0000000000000068	movl	%eax, -0x34(%rbp)
000000000000006b	callq	*(%rip)
0000000000000071	leaq	0x78(%rip), %rcx
0000000000000078	movq	%rax, -0x20(%rbp)
000000000000007c	movq	-0x20(%rbp), %rsi
0000000000000080	movq	%rcx, %rdi
0000000000000083	movb	$0x0, %al
0000000000000085	callq	0x8a
000000000000008a	movq	-0x28(%rbp), %rdi
000000000000008e	callq	0x93
0000000000000093	xorl	%eax, %eax
0000000000000095	addq	$0x40, %rsp
0000000000000099	popq	%rbp
000000000000009a	retq

otool(1) 可用于查看一个 section 中的内容:

WYW:clang wangyongwangyongwang$ xcrun otool -s __TEXT __cstring main_obj.o
main_obj.o:
Contents of (__TEXT,__cstring) section
000000000000009b	48 65 6c 6c 6f 20 43 6c 61 6e 67 20 43 6f 6d 70 
00000000000000ab	69 6c 65 00 51 69 53 68 61 72 65 22 57 59 57 22 
00000000000000bb	00 6d 65 6d 62 65 72 ef bc 9a 25 73 20 0a 00 
2.5.3 反汇编目标文件

下边我们可以反汇编目标文件,来查看逆向反汇编后得到的汇编文件,和之前正向用于生成目标文件的汇编文件是否一致。 下图表明,反汇编的汇编文件,和之前正向生成目标文件的汇编文件的内容基本一样。 只是反汇编文件中去除了部分命令。

目标文件反汇编

2.6 生成可执行文件及执行文件
// 生成执行文件
WYW:clang wangyongwangyongwang$ clang main_obj.o -o mainExec

// 执行可执行文件
WYW:clang wangyongwangyongwang$ ./mainExec
2020-04-01 22:12:15.766 mainExec[84398:2934623] Hello Clang Compile
member:QiShare"WYW" 
2020-04-01 22:12:15.767 mainExec[84398:2934623] member:QiShare"WYW"

四、参考学习网址