Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

编译、链接和装载 #47

Open
ShannonChenCHN opened this issue Apr 29, 2017 · 9 comments
Open

编译、链接和装载 #47

ShannonChenCHN opened this issue Apr 29, 2017 · 9 comments

Comments

@ShannonChenCHN
Copy link
Owner

ShannonChenCHN commented Apr 29, 2017


@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 26, 2017

延伸阅读

@ShannonChenCHN ShannonChenCHN changed the title 【专题】编译原理 编译原理 Jul 2, 2017
@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Oct 11, 2017

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Feb 27, 2018

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jan 28, 2019

关于静态库、动态库和 framework

1. 静态库(Static Libraries)

什么是目标文件(object file)?

使用 objdump -macho -section-headers /bin/ls 可以查看可执行文件 /bin/ls 的信息。

目标文件有四种形式:

  • 可重定位目标文件(relocatable)
  • 可执行目标文件(executable)
  • 共享目标文件(shared)
  • bundle

如果你编译一个没有 main 函数的 C 源代码文件,得到的结果就是一个 relocatable 的目标文件。

多个目标文件可以被归档成一个 .a 文件,这个 .a 文件就是一个静态库。

.a 文件在链接成可执行文件时,其中的地址都是固定的。因此,可执行文件越大,加载进内存就越慢。如果有 10 个程序用到同一个静态库,那么每个程序都会有一份拷贝。

另外,如果这个 .a 静态库的作者修改了源代码,那么就需要重新编译和分发包含这个静态库的可执行文件了。

延伸阅读:
https://zh.wikipedia.org/wiki/%E7%9B%AE%E6%A0%87%E4%BB%A3%E7%A0%81
https://github.com/johnno1962/injectionforxcode
https://blog.sunnyxx.com/2014/08/30/objc-pre-main/
https://stackoverflow.com/questions/40841670/what-is-the-difference-between-dylib-and-a-lib-in-ios

2. 动态库(Dynamic Libraries)

动态库也是由目标文件打包而成的,它可以被加载到内存中非固定的地址,并且内存中的程序可以在加载时或者运行时链接这些动态库。

MacOS 上的动态链接器(Dynamic Linker)dyld 专门负责这些动态链接。

MacOS 上的动态库一般以 .dylib 作为文件后缀,Windows 上的动态库一般以 .DLL 作为文件后缀,Linux 上是 .so

相比于静态库是在编译时由静态链接器静态链接的,动态库是在加载时或者运行时由动态链接库动态链接的。

动态库具有静态库不具备的一些优点,比如上面提到的那几点——内存中的多个程序库可以共享同一个动态库;动态库更新后不需要更新可执行文件。

虽然动态库更新后不需要更新可执行文件,但是你需要考虑 API 的向后兼容。

3. Frameworks and Their Structure

Framework 实际上是一个包装各种资源的文件夹,其中包含了 images, xibs, dynamic libraries, static libraries, documentation files, localization files 等文件。

Framework 的目录结构:

  • Headers
  • Modules
  • Resources
  • Unix executable
  • All of the Symlinks

4. Linking vs Embedding Frameworks in Xcode

在 Xcode 的 target->build settings 中有 Linked Frameworks and LibrariesEmbed Binaries 两个 section:

  • Embed Binaries:Embedding 会复制一份 framework 到你的 app bundle 的 Frameworks 目录下。Embed Binaries 是动态加载的,跟动态库一样,因为系统的 framework 是系统内置的,而我们自己自定义的 framework 是不会存在于系统目录中的,所以就需要打到 app bundle 中。
  • Linked Frameworks and Libraries:除非你需要在运行时链接和加载库,一般我们都是在这个 section 添加所需的库。

读后感:光读文章感觉很难理解,如果有配套的实例,跟着操作一遍就好理解多了。文章末尾的参考资料都非常赞。

参考

延伸阅读

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jan 30, 2019

Xcode Build System

一、Language Processing System

我们平时在开发应用程序时都是用高级语言编写代码的,但是计算机硬件执行只认识机器代码,所以我们需要一个机制将高级语言编写的源代码文件转成二进制可执行文件,这就是 Language Processing System 所做的工作。

在 iOS 和 MacOS 应用开发中也有一个专门的 Language Processing System,叫做 Xcode Build System。

二、Xcode Build System

language processing systems 一般包括以下 5 个部分:

  • Preprocessor
  • Compiler
  • Assembler
  • Linker
  • Loader

image

1. 预处理

预处理就是把你写的代码转成编译器可以处理的源代码,主要工作是替换宏定义、分析依赖和解析预处理命令(比如 #if)。

Swift 编译器没有预处理器,当然,在 Swift 项目中也看不到宏。

在 Xcode 面板中有一个 preprocess 的选项可以查看 Objective-C 代码被预处理后的结果。

2. 编译器

编译过程实际上就是将 C/C++、Objective-C 这种高级语言的代码翻译成机器代码的过程。

Xcode 中有两种不同的编译器,一种是 Swift 的编译器,另一种是 C 家族语言的编译器。

clang 是苹果官方提供的专门用来编译 C 家族语言的编译器,而负责编译 Swift 的编译器是 swiftc

image

编译器一般分为两个部分:

  • 前端:编译器前端将源代码拆分成不含语义和类型信息的程序块,同时给这些程序块加上一个语法结构,然后编译器根据这个结构生成中间代码(intermediate representation),同时创建和管理一个包含源代码信息的符号表。
  • 后端:编译器后端负责将中间代码(intermediate representation)转成汇编代码。

什么是符号?
符号就是一段代码或者数据的名字。符号表中保存的是变量、函数和类的名字,每个符号对应一个函数、一个全局变量或者一个静态变量。

3. 汇编器

汇编器负责将汇编代码转成机器代码,也就是可重定位目标文件。

Mac OS 上的目标文件就是我们经常所说的 Mach-O 文件。

4. 链接器

链接器负责将汇编器生成的目标文件和库合并成一个 Mach-O 可执行文件。

链接器只接受两种形式的输入源:

  • 汇编阶段产生的目标文件
  • 库(.a.dylib.tbd)

链接器生成的 Mach-O 文件和汇编阶段产生的 Mach-O 文件有什么区别吗?
链接器生成的 Mach-O 文件是可以直接在 Mac OS 上直接运行的,而汇编阶段产生的 Mach-O 文件实际上不是一个完整的目标文件,因为其中可能引用了其他目标文件或者库。
比如说你在你的代码中调用了 printf 函数,而这个 printf 函数定义在 libc 库中,链接器就会把你代码中的这个符号跟libc 库中定义该函数的地方关联起来。链接器会借助编译阶段创建的符号表来解析不同目标文件、静态库之间的引用。

5. 加载器

加载器实际上是操作系统的一部分,加载器将一个程序(也就是可执行文件)加载到内存中并且执行它,加载器会为程序的执行申请空间,并将寄存器设置为初始状态。

参考

@ShannonChenCHN ShannonChenCHN changed the title 编译原理 编译、链接和装载 Jan 30, 2019
@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jan 31, 2019

Xcode Build Process

一、build 日志

在 Xcode 的 Report navigator 中可以看到 build 日志(如下图所示)。

2019-1-31 2 30

每一行任务就是一个 task,每个 task显示的信息有状态、任务名、耗时,点击最右边的箭头,可以展开每条任务的执行细节。类似这种格式:

CompileC /Users/xianglongchen/Library/Developer/Xcode/DerivedData/Playground_iOS-hfqoqrhlwhirthhjzanxhdrxyfoz/Build/Intermediates.noindex/Pods.build/Debug-iphonesimulator/SDWebImage.build/Objects-normal/x86_64/UIView+WebCacheOperation.o /Users/xianglongchen/Desktop/Playground_iOS/Pods/SDWebImage/SDWebImage/UIView+WebCacheOperation.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler (in target: SDWebImage)
    cd /Users/xianglongchen/Desktop/Playground_iOS/Pods
    export LANG=en_US.US-ASCII
...

可以看到上面的任务细节中其实就是由很多条 shell 命令组成的,包括 CompileCLd等。

题外话:在 build log 中可以看到 Xcode 在构建过程中会在 DerivedData 这个目录下生成一些临时数据

二、build 流程设置

1. Build Phases

Build Phases 从一个高层面上展示了整个构建过程,我们可以在 Build Phases 里设置各个 Phases 的配置项,也可以新加一个或者多个 Build Phase。

2019-1-31 2 53

上图中所示的工程一共有 5 个 Phases,build 时会按照其指定的顺序执行:

  • Target Dependencies:用来告诉 build system 在开始构建当前 target 之前需要先构建好哪些 target
  • [CP]Check Pods Manifest.lock:这是 CocoaPods 自动添加的一个 phase,因为项目中使用了 CocoaPods
  • Compile Sources:告诉 build system 哪些资源需要参与编译
  • Link Binary With Libraries:告诉 build system 在当前 target 的所有源码编译成目标文件后要跟哪些库链接
  • Copy Bundle Resources:拷贝静态资源(比如图片、字体)到 app bundle 中

自定义 Build Phases

就像 CocoaPods 所做的那样,我们可以添加自定义的 Phases,自定义 Build Phases 可以用来执行脚本、拷贝资源等等。

2. Build Rules

Build Rules 用来指定不同文件类型分别应该如何被编译。通常情况下,我们不需要修改默认的规则,但是如果你想为一些新的文件类型添加自定义处理,你可以添加新的编译规则。

3. Build Settings

在 Build Settings 中,我们可以设置每一个具体的 build 任务的细节。

Build Settings 中提供的选项非常丰富,涉及到了 build 的每个阶段,从编译到链接再到代码签名和打包。

更详细介绍的见 Xcode Build System Guide(最新版的 Build Settings Reference 可以在 Xcode Help中查看: 1. In Xcode, choose Help > Xcode Help, or open the Xcode Help website.2. Search for “build settings.”)。

Tips:
在 Xcode 的 build setting 里面也可以通过 “option+鼠标左键双击” 来显示帮助弹窗。
image

三、Project 文件和 Workspace

Xcode Concepts: Xcode Project - Apple Developer

An Xcode project is a repository for all the files, resources, and information required to build one or more software products. A project contains all the elements used to build your products and maintains the relationships between those elements. It contains one or more targets, which specify how to build products. A project defines default build settings for all the targets in the project (each target can also specify its own build settings, which override the project build settings).

Xcode Concepts: Xcode Workspace - Apple Developer

A workspace is an Xcode document that groups projects and other documents so you can work on them together. A workspace can contain any number of Xcode projects, plus any other files you want to include. In addition to organizing all the files in each Xcode project, a workspace provides implicit and explicit relationships among the included projects and their targets.

  • Workspaces Extend the Scope of Your Workflow
  • Projects in a Workspace Share a Build Directory

我们在前面所讨论的所有设置最终都会被保存到 Xcode Project 文件中去,Xcode Project 文件的格式是 .xcodeproj,这个文件实际上是一个文件夹。

平时我们基本上不会去看这个 .xcodeproj 文件的细节,除非你在 git merge 时遇到了冲突(严格上来讲是 .xcodeproj 文件夹中的 project.pbxproj 文件)。

我们可以把这个 project.pbxproj 文件在文本编辑器中打开,这个文件中的内容的可读性还是比较高的。

下面是一个示例项目的 project.pbxproj 文件简化后的内容(只保留了 PBXProject 部分的内容):

// !$*UTF8*$!
{
	archiveVersion = 1;
	classes = {
	};
	objectVersion = 50;
	objects = {

/* Begin PBXBuildFile section */
/* End PBXBuildFile section */

/* Begin PBXBuildRule section */
/* End PBXBuildRule section */

/* Begin PBXFileReference section */
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
/* End PBXNativeTarget section */

/* Begin PBXProject section */
		856951B22201BD8C0098E0BE /* Project object */ = {
			isa = PBXProject;
			attributes = {
				LastUpgradeCheck = 1000;
				ORGANIZATIONNAME = xianglongchen;
				TargetAttributes = {
					856951B92201BD8C0098E0BE = {
						CreatedOnToolsVersion = 10.0;
					};
				};
			};
			buildConfigurationList = 856951B52201BD8C0098E0BE /* Build configuration list for PBXProject "Playground_iOS" */;
			compatibilityVersion = "Xcode 9.3";
			developmentRegion = en;
			hasScannedForEncodings = 0;
			knownRegions = (
				en,
				Base,
			);
			mainGroup = 856951B12201BD8C0098E0BE;
			productRefGroup = 856951BB2201BD8C0098E0BE /* Products */;
			projectDirPath = "";
			projectRoot = "";
			targets = (
				856951B92201BD8C0098E0BE /* Playground_iOS */,
			);
		};
/* End PBXProject section */

/* Begin PBXResourcesBuildPhase section */
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
/* End PBXSourcesBuildPhase section */

/* Begin PBXVariantGroup section */
/* End PBXVariantGroup section */

/* Begin XCBuildConfiguration section */
/* End XCBuildConfiguration section */

/* Begin XCConfigurationList section */
/* End XCConfigurationList section */
	};
	rootObject = 856951B22201BD8C0098E0BE /* Project object */;
}

可以看到这个 project.pbxproj 文件中的内容非常清晰,甚至还有注释,仔细对比一下,跟 Xcode 中打开时的信息可以一一对应起来,包括文件目录结构、build settings、build phases、build rules、target 等。

当你需要自动添加文件到 Xcode 工程中来时,可能就需要好好了解一下这个 project.pbxproj 文件了,不过已经有不少大神们踩过坑了,目前已知的一些不错的解决方案:

参考

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Feb 1, 2019

The Compiler

一、编译器做了什么?

预处理(Preprocessing) -> 扫描(Tokenization/Lexing) -> 语法分析(Parsing) -> 语义分析(Static Analysis) -> 源代码优化 -> 代码生成 -> 目标代码优化

1. 预处理(Preprocessing)

宏替换

2. 扫描(Tokenization/Lexing)

源文件内容如下:

int main() {
  NSLog(@"hello, %@", @"world");
  return 0;
}

使用下面的命令提取标记:

$ clang -Xclang -dump-tokens hello.m

结果如下:

int 'int'	 [StartOfLine]	Loc=<hello.m:1:1>
identifier 'main'	 [LeadingSpace]	Loc=<hello.m:1:5>
l_paren '('		Loc=<hello.m:1:9>
r_paren ')'		Loc=<hello.m:1:10>
l_brace '{'	 [LeadingSpace]	Loc=<hello.m:1:12>
identifier 'NSLog'	 [StartOfLine] [LeadingSpace]	Loc=<hello.m:2:3>
l_paren '('		Loc=<hello.m:2:8>
at '@'		Loc=<hello.m:2:9>
string_literal '"hello, %@"'		Loc=<hello.m:2:10>
comma ','		Loc=<hello.m:2:21>
at '@'	 [LeadingSpace]	Loc=<hello.m:2:23>
string_literal '"world"'		Loc=<hello.m:2:24>
r_paren ')'		Loc=<hello.m:2:31>
semi ';'		Loc=<hello.m:2:32>
return 'return'	 [StartOfLine] [LeadingSpace]	Loc=<hello.m:3:3>
numeric_constant '0'	 [LeadingSpace]	Loc=<hello.m:3:10>
semi ';'		Loc=<hello.m:3:11>
r_brace '}'	 [StartOfLine]	Loc=<hello.m:4:1>
eof ''		Loc=<hello.m:4:2>

3. 语法分析(Parsing)

ObjCInterfaceDecl 0x7fa91ce7c470 <hello.m:3:1, line:5:2> line:3:12 World
| |-ObjCImplementation 0x7fa91ce7c608 'World'
| `-ObjCMethodDecl 0x7fa91ce7c580 <line:4:1, col:14> col:1 - hello 'void'
|-ObjCImplementationDecl 0x7fa91ce7c608 <line:7:1, line:11:1> line:7:17 World
| |-ObjCInterface 0x7fa91ce7c470 'World'
| `-ObjCMethodDecl 0x7fa91ce7c6a0 <line:8:1, line:10:1> line:8:1 - hello 'void'
|   |-ImplicitParamDecl 0x7fa91ce7c758 <<invalid sloc>> <invalid sloc> implicit self 'World *'
|   |-ImplicitParamDecl 0x7fa91ce7c7b8 <<invalid sloc>> <invalid sloc> implicit _cmd 'SEL':'SEL *'
|   `-CompoundStmt 0x7fa91ce7c958 <col:15, line:10:1>
|     `-CallExpr 0x7fa91ce7c910 <line:9:3, col:24> 'void'
|       |-ImplicitCastExpr 0x7fa91ce7c8f8 <col:3> 'void (*)(id, ...)' <FunctionToPointerDecay>
|       | `-DeclRefExpr 0x7fa91ce7c818 <col:3> 'void (id, ...)' Function 0x7fa919d19960 'NSLog' 'void (id, ...)'
|       `-ImplicitCastExpr 0x7fa91ce7c940 <col:9, col:10> 'id':'id' <BitCast>
|         `-ObjCStringLiteral 0x7fa91ce7c878 <col:9, col:10> 'NSString *'
|           `-StringLiteral 0x7fa91ce7c840 <col:10> 'char [13]' lvalue "hello, world"
`-FunctionDecl 0x7fa91ce7c9b0 <line:13:1, line:16:1> line:13:5 main 'int ()'
  `-CompoundStmt 0x7fa91ce7cba8 <col:12, line:16:1>
    |-DeclStmt 0x7fa91ce7cb20 <line:14:4, col:30>
    | `-VarDecl 0x7fa91ce7ca68 <col:4, col:29> col:11 used world 'World *' cinit
    |   `-ImplicitCastExpr 0x7fa91ce7cb08 <col:19, col:29> 'World *' <BitCast>
    |     `-ObjCMessageExpr 0x7fa91ce7cad8 <col:19, col:29> 'id':'id' selector=new class='World'
    `-ObjCMessageExpr 0x7fa91ce7cb78 <line:15:4, col:16> 'void' selector=hello
      `-ImplicitCastExpr 0x7fa91ce7cb60 <col:5> 'World *' <LValueToRValue>
        `-DeclRefExpr 0x7fa91ce7cb38 <col:5> 'World *' lvalue Var 0x7fa91ce7ca68 'world' 'World *'

4. 语义分析(Static Analysis)

  • 类型检测(Type Checking)

两种类型推断:静态类型和动态类型

  • 其他分析
    • 不用的变量
    • 内存泄漏
    • ...

5. 代码生成

#include <stdio.h>
#import <Foundation/Foundation.h>

int main() {
  NSLog(@"%@", [@5 description]);
  return 0;
}

...

@__CFConstantStringClassReference = external global [0 x i32]
@.str = private unnamed_addr constant [3 x i8] c"%@\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 ([3 x i8], [3 x i8]* @.str, i32 0, i32 0), i64 2 }, section "__DATA,__cfstring", align 8
@"OBJC_CLASS_$_NSNumber" = external global %struct._class_t
@"OBJC_CLASSLIST_REFERENCES_$_" = private global %struct._class_t* @"OBJC_CLASS_$_NSNumber", section "__DATA,__objc_classrefs,regular,no_dead_strip", align 8
@OBJC_METH_VAR_NAME_ = private unnamed_addr constant [15 x i8] c"numberWithInt:\00", section "__TEXT,__objc_methname,cstring_literals", align 1
@OBJC_SELECTOR_REFERENCES_ = private externally_initialized global i8* getelementptr inbounds ([15 x i8], [15 x i8]* @OBJC_METH_VAR_NAME_, i64 0, i64 0), section "__DATA,__objc_selrefs,literal_pointers,no_dead_strip", align 8
@OBJC_METH_VAR_NAME_.1 = private unnamed_addr constant [12 x i8] c"description\00", section "__TEXT,__objc_methname,cstring_literals", align 1
@OBJC_SELECTOR_REFERENCES_.2 = private externally_initialized global i8* getelementptr inbounds ([12 x i8], [12 x i8]* @OBJC_METH_VAR_NAME_.1, i64 0, i64 0), section "__DATA,__objc_selrefs,literal_pointers,no_dead_strip", align 8
@llvm.compiler.used = appending global [5 x i8*] [i8* bitcast (%struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_" to i8*), i8* getelementptr inbounds ([15 x i8], [15 x i8]* @OBJC_METH_VAR_NAME_, i32 0, i32 0), i8* getelementptr inbounds ([12 x i8], [12 x i8]* @OBJC_METH_VAR_NAME_.1, i32 0, i32 0), i8* bitcast (i8** @OBJC_SELECTOR_REFERENCES_ to i8*), i8* bitcast (i8** @OBJC_SELECTOR_REFERENCES_.2 to i8*)], section "llvm.metadata"

; Function Attrs: ssp uwtable
define i32 @main() local_unnamed_addr #0 {
  %1 = load i8*, i8** bitcast (%struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_" to i8**), align 8
  %2 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !8
  %3 = tail call %0* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to %0* (i8*, i8*, i32)*)(i8* %1, i8* %2, i32 5)
  %4 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.2, align 8, !invariant.load !8
  %5 = bitcast %0* %3 to i8*
  %6 = tail call %1* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to %1* (i8*, i8*)*)(i8* %5, i8* %4)
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*), %1* %6)
  ret i32 0
}

...

6. 目标代码优化

  • 尾递归优化

二、实践应用

1. 使用 libclang 或者 clang 插件打造你自己的 clang

  • libclang(c)
  • ClangKit(Objective-C)
  • LibTooling(C++)

2. 借助 LibTooling 自己写一个代码分析器

参考

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Feb 1, 2019

Mach-O 可执行文件

这篇文章主要了介绍以下两点:

  • 从源代码到可执行文件,编译器都做了什么?
  • Mach-O 可执行文件里面是什么?

注:这篇文章的讨论和示例不使用 Xcode,只使用命令行。

准备工作:Xcode 工具链

xcrun 是 Xcode 基本的命令行工具,使用 xcrun 可以调用其他工具。

比如查看 clang 的版本,我们可以执行下面的命令:

$ xcrun clang -v

而不是:

$ clang -v

如果要使用某个工具,直接执行那个工具的命令就行了,为什么要使用 xcrun 呢?
因为如果你的电脑上安装有多个不同版本的 Xcode,借助 xcrunxcode-select 你可以:

  • 选择指定 Xcode 版本下的工具
  • 选择指定 Xcode 版本下的 SDK

如果你的电脑上只安装了一个 Xcode,就没必要使用 xcrun 了。

一、不使用 IDE 来实现一个 Hello World

使用 clang 编译一个简单的 Hello World 小程序,然后就可以直接执行最后生成的 a.out 文件了。

编写 helloworld.c:

#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("Hello World!\n");
    return 0;
}

然后使用 clang 将该文件编译成一个 Mach-O 二进制文件 a.out,并执行这个 a.out 文件:

$ xcrun clang helloworld.c
$ ./a.out

最终可以看到终端上输出了 Hello World!

这个 a.out 是怎么生成的呢?

二、编译器是如何工作的

在上面的例子中,我们所选用的编译器是 clang,编译器在将 helloworld.c 编译成一个可执行文件时,需要经过好几步。

编译器处理的几个步骤:

  • Preprocessing
    • Tokenization
    • Macro expansion
    • #include expansion
  • Parsing and Semantic Analysis
    • Translates preprocessor tokens into a parse tree
    • Applies semantic analysis to the parse tree
    • Outputs an Abstract Syntax Tree (AST)
  • Code Generation and Optimization
    • Translates an AST into low-level intermediate code (LLVM IR)
    • Responsible for optimizing the generated code
    • target-specific code generation
    • Outputs assembly
  • Assembler
    • Translates assembly code into a target object file
  • Linker
    • Merges multiple object files into an executable (or a dynamic library)

1. 预处理

这个过程主要是对源代码进行标记拆分、宏展开、#include 展开等等。

使用下面的命令可以看到 helloworld.c 预处理后的结果:

$ xcrun clang -E helloworld.c

我们也可以将输出的结果在文本编辑器中打开:

$ xcrun clang -E helloworld.c | open -f

最后得到的预处理结果大概有 542 行:

...

# 52 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/secure/_stdio.h" 3 4
extern int __snprintf_chk (char * restrict, size_t, int, size_t,
      const char * restrict, ...);

extern int __vsprintf_chk (char * restrict, int, size_t,
      const char * restrict, va_list);

extern int __vsnprintf_chk (char * restrict, size_t, int, size_t,
       const char * restrict, va_list);
# 412 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/stdio.h" 2 3 4
# 2 "helloworld.c" 2
int main(int argc, char *argv[])
{
    printf("Hello World!\n");
    return 0;
}

与处理结果中那些 # 开头的语句表示行标记(linemarker),告诉我们后面接下来的内容来自哪个文件的哪一行。

helloworld.c 中的 #include <stdio.h> 告诉预处理器要在那个地方插入 stdio.h 的内容。这是一个递归的过程,如果 stdio.h 中也引入了其他的 .h 文件,在预处理时同样也会把这些语句替换成源文件中的内容。

Tips: 在 Xcode 中打开菜单 Product -> Perform Action -> Preprocess,可以查看当前打开文件的预处理结果。

2. 编译

这个过程主要是对预处理后的代码进行语法分析、语义分析,并生成语法树(AST),然后再翻译成中间代码,并优化代码,最后再针对不同平台生成对应的代码,并转成汇编代码。

我们可以使用下面的命令生成汇编代码:

$ xcrun clang -S -o - helloworld.c | open -f

生成的汇编代码如下:

  .section  __TEXT,__text,regular,pure_instructions
  .macosx_version_min 10, 13
  .globl  _main                   ## -- Begin function main
  .p2align  4, 0x90
_main:                                  ## @main
  .cfi_startproc
## %bb.0:
  pushq %rbp
  .cfi_def_cfa_offset 16
  .cfi_offset %rbp, -16
  movq  %rsp, %rbp
  .cfi_def_cfa_register %rbp
  subq  $32, %rsp
  leaq  L_.str(%rip), %rax
  movl  $0, -4(%rbp)
  movl  %edi, -8(%rbp)
  movq  %rsi, -16(%rbp)
  movq  %rax, %rdi
  movb  $0, %al
  callq _printf
  xorl  %ecx, %ecx
  movl  %eax, -20(%rbp)         ## 4-byte Spill
  movl  %ecx, %eax
  addq  $32, %rsp
  popq  %rbp
  retq
  .cfi_endproc
                                        ## -- End function
  .section  __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
  .asciz  "Hello World!\n"


.subsections_via_symbols

. 开头的是汇编器的指令。

.section 指令表示的是接下来的 section 是什么内容。

.globl 指令表示 _main 是一个外部符号,也就是要暴露给其他模块使用的符号。

.p2align 指令表示的是字节对齐的规则是什么。

.cfi_startproc 表示一个函数的开始,相应地,.cfi_endproc表示一个函数的结束。cfi 是 Call Frame Information 的缩写。

.cfi_def_cfa_offset 16.cfi_offset %rbp, -16 也是 cfi 指令,用来输出一些函数堆栈展开信息和调试信息的。

L_.str 标签可以让我们在代码中通过指针访问到一个字符串常量。

.asciz 命令告诉汇编器输出一个字面量字符串。

最后的 .subsections_via_symbols 是留给静态链接编辑器使用的。

Tips: 类似地,在 Xcode 中打开菜单 Product -> Perform Action -> Assemble,可以查看当前打开文件的汇编代码。

3. 汇编

汇编的过程就是将汇编代码翻译成机器代码,生成目标文件。

当你用 Xcode 构建你的 iOS App 时,你可以在你的项目的 Derived Data 目录下找到一个 Objects-normal 文件夹,里面就是 .m 文件编译后生成的目标文件。

4. 链接

链接器负责将各个目标文件和库合并成一个完整的可执行文件。在这个过程中,链接器需要解析各个目标文件和库之间的符号引用。

helloworld.c 中调用了 printf() 函数,这个函数定义在 libc 库中,但是最终的可执行文件需要知道 printf() 在内存中的什么地方,也就是 _printf 符号的地址。

链接器在链接时就会把所有的目标文件(在我们这个例子中就是 helloworld.o)和库(libc)作为输入文件,然后解析它们之间符号引用(_printf 符号),最终生成一个可以运行的可执行文件。

二、可执行文件

一个可执行文件中包含多个不同的 segment,,一个 segment 又包含一个或多个 section。

我们可以使用 size 工具查看目标文件中的各个 section:

xcrun size -x -l -m a.out 

下面是 helloworld.c 的目标文件的各个 segment 和 section 的内容:

Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
  Section __text: 0x34 (addr 0x100000f50 offset 3920)
  Section __stubs: 0x6 (addr 0x100000f84 offset 3972)
  Section __stub_helper: 0x1a (addr 0x100000f8c offset 3980)
  Section __cstring: 0xe (addr 0x100000fa6 offset 4006)
  Section __unwind_info: 0x48 (addr 0x100000fb4 offset 4020)
  total 0xaa
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
  Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
  Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
  total 0x18
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000

当我们运行可执行文件时,系统会把各个 segment 映射到进程的地址空间中,在映射时,各个 segment 和 section 被分配不同的属性,也就是权限。

我们来看看各个 segment 和 section 的具体含义:

  • __PAGEZERO:从上面的信息中可以看出,这块区域占 4 个 G 的大小,不可读不可写,不可执行,
  • __TEXT:代码区,具有只读、可执行的权限
    • __text:编译后生成的机器码
    • __stubs:用于动态链接
    • __stub_helper:用于动态链接
    • __cstring:字面量字符串,也就是写在代码里的字符串
    • __unwind_info
    • __const:常量
  • __DATA:数据区,可读可写,但是不可执行
    • __nl_symbol_ptr:non-lazy symbol pointers,局部符号,也就是定义在该文件内的符号
    • __la_symbol_ptr:lazy symbol pointers,外部符号,也就是定义在该文件外的符号
    • __const:需要重定位的常量
    • __bss:未初始化的静态变量
    • __common:未初始化的外部全局变量
    • __dyld:给动态链接器使用的
  • __LINKEDIT

1. Section Content

我们可以使用 otool 查看目标文件中指定 section 的内容:

xcrun otool -s __TEXT __text a.out 

得到的结果如下:

a.out:
Contents of (__TEXT,__text) section
0000000100000f50  55 48 89 e5 48 83 ec 20 48 8d 05 47 00 00 00 c7
0000000100000f60  45 fc 00 00 00 00 89 7d f8 48 89 75 f0 48 89 c7
0000000100000f70  b0 00 e8 0d 00 00 00 31 c9 89 45 ec 89 c8 48 83
0000000100000f80  c4 20 5d c3

上面的机器代码几乎没办法看懂,不过我们可以使用 otool 来查看反汇编后的代码:

xcrun otool -v -t a.out

得到的结果如下:

a.out:
(__TEXT,__text) section
_main:
0000000100000f50  pushq %rbp
0000000100000f51  movq  %rsp, %rbp
0000000100000f54  subq  $0x20, %rsp
0000000100000f58  leaq  0x47(%rip), %rax
0000000100000f5f  movl  $0x0, -0x4(%rbp)
0000000100000f66  movl  %edi, -0x8(%rbp)
0000000100000f69  movq  %rsi, -0x10(%rbp)
0000000100000f6d  movq  %rax, %rdi
0000000100000f70  movb  $0x0, %al
0000000100000f72  callq 0x100000f84
0000000100000f77  xorl  %ecx, %ecx
0000000100000f79  movl  %eax, -0x14(%rbp)
0000000100000f7c  movl  %ecx, %eax
0000000100000f7e  addq  $0x20, %rsp
0000000100000f82  popq  %rbp
0000000100000f83  retq

2. Mach-O

Mach-O 是 Mach object file 格式的缩写,Mach-O 是一种可执行文件,Mac OS 上的可执行文件都是 Mach-O 格式的。

使用下面的命令可以查看一下 a.out 的文件格式:

$ file a.out 
a.out: Mach-O 64-bit executable x86_64

我们可以使用 otool 查看可执行文件的 Mach-O header:

$ otool -v -h a.out

得到的结果如下:

Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64  X86_64        ALL LIB64     EXECUTE    15       1200   NOUNDEFS DYLDLINK TWOLEVEL PIE

ncmds 和 sizeofcmds 表示的是加载命令(load commands),可以通过 -l 参数查看详细信息:

otool -v -l a.out | open -f
a.out:
Load command 0
      cmd LC_SEGMENT_64
  cmdsize 72
  segname __PAGEZERO
   vmaddr 0x0000000000000000
   vmsize 0x0000000100000000
...

找到 Load command 1 部分的 initprot 字段,其值为 r-x,表示 read-only 和 executable。

load command 指定了每一个 segment 和每个 section 的内存地址以及权限保护。

下面是 __TEXT __text section 的信息:

...
Section
  sectname __text
   segname __TEXT
      addr 0x0000000100000f50
      size 0x0000000000000034
    offset 3920
     align 2^4 (16)
    reloff 0
    nreloc 0
      type S_REGULAR
attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
 reserved1 0
 reserved2 0

 ...

这段代码的 addr 值是 0x0000000100000f50,跟上面用 xcrun otool -v -t a.out 查看的 _main 的入口地址是一样的。

三、一个更复杂的例子

我们现在有三个文件,Foo.h、Foo.m 和 helloworld.m,如下。

Foo.h:

#import <Foundation/Foundation.h>

@interface Foo : NSObject

- (void)run;

@end

Foo.m:

#import "Foo.h"

@implementation Foo

- (void)run
{
    NSLog(@"%@", NSFullUserName());
}

@end

helloworld.m:

#import "Foo.h"

int main(int argc, char *argv[])
{
    @autoreleasepool {
        Foo *foo = [[Foo alloc] init];
        [foo run];
        return 0;
    }
}

1. 编译

分别编译 Foo.m 和 helloworld.m 这两个文件:

$ xcrun clang -c Foo.m
$ xcrun clang -c helloworld.m

问题:为什么我们不需要编译 .h 文件?
因为头文件存在的目的,就是为了让我们能通过 importinclude 实现在多个不同的文件中共享一些代码(比如函数声明、变量声明和类声明等),这样我们就不用在每个用到相同声明的地方写重复代码了。

得到两个目标文件:

$ file Foo.o helloworld.o
Foo.o:        Mach-O 64-bit object x86_64
helloworld.o: Mach-O 64-bit object x86_64

为了能够得到一个可执行文件,我们需要将这两个目标文件以及 Foundation 框架链接起来:

xcrun clang helloworld.o Foo.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation

我们会得到一个最终的可执行文件 a.out,然后我们在执行这个文件,可以看到打印的结果:

$ ./a.out
2019-02-02 17:27:18.207 a.out[4181:265495] ShannonChen

2. 符号解析和链接

Foo.ohelloworld.o 都用到了 Foundation 框架,helloworld.o 中用到了 autorelease pool,而且 Foo.ohelloworld.o 都在 libobjc.dylib 的帮助下间接使用了 Objective-C runtime,因为 Objective-C 方法调用时发送消息需要用到 runtime。

什么是符号?

每一个我们定义的或者用到的函数、全局变量和类都是符号。

在链接时,链接器会解析各个目标文件以及库之间的符号,每个目标文件都有一个符号表来说明它的符号。

我们可以使用工具 nm 来查看目标文件 helloworld.o 的所有符号:

$ xcrun nm -nm helloworld.o
                 (undefined) external _OBJC_CLASS_$_Foo
                 (undefined) external _objc_autoreleasePoolPop
                 (undefined) external _objc_autoreleasePoolPush
                 (undefined) external _objc_msgSend
0000000000000000 (__TEXT,__text) external _main

_OBJC_CLASS_$_Foo 符号就是我们定义的 Objective-C 类 Foo,我可以看到,这个符号的解析状态是 undefined(因为 helloworld.o 中引用了 Foo 类,但是没有定义这个类),属性是 external(表示这个 Foo 类不是私有的)。

_main 符号对应的就是我们的 main() 函数,它的属性也是 external,因为它是入口函数,需要暴露出来被系统调用(值得注意的是,它的地址是 0)。

然后,我们再看看目标文件 Foo.o 中的所有符号:

xcrun nm -nm Foo.o
                 (undefined) external _NSFullUserName
                 (undefined) external _NSLog
                 (undefined) external _OBJC_CLASS_$_NSObject
                 (undefined) external _OBJC_METACLASS_$_NSObject
                 (undefined) external ___CFConstantStringClassReference
                 (undefined) external __objc_empty_cache
0000000000000000 (__TEXT,__text) non-external -[Foo run]
0000000000000060 (__DATA,__objc_const) non-external l_OBJC_METACLASS_RO_$_Foo
00000000000000a8 (__DATA,__objc_const) non-external l_OBJC_$_INSTANCE_METHODS_Foo
00000000000000c8 (__DATA,__objc_const) non-external l_OBJC_CLASS_RO_$_Foo
0000000000000110 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo
0000000000000138 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo

在这里,_OBJC_CLASS_$_Foo 符号不再是 undefined 的了,因为 Foo.o 中定义了 Foo 这个类。

当这两个目标文件和 Foundation 库链接时,链接器就会根据上面的这些符号表解析目标文件中的符号,解析成功后就能知道这个符号的地址了。

最后,我们再看看最终生成的可执行文件的符号表信息:

xcrun nm -nm a.out
                 (undefined) external _NSFullUserName (from Foundation)
                 (undefined) external _NSLog (from Foundation)
                 (undefined) external _OBJC_CLASS_$_NSObject (from libobjc)
                 (undefined) external _OBJC_METACLASS_$_NSObject (from libobjc)
                 (undefined) external ___CFConstantStringClassReference (from CoreFoundation)
                 (undefined) external __objc_empty_cache (from libobjc)
                 (undefined) external _objc_autoreleasePoolPop (from libobjc)
                 (undefined) external _objc_autoreleasePoolPush (from libobjc)
                 (undefined) external _objc_msgSend (from libobjc)
                 (undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000e90 (__TEXT,__text) external _main
0000000100000f10 (__TEXT,__text) non-external -[Foo run]
0000000100001138 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo
0000000100001160 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo

我们可以看到,跟 Foundation 和 Objective-C runtime 相关的符号依然是 undefined 状态(这些需要在加载程序进行动态链接时来解析),但是这个符号表中已经有了如何解析这些符号的信息,也就是从哪里可以找到这些符号。

比如,符号 _NSLog 后面有一个 from Foundation 的说明,这样在动态链接时就知道是去 Foundation 库找 _NSLog 这个符号的定义了。

而且,可执行文件知道去哪里找到这些需要参与链接的动态库:

$ xcrun otool -L a.out
a.out:
  /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1555.10.0)
  /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.200.5)
  /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1555.10.0)
  /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)

这些 undefined symbols 会在运行时被动态链接器 dyld 解析,当我们运行这个可执行文件时, dyld 可以保证 _NSFullUserName 这些符号能够指向它们在 Foundation 以及其他动态库中的实现。

3. dyld 的共享缓存

有些应用程序可能会用到大量的 framework 和动态库,这样在链接时就会有成千上万的符号需要解析,从而影响链接速度。

为了缩短这个流程,在 macOS 和 iOS 上会针对每个架构,预先将所有的动态库链接成一个库,缓存到 /var/db/dyld/ 目录下。当一个 Mach-O 文件被加载到内存中时,动态链接器首先去缓存目录中检查是否有缓存,如果有就直接使用缓存好的动态库。通过这种方式,大大提高了 macOS 和 iOS 上的应用启动速度。

参考

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Feb 24, 2019

ABI (Application Binary Interface)

1. 什么是 ABI

2. ABI vs. API

3. Swift ABI 稳定性意味着什么

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant