我一直在研究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;
}
在x86-64的程序集中,这将执行以下操作:
对齐堆栈指针(因为默认情况下它关闭了8个字节)。
为调用
seccomp
的任何参数设置寄存器和堆栈。执行以下程序集
call seccomp
。当
seccomp
返回时,据我所知,c可能会调用exit(0)
。我想谈谈上面第三步到第四步之间发生了什么。
我目前有我的堆栈为当前正在运行的进程与它自己的数据在寄存器和堆栈上。用户空间进程如何将执行移交给内核?内核是在调用时才开始接收,然后从同一个堆栈中推送和弹出的吗?
我相信我在某个地方听说系统调用不是立即发生的,而是在某些CPU滴答声或中断时。这是真的吗?例如,在Linux上,这是如何发生的?
最佳答案
系统调用不会立即发生,但会在某些CPU计时或中断时发生
完全错了。CPU不只是坐在那里无所事事,直到计时器中断。在大多数架构(包括x86-64)上,切换到内核模式需要数十到数百个周期,但不是因为cpu在等待任何东西。只是一个缓慢的操作。
注意,glibc几乎在每个系统调用周围都提供了函数包装器,所以如果您查看反汇编,您将看到一个外观正常的函数调用。
实际情况(以x86-64为例):
请参阅从x86标记wiki链接的amd64 sysv abi文档。它指定要将参数放入哪个寄存器,并使用syscall
指令进行系统调用。英特尔的insn ref手册(也链接到tag wiki)详细记录了syscall
对cpu体系结构状态所做的每一个更改。如果您对它的设计历史感兴趣,可以从amd64邮件列表中的amd架构师和内核开发人员之间联系。AMD在第一个AMD64硬件发布之前更新了行为。
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;
}
I dug up some interesting mailing list posts
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并运行syscall
,然后检查返回值并设置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