如何在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
相关产品推荐
相关产品推荐

