示例如下: 
直接断点运行查看汇编实现
由于我们对 thread_local tls_variable 变量进行了 ++ 操作,因此在汇编中大概率会有一个 add x?, x?, #1 的指令,因此通过观察下图划线的三条指令,可以得知 x8 寄存器中存储的地址就是获取 tls_variable 变量的 dyld 函数 tlv_get_addr。
对 tlv_get_addr 进行符号断点分析发现:
- 当
TPIDRRO_EL0寄存器对应内存中存在pthread_key_t key对应的值,则直接返回内存地址 ( 函数instantiateTLVs_thunk的第一个参数的签名为 pthread_key_t ) - 如果不符合 1,则执行 dyld
instantiateTLVs_thunk以及RuntimeState::_instantiateTLVs
tlv_get_addr 函数的源码也可通过 dyld 的 threadLocalHelpers.s 文件查看
instantiateTLVs_thunk 的实现主要是对 RuntimeState::_instantiateTLVs 的包装 
RuntimeState::_instantiateTLVs 实现如下:
针对单个 pthread_key_t 的 lazy 实现,使用 libsystem 的 malloc 开辟相关的内存,再保存到 pthread 的 tsd 数组中
libpthread 中 _pthread_setspecific 的实现如下: 
基本流程了解后,目前未解决的问题有如下:
- 变量
thread_local int tls_variable是如何访问到的? tlv_get_addr函数是如何被设置到 x8 寄存器对应内存?其中偏移值为 #0x8 #0x10 的内存具体有什么含义?TPIDRRO_EL0寄存器是何时被赋值的?
问题一:
tls_variable 变量是如何访问到的?
注意这里的 adrp x0, 5 指令,代表 ( 当前 pc 寄存器值 & page_size ) + 5 * page_size 的结果赋值到 x0 寄存器。由于在 Macos 下 page_size 是 4K,因此这里的计算方式为 x0 = (0x1000030a4 & 0x1000) + 5 * 0x1000 = 0x100008000

同时该内存在进程中所在的 section 为 __DATA,__thread_vars,我们的进程中有两个 thread_local 变量,此 section 的大小却为 0x30,因此推断每个变量在 Section 中占用 0x18 字节,同时也能和汇编中的 #0x8, #0x10 的偏移量访问对应。同时 thread_local 变量的初始值是通过 __DATA,__thread_data 和 __DATA,__thread_bss 两个 Section 来初始化的(相关代码可以在 ld64 和 dyld 中找到) 
问题二:
tlv_get_addr 函数是如何被设置到 x8 寄存器对应内存?其中偏移值为 #0x8 #0x10 的内存具体有什么含义?

arm64 dyld 在进程启动时,forEachThreadLocalVariable函数会以单次 0x18 (struct TLV_Info) 字节大小遍历 __DATA,__thread_vars,同时在 #0x0 设置 tlv_get_addr 函数指针,#0x8 设置 pthread_key_t,#0x10 代表 offset。TLV_Info 结构体如下:
C
struct TLV_Thunk
{
void* (*thunk)(TLV_Thunk*);
size_t key;
size_t offset;
};
因此 #0x0 指的是此处的 thunk, #0x8 是 pthread_key,#0x16 是 offset 变量
问题三: TPIDRRO_EL0 寄存器是何时被赋值的?
明确一个结论:用户态下 TPIDRRO_EL0 是无法被设置的,只有在内核态才能。
默认情况下, libpthread 在初始化线程时将会使用 struct phthread_s 成员变量 tsd 的起始地址作为 TPIDRRO_EL0 寄存器的值

最终在内核态的 xnu/osfmk/arm/machdep_call.c 设置 TPIDRRO_EL0 寄存器 
因此,如果我们能使用用户态 API 直接设置 TPIDRRO_EL0 寄存器,即可伪造指定线程的 TLS