C 当用户空间程序调用系统调用时,执行如何转移回内核空间?
我一直在研究很多关于x86-64的ABI,编写汇编,以及研究堆栈和堆是如何工作的 给定以下代码:C 当用户空间程序调用系统调用时,执行如何转移回内核空间?,c,assembly,linux-kernel,kernel,x86,C,Assembly,Linux Kernel,Kernel,X86,我一直在研究很多关于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(...);
#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;
}
#包括
#包括
#包括
int main(int argc,char*argv[]){
//执行seccomp系统调用(可以是任何系统调用)
seccomp(…);
返回0;
}
在x86-64的汇编中,这将执行以下操作:
seccomp
的任何参数设置寄存器和堆栈调用seccomp
seccomp
返回时,据我所知,C很可能会调用exit(0)
libc
组装,然后生成处理器异常以更改上下文
有关其他详细信息,请参阅:“
系统调用不会立即发生,但会在某些CPU滴答声或中断时发生
完全错了。CPU不只是坐在那里无所事事,直到计时器中断。在大多数体系结构上,包括x86-64,切换到内核模式需要几十到几百个周期,但这并不是因为CPU在等待什么。这只是一个缓慢的操作
请注意,glibc几乎为每个系统调用都提供了函数包装器,所以如果您查看反汇编,您将只看到一个外观正常的函数调用
实际发生的情况(以x86-64为例): 请参阅从标记wiki链接的AMD64 SysV ABI文档。它指定将参数放入哪些寄存器,以及使用
syscall
指令进行系统调用。英特尔的insn ref手册(也链接自tag wiki)详细记录了syscall
对CPU体系结构状态所做的每一项更改。如果您对它的设计历史感兴趣,请参阅AMD架构师和内核开发人员之间的amd64邮件列表。AMD在第一款AMD64硬件发布之前更新了这一行为
32位x86使用int 0x80
指令进行系统调用,或sysenter
syscall
在32位模式下不可用,sysenter
在64位模式下不可用。您可以在64位代码中运行int0x80
,但仍然可以得到32位API,该API将指针视为32位。(即不要这样做)。顺便说一句,您可能对系统调用必须等待中断感到困惑,因为int0x80
?运行该指令会立即触发中断,直接跳到中断处理程序<代码>0x80也不是硬件可以触发的中断,因此中断处理程序只能在软件触发的系统调用之后运行
AMD64系统调用示例: glibc的
write
包装函数只在eax中放入1并运行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
//objdump-R-Mintel-d/lib/x86_64-linux-gnu/libc.so.6
...
00000000000f7480:
f7480:83 3d f9 27 2d 00 cmp DWORD PTR[rip+0x2d27f9],0x0#3c9c80
f7487:7510JNE f7499
f7489:b8 01 00 mov eax,0x1
f748e:0f 05系统调用
f7490:48 3d 01 f0 ff ff cmp rax,0xFFFFFFFFFFF001//我想那是-EINTR
f7496:7331 jae f74c9
f7498:c3 ret
... 更多代码用于处理其中一个分支被执行的情况
对于函数调用,您是正确的。但是,对于系统调用,没有。函数seccomp()
将以特定顺序从堆栈中加载参数并加载到特定寄存器中,将seccomp
的系统调用号加载到rax
,然后执行指令int 0x80
,syscenter
或syscall
。CPU“陷阱”并将控制权交给内核,内核执行一个中断服务例程,该例程确定请求的系统调用编号为rax
,并执行它。当内核将控制权返回给进程时,它会在rax
中报告返回值。@iwillnotexistidnotexist极好的回答,应该是一个答案@彼得考德斯对我的评论作了极好的阐述。正如他所指出的,CPU不会等待下一个滴答声。如果CPU被中断,它将尽可能快地为中断提供服务(由于上下文切换,在几百个周期内)。不同之处在于,程序可以通过syscall
指令自动中断自身,或者计时器可以非自愿中断程序的执行,将控制权交给内核。内核为inte的明确目的设置此计时器
.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
// 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