草庐IT

iOS 编译与链接一:编译的过程

Trigger_o 2023-03-28 原文

一:编译器

编译器是什么已不用多说,一句话从代码到机器码就是编译器的工作.


编译器的架构

左边输入源码,右边输出机器码

Frontend表示前端,主要负责词法分析、语法分析、语义分析、生成中间代码.这时就会进行各种检查,会报错或者警告.
Optimizer表示优化器,负责中间代码的优化,去除冗余代码,优化结构
Backend表示后端,生成机器码,并且进行链接,也就是将不同的二进制文件合并成一个可执行文件.

1.LLVM
Xcode5之后完全使用LLVM作为编译器.

LLVM的架构

LLVM也是上面说的那种Frontend -> Optimizer ->Backend架构.
不过LLVM路子很野,可以有很多个接口,也就是前端(Frontend),每一种前端对应一种或多种语言,这些前端最终都会生成相同的中间代码,叫做LLVM IR;
优化器的从始至终只处理LLVM IR.
新增一个前端不需要对LLVM的优化器进行调整,只需要新增一个前端; 增加一个新的平台只需要增加一个后端即可.
相比较而言,GCC就支持一个新的前端或者后端就要麻烦的多,原本的GCC家族(C,C++,OC),以及Java、.NET、Python、Ruby等都可以使用LLVM编译.

LLVM IR有3种表示形式,
存在内存中.
存在硬盘中的.ll代码文件,可以阅读.
存在硬盘中的二进制文件,扩展名是.bc,也就是bitcode.

2.Clang
Clang就是一个LLVM前端,负责将C,C++,OC翻译成LLVM IR.

Clang的工作内容:
预处理, 去掉注释,头文件的引用关系,把宏定义对应到各个位置
静态分析,给出错误信息,警告信息和修复方案
词法分析,这里会把代码切成一个个 Token,括号,符号,关键字等等都被切割出来
语法分析,验证语法是否正确,将所有节点组成抽象语法树AST
生成 LLVM IR, CodeGen会负责将语法树自顶向下遍历逐步翻译成 LLVM IR

3.Swift
同样LLVM中还需要一个前端负责对 Swift 源代码进行静态分析和纠错,并转换为 LLVM IR,这个前端也叫swift.
不过swift比clang的过程要复杂一些,多了一个生成SIL的过程.

Swift的工作内容:
导入Clang模块并将它们导出的OC API 映射到相应的 Swift API
解析生成AST
生成SIL,将经过类型检查的 AST 降级为 SIL
优化SIL,为程序执行额外的高级 Swift 特定优化,包括自动引用计数优化,虚拟化和通用专业化
将 SIL 降级到 LLVM IR

二.编译流程

引用戴铭老师的例子走一遍,原文

1.编译一个main.m
装了Xcode就自带LLVM,可以直接试一试.
创建一个项目,覆盖main.m的代码

#import <Foundation/Foundation.h>
#define DEFINEEight 8

#pragma 这是标记
//这是注释
int main(){
    @autoreleasepool {
        int eight = DEFINEEight;
        int six = 6;
        NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
        int rank = eight + six;
        NSLog(@"%@ rank %d", site, rank);
    }
    return 0;
}

运行:

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, "arm64", {5}, image

第0步引入文件
第1步预编译,输出c++文件
第2步编译为LLVM IR文件
第3步输出汇编文件
第4步输出二进制文件
第5步链接各二进制文件
第6步根据架构输出对应可执行文件

执行:

clang -E main.m

输出了非常多的东西,因为导入了foundation,先看最后几行

# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3
# 187 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 22 "main.m" 2


#pragma 这是标记

int main(){
    @autoreleasepool {
        int eight = 8;
        int six = 6;
        NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
        int rank = eight + six;
        NSLog(@"%@ rank %d", site, rank);
    }
    return 0;
}

除了foundation,还可以看到DEFINEEight被替换成了8,注释没了,但是#pragma还在
所以预编译就做了这些事:导入文件,去除注释,替换宏定义.

这一步会生成main.cpp文件,在main.m的同一路径.几万行代码,在最后可以找到替换为C++的main函数

int main(){
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int eight = 8;
        int six = 6;
        NSString* site = ((NSString * _Nullable (*)(id, SEL, const char * _Nonnull))(void *)objc_msgSend)((id)((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("alloc")), sel_registerName("initWithUTF8String:"), (const char *)"starming");
        int rank = eight + six;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_681pp9bd3c31j1_m0jqnt4h00000gn_T_main_3eda9c_mi_0, site, rank);
    }
    return 0;
}

接下来是词法分析
执行:

clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

输出

annot_module_include '#import <Foundation/Foundation.h>
#define DEFINEEight 8

#pragma 这是标记
//这是注释
int main(){
    @autoreleasepool {
        int eight = DEFINEEight;
        int six = 6;
        NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
        int rank = eight + six;
        NSLog(@"%@ rank %d", site, rank);
    }
    return 0;
}
@???W?\?V?A?`'      Loc=<main.m:21:1>
int 'int'    [StartOfLine]  Loc=<main.m:26:1>
identifier 'main'    [LeadingSpace] Loc=<main.m:26:5>
l_paren '('     Loc=<main.m:26:9>
r_paren ')'     Loc=<main.m:26:10>
l_brace '{'     Loc=<main.m:26:11>
at '@'   [StartOfLine] [LeadingSpace]   Loc=<main.m:27:5>
identifier 'autoreleasepool'        Loc=<main.m:27:6>
l_brace '{'  [LeadingSpace] Loc=<main.m:27:22>
int 'int'    [StartOfLine] [LeadingSpace]   Loc=<main.m:28:9>
identifier 'eight'   [LeadingSpace] Loc=<main.m:28:13>
equal '='    [LeadingSpace] Loc=<main.m:28:19>
numeric_constant '8'     [LeadingSpace] Loc=<main.m:28:21 <Spelling=main.m:22:21>>
semi ';'        Loc=<main.m:28:32>
int 'int'    [StartOfLine] [LeadingSpace]   Loc=<main.m:29:9>
identifier 'six'     [LeadingSpace] Loc=<main.m:29:13>
equal '='    [LeadingSpace] Loc=<main.m:29:17>
numeric_constant '6'     [LeadingSpace] Loc=<main.m:29:19>
semi ';'        Loc=<main.m:29:20>
identifier 'NSString'    [StartOfLine] [LeadingSpace]   Loc=<main.m:30:9>
star '*'        Loc=<main.m:30:17>
identifier 'site'    [LeadingSpace] Loc=<main.m:30:19>
equal '='    [LeadingSpace] Loc=<main.m:30:24>
l_square '['     [LeadingSpace] Loc=<main.m:30:26>
l_square '['        Loc=<main.m:30:27>
identifier 'NSString'       Loc=<main.m:30:28>
identifier 'alloc'   [LeadingSpace] Loc=<main.m:30:37>
r_square ']'        Loc=<main.m:30:42>
identifier 'initWithUTF8String'  [LeadingSpace] Loc=<main.m:30:44>
colon ':'       Loc=<main.m:30:62>
string_literal '"starming"'     Loc=<main.m:30:63>
r_square ']'        Loc=<main.m:30:73>
semi ';'        Loc=<main.m:30:74>
int 'int'    [StartOfLine] [LeadingSpace]   Loc=<main.m:31:9>
identifier 'rank'    [LeadingSpace] Loc=<main.m:31:13>
equal '='    [LeadingSpace] Loc=<main.m:31:18>
identifier 'eight'   [LeadingSpace] Loc=<main.m:31:20>
plus '+'     [LeadingSpace] Loc=<main.m:31:26>
identifier 'six'     [LeadingSpace] Loc=<main.m:31:28>
semi ';'        Loc=<main.m:31:31>
identifier 'NSLog'   [StartOfLine] [LeadingSpace]   Loc=<main.m:32:9>
l_paren '('     Loc=<main.m:32:14>
at '@'      Loc=<main.m:32:15>
string_literal '"%@ rank %d"'       Loc=<main.m:32:16>
comma ','       Loc=<main.m:32:28>
identifier 'site'    [LeadingSpace] Loc=<main.m:32:30>
comma ','       Loc=<main.m:32:34>
identifier 'rank'    [LeadingSpace] Loc=<main.m:32:36>
r_paren ')'     Loc=<main.m:32:40>
semi ';'        Loc=<main.m:32:41>
r_brace '}'  [StartOfLine] [LeadingSpace]   Loc=<main.m:33:5>
return 'return'  [StartOfLine] [LeadingSpace]   Loc=<main.m:34:5>
numeric_constant '0'     [LeadingSpace] Loc=<main.m:34:12>
semi ';'        Loc=<main.m:34:13>
r_brace '}'  [StartOfLine]  Loc=<main.m:35:1>
eof ''      Loc=<main.m:35:2>

可以看出词法分析只需要处理main.m中的代码,把所有的字符串,符号,括号都拆分开,拆出来的每一个部分,叫做token.

在接下来是语法分析
执行:

clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

输出

-FunctionDecl 0x15ab12390 <line:26:1, line:35:1> line:26:5 main 'int ()'
  `-CompoundStmt 0x15b026d48 <col:11, line:35:1>
    |-ObjCAutoreleasePoolStmt 0x15b026d00 <line:27:5, line:33:5>
    | `-CompoundStmt 0x15b026cc8 <line:27:22, line:33:5>
    |   |-DeclStmt 0x15ab12530 <line:28:9, col:32>
    |   | `-VarDecl 0x15ab124a8 <col:9, line:22:21> line:28:13 used eight 'int' cinit
    |   |   `-IntegerLiteral 0x15ab12510 <line:22:21> 'int' 8
    |   |-DeclStmt 0x15ab4e6f8 <line:29:9, col:20>
    |   | `-VarDecl 0x15ab12560 <col:9, col:19> col:13 used six 'int' cinit
    |   |   `-IntegerLiteral 0x15ab125c8 <col:19> 'int' 6
    |   |-DeclStmt 0x15b024be8 <line:30:9, col:74>
    |   | `-VarDecl 0x15ab4e750 <col:9, col:73> col:19 used site 'NSString *' cinit
    |   |   `-ObjCMessageExpr 0x15ab50b90 <col:26, col:73> 'NSString * _Nullable':'NSString *' selector=initWithUTF8String:
    |   |     |-ObjCMessageExpr 0x15ab4eb58 <col:27, col:42> 'NSString *' selector=alloc class='NSString'
    |   |     `-ImplicitCastExpr 0x15ab50b78 <col:63> 'const char * _Nonnull':'const char *' <NoOp>
    |   |       `-ImplicitCastExpr 0x15ab50b60 <col:63> 'char *' <ArrayToPointerDecay>
    |   |         `-StringLiteral 0x15ab4ebc8 <col:63> 'char [9]' lvalue "starming"
    |   |-DeclStmt 0x15b0252a8 <line:31:9, col:31>
    |   | `-VarDecl 0x15b024c18 <col:9, col:28> col:13 used rank 'int' cinit
    |   |   `-BinaryOperator 0x15b024d20 <col:20, col:28> 'int' '+'
    |   |     |-ImplicitCastExpr 0x15b024cf0 <col:20> 'int' <LValueToRValue>
    |   |     | `-DeclRefExpr 0x15b024c80 <col:20> 'int' lvalue Var 0x15ab124a8 'eight' 'int'
    |   |     `-ImplicitCastExpr 0x15b024d08 <col:28> 'int' <LValueToRValue>
    |   |       `-DeclRefExpr 0x15b024cb8 <col:28> 'int' lvalue Var 0x15ab12560 'six' 'int'
    |   `-CallExpr 0x15b026c48 <line:32:9, col:40> 'void'
    |     |-ImplicitCastExpr 0x15b026c30 <col:9> 'void (*)(id, ...)' <FunctionToPointerDecay>
    |     | `-DeclRefExpr 0x15b0252c0 <col:9> 'void (id, ...)' Function 0x15b024d48 'NSLog' 'void (id, ...)'
    |     |-ImplicitCastExpr 0x15b026c80 <col:15, col:16> 'id':'id' <BitCast>
    |     | `-ObjCStringLiteral 0x15b025340 <col:15, col:16> 'NSString *'
    |     |   `-StringLiteral 0x15b025318 <col:16> 'char [11]' lvalue "%@ rank %d"
    |     |-ImplicitCastExpr 0x15b026c98 <col:30> 'NSString *' <LValueToRValue>
    |     | `-DeclRefExpr 0x15b025360 <col:30> 'NSString *' lvalue Var 0x15ab4e750 'site' 'NSString *'
    |     `-ImplicitCastExpr 0x15b026cb0 <col:36> 'int' <LValueToRValue>
    |       `-DeclRefExpr 0x15b025398 <col:36> 'int' lvalue Var 0x15b024c18 'rank' 'int'
    `-ReturnStmt 0x15b026d38 <line:34:5, col:12>
      `-IntegerLiteral 0x15b026d18 <col:12> 'int' 0

这一步会检查语法的正确性,给出警告,报错,以及修改建议.生成的内容叫做抽象语法树AST.

生成AST之后就可以开始生成IR代码了
执行:

clang -O3 -S -fobjc-arc -emit-llvm main.m -o main.ll

输出main.ll文件,路径和main.m相同,是可读的.
这里"-O3"是LLVM的优化策略,有-O1,-O3,-Os,也可以不设置.
如果设置了bitcode,还可以进一步优化.

接下来生成汇编

clang -S -fobjc-arc main.m -o main.s

生成目标文件

clang -fmodules -c main.m -o main.o

生成可执行文件并执行

clang main.o -o main
./main

输出

starming rank 14

2.从Xcode观察编译过程

这里的过程可以在buildsetting,Build Phases 和 Build Rules中进行配置


查看

首先是预编译,可以看到new build system等内容.


预编译

然后是编译cocoapods的targets,包括创建framework(cocoapods使用use framework),copy头文件,以及编译.m文件


targets

接下来是主target,其实是与cocoapods的targes一致的,这一步主target也会被打包成framework.
也是拷贝.h文件,编译swift文件,编译.m文件,编译xib文件,拷贝资源文件,


主target

接下来执行cocoapods脚本,Build Phases的脚本.


脚本

最后拷贝swift标准库以及签名.


image.png

每一个都可以点开详情.
比如编译.m文件可以看到clang信息,这些基本是可以在build setting中进行配置的.
前面是CompileC任务描述
然后是切换路径
最后clang -x objective-c -target x86_64-apple-ios12.0-simulator ...就是编译的命令


编译一个.m文件
  • 编译的流程
    1.处理文件信息
    2.执行CocoaPod编译前脚本
    3.编译.m文件(h文件是不需要编译的),执行clang命令
    4.链接framework
    5.拷贝和编译xib,bundle文件
    6.编译 ImageAssets
    7.处理 info.plist
    8.执行CocoaPod脚本
    9.拷贝Swift标准库
    10.创建.app文件和签名

3.配置编译选项

  • Build settings设置在build的过程中各个阶段的选项,clang的配置就属于这个范围.

  • Build Phases构建可执行文件的规则,指定 target 的依赖项目,指定在target build之前需要先build的依赖.
    在Compile Source中指定必须编译的文件,这些文件同样会根据Build Setting和Build Rules里的设置来处理.
    在Link Binary With Libraries里会列出所有的静态库和动态库,它们会和编译生成的目标文件链接.
    把静态资源拷贝到bundle里.
    另外还可以通过在build phases里添加自定义脚本来做些事情,比如像CocoaPods所做的那样.

  • Build Rules指定不同文件类型如何编译,每条build rule指定了该类型如何处理以及输出在哪,可以增加新规则对特定文件类型添加处理方法.

上面这些都是在Xcode UI中可视化的,这些信息最终需要以文件的格式保存下来,那就是.pbxproj文件,
路径在[项目名称].xcodeproj包里的project.pbxproj.

打开这个文件,在最后一行有一个

rootObject = F7036F002511EC050031CE83 /* Project object */;

搜索这个rootObject ID,可以找到PBXProject section,
这个文件就是以section为单位描述配置.

/* Begin PBXProject section */
        F7036F002511EC050031CE83 /* Project object */ = {
            isa = PBXProject;
            attributes = {
                CLASSPREFIX = XX;
                LastUpgradeCheck = 1310;
                ORGANIZATIONNAME = XXXX;
                TargetAttributes = {
                    F7036F072511EC050031CE83 = {
                        CreatedOnToolsVersion = 11.6;
                        LastSwiftMigration = 1240;
                    };
                };
            };
    ...

这PBXProject section里找到target

targets = (
            F7036F072511EC050031CE83 /* XXXXX */,
        );

再搜索这个ID,就可以找到更多配置,这个.pbxproj文件就是以这种id索引的方式进行记录和查找.
比如继续顺着这个ID,可以找到更多的定义,可以看到buildPhases,buildConfiguration.
再顺着找可以看到cocoapods, copy resource等等定义.

/* Begin PBXNativeTarget section */
        F7036F072511EC050031CE83 /* XXXXX */ = {
            isa = PBXNativeTarget;
            buildConfigurationList = F7036F212511EC080031CE83 /* Build configuration list for PBXNativeTarget "XXXXX" */;
            buildPhases = (
                2E9C3A1138A1AED934114EBC /* [CP] Check Pods Manifest.lock */,
                F7036F042511EC050031CE83 /* Sources */,
                F7036F052511EC050031CE83 /* Frameworks */,
                F7036F062511EC050031CE83 /* Resources */,
                34DB7EF940E3C9401AD2798F /* [CP] Embed Pods Frameworks */,
                25DB3E569FEFB4DC91CF364D /* [CP] Copy Pods Resources */,
            );
  ...

有关iOS 编译与链接一:编译的过程的更多相关文章

  1. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  2. ruby-on-rails - Ruby url 到 html 链接转换 - 2

    我正在使用Rails构建一个简单的聊天应用程序。当用户输入url时,我希望将其输出为html链接(即“url”)。我想知道在Ruby中是否有任何库或众所周知的方法可以做到这一点。如果没有,我有一些不错的正则表达式示例代码可以使用... 最佳答案 查看auto_linkRails提供的辅助方法。这会将所有URL和电子邮件地址变成可点击的链接(htmlanchor标记)。这是文档中的代码示例。auto_link("Gotohttp://www.rubyonrails.organdsayhellotodavid@loudthinking.

  3. Ruby 文件 IO 定界符? - 2

    我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

  4. ruby - Sinatra set cache_control to static files in public folder编译错误 - 2

    我不知道为什么,但是当我设置这个设置时它无法编译设置:static_cache_control,[:public,:max_age=>300]这是我得到的syntaxerror,unexpectedtASSOC,expecting']'(SyntaxError)set:static_cache_control,[:public,:max_age=>300]^我只想将“过期”header设置为css、javaascript和图像文件。谢谢。 最佳答案 我猜您使用的是Ruby1.8.7。Sinatra文档中显示的语法似乎是在Ruby1.

  5. ruby-on-rails - Prawn - 表格单元格内的链接 - 2

    我正在尝试用Prawn生成PDF。在我的PDF模板中,我有带单元格的表格。在其中一个单元格中,我有一个电子邮件地址:cell_email=pdf.make_cell(:content=>booking.user_email,:border_width=>0)我想让电子邮件链接到“mailto”链接。我知道我可以这样链接:pdf.formatted_text([{:text=>booking.user_email,:link=>"mailto:#{booking.user_email}"}])但是将这两行组合起来(将格式化文本作为内容)不起作用:cell_email=pdf.make_c

  6. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

  7. 安卓apk修改(Android反编译apk) - 2

    最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路

  8. ruby - 为什么不能使用类IO的实例方法noecho? - 2

    print"Enteryourpassword:"pass=STDIN.noecho(&:gets)puts"Yourpasswordis#{pass}!"输出:Enteryourpassword:input.rb:2:in`':undefinedmethod`noecho'for#>(NoMethodError) 最佳答案 一开始require'io/console'后来的Ruby1.9.3 关于ruby-为什么不能使用类IO的实例方法noecho?,我们在StackOverflow上

  9. ruby - 使用 Watir 检查错误链接 - 2

    我有一个未排序的链接列表,我将其保存在旁边,我想单击每个链接并确保它转到真实页面而不是404、500等。问题是我不知道该怎么做。是否有一些我可以检查的对象会给我http状态代码或任何东西?mylinks=Browser.ul(:id,'my_ul_id').linksmylinks.eachdo|link|link.click#needtocheckfora200statusorsomethinghere!how?Browser.backend 最佳答案 我的回答与铁皮人的想法类似。require'net/http'require'

  10. ruby - 如何为 pbcopy 生成富文本链接 - 2

    我一直在玩一个脚本,它在Chrome中获取选定的文本并在Google中查找它,提供四个最佳选择,然后粘贴相关链接。它以不同的格式粘贴,具体取决于当前在Chrome中打开的页面-DokuWiki打开的DokuWiki格式,普通网站的HTML,我想要我的WordPress所见即所得编辑器的富文本。我尝试使用pbpaste-Preferrtf来查看没有其他样式的富文本链接在粘贴板上的样子,但它仍然输出纯文本。在文本编辑中保存文件并进行试验后,我想出了以下内容text=%q|{\rtf1{\field{\*\fldinst{HYPERLINK"URL"}}{\fldrsltTEXT}}}|te

随机推荐