你的App启动时,符号绑定在做什么?
// main.cexternintanswer(void); /* 声明:编译器知道“有个函数叫 answer”,不知道地址 */intmain(void) {return answer(); /* 调用:这里要生成“去某个地址执行”的机器码 */}
以上代码没有answer的具体实现
编译 main.c 成 main.o(不链接) 时:
answer 的引用,但 本文件里没有 answer 的定义。answer 就是一个 未定义符号(undefined symbol):名字在,地址不在。用工具看(概念上)会像:
$ nm -u main.o # 未定义(U = undefined)U _answer
answer 这个名字,绑到真正的机器码地址上。静态链接会把符号“绑死”到可执行文件里
lib.c:// lib.cint answer(void) { return 42; }
链接时,链接器发现:
main.o需要_answer,lib.o提供_answer于是它把两处合并进 a.out,重定位:把 main 里调用 answer 的那条指令,填成指向最终 answer 在可执行文件里的地址。
这就是 静态链接阶段的符号解析 + 重定位——也是一种绑定,通常发生在 链接这一刻,结果写进 a.out。
cc -c main.c -o main.o //这里有-c,表示只编译,不链接cc -c lib.c -o lib.occ main.o lib.o -o a.out //链接已经包含在这条命令里了,只是没有单独写一个ld。
不得不说一下cc:
在 macOS / Linux 上,cc 一般是 C 语言编译器的入口名字(compiler driver):你敲一条 cc ...,它会在内部去调 编译器前端(如 clang 或 gcc),需要时还会自动调 汇编器、链接器。
在 Apple 上执行 which cc / cc --version,通常会看到实际就是 Clang(或指向 clang 的包装)。
所以它不是只编译不链接的专用命令,而是一个总控:后面跟什么参数,就决定只做编译还是连链接一起做。
编译时遇到动态库的符号时在做什么(绑定推迟到动态库加载/首次调用)
externvoidfoo(void);voidbar(void) {foo(); /* 编译后:并不是直接 call 到系统库里的某固定数字地址 */}
编译链接成 App 后:你的程序里要调用 NSLog
NSLog 的机器码在系统/Framework 的 .dylib / framework 里。然而CPU 只会跳到某个地址执行。所以问题是:
NSLog 的绝对地址写死在指令里,那只能运行时动态绑定了。GOT(Global Offset Table) 是 ELF 体系里的经典名字:一张表,表里每行是一个函数名,和一个指针(地址)。在 Mach-O(iOS/macOS) 里,这个指针通常是__DATA 段里的符号指针 / lazy 符号指针
GOT可以理解为一本通讯录,每一行是一个函数名对应的电话号码栏;启动前栏里的函数名是写好的,但是电话号码是待填 / 临时号码,dyld在运行时填成真实号码后,stub 才能转接到正确分机。
call 0x真实地址,因为那时还不知道。于是生成的是call _foo_stub// 先跳到「本镜像里的」一个小函数 foo 的 stub
Stub 里做的事:
-
从 GOT 的某一格读出当前应跳向的地址; jmp过去 —— 就把执行流交给了 真正的foo实现(在系统 dylib 里)。
stub 自己不”计算“或者”检索“出目标地址,而是 从 GOT 里取出当前存着的那个地址,再跳转过去。
/ 某个外部函数 在GOT中固定好了第几个槽位;stub 里写的是:去第 N 个 GOT 槽读 8 字节(地址)→ br/jmp 过去。Stub主要是静态链接器生成的
在GOT(以及 Mach-O 里常见的 lazy 指针表)中填真实地址主要是 dyld 做的
接下来再区分一下Lazy 和非 Lazy动态绑定
它们差别在 dyld 何时把地址写进槽
- 非 lazy(立即绑定):镜像加载过程中,dyld 就把这些外部符号解析好,启动前/早期
- Lazy:槽里先可以是占位或指向 dyld 的解析助手;第一次经 stub 走到那里时才解析,再把真地址写进槽;以后再走 stub → 读槽 → 直接跳进真实现。
所以:是否 lazy 决定的是 完善 GOT/懒指针槽的时机(加载时 vs 第一次调用时)——无论是否lazy动态绑定,stub 始终在读自己那一个槽。
所以:动态链接讲依赖关系;动态绑定讲把地址写进槽里那一步。 日常说动态绑定≈ dyld 在完成解析并填指针。
再区分一下动态链接和动态绑定
- 静态链接:链接器把未定义符号解析到某个 .o/静态库里的定义,并写重定位。发生在app构建时,也就是说是在开发这个阶段做的,为什么叫静态:绑定关系在链接完成那一刻就固定了,不随进程启动时的环境再变。
- 动态加载:可执行文件 / dylib 里有很多 imported 符号,加载时(或首次调用时)dyld 要把它们绑到实际地址(Mach-O 里常见的是 bind / lazy bind)。为什么叫动态:最终用哪段实现、地址是多少,是在进程运行、库被加载时才定下来的;同一份可执行文件可以对着不同版本/路径的系统库跑(在系统规则允许范围内),多进程还能共享同一份物理页的 dylib。
明白什么是符号绑定后就可以看下启动优化了,关注我,下回总结
夜雨聆风