谁创建和拥有调用堆栈以及调用堆栈如何在多线程中工作?

Posted

技术标签:

【中文标题】谁创建和拥有调用堆栈以及调用堆栈如何在多线程中工作?【英文标题】:Who creates and owns the call stack and how does call stack works in multithread? 【发布时间】:2020-02-12 18:40:05 【问题描述】:

我知道每个线程通常都有一个调用栈,它只是一块内存,由 esp 和 ebp 控制。

1,这些调用堆栈是如何创建的?谁负责这样做?我的猜测是运行时,例如 ios 应用程序的 Swift 运行时。线程是通过 esp 和 ebp 还是通过运行时直接与自己的调用堆栈对话?

2,对于每个调用堆栈,它们必须使用 esp 和 ebb cpu 寄存器,如果我的 CPU 有 2 核 4 线程,那么假设它有 4 核(指令集)。这是否意味着每个调用堆栈只能在特定内核中使用这些寄存器?

【问题讨论】:

每个线程都有自己的堆栈,由mmap或其他东西分配在进程的地址空间中。是的,每个软件线程都有自己的架构状态,包括 x86-64 上的 RSP 或 sp on AArch64。 (或 ESP,如果您制作过时的 32 位 x86 代码)。我假设帧指针对于 swift 是可选的。是的,每个逻辑核心都有自己的架构状态(寄存器);软件线程上下文切换到硬件逻辑核心。 要跟进@PeterCordes 的评论,每个线程都有自己的调用堆栈,与内核和/或超线程的数量无关。在 Windows 的情况下,线程可以在时间片边界或任何事件触发的唤醒上在内核之间切换,以平衡负载。 不要忘记堆栈指针(你所谓的 ESP)和帧指针 (EBP) 可以像几乎所有架构上的任何其他寄存器一样被加载和保存。处理器的完整状态,包括其 ESP 和 EBP,称为“执行上下文”。将 CPU 从一个线程切换到另一个线程只需保存当前线程的执行上下文,并恢复不同线程的执行上下文。此外,当一个线程的上下文被恢复时,它不一定要恢复到它被保存的同一个处理器。 【参考方案1】:

XNU 内核做到了。 Swift 线程是 POSIX pthread 又名 Mach 线程。在程序启动期间,XNU 内核解析 Mach-O 可执行格式并处理现代 LC_MAIN 或旧版 LC_UNIXTHREAD 加载命令(等等)。这是在内核函数中处理的:

static
load_return_t
load_main(
        struct entry_point_command  *epc,
        thread_t        thread,
        int64_t             slide,
        load_result_t       *result
    )

&

static
load_return_t
load_unixthread(
    struct thread_command   *tcp,
    thread_t        thread,
    int64_t             slide,
    load_result_t       *result
)

恰好是open source

LC_MAIN通过thread_userstackdefault初始化栈

LC_UNIXTHREADload_threadstack

正如@PeterCordes 在 cmets 中提到的,只有当内核创建主线程时,启动的进程本身才能通过 GCD 之类的 api 或直接通过系统调用从它自己的主线程生成子线程(bsdthread_create,不确定是否有其他)。系统调用恰好有 user_addr_t stack 作为第三个参数(即 MacOS 使用的 x86-64 System V 内核 ABI 中的 rdx)。 Reference for MacOS syscalls 我还没有彻底调查这个特定堆栈参数的细节,但我想它类似于 thread_userstackdefault / load_threadstack 方法。

我确实相信您对 Swift 运行时责任的怀疑可能是由于经常提到存储在堆栈中的数据结构(例如 Swift struct - 没有双关语)(这是顺便说一句的实现细节,并且不能保证运行时的功能)。

更新: 他是一个例子main.swift 命令行程序说明了这个想法。

import Foundation

struct testStruct 
    var a: Int


class testClass 


func testLocalVariables() 
    print("main thread function with local varablies")
    var struct1 = testStruct(a: 5)
    withUnsafeBytes(of: &struct1)  print($0) 
    var classInstance = testClass()
    print(NSString(format: "%p", unsafeBitCast(classInstance, to: Int.self)))

testLocalVariables()

print("Main thread", Thread.isMainThread)
var struct1 = testStruct(a: 5)
var struct1Copy = struct1

withUnsafeBytes(of: &struct1)  print($0) 
withUnsafeBytes(of: &struct1Copy)  print($0) 

var string = "testString"
var stringCopy = string

withUnsafeBytes(of: &string)  print($0) 
withUnsafeBytes(of: &stringCopy)  print($0) 

var classInstance = testClass()
var classInstanceAssignment = classInstance
var classInstance2 = testClass()

print(NSString(format: "%p", unsafeBitCast(classInstance, to: Int.self)))
print(NSString(format: "%p", unsafeBitCast(classInstanceAssignment, to: Int.self)))
print(NSString(format: "%p", unsafeBitCast(classInstance2, to: Int.self)))

DispatchQueue.global(qos: .background).async 
    print("Child thread", Thread.isMainThread)
    withUnsafeBytes(of: &struct1)  print($0) 
    withUnsafeBytes(of: &struct1Copy)  print($0) 
    withUnsafeBytes(of: &string)  print($0) 
    withUnsafeBytes(of: &stringCopy)  print($0) 
    print(NSString(format: "%p", unsafeBitCast(classInstance, to: Int.self)))
    print(NSString(format: "%p", unsafeBitCast(classInstanceAssignment, to: Int.self)))
    print(NSString(format: "%p", unsafeBitCast(classInstance2, to: Int.self)))


//Keep main thread alive indefinitely so that process doesn't exit
CFRunLoopRun()

我的输出如下所示:

main thread function with local varablies
UnsafeRawBufferPointer(start: 0x00007ffeefbfeff8, count: 8)
0x7fcd0940cd30
Main thread true
UnsafeRawBufferPointer(start: 0x000000010058a6f0, count: 8)
UnsafeRawBufferPointer(start: 0x000000010058a6f8, count: 8)
UnsafeRawBufferPointer(start: 0x000000010058a700, count: 16)
UnsafeRawBufferPointer(start: 0x000000010058a710, count: 16)
0x7fcd0940cd40
0x7fcd0940cd40
0x7fcd0940c900
Child thread false
UnsafeRawBufferPointer(start: 0x000000010058a6f0, count: 8)
UnsafeRawBufferPointer(start: 0x000000010058a6f8, count: 8)
UnsafeRawBufferPointer(start: 0x000000010058a700, count: 16)
UnsafeRawBufferPointer(start: 0x000000010058a710, count: 16)
0x7fcd0940cd40
0x7fcd0940cd40
0x7fcd0940c900

现在我们可以观察到一些有趣的事情:

    Class 实例显然占用了与 Structs 不同的内存部分 将结构分配给新变量会复制到新的内存地址 分配类实例只是复制指针。 当引用全局Structs 时,主线程和子线程都指向完全相同的内存 字符串确实有一个结构容器。

Update2 - 4^ 的证明 我们实际上可以检查下面的内存:

x 0x10058a6f0 -c 8
0x10058a6f0: 05 00 00 00 00 00 00 00                          ........
x 0x10058a6f8 -c 8
0x10058a6f8: 05 00 00 00 00 00 00 00                          ........

所以这绝对是实际的结构原始数据,即结构本身

更新 3

我添加了一个testLocalVariables() 函数,用来区分Swift 中定义为全局变量和局部变量的Struct。在这种情况下

x 0x00007ffeefbfeff8 -c 8
0x7ffeefbfeff8: 05 00 00 00 00 00 00 00                          ........

它显然存在于线程堆栈中。

最后但并非最不重要的是,当我在 lldb 时:

re read rsp
rsp = 0x00007ffeefbfefc0  from main thread
re read rsp
rsp = 0x000070000291ea40  from child thread

它为每个线程产生不同的值,因此线程堆栈明显不同。

进一步挖掘 有一个方便的 memory region lldb 命令可以揭示发生了什么。

memory region 0x000000010058a6f0
[0x000000010053d000-0x000000010058b000) rw- __DATA

所以全局Structs 位于预分配的可执行可写__DATA 内存页(与您的全局变量所在的内存页相同)。类0x7fcd0940cd40 地址的相同命令并不那么壮观(我认为这是一个动态分配的堆)。类似于线程堆栈地址0x7ffeefbfefc0,它显然不是进程内存区域。

幸运的是,还有最后一个工具可以进一步深入兔子洞。vmmap -v -purge pid 确实确认类位于MALLOC_ed 堆中,同样可以交叉引用线程堆栈(至少对于主线程)到Stack

有点相关的问题也是here。

HTH

【讨论】:

你是说Swift线程不只是在进程创建后由主线程启动吗?就像它在 Mach-O 元数据中可见一样,无需反汇编代码并找到对pthread_create 的调用,内核会为您启动它们作为进程创建的一部分?在普通的 POSIX 模型中,main 或其子进程进程启动后创建额外的线程。 或者LC_MAINLC_UNIXTHREAD 只是主/初始线程的不同风格? (通过系统调用手动创建额外的线程。) @PeterCordes 它们完全按照您的说法由主线程启动。它们是否是 Swift 并不重要,它与底层的 BSD 机制完全相同。我将在我的答案中更新后续线程部分,以消除未来读者的困惑 啊,我现在明白了。您在大多数答案中都在谈论为 main 线程创建堆栈。我在回答中完全忽略了这一点,因为它需要在进入用户空间之前创建。 (SysV ABI 将 argc、argv 和 envp 传递到堆栈的用户空间;进程入口点处的堆栈指针指向argc,而不是返回地址。否则它会理论上可能(但很可怕)要求主线程使用mmaplea rsp, [rax + 8MB] 为自己创建一个堆栈,但在系统调用约定在堆栈上传递参数的 32 位 BSD 中除外) @TonyLin 我可能得出了一些误导性的结论,我试图用 update3 来消除这些结论。然而,主要结论是我们不能将Struct live on (thread) stack 视为口头禅,显然情况可能并非如此。【参考方案2】:

(我假设 Swift 线程就像其他语言中的线程一样。确实没有很多好的选择,无论是普通的操作系统级线程还是用户空间的“绿色线程”,或者两者兼而有之。区别只是发生上下文切换的地方;主要概念仍然相同)


每个线程都有自己的堆栈,由mmap 或父线程分配的进程地址空间中分配,或者可能由创建线程的同一系统调用分配。 IDK iOS 系统调用。在 Linux 中,您必须将 void *child_stack 传递给实际创建新线程的 Linux-specific clone(2) 系统调用。直接使用低级别的特定于操作系统的系统调用是非常罕见的;语言运行时可能会在诸如pthread_create 之类的 pthreads 函数之上进行线程处理,并且 pthreads 库将处理特定于操作系统的细节。


是的,每个软件线程都有自己的架构状态,包括 x86-64 上的 RSP 或 sp on AArch64。 (或 ESP,如果您制作过时的 32 位 x86 代码)。我假设帧指针对于 swift 来说是可选的。

是的每个逻辑核心都有自己的架构状态(包括堆栈指针的寄存器);软件线程在逻辑内核上运行,软件线程保存/恢复寄存器之间的上下文切换。相关,可能与What resources are shared between threads? 重复。

软件线程共享相同的页表(虚拟地址空间),但寄存器。

【讨论】:

是操作系统创建了堆栈还是运行时? “每个线程都有自己的栈,通过mmap之类的分配在进程的地址空间中。”...由操作系统,操作系统将栈与线程关联起来。 @TonyLin:我不知道 iOS 系统调用。在 Linux 上,您通常会 mmap 一个 8MB 内存块并将其传递给线程创建系统调用,以创建堆栈指针指向其顶部的线程。但是任何线程的包装库都会为您处理。在 iOS 上可能有一个系统调用会创建一个线程并为其分配一个堆栈,如果您为初始线程堆栈或其他东西传递一个 NULL 指针,这将是一个合理的设计。 @ErikEidt:“操作系统将堆栈与线程相关联”。我不认为在 Linux 中是这种情况。在 MacOS 或 iOS 中是这样吗?在 Linux 中,我认为内核没有任何东西可以跟踪哪个线程正在使用分配的内存区域。显然,保存的 SP / RSP 值将指向那里的某个地方......除非用户空间暂时使用 RSP 作为暂存空间而不是堆栈指针。但除此之外,我不认为还有什么。您可以使用sigaction 告诉内核有关信号处理的不同堆栈,否则它默认为当前的RSP。 @ErikEidt: 还是Linux clone(2)void *child_stack 保存在某处?我不确定它是否需要,但它可能。无论如何,线程堆栈都可以是更大分配的单独块,但是没有保护页面来阻止堆栈冲突。

以上是关于谁创建和拥有调用堆栈以及调用堆栈如何在多线程中工作?的主要内容,如果未能解决你的问题,请参考以下文章

堆栈和堆的内容和位置是什么?

堆栈指针如何在多个进程中工作?

malloc 如何在多线程环境中工作?

如何使用 WinDBG 列出所有托管线程的调用堆栈?

记录不同线程的出错的堆栈

push 和 pop 如何在装配中工作