这篇文章来看看 ARM64 架构下 libc++ 的 std::string 内存布局。libc++ 实现了短字符串优化(SSO, Small String Optimization),让小字符串直接存储在对象内部,避免堆内存分配。有趣的是,根据 ABI 的不同,libc++ 实际存在两种不同的内存布局方案,由 _LIBCPP_ABI_ALTERNATE_STRING_LAYOUT 宏控制。
默认布局
在未定义特殊宏的情况下,libc++ 使用这种标准布局。__long 和 __short 结构体的字段顺序如下:
__long(长字符串):__is_long_ + __cap_ 位域在前,然后是 __size_(长度),最后是 __data_(指向堆内存的指针)
__short(短字符串):__is_long_ + __size_ 位域在前,后面是 __data_(内联缓冲区,23 字节)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| #import "@preview/rivet:0.3.0": schema, config
#set page(height: auto, margin: 0pt)
#let default_layout = schema.load(```yaml structures: main: bits: 24 ranges: 22-0: name: Payload depends-on: 23 values: "0": description: # Short String (SSO) structure: __short "1": description: # Long String (Heap) structure: __long 23: name: F description: flag (MSB) values: "0": is_long=0, SSO mode "1": is_long=1, heap mode
__short: bits: 23 ranges: 22-0: name: __data_ description: inline string buffer (23 bytes) 23: name: '' # F description: is_long (MSB) + size (7 LSBs)
__long: bits: 23 ranges: 23-16: name: __cap_ description: capacity (8 bytes, MSB is is_long) 15-8: name: __size_ description: length (8 bytes) 7-0: name: __data_ description: pointer to data (8 bytes) 23: name: '' # F description: is_long flag (MSB) ```)
#schema.render(default_layout, config: config.config( default-font-family: "Monaspace Neon", default-font-size: 14pt, italic-font-family: "Monaspace Neon", italic-font-size: 12pt ))
|
Alternate Layout
当 ABI 定义了 _LIBCPP_ABI_ALTERNATE_STRING_LAYOUT 宏时,libc++ 使用这种布局。字段顺序与默认布局相反:
__long(长字符串):__data_ 指针在前,接着是 __size_,最后是 __cap_ + __is_long_ 位域
__short(短字符串):__data_ 缓冲区在前,__size_ + __is_long_ 位域在后
核心差异:两种布局的区别主要体现在字段顺序和标志位位置。
简单对比:
- 默认布局:位域在结构体开头,
__is_long_ 位于最高位(MSB)
- Alternate Layout:位域在结构体末尾,
__is_long_ 位于最低位(LSB)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| #import "@preview/rivet:0.3.0": schema, config
#set page(height: auto, margin: 0pt)
#let alternate_layout = schema.load(```yaml structures: main: bits: 24 ranges: 23-1: name: Payload depends-on: 0 values: "0": description: # Short String (SSO) structure: __short "1": description: # Long String (Heap) structure: __long 0: name: F description: flag (LSB) values: "0": is_long=0, SSO mode "1": is_long=1, heap mode
__long: bits: 24 ranges: 23-16: name: __data_ description: pointer to data (8 bytes) 15-8: name: __size_ description: length (8 bytes) 7-0: name: __cap_ description: capacity (7 bytes, byte 0 holds cap_lo + is_long) 0: name: '1' description: is_long (LSB) and cap_lo (7 MSBs)
__short: bits: 24 ranges: 23-1: name: __data_ description: inline string buffer (23 bytes) 0: name: '0' description: is_long (LSB) and size (7 MSBs) ```)
#schema.render(alternate_layout, config: config.config( default-font-family: "Monaspace Neon", default-font-size: 14pt, italic-font-family: "Monaspace Neon", italic-font-size: 12pt ))
|
两种布局的核心差异
| 特性 |
默认布局 |
Alternate Layout |
__is_long_ 位置 |
最高位(MSB) |
最低位(LSB) |
| 位域位置 |
结构体开头 |
结构体末尾 |
两者的本质区别是位域和标志位的位置安排不同,一个前置一个后置。
代码层面的差异
以下是两种布局在 libc++ 源码中的简化对比:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #ifdef _LIBCPP_ABI_ALTERNATE_STRING_LAYOUT
struct __long { pointer __data_; size_type __size_; size_type __cap_ : sizeof(size_type) * CHAR_BIT - 1; size_type __is_long_ : 1; };
struct __short { value_type __data_[__min_cap]; unsigned char __padding_[sizeof(value_type) - 1]; unsigned char __size_ : 7; unsigned char __is_long_ : 1; }; #else
struct __long { struct { size_type __is_long_ : 1; size_type __cap_ : sizeof(size_type) * CHAR_BIT - 1; }; size_type __size_; pointer __data_; };
struct __short { struct { unsigned char __is_long_ : 1; unsigned char __size_ : 7; }; value_type __data_[__min_cap]; }; #endif
|
总结
_LIBCPP_ABI_ALTERNATE_STRING_LAYOUT 宏由 ABI 层面定义,控制着 libc++ std::string 的两种内存布局:
- 默认布局:位域字段在结构体开头,
__is_long_ 位于最高位(MSB)
- Alternate Layout:位域字段在结构体末尾,
__is_long_ 位于最低位(LSB)
两种布局在 ARM64 上 SSO 容量均为 23 字节(64 位系统),足以容纳大多数日常使用的短字符串。两种布局并存的原因主要与 ABI 兼容性有关,不同平台或历史版本可能采用不同的默认设置。
实际应用
在逆向工程或跨语言交互场景中,正确识别目标平台使用的布局至关重要。例如,在 Android 平台上与 libc++ 的 std::string 进行交互时,需要运行时检测当前使用的是哪种布局。相关实现可以参考我的 MistService-PoC,其中展示了如何通过运行时检测来确定 libc++ 的布局类型,并据此正确解析字符串内容。