x64上Linux的系统调用

x64上Linux的系统调用

写在前面:本文希望读者有一定的Linux基础,了解过系统调用和crt的包装函数的区别。可以看我之前写过的关于IA32上Linux系统调用的简介,以及《Linux内核设计与实现》一书中对系统调用的笔记

众所周知,在IA32上,Linux的系统调用是通过int 0x80中断,访问中断向量表,调用sys_call()。它通过eax传递系统调用号;其他一系列寄存器传递参数,分别存储在ebxecxedxesiediebp;返回值存储在eax

现今,x86 64体系结构引入了一条专用指令syscall。它不访问中断描述符表,速度更快。它通过rax传递系统调用号;其他一系列寄存器传递参数,分别存储在rdirsirdxr10r8r9;返回值存储在rax

很明显,系统调用的ABI发生了剧烈的改变。进行系统调用的指令,传递系统调用号的寄存器,传递参数的寄存器,返回值的寄存器,甚至系统调用对应的编号,32位与64位都存在着很大的差异。理论上系统调用表都是向后兼容的,每次更新时只能往后添加系统调用号,已有的系统调用号则保持。我在Stack Exchange上找到了一个回答,解释了从32位到64位系统调用表更改的原因:x86 64体系结构出现时,ABI(传递参数、返回值)是不同的,因此内核开发人员利用这个机会带来了期待已久的更改,为了对高速缓存行使用级别进行优化。比如,常用的sys_read/sys_write/sys_open/sys_close分别位于前四个系统调用号;sys_exit原本很靠前(原本系统调用号为1),但每个进程都在退出时才调用一次,所以现在是靠后的60作为系统调用号。

目测是为了兼容,我在内核版本为4.14.0的ubuntu上仍然能通int 0x80进行系统调用,下面是测试的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
section .data
str: db "Hello world"
str_len equ $-str

section .text
global _start
[bits 64]
_start:
mov eax, 4 ; sys_write的系统调用号
mov ebx, 1 ; 第一个参数为int fd
mov ecx, str ; 第二个参数为char *buf
mov edx, str_len ; 第三个参数为size_t count
int 0x80

mov eax, 1 ; sys_exit的系统调用号
mov ebx, 0 ; 第一个参数为int status
int 0x80

不过,x86_64的Linux最好还是通过syscall进行系统调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
section .data
str: db "Hello world"
str_len equ $-str

section .text
global _start
[bits 64]
_start:
mov eax, 1 ; 代表sys_write
mov rdi, 1 ; 第一个参数为int fd
mov rsi, str ; 第二个参数为char *buf
mov rdx, str_len ; 第三个参数为size_t count
syscall

mov eax, 60 ; sys_exit的系统调用号
mov rdi, 0 ; 第一个参数为int status
syscall