Case: NSString - Reversing OC in 2025
🧺

Case: NSString - Reversing OC in 2025

Tags
Reverse
Published
January 9, 2025
Author
Mason
众所周知 Objective-C 是一门 面向 OOP 的动态语言。
与传统 C/C++逆向不同,OC 逆向中通常会遇到抽象的对象,而不是底层类型。
本文章将通过编程,阅读 OC runtime, Swift CoreFoundation 源码来解释逆向过程遇到的一些问题。
个人能力所限,如文中包含错误欢迎路过的各位留言指正。

id

OC中,每一个类都代表一个对象,而 id 可以表示一个任意的 Objective-C 对象。
翻阅其 runtime:runtime/objc-private.h#L83
typedef struct objc_object *id;
可以看到 id 本质上是一个指向 objc_object 的指针,而  objc_object class 中仅有一个名为 isa 的变量:
struct objc_object { private: char isa_storage[sizeof(isa_t)]; [...]
isa_storage 实际存储结构为 isa_t
union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } uintptr_t bits; private: // Accessing the class requires custom ptrauth operations, so // force clients to go through setClass/getClass by making this // private. Class cls; public: #if defined(ISA_BITFIELD) struct { ISA_BITFIELD; // defined in isa.h }; [...]
ISA_BITFIELD 作为 macro 在 runtime/isa.h 定义,这里以 ARM64 为例:runtime/isa.h#L95-L104
# define ISA_BITFIELD \ uintptr_t nonpointer : 1; \ uintptr_t has_assoc : 1; \ uintptr_t has_cxx_dtor : 1; \ uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \ uintptr_t magic : 6; \ uintptr_t weakly_referenced : 1; \ uintptr_t unused : 1; \ uintptr_t has_sidetable_rc : 1; \ uintptr_t extra_rc : 19
因此实际上 isa_t 可被简化为:
union isa_t { uintptr_t bits; Class cls; struct { ISA_BITFIELD; // defined in isa.h }; } // sizeof(isa_t) == 8
通常情况下,isa 并不是一个 raw pointer
在具体值中表现为最低位为1,即 nonpointer=1
例如我们有以下:
@interface MyClass : NSObject @property NSUInteger v1; @property NSUInteger v2; @end @implementation MyClass - (instancetype)init { self = [super init]; return self; } - (void)Print { NSLog(@"v1=%lx", self.v1); NSLog(@"v2=%lx", self.v2); } @end
实例化此对象后,我们可以看到:
(lldb) x myClass 0x6000004297e0: 39 81 00 00 01 00 00 01 34 12 00 00 00 00 00 00 9.......4....... 0x6000004297f0: 78 56 00 00 00 00 00 00 00 00 00 00 00 00 00 00 xV.............. (lldb) x/5wgx myClass 0x6000004297e0: 0x0100000100008139 0x0000000000001234 0x6000004297f0: 0x0000000000005678 0x0000000000000000 0x600000429800: 0x0000000100003bb8
很容易发现:
isa:0x0100000100008139
v1: 0x0000000000001234
v2: 0x0000000000005678
则我们可以根据 isa 知道各个 bitfield:
nonpointer: 1 has_assoc: 0 has_cxx_dtor: 0 shiftcls: 0x20001027 magic: 0 weakly_referenced: 0 unused: 0 has_sidetable_rc: 0 extra_rc: 2048
此时 nonpointer=1 则其中的 shiftcls 指代实际内存地址,对应地址即对应实例对应的 objc class:
(lldb) po 0x20001027UL<<3 // 0x100008138 MyClass

NSString

在传统C/C++中,字符串的底层类型通常以 char* 指代,我们可以通过指针值读取对应内存上的字符串。
在 OC 中,字符串通常以 NSString 指代,例如有以下代码:
NSString *s = [NSString stringWithFormat:@"Hello Objective-C"]; NSLog(@"ptr=%p", s);
其打印内容为一个指针值:
ptr=0x600000a546c0
通过lldb 我们可以读取内存:
(lldb) x s2 0x600003570570: b1 c5 f1 fb 01 00 00 01 8c 07 00 1b 86 23 02 00 .............#.. 0x600003570580: 11 48 65 6c 6c 6f 20 4f 62 6a 65 63 74 69 76 65 .Hello Objective (lldb) x/5wgx s2 0x600003570570: 0x01000001fbf1c5b1 0x000223861b00078c 0x600003570580: 0x4f206f6c6c654811 0x6576697463656a62 0x600003570590: 0x000000000000432d
可以看到其内容不单单是 C/C++ 中的字符串值,而是特定的一个 NSString 对象。
那么 NSString 的内存排布是什么样的?
翻阅 CoreFoundation 的源码我们可以看到其 __CFString 定义:
// This is separate for C++ struct __notInlineMutable { void *buffer; CFIndex length; CFIndex capacity; // Capacity in bytes unsigned int hasGap:1; // Currently unused unsigned int isFixedCapacity:1; unsigned int isExternalMutable:1; unsigned int capacityProvidedExternally:1; #if __LP64__ unsigned long desiredCapacity:60; #elif __LLP64__ unsigned long long desiredCapacity:60; #else unsigned long desiredCapacity:28; #endif CFAllocatorRef contentsAllocator; // Optional }; // The only mutable variant for CFString /* !!! Never do sizeof(CFString); the union is here just to make it easier to access some fields. */ struct __attribute__((__aligned__(8))) __CFString { CFRuntimeBase base; union { // In many cases the allocated structs are smaller than these struct __inline1 { CFIndex length; } inline1; // Bytes follow the length struct __notInlineImmutable1 { void *buffer; // Note that the buffer is in the same place for all non-inline variants of CFString CFIndex length; CFAllocatorRef contentsDeallocator; // Optional; just the dealloc func is used } notInlineImmutable1; // This is the usual not-inline immutable CFString struct __notInlineImmutable2 { void *buffer; CFAllocatorRef contentsDeallocator; // Optional; just the dealloc func is used } notInlineImmutable2; // This is the not-inline immutable CFString when length is stored with the contents (first byte) struct __notInlineMutable notInlineMutable; } variants; };
typedef struct __CFRuntimeBase { __ptrauth_cf_objc_isa_pointer uintptr_t _cfisa; #if defined(__LP64__) || defined(__LLP64__) _Atomic(uint64_t) _cfinfoa; #else _Atomic(uint32_t) _cfinfoa; #endif } CFRuntimeBase;
简单来讲,NSString 中结构为:
Name
Type
Offset
Size(ARM64)
isa
isa_t
0
8
infoa
uint64_t
8
8
variants
union XXX
16
-
本质上 variants 对应多个 union 结构体,其结构需要对应实际优化。
结构上,CFString 分为四种类型:
inline
not inline & Immutable
not inline & Immutable & length stored with the contents (first byte)
not inline & Mutable
参考注释可以看到,infoa 低位字节 8bit 分别代表不同的含义:
/* I = is immutable E = not inline contents U = is Unicode N = has NULL byte L = has length byte D = explicit deallocator for contents (for mutable objects, allocator) C = length field is CFIndex (rather than UInt32); only meaningful for 64-bit, really if needed this bit (valuable real-estate) can be given up for another bit elsewhere, since this info is needed just for 64-bit Also need (only for mutable) F = is fixed G = has gap Cap, DesCap = capacity B7 B6 B5 B4 B3 B2 B1 B0 U N L C I B6 B5 0 0 inline contents 0 1 E (freed with default allocator) 1 0 E (not freed) 1 1 E D !!! Note: Constant CFStrings use the bit patterns: C8 (11001000 = default allocator, not inline, not freed contents; 8-bit; has NULL byte; doesn't have length; is immutable) D0 (11010000 = default allocator, not inline, not freed contents; Unicode; is immutable) The bit usages should not be modified in a way that would effect these bit patterns. Note that some of the bit patterns in the enum below overlap and are duplicated. Keep this in mind as you do searches for use cases. */
同时我们可以参考:
etc.
// CFInternal.h static inline int __CFRuntimeGetFlag(CFTypeRef cf, uint8_t n) { return __CFRuntimeGetValue(cf, n, n) == 1; } // CFString.c CF_INLINE void __CFStrSetInlineContents(CFStringRef str, _CFStringInlineContents contents) {__CFRuntimeSetValue(str, 6, 5, contents);} CF_INLINE Boolean __CFStrIsInline(CFStringRef str) {return __CFRuntimeGetValue(str, 6, 5) == __kCFHasInlineContents;} [...] CF_INLINE Boolean __CFStrHasExplicitLength(CFStringRef str) { // Has explicit length if (1) mutable or (2) not mutable and no length byte const uint8_t isMutableMask = 1 | 4; // is_mutable_mask | has_length_byte_mask const uint8_t hasLengthByteMask = 4; // has_length_byte_mask return (__CFRuntimeGetValue(str, 2, 0) & isMutableMask) != hasLengthByteMask; }
其 rumtime 通过 __CFRuntimeGetValue , __CFRuntimeGetFlag 获取 infoa 中的特定位标记来确定处理逻辑。
例如:我们有 infoa: 0x2b7dffd00078c
其中低8bit 为 0x8c, 分解为 bitfield, 如下图所示:
notion image
结合上面注释中所描述的,不难得出,这个 NSString 的类型为 inline, contents 中存在空字节、长度字节。
为了了解 Core Foundation 如何解析 CFString,我们关注到以下几个函数:
CF_INLINE Boolean __CFStrHasExplicitLength(CFStringRef str) { // Has explicit length if (1) mutable or (2) not mutable and no length byte const uint8_t isMutableMask = 1 | 4; // is_mutable_mask | has_length_byte_mask const uint8_t hasLengthByteMask = 4; // has_length_byte_mask return (__CFRuntimeGetValue(str, 2, 0) & isMutableMask) != hasLengthByteMask; } [...] /* Returns ptr to the buffer (which might include the length byte). */ CF_INLINE const void * _Nullable __CFStrContents(CFStringRef str) { if (__CFStrIsInline(str)) { return (const void *)(((uintptr_t)&(str->variants)) + (__CFStrHasExplicitLength(str) ? sizeof(CFIndex) : 0)); } else { // Not inline; pointer is always word 2 return str->variants.notInlineImmutable1.buffer; } } [...] /* Returns length; use __CFStrLength2 if contents buffer pointer has already been computed. */ CF_INLINE CFIndex __CFStrLength(CFStringRef str) { if (__CFStrHasExplicitLength(str)) { if (__CFStrIsInline(str)) { return str->variants.inline1.length; } else { return str->variants.notInlineImmutable1.length; } } else { return (CFIndex)(*((uint8_t *)__CFStrContents(str))); } }
函数 __CFStrLength 用于获取 NSString 的长度,可以看到其根据 __CFStrHasExplicitLength 调用结果为条件进入分支,当 __CFStrHasExplicitLength 返回 FALSE,那么其将调用 __CFStrContents 读取 str->variants 的第一个字节返回长度,否则将根据是否 inline 分别处理。
对于上述内存展示的 NSString,根据现有知识以及推测,我们不难将其分解为:
+0x00: 0x01000001fbf1c5b1 <-- isa +0x08: 0x000223861b00078c <-- infoa +0x10: 0x11 <-- length +0x11: <-- "Hello Objective-C"
显然在这里 __CFStrHasExplicitLength 应当返回 FALSE。
再来看下此函数:
CF_INLINE Boolean __CFStrHasExplicitLength(CFStringRef str) { // Has explicit length if (1) mutable or (2) not mutable and no length byte const uint8_t isMutableMask = 1 | 4; // is_mutable_mask | has_length_byte_mask const uint8_t hasLengthByteMask = 4; // has_length_byte_mask return (__CFRuntimeGetValue(str, 2, 0) & isMutableMask) != hasLengthByteMask; }
正如注释写的,__CFStrHasExplicitLength 返回 TRUE 对应两种情况:
字符串对象可变
字符串对象不可变且 content 中不包含长度字节
而在上述内存展示的 NSString 中,该字符串对象不可变但包含长度字节,__CFStrHasExplicitLength 将返回 FALSE,满足假设。
下面来看看,不同情况的 NSString。

No-Inline

有以下代码:
NSString *s3 = [[NSString alloc] initWithFormat:@"Hello Objective-C"]; s3 = [s3 stringByAppendingString:@"123"]; NSLog(@"ptr=%p", s3);
lldb 查看内存:
(lldb) x/5wgx s3 0x600001a14480: 0x01000001fbf1c5b1 0x0001370f8a0007f0 0x600001a14490: 0x0000600001a143f0 0x0000000000000014 0x600001a144a0: 0x00000001fd0622a0
对应于结构:
struct __notInlineImmutable1 { void *buffer; // Note that the buffer is in the same place for all non-inline variants of CFString CFIndex length; CFAllocatorRef contentsDeallocator; // Optional; just the dealloc func is used } notInlineImmutable1; // This is the usual not-inline immutable CFString
则我们有:
(lldb) x 0x0000600001a143f0 0x600001a143f0: 48 00 65 00 6c 00 6c 00 6f 00 20 00 4f 00 62 00 H.e.l.l.o. .O.b. 0x600001a14400: 6a 00 65 00 63 00 74 00 69 00 76 00 65 00 2d 00 j.e.c.t.i.v.e.-. (lldb) po 0x00000001fd0622a0 <CFAllocator 0x1fd0622a0 [0x1fd0621c0]>{info = 0x0}
符合实际。

NSMutableString

有以下代码:
NSMutableString *ms = [NSMutableString stringWithFormat:@"Hello Objective-C"]; NSLog(@"ptr=%p", ms);
lldb 查看内存地址:
(lldb) x/8wgx ms 0x6000023b8000: 0x01000001fbf1c5b1 0x00029486a50007ad 0x6000023b8010: 0x0000600002db81c0 0x0000000000000011 0x6000023b8020: 0x0000000000000020 0x0000000000000200 0x6000023b8030: 0x01000001fbf1c5b1 0x00025c38c700078c
不妨先猜测一番:
+0x00 -> isa -> 0x01000001fbf1c5b1 +0x08 -> infoa -> 0x00029486a50007ad +0x10 -> buffer -> 0x0000600002db81c0 +0x18 -> length -> 0x0000000000000011
看看 buffer:
(lldb) x 0x0000600002db81c0 0x600002db81c0: 11 48 65 6c 6c 6f 20 4f 62 6a 65 63 74 69 76 65 .Hello Objective 0x600002db81d0: 2d 43 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -C..............
符合预期结果。
回到源码,可变字符串对象对应结构为 __notInlineMutable
// This is separate for C++ struct __notInlineMutable { void *buffer; CFIndex length; CFIndex capacity; // Capacity in bytes unsigned int hasGap:1; // Currently unused unsigned int isFixedCapacity:1; unsigned int isExternalMutable:1; unsigned int capacityProvidedExternally:1; #if __LP64__ unsigned long desiredCapacity:60; #elif __LLP64__ unsigned long long desiredCapacity:60; #else unsigned long desiredCapacity:28; #endif CFAllocatorRef contentsAllocator; // Optional }; // The only mutable variant for CFString
根据此结构,我们可以补充得到:
+0x00 -> isa -> 0x01000001fbf1c5b1 +0x08 -> infoa -> 0x00029486a50007ad +0x10 -> buffer -> 0x0000600002db81c0 +0x18 -> length -> 0x0000000000000011 +0x20 -> capacity -> 0x0000000000000020 +0x28 -> bitfields -> 0x0000000000000200 -> (1<<9)

NSTaggedPointerString

写一段简单代码
NSString *s = [NSString stringWithFormat:@"abc"]; NSLog(@"ptr=%p", s);
预期我们得到的应是一个合法地址,但事实并非如此:
ptr=0x9e97e46a21da2ea0
使用 lldb 查看此地址,得到错误返回:
(lldb) x s error: memory read failed for 0xffffe46a21da2e00
查看其类型为:
(lldb) expr s.class (Class) $1 = NSTaggedPointerString
根据已有材料可知:
tagged pointer专门用来存储小的对象,例如NSNumberNSDateNSString
tagged pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要mallocfree
tagged pointer 默认情况下被混淆了。
翻阅 runtime: runtime/objc-runtime-new.mm#L9498-L9539 可以看到:
/*********************************************************************** * initializeTaggedPointerObfuscator * Initialize objc_debug_taggedpointer_obfuscator with randomness. * * The tagged pointer obfuscator is intended to make it more difficult * for an attacker to construct a particular object as a tagged pointer, * in the presence of a buffer overflow or other write control over some * memory. The obfuscator is XORed with the tagged pointers when setting * or retrieving payload values. They are filled with randomness on first * use. **********************************************************************/ static void initializeTaggedPointerObfuscator(void) { if (!DisableTaggedPointerObfuscation #if !TARGET_OS_EXCLAVEKIT && dyld_program_sdk_at_least(dyld_fall_2018_os_versions) #endif ) { // Pull random data into the variable, then shift away all non-payload bits. arc4random_buf(&objc_debug_taggedpointer_obfuscator, sizeof(objc_debug_taggedpointer_obfuscator)); objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK; #if OBJC_SPLIT_TAGGED_POINTERS // The obfuscator doesn't apply to any of the extended tag mask or the no-obfuscation bit. objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK); // Shuffle the first seven entries of the tag permutator. int max = 7; for (int i = max - 1; i >= 0; i--) { int target = uniformRandom(i + 1); swap(objc_debug_tag60_permutations[i], objc_debug_tag60_permutations[target]); } #endif } else { // Set the obfuscator to zero for apps linked against older SDKs, // in case they're relying on the tagged pointer representation. objc_debug_taggedpointer_obfuscator = 0; } }
objc_debug_taggedpointer_obfuscator 存储一个随机数生成的 xorkey。
而后在 runtime/objc-internal.h#L859-L863 中进行解密:
static inline uintptr_t _objc_decodeTaggedPointer_noPermute_withObfuscator(const void * _Nullable ptr, uintptr_t obfuscator) { uintptr_t value = (uintptr_t)ptr; #if OBJC_SPLIT_TAGGED_POINTERS if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK) return value; #endif return value ^ obfuscator; } static inline uintptr_t _objc_decodeTaggedPointer_withObfuscator(const void * _Nullable ptr, uintptr_t obfuscator) { uintptr_t value = _objc_decodeTaggedPointer_noPermute_withObfuscator(ptr, obfuscator); #if OBJC_SPLIT_TAGGED_POINTERS uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK; value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT); value |= _objc_obfuscatedTagToBasicTag(basicTag) << _OBJC_TAG_INDEX_SHIFT; #endif return value; } [...] static inline uintptr_t _objc_getTaggedPointerValue(const void * _Nullable ptr) { return _objc_getTaggedPointerValue_withObfuscator(ptr, objc_debug_taggedpointer_obfuscator); }
从开发者角度我们可以通过引入全局变量,得到 xorkey
extern uintptr_t objc_debug_taggedpointer_obfuscator;
以此来解密得到原始 tagged pointer
当然也可以通过指定编译时环境变量 OBJC_DISABLE_TAG_OBFUSCATION=YES 来取消加密。
Q: 从逆向工程角度,我们应该如何解密 tagged pointer?
答案很简单,由于 objc_debug_taggedpointer_obfuscator 作为导出变量存在,我们可以通过 libobjc 的导出表获取变量地址,得到 xorkey 并恢复得到 raw tagged pointer:
NSString *s = [NSString stringWithFormat:@"abc"]; NSLog(@"ptr=%p", s); uintptr_t *objc_debug_taggedpointer_obfuscator = dlsym(RTLD_DEFAULT, "objc_debug_taggedpointer_obfuscator"); NSLog(@"objc_debug_taggedpointer_obfuscator=0x%llx", *objc_debug_taggedpointer_obfuscator); NSLog(@"restore ptr=%p", (void *)(*objc_debug_taggedpointer_obfuscator ^ (uintptr_t)s));
得到 raw tagged pointer 后,我们进一步思考:这个值意味着什么?
根据函数 _objc_makeTaggedPointer_withObfuscator
#define _OBJC_TAG_INDEX_MASK 0x7UL [...] # define _OBJC_TAG_INDEX_SHIFT 0 [...] __attribute__((no_sanitize("unsigned-shift-base"))) static inline void * _Nonnull _objc_makeTaggedPointer_withObfuscator(objc_tag_index_t tag, uintptr_t value, uintptr_t obfuscator) { // PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts. // They are reversed here for payload insertion. // ASSERT(_objc_taggedPointersEnabled()); if (tag <= OBJC_TAG_Last60BitPayload) { // ASSERT(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value); uintptr_t result = (_OBJC_TAG_MASK | ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) | ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT)); return _objc_encodeTaggedPointer_withObfuscator(result, obfuscator); } else { // ASSERT(tag >= OBJC_TAG_First52BitPayload); // ASSERT(tag <= OBJC_TAG_Last52BitPayload); // ASSERT(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value); uintptr_t result = (_OBJC_TAG_EXT_MASK | ((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) | ((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT)); return _objc_encodeTaggedPointer_withObfuscator(result, obfuscator); } }
可知其值的最高位表示是否为 tagged pointer
低 3bits 为 tag,对应类型位于 runtime/objc-internal.h#L497
// 60-bit payloads OBJC_TAG_NSAtom = 0, OBJC_TAG_1 = 1, OBJC_TAG_NSString = 2, OBJC_TAG_NSNumber = 3, OBJC_TAG_NSIndexPath = 4, OBJC_TAG_NSManagedObjectID = 5, OBJC_TAG_NSDate = 6,
假定我们构造 NSString,得到 raw tagged pointer 为 0x8000000031b13098
我们分解得到最高位为 1 符合预期
再来看低三位为 000 对应类型为 OBJC_TAG_NSAtom
?我们构造的是 NSString 却得到了 NSAtom,这并不符合直觉,仔细阅读源码发现,tag 同样被混淆了:
#if OBJC_SPLIT_TAGGED_POINTERS extern uint8_t objc_debug_tag60_permutations[8]; static inline uintptr_t _objc_basicTagToObfuscatedTag(uintptr_t tag) { return objc_debug_tag60_permutations[tag]; } static inline uintptr_t _objc_obfuscatedTagToBasicTag(uintptr_t tag) { for (unsigned i = 0; i < 7; i++) if (objc_debug_tag60_permutations[i] == tag) return i; return 7; } #endif
同理我们可以通过 dlsym 得到 objc_debug_tag60_permutations 进而解混淆得到正确的 tag:
uintptr_t tagged_s = *objc_debug_taggedpointer_obfuscator ^ (uintptr_t)s; NSLog(@"restore ptr=0x%lx", tagged_s); uint8_t obf_tag = tagged_s & 0x7; uint8_t tag = 7; for (uint i = 0; i < 7; i++) { if (objc_debug_tag60_permutations[i] == obf_tag) { tag = i; break; } } NSLog(@"tag=%d", tag); // tag=2
回到 _objc_getTaggedPointerValue_withObfuscator:
__attribute__((no_sanitize("unsigned-shift-base"))) static inline uintptr_t _objc_getTaggedPointerValue_withObfuscator(const void * _Nullable ptr, uintptr_t obfuscator) { // ASSERT(_objc_isTaggedPointer(ptr)); uintptr_t value = _objc_decodeTaggedPointer_noPermute_withObfuscator(ptr, obfuscator); uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK; if (basicTag == _OBJC_TAG_INDEX_MASK) { return (value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT; } else { return (value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT; } }
我们最终通过舍去最高位以及低3位得到 content,例如 0x803332b231b130b3:
((0x803332b231b130b3<<1)>>4) -> 0x6665646362616 -> "abcdef"
实际上我们可以存储最大 60 bits 的内容(包含4bits的长度字段)。
当字符串长度大于 7 小于 0xa 时,又是另一种优化策略了 :P