目录
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!