You need to enable JavaScript to run this app. 如何在x64 NASM汇编中使用clone系统调用实现Linux线程
优惠活动
大模型
产品
解决方案
定价
更多

如何在x64 NASM汇编中使用clone系统调用实现Linux线程

x64 NASM下通过sys_clone实现兼容pthread行为的线程实操

核心认知先理清楚

很多刚接触系统调用的开发者都会有个误区:以为sys_clone要传入线程入口函数指针。实际上这个接口本身完全不关心你要执行什么函数,它只负责按照标志位决定新执行流的资源共享规则,然后给新执行流换好指定的栈,剩下的执行逻辑全靠用户态自己实现。你现在用的C语言pthread库,本质就是glibc对sys_clone的一层封装,根本没有什么藏着的魔法。

之前你在文档里看到的int __user *只是内核代码里的注释标记,意思是这个指针是用户态程序传递进来的、指向用户空间内存的地址,不是什么需要特殊处理的内核指针,你自己在栈上或者堆上申请一个4字节的int变量,把它的地址传进去就完全符合要求。

逐参数填写对应规则(完全匹配你写的寄存器调用顺序)

x86_64 Linux的系统调用传参顺序是固定的,你写的寄存器使用方式没有任何问题,每个参数的实际填法和对应含义没有那么复杂:

  • mov rdi, clone_flags
    要实现和pthread一致的线程语义,需要组合的标志位固定为CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID,换算成十六进制就是0x3d0f00。
    这些标志的核心作用可以简单理解:CLONE_VM让新执行流和父线程共享全部内存空间(没有fork的写时复制逻辑),CLONE_THREAD让新执行流和父线程属于同一个线程组(共享同一个TGID,不会给它分配独立的进程PID),剩下的标志分别控制共享文件系统上下文、共享文件描述符表、共享信号处理函数,以及让内核帮你自动存TID、设置TLS等辅助功能。
  • mov rsi, newsp
    这是新线程的栈指针,必须指向你提前分配好、按16字节对齐的可用栈空间。
    重点提醒:sys_clone执行完成后,新执行流会从和父执行流相同的下一条指令开始跑,所以你必须提前把线程入口函数的地址压到新栈的栈顶位置。这样新执行流从syscall返回后执行ret指令时,就会自动跳转到你指定的线程入口函数,完全不需要额外做跳转判断。
  • mov rdx, parent_tidptr
    这是一个指向用户态4字节int变量的指针。只要你在clone_flags里带了CLONE_PARENT_SETTID标志,内核会自动把新创建线程的TID写入这个指针指向的内存位置,方便后续父线程做线程等待、管理。不需要这个功能的话直接填0即可。
  • mov r10, child_tidptr
    同样是指向用户态4字节int变量的指针。内核会把新线程自己的TID写入这个位置,如果带了CLONE_CHILD_CLEARTID标志,当这个线程退出时,内核会自动把这个位置的值清0,并唤醒所有在这个地址上等待的futex——这就是pthread_join功能的底层实现原理。
  • mov r8, tls_val
    新线程的线程局部存储(TLS)基地址。pthread实现里每个线程都会有自己独立的TLS区域,用来存errno、线程局部变量这类数据。如果不设置这个值,新线程的fs段寄存器还是指向父线程的TLS地址,多线程同时操作errno时会直接互相踩内存。填了这个值之后,内核会自动把新线程的fs基址设置成你传的地址,和pthread的行为完全对齐。

最小可运行实现片段

下面是可以直接嵌到NASM代码里的调用逻辑,目标是创建一个执行my_thread_entry函数的线程:

; 第一步:通过mmap给新线程分配2MiB大小的栈,和pthread默认栈大小对齐
mov rdi, 0                  ; 栈内存地址由内核自动选择
mov rsi, 0x200000           ; 栈大小:2 * 1024 * 1024
mov rdx, 0x3                ; 栈内存权限:可读可写
mov r10, 0x22               ; 映射类型:私有匿名映射
mov r8, -1
mov r9, 0
mov rax, 9                  ; sys_mmap
syscall
mov rbx, rax                ; 把返回的栈基址存到rbx
; 计算栈顶位置,做16字节对齐
add rbx, 0x200000
and rbx, ~0xF
; 把线程入口地址压到新栈顶,作为新执行流的返回地址
sub rbx, 8
mov qword [rbx], my_thread_entry
; 第二步:填充sys_clone参数
mov rdi, 0x3d0f00           ; 组合好的线程相关标志位
mov rsi, rbx                ; 新栈指针
mov rdx, parent_tid_var     ; 父线程存储新TID的变量地址
mov r10, child_tid_var      ; 子线程存储自身TID的变量地址
mov r8, my_thread_tls       ; 新线程的TLS区域基地址
mov rax, 56                 ; sys_clone
syscall
; 父线程执行到这里时,rax是新线程的TID(非0)
test rax, rax
jnz .parent_thread_flow
; 子线程执行到这里时,rax是0,但因为提前压了返回地址,直接ret就会跳到my_thread_entry
ret
.parent_thread_flow:
; 父线程后续逻辑...
my_thread_entry:
; 你自己的线程逻辑写在这里

补充说明

如果只是做简单的线程功能测试,不需要完全兼容pthread的行为,可以临时省略TLS设置,只要把栈和标志位填对就能跑通。要查系统调用的参数细节,直接装本地的man手册,执行man 2 clone就能看到所有参数的权威说明,不需要找零散的网络资料。

内容的提问来源于stack exchange,提问作者Franco Milich

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.26 13:54:03