一个 .so 文件到底是什么?它是怎么运行起来的

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容