update: 动态库、静态库的编译链接细节

This commit is contained in:
FantasticLBP
2025-06-23 01:18:55 +08:00
parent aca020701b
commit 1142064d28
129 changed files with 10932 additions and 2615 deletions

View File

@@ -1,15 +1,13 @@
# 剖析 Swift String
带着问题研究下 Swift 中的 String
带着问题研究下 Swift 中的 String
- 1个 String 变量占用多少内存?
- String 存放在什么位置?
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MemoryLayout.png)
![](./../assets//MemoryLayout.png)
@@ -21,7 +19,7 @@ var str1: String = "0123456789"
实验很简单,就一行代码。来窥探下 String 的初始化和内存结构。出发断点看到下面汇编:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo1.png" style="zoom:25%">
<img src="./../assets//SwiftStringExploreDemo1.png" style="zoom:25%">
简单分析下:
@@ -47,7 +45,7 @@ QA这个10是什么东西1是什么东西
var str1: String = "01234"
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo2.png" style="zoom:25%">
<img src="./../assets//SwiftStringExploreDemo2.png" style="zoom:25%">
可以看到其他和上面的没变化,唯一不同的是 `movl $0x5, %esi` 将5赋值给寄存器 `esi`,即寄存器 `rsi`。实验的字符串 "01234" UTF8 字符个数为5
@@ -57,7 +55,7 @@ var str1: String = "01234"
var str1: String = "01234😄"
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo3.png" style="zoom:25%">
<img src="./../assets//SwiftStringExploreDemo3.png" style="zoom:25%">
可以看到将9赋值给寄存器 `esi``rsi`,字符串赋值给寄存器 `rdi``xorl %edx, %edx` 异或运算结果 0 赋值给寄存器 `edx``rdx`,也就是不纯粹为
@@ -95,7 +93,7 @@ extension String: _ExpressibleByBuiltinStringLiteral {
继续探索:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo4.png" style="zoom:25%">
<img src="./../assets//SwiftStringExploreDemo4.png" style="zoom:25%">
可以看到 `str1` 地址为 `rip + 0x8853 = 0x1000039a5 + 0x8853 = 0x10000C1F8 `,然后读取对应堆上的内容为:
@@ -130,7 +128,7 @@ var str1: String = "0123456789ABCDEF"
print(Mems.memStr(ofVal: &str1))
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo5.png" style="zoom:25%">
<img src="./../assets//SwiftStringExploreDemo5.png" style="zoom:25%">
分析下:
@@ -142,15 +140,21 @@ print(Mems.memStr(ofVal: &str1))
- 第20行 `movabsq $0x7fffffffffffffe0, %rdx` 则会把立即数 `0x7fffffffffffffe0` 移动到寄存器 `rdx`
- 第21行 `addq %rdx, %rdi``rdx` + ` rdi` 的值赋值给 `rdi`,也就是 `rdi` 存放了: 字符串的真实地址 + `0x7fffffffffffffe0。`所以字符串真实地址 = `rdx 的地址` - `0x7fffffffffffffe0`。寄存器 `rdi` 读取出地址为 `0x8000000100007800` 。所以字符串真实地址为:`0x8000000100007800` - `0x7fffffffffffffe0` = `0x100007820`LLDB 读取下 `x 0x100007820 ` 看到 30、31...46刚好是字符串 `0123456789ABCDEF` 的 ASCII 值。
- 第21行 `addq %rdx, %rdi``rdx` + ` rdi` 的值赋值给 `rdi`,也就是 `rdi` 存放了: 字符串的真实地址 + `0x7fffffffffffffe0 相加后的值。`
所以 `字符串真实地址 = 指针内存8个字节地址 - 0x7fffffffffffffe0`,又等价于 `字符串真实地址 = 指针内存8个字节地址 + 0x20`
所以字符串真实地址 = `rdx 的地址` - `0x7fffffffffffffe0`
寄存器 `rdi` 读取出地址为 `0x8000000100007800` 。所以字符串真实地址为:`0x8000000100007800` - `0x7fffffffffffffe0` = `0x100007820`
LLDB 读取下 `x 0x100007820 ` 看到 30、31...46刚好是字符串 `0123456789ABCDEF` 的 ASCII 值。
所以 **`字符串真实地址 = 指针内存8个字节地址 - 0x7fffffffffffffe0`**,又等价于 `字符串真实地址 = 指针内存8个字节地址 + 0x20`
- 经过23行后 `orq %rdi, %rdx` 可以看到 `rdx``rdi` 里面存储的都是:`字符串真实地址` + `0x7fffffffffffffe0`
- LLDB 输入 finish 结束函数细节外部可以看到第10行 `movq %rdx, 0x8864(%rip) ``rdx` 寄存器里的值(也就是:`字符串真实地址` + `0x7fffffffffffffe0` )赋值给 `str1` 指针的后8个字节
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo6.png" style="zoom:25%">
<img src="./../assets//SwiftStringExploreDemo6.png" style="zoom:25%">
@@ -168,13 +172,13 @@ var str1: String = "0123456789ABCDEF"
字符串地址为 ` 0x10000397f + 0x3ea1 = 0x100007820`,看着像全局变量、也有可能是常量区?字符串内容写死的情况下,编译器编译后内存地址应该可以确定,那到底在什么地方呢?利用 `MachOView` 窥探下 `MachO` 文件吧
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo7.png" style="zoom:25%">
<img src="./../assets//SwiftStringExploreDemo7.png" style="zoom:25%">
利用 MachOView 打开如下
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MacMachOViewExploreStringLocationDemo1.png" style="zoom:25%">
<img src="./../assets//MacMachOViewExploreStringLocationDemo1.png" style="zoom:25%">
@@ -184,12 +188,23 @@ X86_64 架构下,我们在 MachOView 看到的地址,真实对应地址为
在 macOS 和 iOS 的二进制文件(如 Mach-O 格式的可执行文件或动态库)中,`Section64` 是一个结构体用于描述二进制文件中的一个段section。每个段包含特定类型的数据或代码并且具有特定的属性比如是否可写、是否可执行等。
`Section64(__TEXT__,__cstring)` 中:
`Section64(__TEXT,__cstring)` 中:
- `__TEXT__` 是段的段名segment name它通常包含代码(`__text`)和常量数据(如 `__cstring``__const` 等)。
- `__cstring` 是节的节名section name它通常包含 C 字符串字面量。这些字符串字面量在编译时被存储在只读数据段中
- `__TEXT` 是段的段名segment name存储**只读且可执行**的内容,包括代码和只读数据。**典型节Sections**
- `__text`:存放机器指令(代码段)
- `__cstring`:存放字符串常量。
- `__const`:存放其他常量数据。
因此,`Section64(__TEXT__,__cstring)` 描述的是二进制文件中存储 C 字符串字面量的一个节。这个节位于 `__TEXT__` 段中,并且包含的是只读数据
- **`__DATA` 段**:存储**可读写**的数据(如全局变量、静态变量)
- `__cstring` 节:
- **功能**`_cstring` 专门存储硬编码的字符串常量(如 `"Hello, World!"`)。
- **内存权限**:映射到内存时,`__TEXT` 段整体为**只读**`r--``r-x`),但 `_cstring` 本身**不可执行**,仅用于数据存储
- **所属区域**
- 逻辑上属于**常量区**(类似 ELF 格式的 `.rodata`)。
- 物理上可能与代码段(`__text`)同属 `__TEXT` 段,但用途和权限不同。
因此,`Section64(__TEXT,__cstring)` 描述的是二进制文件中存储 C 字符串字面量的一个节。这个节位于 `__TEXT__` 段中,并且包含的是只读数据。
@@ -202,7 +217,7 @@ print(Mems.memStr(ofVal: &str1)) // 0xd000000000000010 0x8000000100007800
print(Mems.memStr(ofVal: &str2)) // 0x0000353433323130 0xe600000000000000
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MacMachOViewExploreStringLocationDemo2.png" style="zoom:25%">
<img src="./../assets//MacMachOViewExploreStringLocationDemo2.png" style="zoom:25%">
可以看到:
@@ -224,6 +239,52 @@ print(Mems.memStr(ofVal: &str3)) // 0xd000000000000015 0x8000000100007800
### Swift 字符串存储本质
Swift 字符串存储的两种模式:
- **内联存储Small String OptimizationSSO**
- **条件**:字符串长度 ≤15 个 **ASCII 字符**(或 ≤7 个 **UTF-16 字符**)。
- **特点**:字符串内容直接存储在 `StringObject` 的 16 字节内存中,无需堆分配。有点类似 Objective-C 的 **Tagged Pointer**
- **堆存储Heap-Allocated**
- **条件**:字符串长度超过上述限制(字符串长度 > 15 个 **ASCII 字符** 或 > 7 个 **UTF-16 字符**
- **特点**:字符串内容存储在堆内存。其指针结构是一个 16 字节的 `StringObject``StringObject` 存储堆地址和元数据
#### 内联存储SSO的具体实现
内存布局前8个字节元数据 + 部分字符) + 后8个字节剩余字符 + 填充)
元数据编码:
最低有效位LSB用于标识存储模式:
- **0**:内联存储
- **1**:堆存储
其余位存储字符串长度和编码信息ASCII 或 UTF-16
Demo
````Swift
let str = "Hello" // 5 个 ASCII 字符
内存布局如下:
0x0000000000000a05 // 元数据(长度=5, ASCII, 内联标志位=0
0x48656c6c6f000000 // ASCII 字符 "Hello" 的十六进制表示 + 填充
````
与 Objective-C Tagged Pointer 的区别
| **特性** | **Swift 内联存储 (SSO)** | **Objective-C Tagged Pointer** |
| :----------- | :------------------------- | :------------------------------ |
| **存储位置** | 字符串对象的 16 字节内存中 | 指针值本身64 位) |
| **标识方式** | 元数据的最低有效位 (LSB) | 指针的最高有效位 (MSB) |
| **兼容性** | 需考虑 Unicode 编码复杂性 | 仅支持有限类型(如短 NSString |
| **内存安全** | 完全由编译器管理 | 需运行时特殊处理 |
@@ -257,12 +318,12 @@ print("explore")
可以看到长度为16的字符串拼接后
- 内存的前8个字节`0xd000000000000010` 变到了 `0xf000000000000011`最后2位代表字符串长度从16位变成17位。
- 内存的前8个字节从 `0xd000000000000010` 变到了 `0xf000000000000011`最后2位代表字符串长度16进制的10就是16。从16位变成17位。
- 内存的后8个字节字符串的地址改变了
上面可知, `字符串真实地址 = 指针内存8个字节地址 - 0x7fffffffffffffe0`,又等价于 `字符串真实地址 = 指针内存8个字节地址 + 0x20`
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MacMachOViewExploreStringLocationDemo3.png" style="zoom:25%">
<img src="./../assets//MacMachOViewExploreStringLocationDemo3.png" style="zoom:25%">
字符串真实地址:`0x0000600001700440 + 0x20 = 0x0000600001700460`LLDB `x 0x0000600001700460 ` 可以看到字符串拼接后的结果。看上去是存储在堆上。如何验证?
@@ -270,11 +331,11 @@ print("explore")
我们知道堆上的内存初始化在 Swift 侧,关键方法为 `swift_allocObject ` -> `swift_slowAlloc` -> `malloc `。给 malloc 下断点,然后在断点出查看调用堆栈,可以看到在 `string.append()` 后有堆分配内存
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MacMachOViewExploreStringLocationDemo4.png" style="zoom:25%">
<img src="./../assets//MacMachOViewExploreStringLocationDemo4.png" style="zoom:25%">
结束当前函数调用,在外层可以看到 str2 地址值的后8个字节为 `0x000060000170410`,再 + `0x20` 就是字符串真实地址,打印如下。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MacMachOViewExploreStringLocationDemo5.png" style="zoom:25%">
<img src="./../assets//MacMachOViewExploreStringLocationDemo5.png" style="zoom:25%">
0x20 是什么这32个字节存放了什么信息存储字符串的描述信息比如引用计数、字符串长度等信息。
@@ -292,17 +353,28 @@ print("explore")
`__TEXT` 是 Mach-O 文件中通常包含代码和只读数据的段。这个段包含了程序的主要执行代码,以及常量字符串、符号表等。
`dyld_stub_binder` 是一个由动态链接器dyld在运行时生成和使用的函数。它的主要目的是在首次调用某个动态链接的符号如函数或方法将该符号的实际地址绑定到调用点
`dyld_stub_binder` 是一个由动态链接器dyld在运行时生成和使用的函数。它的主要目的是在首次调用某个动态链接的符号如函数或方法将该符号的实际地址绑定到调用点
Swift 中 `String` 类型的初始化方法(`init`的地址是否采用延迟绑定Lazy Binding取决于 **编译环境、优化级别和具体方法实现**
### 延迟绑定的基本原理
在编译时对于动态链接的符号编译器会生成一个桩stub而不是直接调用该符号。桩是一个小段的代码当被首次执行时它会触发 `dyld_stub_binder` 的调用。`dyld_stub_binder` 的任务就是找到该符号的实际地址,并将其写入桩中,从而替换桩的原始代,这样,下一次调用该符号时,就可以直接跳转到实际的地址,而无需再次通过桩和 `dyld_stub_binder`。
延迟绑定Lazy Binding是动态链接的机制用于推迟符号如函数、方法地址的解析到首次调用时。其核心步骤为
1. **编译阶段**生成存根Stub指向符号占位地址。
2. **启动阶段**:存根指向动态链接器(如 `dyld`)的解析函数(如 `dyld_stub_binder`)。
3. **首次调用**:触发符号解析,动态链接器填充真实地址到存根。
4. **后续调用**:直接跳转到已解析的地址。
替换桩,位于 `__DATA,__la_symbol_ptr` 数据段可读可写,所以可以修改。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MachOStubBinderAndLazyBinding.png" style="zoom:25%">
<img src="./../assets//MachOStubBinderAndLazyBinding.png" style="zoom:25%">
`__stub_helper` 节是 `__TEXT` 段中的一个特定节,它包含了用于处理符号懒加载的辅助函数和代码。当动态链接的符号首次被调用时,这些辅助函数会被触发,以解析符号并将其地址绑定到调用点。