在Linux中syscall
是系统调用
(英文:system call
)的指令。
想要深入了解syscall
的作用,就需要了解特权级别。
现代计算机通常采用名为保护环(Protection Rings)
的机制来保护整个系统的数据和功能,使其免受故障和外部恶意行为的伤害。这种方式通过提供多种不同层次的资源访问级别,即特权级别
,来限制不同代码的执行能力。
Intel x86 架构中,特权级别被分为 4 个层次,即Ring0
~Ring3
。其中,Ring0
层拥有最高特权,它具有对整个系统的最大控制能力,内核代码通常运行于此。相对地,Ring3
层只有最低特权,这里是应用程序代码所处的位置。而位于两者之间的 Ring1
和Ring2
层,则通常被操作系统选择性地作为设备驱动程序的“运行等级”。
根据特权级别的不同,CPU 能够被允许执行的机器指令、可使用的寄存器和可用的硬件资源也随之不同。比如位于Ring3
层的应用程序,可以使用最常见的通用目的寄存器,并通过mov
指令操作其中存放的数据。而位于 Ring0 层的内核代码则可以使用除此之外的cr0
、cr1
等控制寄存器,甚至通过in
与 out
等机器指令,直接与特定端口进行 IO 操作。但如果应用程序尝试跨级别非法访问这些被限制的资源,CPU 将抛出相应异常,阻止相关代码的执行。
系统调用
是操作系统提供的接口,逻辑上跟用户函数
相似,能够帮助程序切换到进程的内核空间执行功能。也就是说,应用程序可以通过系统调用
进入到操作系统内核空间完成某项功能。系统调用过程通常称为特权模式切换
。
系统调用
与一般函数
(或者说用户函数
)的最大区别在于,系统调用
执行的代码位于操作系统底层的内核环境
(内核环境也称作内核空间或应用程序的内核态,处于CPU特权等级Ring0
)中,而用户函数代码则位于内核之上的应用环境
(应用环境也称作用户空间或者应用程序的用户态,处于Ring3
)中。
在使用系统调用时,rax
寄存器里边需要放入系统调用号
,表明需要执行的系统调用,/usr/include/asm/unistd_64.h
可以看64位Linux系统调用和系统调用号的对应关系,可以使用man 2 系统调用
查询如何使用系统调用
,而系统调用
就是在/usr/include/asm/unistd_64.h
里__NR_
后边的字符串,比如read
、select
、socket
等。
cat /usr/include/asm/unistd_64.h
可以看64位Linux系统调用和系统调用号的对应关系。
man 2 exit
可以看一下系统调用exit
的相关信息,按q可以退出man
界面。
系统调用
使用到的寄存器:
寄存器 | 作用 |
---|---|
rax | 放入系统调用号 |
rdi | 第1个参数 |
rsi | 第2个参数 |
rdx | 第3个参数 |
r10 | 第4个参数 |
r9 | 第5个参数 |
r8 | 第6个参数 |
除了系统调用号
,内核还需要知道需要处理的参数,这就需要使用rdi
、rsi
、rdx
、r10
、r9
、r8
等六个寄存器传递参数,系统调用最多只能传递6
个参数。
返回值会放到rax
寄存器里边,rcx
在系统调用时会保存下一条指令位置,r11
会保存eflags
的数值。
在C语言中使用系统调用,可以参考博客。
输出字符串使用到的寄存器和应该赋予的值:
寄存器 | 值 |
---|---|
rax | 1 |
rdi | 文件描述符,要是想要输出到屏幕上,那么就需要把rdi赋值为1 |
rsi | 输出字符串的地址 |
rdx | 输出的字符个数 |
上边的表是系统调用之前进行放置的。
在系统调用完成之后rax
会被放入返回值。
AT&T汇编代码displayStringATT64.s
里边的代码如下:
.global main
.section .data
# 需要输出的字符串
stringToShow:
.ascii "hello world\n\0"
.section .text
main:
# 系统调用号,可以看一下/usr/include/asm/unistd_64.h里边功能和调用号对应关系
movq $1,%rax
# %rdi里边放的是系统调用write函数第一个参数,表示输出的位置,当rdi里边的数值是1,表明需要输出到标准输出
movq $1,%rdi
# %rsi里边放入的是系统调用write函数第二个参数,表示输出的内容
movq $stringToShow,%rsi
# %rsi里边放入的是系统调用write函数第三个参数,表示输出的内容长度,这里字符串的长度,包括“\n”,而不包括“\0”
movq $12,%rdx
syscall
# 相当于C语言中的return 0
movq $60,%rax
movq $0,%rdi
syscall
gcc displayStringATT64.s -o displayStringATT64
把汇编代码进行编译,编译完成之后./displayStringATT64
执行就会输出hello world
。
上边的汇编代码相当于下边的C语言stdoutputSimple.c
代码:
#include
int main()
{
write(1,"hello world\n",12);
return 0;
}
使用gcc stdoutputSimple.c -o stdoutputSimple
进行编译,然后使用./stdoutputSimple
执行输出hello world
。
输入字符串需要使用的寄存器及相应的赋值:
寄存器 | 值 |
---|---|
rax | 0 |
rdi | 输入的位置,使用文件描述符进行表示,要是从标准输入读取的话,赋值为0 |
rsi | 读取字符串的位置 |
rdx | 读取的字符个数 |
从键盘上输入字符串到屏幕的AT&T汇编代码consoleInputATT.s
里边的内容如下:
.section .data
stringLength: .quad 5
prompt:
.ascii "Please input:"
.section .bss
stringToFile: .skip 7
oneChar: .skip 1
.section .text
.global main
main:
# rax = 1,输出的系统调用号
movq $1,%rax
movq $1,%rdi
movq $prompt,%rsi
movq $13,%rdx
syscall
# 此处取地址
leaq oneChar,%rbx
leaq stringToFile,%r10
movq $0,%r12
readCharacters:
movq $0,%rax
movq $0,%rdi
movq %rbx,%rsi
movq $1,%rdx
syscall
# 取值
movq (%rbx),%rax
cmpb $10,%al
je printString
incq %r12
cmpq %r12,stringLength
jb readCharacters
movb %al,(%r10)
incq %r10
jmp readCharacters
printString:
incq %r10
movb $10,(%r10)
incq %r10
movb $0,(%r10)
movq $1,%rax
movq $1,%rdi
movq $stringToFile,%rsi
movq %r12,%rdx
syscall
movq $60,%rax
movq $0,%rdi
syscall
gcc -g consoleInputATT.s -o consoleInputATT
进行编译,./consoleInputATT
执行,然后输入1234567
,发现最后输出的是12345
,符合预期,最多只能输入5个字符。
在系统调用之前,需要放入值的寄存器和对应的值:
寄存器 | 值 |
---|---|
rax | 85 |
rdi | 文件名称,需要以ASCII中的0(NULL)结束 |
rsi | 文件访问权限 |
fileCreateATT.s
里边的代码如下:
.global main
.section .data
# 文件名称,”\0“是NULL的含义
fileName:
.ascii "fileText.txt\0"
.section .text
# main函数
main:
# rax = 85,告诉内核需要创建文件
movq $85,%rax
# rdi = 文件名称,告诉内核创建文件的名称
movq $fileName,%rdi
# rsi = 文件访问权限,告诉内核文件是否有读、写、执行等权限
movq $0600,%rsi
syscall
# rax = 60,这是退出程序的系统调用号
movq $60,%rax
movq $0,%rdi
syscall
gcc fileCreateATT.s -o fileCreateATT
进行编译,ls -l fileText.txt
若是显示ls: cannot access fileText.txt: No such file or directory
,那么说明没有fileText.txt
这个文件,./fileCreateATT
进行执行,ls -l fileText.txt
就可以显示文件的信息了,这说明创建成功了fileText.txt
文件。
可以使用rm -rf fileText.txt
删除已经创建的文件
fileWriteATT.s
里边的代码如下:
.global main
.section .data
fileName:
.ascii "writeToFile.txt\0"
fileContextString:
.ascii "good learn!\n\0"
.section .text
main:
movq $85,%rax
movq $fileName,%rdi
movq $0600,%rsi
syscall
# rax在系统调用之后就保存文件描述符,这里把文件描述符从rax中保存到rdi中
movq %rax,%rdi
movq $1,%rax
movq $fileContextString,%rsi
movq $12,%rdx
syscall
movq $60,%rax
movq $0,%rdi
syscall
gcc fileWriteATT.s -o fileWriteATT
进行编译,./fileWriteATT
进行执行,cat writeToFile.txt
查看写入writeToFile.txt
文件里边的内容。
关闭一个文件需要使用到的寄存器及相应的赋值:
寄存器 | 值 |
---|---|
rax | 3 |
rdi | 文件描述符 |
此处的例子跟底下的打开文件读取文件里边内容
例子写在一起。
打开一个文件需要使用到的寄存器及相应的赋值:
寄存器 | 值 |
---|---|
rax | 2 |
rdi | 文件名称 |
rsi | 文件访问权限 |
需要注意的是,在系统调用之后,rax
里边就会放入文件描述符
返回,之后可以通过这个文件描述符才可以对这个文件进行操作,比如读写。
fileReadATT.s
里边的内容如下:
.section .data
fileName:
.ascii "writeToFile.txt\0"
fileDescriptor: .quad 0
.section .bss
stringFromFile: .skip 14
.global main
.section .text
main:
movq $2,%rax
movq $fileName,%rdi
movq $00,%rsi
movq $0444, %rdx
syscall
cmpq $0,%rax
jbe done
movq %rax,fileDescriptor
movq $0,%rax
movq fileDescriptor,%rdi
movq $stringFromFile,%rsi
movq $11,%rdx
syscall
movq $stringFromFile,%rdi
movb $'\n',12(%rdi)
# movb $'\0',13(%rdi)
movq $1,%rax
movq $1,%rdi
movq $stringFromFile,%rsi
movq $13,%rdx
syscall
movq $3,%rax
movq fileDescriptor,%rdi
syscall
done:
movq $60,%rax
movq $0,%rdi
syscall
gcc -g fileReadATT.s -o fileReadATT
进行编译,上边产生可执行文件./fileWriteATT
先执行产生一个writeToFile.txt
文件,cat writeToFile.txt
可以看一下writeToFile.txt
文件里边的内容,确保上边程序执行正确,./fileReadATT
执行读取文件内容。
讲到系统调用的汇编语言书籍:
书籍 | 作者 | 章节 | 章节名 | 语言 | 汇编语言风格 | 汇编器 |
---|---|---|---|---|---|---|
x64汇编语言:从新手到AVX专家 | Jo Van Hoey | 第20章 | 文件I/O | 汉语 | Intel | nasm |
Low-Level Programming: C, Assembly, and Program Execution on Intel 64 Architecture | Igor Zhirkov | 第6章 | Interrupts and System Calls | 英语 | Intel | nasm |
Introduction to 64 Bit Intel Assembly Language Programming for Linux | Ray Seyfarth | 第12章 | System calls | 英语 | Intel | yasm |
x86-64 Assembly Language Programming with Ubuntu | Ed Jorgensen | 第13章 | System Services | 英语 | Intel | yasm |
Introduction to Computer Organization: An Under-the-Hood Look at Hardware and x86-64 Assembly | Robert G. Plantz | 第21章 | Interrupts and Exceptions | 英语 | Intel | gas |
Learn to Program with Assembly: Foundational Learning for New Programmers | Jonathan Bartlett | 第10章 | Making a System Calls | 英语 | AT&T | gas |
此文章为10月Day 24学习笔记,内容来源于极客时间《深入 C 语言和程序运行原理》。