绕道的 pthread_create 产生的线程不执行指令

Posted

技术标签:

【中文标题】绕道的 pthread_create 产生的线程不执行指令【英文标题】:Threads spawned by a detoured pthread_create do not execute instructions 【发布时间】:2020-09-10 16:41:49 【问题描述】:

我在 macOS 上有一个 detours 的自定义实现和一个使用它的测试应用程序,它是用 C 编写的,为 macOS x86_64 编译,在 Intel i9 处理器上运行。

该实现适用于多种功能。但是,如果我绕道pthread_create,我会遇到奇怪的行为:通过绕道的 pthread_create 生成的线程不执行指令。我可以一步一步完成说明,但只要我continue 它就没有进展。不涉及互斥锁或同步,函数的结果为 0(成功)。关闭 detours 的完全相同的应用程序运行良好,因此不太可能是罪魁祸首。

这不会一直发生 - 有时它们很好,但有时测试应用程序会停滞在以下状态:

(lldb) bt all
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
  * frame #0: 0x00007fff7296f55e libsystem_kernel.dylib`__ulock_wait + 10
    frame #1: 0x00007fff72a325c2 libsystem_pthread.dylib`_pthread_join + 347
    frame #2: 0x0000000100001186 DetoursTestApp`main + 262
    frame #3: 0x00007fff7282ccc9 libdyld.dylib`start + 1
    frame #4: 0x00007fff7282ccc9 libdyld.dylib`start + 1
  thread #2
    frame #0: 0x00007fff72a2cb7c libsystem_pthread.dylib`thread_start

相关内存页设置了可执行标志。拦截线程创建的detour函数如下所示:

static int pthread_create_detour(pthread_t* thread,
                                 const pthread_attr_t* attr,
                                 void* (*start_routine)(void*),
                                 void* arg)

    detour_count++;
    pthread_fn original = (pthread_fn)detour_original(dlsym((void*)-1, "pthread_create"));
    return original(thread, attr, start_routine, arg);

detour_original 检索指向 [原始函数 + 函数序言的大小] 的指针。 跟踪说明,一切似乎都正常工作,pthread_create 成功终止。通过dtruss 跟踪应用程序的系统调用确实显示调用

bsdthread_create(0x10DB964B0, 0x0, 0x7000080DB000)               = 29646848 0

我已经确认的是正确的论点。

这种行为仅在发布版本中观察到 - 调试工作正常,但在这两种情况下,反汇编和执行绕行的 pthread_create 和相关的绕行代码似乎是相同的。


解决方法

对于这个问题,我发现了一些没有多大意义的奇怪解决方法。给定 detour 函数,许多东西可以代入以下内容:

static int pthread_create_detour(pthread_t* thread,
                                 const pthread_attr_t* attr,
                                 void* (*start_routine)(void*),
                                 void* arg)

    detour_count++;
    pthread_fn original = (pthread_fn)detour_original(dlsym((void*)-1, "pthread_create"));
    <...> <== SUBSTITUTE HERE
    return original(thread, attr, start_routine, arg);

    缓存刷新。
    __asm__ __volatile__("" ::: "memory");
    _mm_clflush(real_pthread_create);
    任意时间的睡眠 - usleep(1) printf 声明。 大于 32768 字节的内存分配,例如void *data = malloc(40000);

缓存?

所有这些似乎都指向一个陈旧的指令缓存。但是,英特尔手册声明如下:

对当前缓存在处理器中的代码段中的内存位置进行写入会导致相关的缓存行(或多个行)无效。此检查基于指令的物理地址。此外,P6 系列和 Pentium 处理器检查对代码段的写入是否会修改已预取执行的指令。如果写入影响预取指令,则预取队列无效。后一种检查基于指令的线性地址。

更有趣的是,这些变通方法必须为每个创建的新线程执行,并且执行发生在主线程上,因此它不太可能是缓存。我还尝试在每次写入指令的内存写入时都进行缓存刷新,但这没有帮助。我还编写了一个memcpy,它使用英特尔的内在_mm_stream_si32 绕过缓存,并在我的实现中为每个指令内存写入换出它,但没有任何成功。


比赛条件?

排队的下一个嫌疑人是竞争条件。但是,目前尚不清楚会发生什么,因为一开始没有其他线程。我已经为一个随机生成的数字进行了斐波那契数列计算,但这仍然会停止新生成的线程。


问题

是什么导致了这个问题?还有哪些其他机制可能对此负责?

在这一点上,我已经用完了要检查的东西,所以欢迎任何建议。

【问题讨论】:

它不是 CPU 级别的陈旧缓存;我们知道,根据您找到的文档和实验,这在实际硬件上不会发生:Observing stale instruction fetching on x86 with self-modifying code 这可能是 compile-time 通过调用重新排序存储,因为编译器假定存储除非您另有说明,否则不要给代码起别名。 (例如,在您存储到的一系列 insns 上使用 GNU C __builtin___clear_cache(start, end))。 Related re: 用 C 编写机器代码 谢谢,这些读起来很有趣。我试了一下__builtin___clear_cache,但并没有解决问题。事实上,通过lldb 逐条指令迭代代码显示了正确的指令被写入内存,并且它们似乎也被正确执行。我还必须指出,这种行为仅在发布版本中观察到 - 调试工作正常,但在这两种情况下,绕行的 pthread_create 和相关代码的反汇编和执行似乎是相同的。我还检查了内存页是否可执行,它们是。 是否有可能创建一个实际的minimal reproducible example 其他人可以用来在 MacOS 系统上重现它?我没有,但任何人都可能很难在无法尝试的情况下帮助您。我仍然怀疑某种编译时重新排序很可能是问题所在,特别是如果优化与未优化问题,asm("" ::: "memory"); 修复了它。 (我猜编译器内存屏障可以在没有实际 clflush 的情况下工作,而 printf 或 malloc 之类的其他东西也可以工作,因为它们也是有效的编译器屏障的非内联函数调用) 或者可能存在某种时间问题,因为您说较小的 malloc 不起作用?那么可能会耗尽该核心上的存储缓冲区,以便您的存储对其他核心可见? clflush 可能足够慢来做到这一点。也许可以试试 C11 中的 atomic_thread_fence(memory_order_seq_cst) &lt;stdatomic.h&gt;。 (即mfence 或虚拟locked 操作) 谢谢你,彼得。由于实现与专有代码捆绑在一起,我无法提供最小的可重现示例。幸运的是,我最终确实找到了解决方案(见答案)。上述所有解决方法的共同点是那些特定的命令序列将清除r8。如果没有我分享实现,我认为不可能为其他任何人解决它。 【参考方案1】:

我发现生成的线程没有执行指令的原因是由于我的 detours 实现出现问题,r8 寄存器在执行pthread_create 时没有在正确的时间被清除。

如果我们看一下函数的反汇编,它被分成两部分——在内部_pthread_create 函数中找到的“头”和“主体”。头部做了两件事 - 将 r8 归零并跳转到身体:

libsystem_pthread.dylib`pthread_create:
    0x7fff72a2e236 <+0>: 45 31 c0        xor    r8d, r8d
    0x7fff72a2e239 <+3>: e9 40 37 00 00  jmp    0x7fff72a3197e            ; _pthread_create

libsystem_pthread.dylib`_pthread_create:
    0x7fff72a3197e <+0>:    55                                push   rbp
    0x7fff72a3197f <+1>:    48 89 e5                          mov    rbp, rsp
    0x7fff72a31982 <+4>:    41 57                             push   r15
    <...> // the rest of the 1409 instructions

我的实现会绕道内部_pthread_create 函数而不是包含实际入口点的头部,这意味着r8 将在错误的时间(绕道之前)被清除。由于 detour 函数将包含一些可能,因此执行将类似于:

pthread_creater8 被清除) -> _pthread_create -> 跳跃链 -> pthread_create_detour -> 蹦床(包含 _pthread_create 的开头) -> _pthread_create + 6

这意味着根据pthread_create_detour 函数的内容,r8 在返回到内部函数时并不总是以 0 结尾。

目前尚不清楚为什么在_pthread_create 之前将r8 设置为0 以外的值不会崩溃,而是会在锁定状态下启动线程。一个重要的细节是,停滞的线程会将rflags 寄存器设置为0x200,根据Intel's manual,这绝不应该是这种情况。这就是导致我更仔细地检查 CPU 状态并得出答案的原因。

【讨论】:

以上是关于绕道的 pthread_create 产生的线程不执行指令的主要内容,如果未能解决你的问题,请参考以下文章

为啥我得到一个未定义的 pthread_create 引用? [复制]

pthread_create创建线程失败问题排查

等待 pthread_create 完成而不使用 pthread_join

多线程pthread_create的参数

使用 pthread_create 时出现“分段错误(核心转储)”

linux创建线程之pthread_create