在 MUSL 的 x86_64 __syscall_cp_asm 包装器中的系统调用之前将传入的 pthread 地址保存在堆栈上的目的?
Purpose of saving an incoming pthread address on the stack before syscall in MUSL's x86_64 __syscall_cp_asm wrapper?
这是from musl's source代码:
1 __syscall_cp_asm:
2 __cp_begin:
3 mov (%rdi),%eax
4 test %eax,%eax
5 jnz __cp_cancel
6 mov %rdi,%r11
7 mov %rsi,%rax
8 mov %rdx,%rdi
9 mov %rcx,%rsi
10 mov %r8,%rdx
11 mov %r9,%r10
12 mov 8(%rsp),%r8
13 mov 16(%rsp),%r9
14 mov %r11,8(%rsp)
15 syscall
16 __cp_end:
17 ret
18 __cp_cancel:
19 jmp __cancel
我很好奇第 6 行和第 14 行的目的是什么(从链接源重新编号)。
根据我的理解,代码的开头测试了作为第一个参数传递的指针的目标(第 3–5 行),然后第 6 行将指针移动到 r11然后第 14 行将其移动到堆栈上用于传递第 7 个参数的位置。
这似乎没有用。这些动作有什么用吗?
第 7 - 14 行的代码按参数顺序将参数加载到系统调用。由于 RDI 在第 8 行加载,它的值保存在 R11 中,以便它可以在第 14 行写入参数 8(在堆栈上)。
在hand-written汇编代码中,像这样组织起来可以更容易理解和维护,这超过了额外移动指令的成本。
这是为了支持pthread取消点;信号处理程序稍后可以查看堆栈。
commit log for the commit that introduced this code 解释说,在系统调用之前将指针存储在堆栈上的已知位置可以使“取消信号处理程序”确定“中断的代码是否处于可取消状态”。 (该代码的初始版本还保存了 syscall
指令的地址,但后来的提交更改了它。)
第一个 arg(asm 函数存储在堆栈中)来自 its C caller、__syscall_cp_c
,它传递 __syscall_cp_asm(&self->cancel, nr, u, v, w, x, y, z);
,其中 self
来自 __pthread_self()
.
你是对的,用不同的传入 arg 覆盖调用者的堆栈 arg 对于遵循 x86-64 System V ABI 的 C 调用者来说是“不可见的”。 (被调用者拥有其堆栈参数;调用者必须假设它们已被覆盖,因此编译器生成的代码永远不会读取该内存位置作为输出)。所以我们需要寻找其他解释。
我认为有必要使用 2 条总 mov 指令将传入的 RDI 复制到 8(%rsp)
读取该内存位置之后。我们不能将 mov %rdx,%rdi
延迟到负载之后,因为我们需要释放 RDX 来容纳 R8,以释放 R8 来容纳负载。您可以在使用 R10 加载另一个 arg 之前使用 R10 来避免接触“额外”寄存器,但它仍然需要至少 2 条指令。
或者可以优化 arg 顺序以在后面的 arg 中传递该指针,也许最后传递调用号和最后一个寄存器 arg 中的 pthread 指针(最小的改组但避免需要双重取消引用 test/branch) 或第一个堆栈参数(无论如何你想要它)。或者匹配 __syscall
包装器的 arg 顺序,该包装器首先采用 nr
,没有 pthread 指针。
这是from musl's source代码:
1 __syscall_cp_asm:
2 __cp_begin:
3 mov (%rdi),%eax
4 test %eax,%eax
5 jnz __cp_cancel
6 mov %rdi,%r11
7 mov %rsi,%rax
8 mov %rdx,%rdi
9 mov %rcx,%rsi
10 mov %r8,%rdx
11 mov %r9,%r10
12 mov 8(%rsp),%r8
13 mov 16(%rsp),%r9
14 mov %r11,8(%rsp)
15 syscall
16 __cp_end:
17 ret
18 __cp_cancel:
19 jmp __cancel
我很好奇第 6 行和第 14 行的目的是什么(从链接源重新编号)。
根据我的理解,代码的开头测试了作为第一个参数传递的指针的目标(第 3–5 行),然后第 6 行将指针移动到 r11然后第 14 行将其移动到堆栈上用于传递第 7 个参数的位置。
这似乎没有用。这些动作有什么用吗?
第 7 - 14 行的代码按参数顺序将参数加载到系统调用。由于 RDI 在第 8 行加载,它的值保存在 R11 中,以便它可以在第 14 行写入参数 8(在堆栈上)。
在hand-written汇编代码中,像这样组织起来可以更容易理解和维护,这超过了额外移动指令的成本。
这是为了支持pthread取消点;信号处理程序稍后可以查看堆栈。
commit log for the commit that introduced this code 解释说,在系统调用之前将指针存储在堆栈上的已知位置可以使“取消信号处理程序”确定“中断的代码是否处于可取消状态”。 (该代码的初始版本还保存了 syscall
指令的地址,但后来的提交更改了它。)
第一个 arg(asm 函数存储在堆栈中)来自 its C caller、__syscall_cp_c
,它传递 __syscall_cp_asm(&self->cancel, nr, u, v, w, x, y, z);
,其中 self
来自 __pthread_self()
.
你是对的,用不同的传入 arg 覆盖调用者的堆栈 arg 对于遵循 x86-64 System V ABI 的 C 调用者来说是“不可见的”。 (被调用者拥有其堆栈参数;调用者必须假设它们已被覆盖,因此编译器生成的代码永远不会读取该内存位置作为输出)。所以我们需要寻找其他解释。
我认为有必要使用 2 条总 mov 指令将传入的 RDI 复制到 8(%rsp)
读取该内存位置之后。我们不能将 mov %rdx,%rdi
延迟到负载之后,因为我们需要释放 RDX 来容纳 R8,以释放 R8 来容纳负载。您可以在使用 R10 加载另一个 arg 之前使用 R10 来避免接触“额外”寄存器,但它仍然需要至少 2 条指令。
或者可以优化 arg 顺序以在后面的 arg 中传递该指针,也许最后传递调用号和最后一个寄存器 arg 中的 pthread 指针(最小的改组但避免需要双重取消引用 test/branch) 或第一个堆栈参数(无论如何你想要它)。或者匹配 __syscall
包装器的 arg 顺序,该包装器首先采用 nr
,没有 pthread 指针。