相关概念
在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语言中使用系统调用,可以参考博客。
退出程序
关于c语言中return与exit的差异,可以参考博客《C语言中的exit()函数》
输出字符串使用到的寄存器和应该赋予的值:
寄存器 | 值 |
---|---|
rax | 60 |
rdi | 返回给操作系统的值 |
testExit.s
里边的代码如下:
.global main
.section .data
.section .text
main:
# 下边这三条语句相当于C语言在main方法中使用return 0语句,或者在任意函数中使用exit(0)语句
# rax = 60,这是程序退出的系统调用
movq $60,%rax
# 告诉操作系统返回值为0
movq $0,%rdi
syscall
gcc testExit.s -o testExit
进行编译,./testExit
执行程序,echo $?
可以查看最近一个程序返回给操作系统的值。
CPU写入字符串到标准输出(输出字符串到屏幕)
输出字符串(从屏幕的角度看)使用到的寄存器和应该赋予的值:
寄存器 | 值 |
---|---|
rax | 1 |
rdi | 写入的文件描述符,要是想要输出到屏幕上,那么就需要把rdi赋值为1 |
rsi | 内存中字符串的读取地址 |
rdx | 读取的字符个数 |
上边的表是系统调用之前进行放置的。
在系统调用完成之后rax
会被放入返回值。
注意:在CPU的角度,这里的输出字符串相当于CPU写入其他地方,数据流出入方向是rsi->rdi
。
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
# rdx里边放入的是系统调用write函数第三个参数,表示输出的内容长度,这里字符串的长度,包括“\n”,而不包括“\0”
movq $12,%rdx
syscall
# 下边这三条语句相当于C语言在main方法中使用return 0语句,或者在任意函数中使用exit(0)语句
# rax = 60,这是程序退出的系统调用
movq $60,%rax
# 告诉操作系统返回值为0
movq $0,%rdi
syscall
gcc displayStringATT64.s -o displayStringATT64
把汇编代码进行编译,编译完成之后./displayStringATT64
执行就会输出hello world
。
上边的汇编代码相当于下边的C语言stdoutputSimple.c
代码:
#include <unistd.h>
int main()
{
write(1,"hello world\n",12);
return 0;
}
使用gcc stdoutputSimple.c -o stdoutputSimple
进行编译,然后使用./stdoutputSimple
执行输出hello world
。
CPU读取字符串到内存(从键盘输入字符串)
输入字符(这里是从键盘角度理解)需要使用的寄存器及相应的赋值:
寄存器 | 值 |
---|---|
rax | 0 |
rdi | 输入的位置,使用文件描述符进行表示,要是从标准输入读取的话,赋值为0 |
rsi | 读取之后输出字符串的位置 |
rdx | 输出的字符个数 |
注意:在CPU的角度,这里的输入字符串相当于CPU读取内容,数据流出入方向是rdi->rsi
。
从键盘上输入字符串到屏幕的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
# rdi = 1,表示需要输出到标准输出
movq $1,%rdi
# rsi = 字符串的地址,表明需要输出的内容
movq $prompt,%rsi
# rdx = 13,表明需要向屏幕输出的字符个数——13
movq $13,%rdx
syscall
# 此处取oneChar地址到rbx中
leaq oneChar,%rbx
# 此处取stringToFile地址到r10中
leaq stringToFile,%r10
# r12里边放的是字符的个数,用来限制最后存入的字符长度
movq $0,%r12
# CPU开始从键盘读取字符串到内存
readCharacters:
# rax = 0,0是输入的系统调用号,表明CPU开始读取字符串
movq $0,%rax
# rdi = 0,代表标准输入,标准输入一般代表键盘
movq $0,%rdi
# rbx里边存放的是oneChar地址,表明需要CPU输出的方向是oneChar所在的内存
movq %rbx,%rsi
# rdx = 1,这里是录入的字符个数,说明这里只录入一个字符
movq $1,%rdx
syscall
# 把rbx代表的oneChar地址里边的值(刚刚输入的字符)取出来,放到rax里边
movq (%rbx),%rax
# 10是ASCII回车字符,要是输入的是回车的话,就跳转到输出字符串地址
cmpb $10,%al
je printString
# r12 = r12 + 1
incq %r12
# stringLength < r12,会接着输入字符,但是不会把输入的字符放到最后输出的字符串(stringToFile)里边。
cmpq %r12,stringLength
jb readCharacters
# stringLength >= r12,才会往下进行执行
# 把字符串放到输出的字符串(stringToFile)里边
movb %al,(%r10)
# 把存放字符的地址往后挪一位
incq %r10
# 接着进行读取字符,这样的话,可以避免缓存区溢出
jmp readCharacters
printString:
# 更新一下存放字符的位置
incq %r10
# 把倒数第二字符变成回车符
movb $10,(%r10)
# 更新一下存放字符的位置
incq %r10
# 把最后一个字符串复制为NULL
movb $0,(%r10)
# rax = 1,表明这是输出系统调用
movq $1,%rax
# rdi = 1,标准输出
movq $1,%rdi
# rsi里边存放输出的内容
movq $stringToFile,%rsi
# rdx = r12,说明最后输入r12的字符串
movq %r12,%rdx
syscall
# 下边这三条语句相当于C语言在main方法中使用return 0语句,或者在任意函数中使用exit(0)语句
# rax = 60,这是程序退出的系统调用
movq $60,%rax
# 告诉操作系统返回值为0
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
# 下边这三条语句相当于C语言在main方法中使用return 0语句,或者在任意函数中使用exit(0)语句
# rax = 60,这是程序退出的系统调用号
movq $60,%rax
# 告诉操作系统返回值为0
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:
# rax = 85,告诉内核需要创建文件
movq $85,%rax
# rdi里边存放创建文件的名称
movq $fileName,%rdi
# rsi放入文件的权限
movq $0600,%rsi
syscall
# rax在系统调用之后就保存文件描述符,这里把文件描述符从rax中保存到rdi中
movq %rax,%rdi
# rax = 1,告诉操作系统需要写入文件
movq $1,%rax
# rsi保存需要写入到文件的字符串
movq $fileContextString,%rsi
# rdx里边是写入文件的字符串长度
movq $12,%rdx
syscall
# 下边这三条语句相当于C语言在main方法中使用return 0语句,或者在任意函数中使用exit(0)语句
# rax = 60,这是程序退出的系统调用号
movq $60,%rax
# 告诉操作系统返回值为0
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是一个8字节的整数
fileDescriptor: .quad 0
.section .bss
stringFromFile: .skip 14
.global main
.section .text
main:
# rax = 2,这是打开文件的系统调用号
movq $2,%rax
# rdi需要打开文件名
movq $fileName,%rdi
# rsi里边存放着文件操作权限
movq $00,%rsi
syscall
# rax里边放着打开文件系统调用的返回值,如果rax<=0,那么就需要退出程序。
cmpq $0,%rax
jbe done
# rax>0,那么rax里边存放的就是上边调用打开文件系统调用的文件描述符,fileDescriptor里边存放这个字符串
movq %rax,fileDescriptor
# rax = 0,CPU读取的系统调用号,表明CPU从文件里边读取内容放入到内存里边,数据流方向:rdi->rsi
movq $0,%rax
# rdi里边存放文件描述符,表明需要CPU读取内容的文件
movq fileDescriptor,%rdi
# rsi表示内容放置的内存位置
movq $stringFromFile,%rsi
# rdx存放内容的长度
movq $11,%rdx
syscall
# rdi里边放入的stringFromFile里边的地址
movq $stringFromFile,%rdi
# 在stringFromFile字符串最后放入一个换行符
movb $'\n',12(%rdi)
# movb $'\0',13(%rdi)
# rax = 1,CPU写入的系统调用,数据流方向:rsi->rdi
movq $1,%rax
# rdi里边是CPU写入的方向
movq $1,%rdi
# rsi是CPU读取的内存位置
movq $stringFromFile,%rsi
# rdx是CPU写入的长度
movq $13,%rdx
syscall
# rax = 3,CPU关闭文件的系统调用号
movq $3,%rax
# rdi里边是文件描述符
movq fileDescriptor,%rdi
syscall
done:
# 下边这三条语句相当于C语言在main方法中使用return 0语句,或者在任意函数中使用exit(0)语句
# rax = 60,这是程序退出的系统调用号
movq $60,%rax
# 告诉操作系统返回值为0
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 |