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

Threads spawned by a detoured pthread_create do not execute instructions

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

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

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

(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 和相关绕行代码的反汇编和执行在这两种情况下似乎是相同的。


解决方法

我发现了一些解决此问题的奇怪解决方法,但没有多大意义。鉴于绕行功能,许多事情可以代入以下内容:

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);
}
  1. 缓存刷新。
    __asm__ __volatile__("" ::: "memory");
    _mm_clflush(real_pthread_create);
  1. 任何时间的睡眠 - usleep(1)
  2. 一个printf语句。
  3. 大于 32768 字节的内存分配,例如void *data = malloc(40000);.

缓存?

所有这些似乎都指向一个过时的指令缓存。但是,Intel 手册说明如下:

A write to a memory location in a code segment that is currently cached in the processor causes the associated cache line (or lines) to be invalidated. This check is based on the physical address of the instruction. In addition, the P6 family and Pentium processors check whether a write to a code segment may modify an instruction that has been prefetched for execution. If the write affects a prefetched instruction, the prefetch queue is invalidated. This latter check is based on the linear address of the instruction.

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


竞争条件?

排队的下一个嫌疑人是竞争条件。然而,由于一开始没有其他线程,所以不清楚会发生什么。我已经为随机生成的数字进行了斐波那契数列计算,但它仍然会拖延新生成的线程。


问题

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

此时我 运行 没有要检查的东西,欢迎提出任何建议。

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

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

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 会在错误的时间(在绕行之前)被清除。由于绕行函数将包含一些可能,因此执行将如下所示:

pthread_creater8 被清除)-> _pthread_create -> 跳转链 -> pthread_create_detour -> 蹦床(包含 [=13= 的开头) ]) -> _pthread_create + 6

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

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