乐于分享
好东西不私藏

你的App启动时,符号绑定在做什么?

你的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 只会跳到某个地址执行。所以问题是:

链接你的 App 时,系统上 Foundation 的实际加载地址还没定(ASLR、版本、路径);所以 不能在编译期就把NSLog 的绝对地址写死在指令里,那只能运行时动态绑定了。
要说到动态绑定,就得先讲GOT

GOT(Global Offset Table) 是 ELF 体系里的经典名字:一张表,表里每行是一个函数名,和一个指针(地址)在 Mach-O(iOS/macOS) 里,这个指针通常是__DATA 段里的符号指针 / lazy 符号指针

GOT可以理解为一本通讯录,每一行是一个函数名对应的电话号码栏;启动前栏里的函数名是写好的,但是电话号码是待填 / 临时号码,dyld在运行时填成真实号码后,stub 才能转接到正确分机。

这里又引入了stub,编译器不能生成 call 0x真实地址,因为那时还不知道。于是生成的是
call _foo_stub    // 先跳到「本镜像里的」一个小函数 foo 的 stub

Stub 里做的事

  1. 从 GOT 的某一格读出当前应跳向的地址;
  2. jmp过去 —— 就把执行流交给了 真正的foo实现(在系统 dylib 里)。

stub 自己不”计算“或者”检索“出目标地址,而是 从 GOT 里取出当前存着的那个地址,再跳转过去

链接时已经为 foo / 某个外部函数 在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 始终在读自己那一个槽

顺便也区分一下动态链接和动态绑定
动态链接(dynamic linking):依赖在别的 dylib 里,运行时才把名字 → 地址对上。
动态绑定(dynamic binding):就是把导入符号解析成地址,并写进进程地址空间(填哪些槽)。在 Mach-O 里常叫 bind(含 lazy bind)。

所以:动态链接讲依赖关系;动态绑定讲把地址写进槽里那一步。 日常说动态绑定≈ dyld 在完成解析并填指针。

再区分一下动态链接和动态绑定

  • 静态链接:链接器把未定义符号解析到某个 .o/静态库里的定义,并写重定位。发生在app构建时,也就是说是在开发这个阶段做的,为什么叫静态:绑定关系在链接完成那一刻就固定了,不随进程启动时的环境再变。
  • 动态加载:可执行文件 / dylib 里有很多 imported 符号,加载时(或首次调用时)dyld 要把它们绑到实际地址(Mach-O 里常见的是 bind / lazy bind)。为什么叫动态:最终用哪段实现、地址是多少,是在进程运行、库被加载时才定下来的;同一份可执行文件可以对着不同版本/路径的系统库跑(在系统规则允许范围内),多进程还能共享同一份物理页的 dylib。

明白什么是符号绑定后就可以看下启动优化了,关注我,下回总结