so 是 Shared Object(共享对象)的缩写。
.so 文件之所以叫做“动态库”(或者“共享动态链接库”),可以从“动态”和“共享”这两个核心特征来理解:
1. 为什么叫“动态链接”?(Dynamic)
与它相对的是“静态库”(在 Linux/Android 下扩展名为 .a,在 Windows 下为 .lib)。
- 静态链接(Static): 在编译打包阶段,编译器就把库里的代码硬拷贝(复制)到了最终的可执行文件或 App 里。这导致产物体积变大;如果静态库更新了,整个软件必须重新编译打包。
- 动态链接(Dynamic): 在编译打包阶段,不把库代码复制进来,只在程序里记录一句“我需要用到某个 .so 文件”。直到程序在运行(Run-time)时,操作系统或 JVM 才会“动态链接”到这个 .so,并把它加载到内存中去调用。如果更新了 .so,只要接口不变,主程序甚至不需要重新编译。
2. 为什么叫“共享”?(Shared / Shared Object)
这是 .so 名字(Shared Object)的直接来源:
-
内存共享: 如果系统里有 10 个程序都需要用到同一个
.so文件,操作系统只需要把这个.so加载到物理内存中一次。这 10 个程序可以共同占用/共享这一份内存代码,极大地节省了系统内存。
不同平台的动态库叫法对比
虽然叫法和后缀不同,但它们在概念上是完全等价的:
| 操作系统 | 动态库后缀 | 全称/含义 |
|---|---|---|
| Linux / Android | .so | Shared Object |
| Windows | .dll | Dynamic-Link Library |
| macOS / iOS | .dylib | Dynamic Library |
在 Java JNI 的场景中,通过 System.loadLibrary("foo") 加载库时:
- 如果运行在 Linux/Android 上,JVM 会自动去找
libfoo.so; - 如果运行在 Windows 上,JVM 会自动去找
foo.dll; - 如果运行在 macOS 上,JVM 会自动去找
libfoo.dylib。
很多 Android 开发者知道:
System.loadLibrary("xxx");
会加载一个:
libxxx.so
但是很多人并不清楚:
-
.so到底是什么? -
.so里面存的是什么? - 它是怎么运行起来?
理解这些问题,需要从 SO 的本质 开始。
一、SO 怎么来的?
C++ 源码
│
├── main.cpp
└── util.cpp
│
▼
│预处理器 Preprocessor
│ 工具:Clang Preprocessor
│ 处理:#include / #define / #if ...
│
▼
预处理后的源码,格式.i/.ii
▼
│编译器 Compiler
│ 工具:Clang
│ C++ → 汇编代码
▼
后缀为.s 汇编文件
│
▼
│ 汇编
│ 工具:Assembler(汇编器)
│ 汇编代码 → 机器码
│
▼
│目标文件 Object File
├── main.o
├── math.o
└── util.o
│
▼
│ 链接器 Linker
│ 工具:LLVM Linker(lld)
│
│ ① 合并各个目标文件中的代码和数据
│ ② 符号解析:确定函数/变量之间的引用关系
│ ③ 重定位:处理最终的地址引用
│ ④ 按 ELF 格式组织最终文件
│
▼
ELF Shared Object
│
▼
│ 转换
▼
libnative.so
二、SO 里面有什么?
一个 .so 不是简单的一堆二进制。
它是一种 ELF 文件,结构类似:
libxxx.so
+----------------+
| ELF Header |
+----------------+
| Program Header |
+----------------+
| .text |
| |
| 机器代码 |
+----------------+
| .rodata |
| 常量 |
+----------------+
| .data |
| 全局变量 |
+----------------+
| .dynsym |
| 符号表 |
+----------------+
| .dynstr |
| 字符串信息 |
+----------------+
| Relocation |
| 重定位信息 |
+----------------+
其中最重要的是:
.text 段
这里存放真正执行的机器码。
例如:
C++:
int add(int a,int b)
最后变成:
0xA8
0x01
0x23
0xD6
...
CPU 执行的就是这些内容。
三、SO 是不是自己会运行?
不会。
这是一个非常重要的概念。
很多人误认为:
加载so
↓
so开始执行
实际上:
SO 本身不是一个独立运行的程序,它只是一个机器代码和数据的集合。
它没有入口文件,比如
main()
没有自己的进程。
它必须被某个程序加载。
例如:
Android:
App进程
|
↓
libxxx.so
Linux:
程序
|
↓
libxxx.so
四、加载 SO 的是谁?
答:动态链接器(Dynamic Linker)
Android 中叫:
linker
linker64
它属于 Android 系统的一部分。
位置类似:
/system/bin/linker64
它负责:
- 读取 ELF
- 加载 SO
- 查找依赖
- 符号解析
- 地址重定位
整个流程:
App
|
|
调用
System.loadLibrary()
|
|
↓
Android Runtime
|
|
↓
Native Loader
|
|
↓
linker64
|
|
↓
加载 libxxx.so
五、SO 运行完整流程
假设:
Java:
System.loadLibrary("native-lib");
发生了什么?
第一步:找到 SO 文件
Android 根据 ABI 找:
例如:
arm64-v8a
寻找:
lib/arm64-v8a/libnative-lib.so
第二步:linker64 读取 ELF
注意:
这里不是 Java 做的。
也不是 JNI 做的。
是:linker64做的。
它打开:
libnative-lib.so
读取:
ELF Header
Program Header
Section信息
了解:
- 代码在哪里
- 数据在哪里
- 依赖哪些其他库
第三步:把 SO 映射到内存
linker 使用:
mmap()
把文件映射到当前进程地址空间。
例如:
磁盘:
libxxx.so
.text
.data
加载后:
App进程内存
0x70000000
.text
机器码
0x70010000
.data
数据
注意:
这里不是复制全部内容。
操作系统通过虚拟内存机制管理。
第四步:加载依赖库
比如:
你的 SO:
libnative.so
依赖:
libc.so
liblog.so
libstdc++.so
那么:
linker继续加载:
libnative.so
↓
liblog.so
↓
libc.so
形成依赖关系。
第五步:符号解析
假设:
你的代码:
printf("hello");
但是:
printf()
不在你的 SO 中。
在哪里?
libc.so
于是 linker 查:
你的so
需要 printf
↓
libc.so
找到 printf 地址
这叫:
Symbol Resolution
符号解析
第六步:地址重定位
问题:
编译 SO 时:
不知道它最终加载到哪里。
比如:
编译:
printf地址:
?????
加载:
libc.so
实际地址:
0x70001000
怎么办?
linker 修改:
?????
↓
0x70001000
这个过程叫:
Relocation
重定位
第七步:执行初始化函数
如果 SO 中有:
JNI_OnLoad()
或者:
constructor函数
linker 会执行。
例如:
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM* vm, void*)
{
return JNI_VERSION_1_6;
}
这里会被调用。
第八步:真正执行 SO 中代码
注意:
前面所有步骤:
只是:
准备环境
并没有真正执行业务代码。
真正执行:
比如:
Java:
nativeAdd(1,2);
流程:
Java
↓
JNI
↓
找到native函数地址
↓
跳转到SO中的.text区域
↓
CPU执行机器指令
↓
返回结果
六、所以到底是谁执行机器码?
这个问题非常关键。
答案分三层:
第一层:linker
负责:
加载SO
解析ELF
找到代码
修正地址
准备环境
但是:
linker不执行你的业务代码。
第二层:CPU
真正执行:
ADD
MOV
BL
RET
这些机器指令的是:
CPU。
第三层:操作系统
操作系统负责:
- 创建进程
- 管理虚拟内存
- 给 CPU 提供执行上下文
- 进行权限控制
完整关系:
Android系统
|
|
linker64
|
|
加载 libxxx.so
|
|
解析 ELF
|
|
得到机器码地址
|
|
JNI调用
|
|
CPU执行
|
|
.text区域中的机器指令
七、一句话总结
SO 的本质是一个 ELF 格式的二进制动态链接库,里面保存的是已经编译好的机器码和运行所需的元数据。它本身不会运行,而是在程序运行时由操作系统提供的动态链接器(Android 中是 linker/linker64)读取 ELF 文件,将 SO 映射到进程内存,完成依赖加载、符号解析和地址重定位;之后,当程序通过 JNI 或其他方式调用其中函数时,CPU 才真正执行 SO 中 .text 段里的机器指令。
简单记:
.so是什么?
↓
机器码 + 数据 + 描述信息
谁加载?
↓
linker/linker64
谁解析?
↓
linker/linker64
谁执行?
↓
CPU