当用户空间程序调用系统调用时,执行如何转移回内核空间?
When a syscall is called by a userspace program, how does execution transfer back to kernelspace?
我一直在研究有关 x86-64 的 ABI、编写汇编以及研究堆栈和堆的工作原理。
给定以下代码:
#include <linux/seccomp.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
// execute the seccomp syscall (could be any syscall)
seccomp(...);
return 0;
}
在 Assembly for x86-64 中,这将执行以下操作:
- 对齐堆栈指针(默认情况下偏移 8 个字节)。
- 为调用
seccomp
. 的任何参数设置寄存器和堆栈
- 执行以下程序集
call seccomp
。
- 据我所知,当
seccomp
returns 时,C 很可能会调用 exit(0)
。
我想谈谈上面第三步和第四步之间发生的事情。
我目前拥有当前 运行 进程的堆栈,在寄存器和堆栈中有自己的数据。用户空间进程如何将执行权交给内核?内核是否只是在调用时启动,然后从同一个堆栈中推送和弹出?
我相信我在某处听说系统调用不会立即发生,而是在某些 CPU 滴答或中断时发生。这是真的?例如,在 Linux?
上,这是如何发生的
syscalls don't happen immediately but on certain CPU ticks or interrupts
当然,您的系统调用的效果可能取决于许多因素,包括滴答。调度器粒度或时序分辨率可以限制在滴答周期内,例如但是调用本身应该发生"immediately"(与执行内联)。
How does the userspace process turn over execution to the kernel? Does the kernel just pick up when the call is made and then push to and pop from the same stack?
它可能在体系结构之间略有不同,但通常系统调用参数由 libc
组装,然后生成处理器异常以更改上下文。
更多详细信息,请参阅:“How system calls work on x86 linux”
syscalls don't happen immediately but on certain CPU ticks or interrupts
完全错误。 CPU 不会坐在那里什么都不做,直到定时器中断。在大多数架构上,包括 x86-64,切换到内核模式需要数十到数百个周期,但这并不是因为 CPU 正在等待任何事情。就是操作慢。
请注意,glibc 提供了几乎每个系统调用的函数包装器,因此如果您查看反汇编,您只会看到一个看起来很正常的函数调用。
到底发生了什么(以x86-64为例):
请参阅链接自 x86 tag wiki. It specifies which registers to put args in, and that system calls are made with the syscall
instruction. Intel's insn ref manual (also linked from the tag wiki) documents in full detail every change that syscall
makes to the architectural state of the CPU. If you're interested in the history of how it was designed, I dug up some interesting mailing list posts from the amd64 mailing list between AMD architects and kernel devs. AMD updated the behaviour before the release of the first AMD64 hardware so it was actually usable for Linux (and other kernels) 的 AMD64 SysV ABI 文档。
32 位 x86 使用 int 0x80
系统调用指令,或 sysenter
。 syscall
在 32 位模式下不可用,sysenter
在 64 位模式下不可用。您可以在 64 位代码中 运行 int 0x80
,但您仍然会得到将指针视为 32 位的 32 位 API。 (即不要这样做)。顺便说一句,也许您对由于 int 0x80
而必须等待中断的系统调用感到困惑? 运行 该指令当场触发该中断,直接跳转到中断处理程序。 0x80
也不是硬件可以触发的中断,因此中断处理程序只会在软件触发的系统调用后 运行 秒。
AMD64 系统调用示例:
#include <stdlib.h>
#include <unistd.h>
#include <linux/unistd.h> // for __NR_write
const char msg[]="hello world!\n";
ssize_t amd64_write(int fd, const char*msg, size_t len) {
ssize_t ret;
asm volatile("syscall" // volatile because we still need the side-effect of making the syscall even if the result is unused
: "=a"(ret) // outputs
: [callnum]"a"(__NR_write), // inputs: syscall number in rax,
"D" (fd), "S"(msg), "d"(len) // and args, in same regs as the function calling convention
: "rcx", "r11", // clobbers: syscall always destroys rcx/r11, but Linux preserves all other regs
"memory" // "memory" to make sure any stores into buffers happen in program order relative to the syscall
);
}
int main(int argc, char *argv[]) {
amd64_write(1, msg, sizeof(msg)-1);
return 0;
}
int glibcwrite(int argc, char**argv) {
write(1, msg, sizeof(msg)-1); // don't write the trailing zero byte
return 0;
}
compiles to this asm output, with the godbolt Compiler Explorer:
gcc 的 -masm=intel
输出有点像 MASM,因为它使用 OFFSET
键来获取标签的地址。
.rodata
msg:
.string "hello world!\n"
.text
main: // using an in-line syscall
mov eax, 1 # __NR_write
mov edx, 13 # string length
mov esi, OFFSET FLAT:msg # string pointer
mov edi, eax # file descriptor = 1 happens to be the same as __NR_write
syscall
xor eax, eax # zero the return value
ret
glibcwrite: // using the normal way that you get from compiler output
sub rsp, 8 // keep the stack 16B-aligned for the function call
mov edx, 13 // put args in registers
mov esi, OFFSET FLAT:msg
mov edi, 1
call write
xor eax, eax
add rsp, 8
ret
glibc 的 write
包装函数只是将 1 放入 eax 和 运行s syscall
,然后检查 return 值并设置 errno。还处理 EINTR 和其他东西上的重新启动系统调用。
// objdump -R -Mintel -d /lib/x86_64-linux-gnu/libc.so.6
...
00000000000f7480 <__write>:
f7480: 83 3d f9 27 2d 00 00 cmp DWORD PTR [rip+0x2d27f9],0x0 # 3c9c80 <argp_program_version_hook+0x1f8>
f7487: 75 10 jne f7499 <__write+0x19>
f7489: b8 01 00 00 00 mov eax,0x1
f748e: 0f 05 syscall
f7490: 48 3d 01 f0 ff ff cmp rax,0xfffffffffffff001 // I think that's -EINTR
f7496: 73 31 jae f74c9 <__write+0x49>
f7498: c3 ret
... more code to handle cases where one of those branches was taken
我一直在研究有关 x86-64 的 ABI、编写汇编以及研究堆栈和堆的工作原理。
给定以下代码:
#include <linux/seccomp.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
// execute the seccomp syscall (could be any syscall)
seccomp(...);
return 0;
}
在 Assembly for x86-64 中,这将执行以下操作:
- 对齐堆栈指针(默认情况下偏移 8 个字节)。
- 为调用
seccomp
. 的任何参数设置寄存器和堆栈
- 执行以下程序集
call seccomp
。 - 据我所知,当
seccomp
returns 时,C 很可能会调用exit(0)
。
我想谈谈上面第三步和第四步之间发生的事情。
我目前拥有当前 运行 进程的堆栈,在寄存器和堆栈中有自己的数据。用户空间进程如何将执行权交给内核?内核是否只是在调用时启动,然后从同一个堆栈中推送和弹出?
我相信我在某处听说系统调用不会立即发生,而是在某些 CPU 滴答或中断时发生。这是真的?例如,在 Linux?
上,这是如何发生的syscalls don't happen immediately but on certain CPU ticks or interrupts
当然,您的系统调用的效果可能取决于许多因素,包括滴答。调度器粒度或时序分辨率可以限制在滴答周期内,例如但是调用本身应该发生"immediately"(与执行内联)。
How does the userspace process turn over execution to the kernel? Does the kernel just pick up when the call is made and then push to and pop from the same stack?
它可能在体系结构之间略有不同,但通常系统调用参数由 libc
组装,然后生成处理器异常以更改上下文。
更多详细信息,请参阅:“How system calls work on x86 linux”
syscalls don't happen immediately but on certain CPU ticks or interrupts
完全错误。 CPU 不会坐在那里什么都不做,直到定时器中断。在大多数架构上,包括 x86-64,切换到内核模式需要数十到数百个周期,但这并不是因为 CPU 正在等待任何事情。就是操作慢。
请注意,glibc 提供了几乎每个系统调用的函数包装器,因此如果您查看反汇编,您只会看到一个看起来很正常的函数调用。
到底发生了什么(以x86-64为例):
请参阅链接自 x86 tag wiki. It specifies which registers to put args in, and that system calls are made with the syscall
instruction. Intel's insn ref manual (also linked from the tag wiki) documents in full detail every change that syscall
makes to the architectural state of the CPU. If you're interested in the history of how it was designed, I dug up some interesting mailing list posts from the amd64 mailing list between AMD architects and kernel devs. AMD updated the behaviour before the release of the first AMD64 hardware so it was actually usable for Linux (and other kernels) 的 AMD64 SysV ABI 文档。
32 位 x86 使用 int 0x80
系统调用指令,或 sysenter
。 syscall
在 32 位模式下不可用,sysenter
在 64 位模式下不可用。您可以在 64 位代码中 运行 int 0x80
,但您仍然会得到将指针视为 32 位的 32 位 API。 (即不要这样做)。顺便说一句,也许您对由于 int 0x80
而必须等待中断的系统调用感到困惑? 运行 该指令当场触发该中断,直接跳转到中断处理程序。 0x80
也不是硬件可以触发的中断,因此中断处理程序只会在软件触发的系统调用后 运行 秒。
AMD64 系统调用示例:
#include <stdlib.h>
#include <unistd.h>
#include <linux/unistd.h> // for __NR_write
const char msg[]="hello world!\n";
ssize_t amd64_write(int fd, const char*msg, size_t len) {
ssize_t ret;
asm volatile("syscall" // volatile because we still need the side-effect of making the syscall even if the result is unused
: "=a"(ret) // outputs
: [callnum]"a"(__NR_write), // inputs: syscall number in rax,
"D" (fd), "S"(msg), "d"(len) // and args, in same regs as the function calling convention
: "rcx", "r11", // clobbers: syscall always destroys rcx/r11, but Linux preserves all other regs
"memory" // "memory" to make sure any stores into buffers happen in program order relative to the syscall
);
}
int main(int argc, char *argv[]) {
amd64_write(1, msg, sizeof(msg)-1);
return 0;
}
int glibcwrite(int argc, char**argv) {
write(1, msg, sizeof(msg)-1); // don't write the trailing zero byte
return 0;
}
compiles to this asm output, with the godbolt Compiler Explorer:
gcc 的 -masm=intel
输出有点像 MASM,因为它使用 OFFSET
键来获取标签的地址。
.rodata
msg:
.string "hello world!\n"
.text
main: // using an in-line syscall
mov eax, 1 # __NR_write
mov edx, 13 # string length
mov esi, OFFSET FLAT:msg # string pointer
mov edi, eax # file descriptor = 1 happens to be the same as __NR_write
syscall
xor eax, eax # zero the return value
ret
glibcwrite: // using the normal way that you get from compiler output
sub rsp, 8 // keep the stack 16B-aligned for the function call
mov edx, 13 // put args in registers
mov esi, OFFSET FLAT:msg
mov edi, 1
call write
xor eax, eax
add rsp, 8
ret
glibc 的 write
包装函数只是将 1 放入 eax 和 运行s syscall
,然后检查 return 值并设置 errno。还处理 EINTR 和其他东西上的重新启动系统调用。
// objdump -R -Mintel -d /lib/x86_64-linux-gnu/libc.so.6
...
00000000000f7480 <__write>:
f7480: 83 3d f9 27 2d 00 00 cmp DWORD PTR [rip+0x2d27f9],0x0 # 3c9c80 <argp_program_version_hook+0x1f8>
f7487: 75 10 jne f7499 <__write+0x19>
f7489: b8 01 00 00 00 mov eax,0x1
f748e: 0f 05 syscall
f7490: 48 3d 01 f0 ff ff cmp rax,0xfffffffffffff001 // I think that's -EINTR
f7496: 73 31 jae f74c9 <__write+0x49>
f7498: c3 ret
... more code to handle cases where one of those branches was taken