JHHK

欢迎来到我的个人网站
行者常至 为者常成

编译原理_随笔

目录

Object-C的编译过程

比如有如下代码
A.m

#import "B.h"
@implementation A
- (void)callTest {
    B *b = [B new];
    [b test];
}
@end

B.m


@implementation B
- (void)test {
    NSLog(@"hello");
}
@end

A.m 中的“外部符号”包括:

 B

方法 test

方法 new

runtime 函数如 objc_msgSend

NSString literal 等符号

下面我们按编译器流水线逐步解析

1 词法分析

@implementation → keyword
A → identifier
- → token
( → token
void → keyword
) → token
callTest → identifier
{ → token
B → identifier
* → token
b → identifier
= → token
[ → token
B → identifier
new → identifier
] → token
...

从左向右扫描,将关键字、标识符、常量、运算符、界限符,形成token。
此阶段只识别单词,不涉及“类 B 是否存在”。不处理外部符号

2 语法分析
基于 Objective-C 的文法(grammar),生成 AST。
语法分析只负责结构正确,不检查符号是否存在。

注意点:

B *b = [B new];

会被解析成一个 ObjC message send 语法树:

ObjCMessageExpr
   receiver: B (class identifier)
   selector: new

3 语义分析
扫描语法树,生成符号表。包括A自身的符号和,B.h导入的外部符号
语义分析检查

此时的符号表大概长这样

编译 A.m 的符号表包含:

🔹 来自 A.m 的符号:
ObjCInterfaceDecl A
 ├─ ObjCPropertyDecl num
 ├─ ObjCMethodDecl - num
 ├─ ObjCMethodDecl - setNum:

ObjCImplementationDecl A
 └─ ObjCMethodDecl - callTest

🔹 来自 B.h 的符号:
ObjCInterfaceDecl B
 ├─ ObjCMethodDecl - test
 ├─ ObjCMethodDecl + new

4 中间代码生成

5 生成.O文件

lxy:此时已经对imp分配了相对地址。
此时地址不是绝对地址,只是一个符号,地址是在链接后确定的

A.o 的符号表大概会包含:

#本地符号(A.m 自己的)         

_OBJC_CLASS_$_A
_I_A_callTest

#外部“未定义符号”(undefined symbols)    
#这些必须在链接阶段解决:      
_U_OBJC_CLASS_$_B            → 来自 B.o
_objc_msgSend                → 来自 libobjc.A.dylib
L_OBJC_SELECTOR_REFERENCES_test → 链接后被合并

6 链接阶段

链接器 ld:

从 B.o 获取类 B 的所有符号

为 selector 去重,保证 selector 字符串唯一

合并所有 symbol table

生成最终 Mach-O

ld 会为每个符号安排最终内存地址:

0x100003F20  _OBJC_CLASS_$_A_callTest

7 运行时绑定(objc_msgSend)

最终在运行时:

objc_msgSend(b, sel_test)
→ 在类B的方法列表中查找 test
→ 获取 IMP _I_B_test
→ 跳转执行

Swift的编译过程

一、编译过程

词法/语法:
把源代码变成 AST(不做名字解析/类型检查)。

语义分析/类型检查:
把 A, B, callTest, test 等声明放入编译器内部的符号结构(Decl/ASTContext),并验证 b.test() 在语义上合法(可见、签名匹配)。这一步不会分配机器码地址。

SIL → IRGen:
将语义信息和 AST 降低为可优化的 SIL,再转为 LLVM IR,决定调用形式(vtable/direct/objc_msgSend)。

目标文件(.o):
每个函数/元数据生成符号和机器码;未定义引用(例如对 runtime 的引用)保留为外部符号。

链接:
符号分配:
为函数/数据分配最终地址(callTest、B.test 的代码地址在这里最终确定);
数据重定位与合并:
class metadata、method tables、selector tables 等被放到相应数据段。合并 class metadata / selector tables。

运行时:
如果调用是 ObjC 消息(objc_msgSend),那真正的 IMP 查找与绑定在第一次调用时由 Objective-C runtime 完成;
如果是 Swift 原生派发,则通过 vtable / class metadata(在可执行文件中已经编码)进行快速分发或直接调用。

二、为什么会有“重定位(Relocation)”?

重定位(Relocation)存在的原因只有一个:
在编译生成 .o 文件时,符号(函数/类/方法/全局变量)的最终地址还未知,只有在链接阶段才知道,所以需要在 .o 中记录一个“待定地址”。

编译器无法提前知道:
这个函数最终放在 Mach-O 的哪里
类的元数据被放到哪个偏移
方法表、selector 表、协议表等最终在 segment 中的布局
某个全局变量地址需要多少对齐
不同 .o 文件之间互相调用的符号最终如何连接

源代码

b.test()

IR 会生成类似:

call @swift_call_test

但 @swift_call_test 的最终内存地址是未知的。 所以 .o 文件会写:

0000  call   <relocation to symbol _swift_method_B_test>

链接器读取 relocation entry,然后在最终链接 Mach-O 时:
计算 test() 最终在 __TEXT 段的真实偏移
回填到指令中的“call offset”位置
删除 relocation entry(因为已完成)
所以重定位存在是因为: 编译器不知道最终地址,链接器才知道。

三、Swift的方法调用

vtable 虚函数表
一张方法地址表

在mach-o中的位置

函数 -> 代码段 -> 指令

全局变量 -> 数据段 -> 值


Nothing is impossible to a willing heart!





R
Valine - A simple comment system based on Leancloud.