翻译自:asm_book/more/system_calls/README.md at main · pkivolowitz/asm_book (github.com)
本书中多次使用“函数”一词,这里不需要额外解释。“系统调用”一词也在多处使用,通常伴随着这样的评论:通过C运行时进行系统调用实际上只是调用一个充当包装器的普通函数。对此的解释已被承诺...
包装器
“包装器”是一个用来描述隐藏其他事物细节的函数的术语,通常是另一个或多个函数。隐藏细节是一种抽象形式,并且可能是一件好事。广义上讲,API(应用程序接口)本身也是常用的包装器的另一个例子。
C运行时作为包装器
许多C运行时函数只是系统调用的包装器。例如,如果你从C运行时调用open(),该函数将执行一些记账操作,然后进行实际的系统调用。
什么是系统调用?
简短的答案是,系统调用是一种由操作系统自身在其自己的私有内存区域中处理的类似函数调用,并可以访问内部特性和数据结构。
我们的程序在“用户空间”中运行。在ARM64处理器上,用户空间的技术名称是EL0(异常级别0)。
我们只能通过精心控制的机制——如系统调用——在内核空间中操作。内核(或系统)通常操作的地方的技术名称称为EL1。
还有两个更高的异常级别(EL2和EL3),这些超出了本书的范围。
进行系统调用的机制
首先,像任何函数调用一样,需要设置参数。第一个参数放入第一个寄存器,以此类推。
其次,与我们希望进行的特定系统调用相关的数字被加载到一个特定的寄存器(w8)中。
最后,一个特殊的指令svc触发了一个陷阱,使我们从用户空间提升到内核空间。换句话说,svc引起了从EL0到EL1的转变。在那里,进行各种检查并运行系统调用的实际代码。
关于从系统调用返回的描述超出了本书的范围。提示:就像有一个特殊的指令从EL0升级到EL1一样,也有一个特殊的指令做相反的操作。
与特定系统调用相关联的数字是什么? 难题。
在一个完美的世界里,每个Linux发行版都会使用相同的系统调用号集。但不是的。
这是我们见过的最全面的系统调用号列表。它显示了多种架构和发行版的系统调用号。
例子:调用getpid()
系统调用getpid()获取正在运行的进程的进程ID。每个执行实体都有一个。
我们展示了同一个程序的四个不同版本:
- 用C++编写
- 用C编写
- 使用汇编语言中的C运行时编写
- 直接从汇编语言调用系统调用
用C++编写
#include <iostream> // 1
#include <unistd.h> // 2
// 3
using std::cout; // 4
using std::endl; // 5
// 6
int main() { // 7
cout << "Greetings from: " << getpid() << endl; // 8
return 0; // 9
} // 10
用C编写
#include <stdio.h> // 1
#include <unistd.h> // 2
// 3
int main() { // 4
printf("Greetings from: %d\n", getpid()); // 5
return 0; // 6
} // 7
使用汇编语言中的C运行时编写
.global main // 1
.text // 2
.align 2 // 3
// 4
main: stp x29, x30, [sp, -16]! // 5
bl getpid // 6
mov w1, w0 // 7
ldr x0, =fmt // 8
bl printf // 9
ldp x29, x30, [sp], 16 // 10
mov w0, wzr // 11
ret // 12
// 13
.data // 14
fmt: .asciz "Greetings from: %d\n" // 15
// 16
.end // 17
直接从汇编语言调用系统调用
.global main // 1
.text // 2
.align 2 // 3
// 4
main: stp x29, x30, [sp, -16]! // 5
mov x8, 172 // getpid on ARM64 // 6
svc 0 // trap to EL1 // 7
mov w1, w0 // 8
ldr x0, =fmt // 9
bl printf // 10
ldp x29, x30, [sp], 16 // 11
mov w0, wzr // 12
ret // 13
// 14
.data // 15
fmt: .asciz "Greetings from: %d\n" // 16
// 17
.end // 18
我们选择getpid()是因为它不需要任何参数。使用C运行时,我们只需简单地跳转到它。直接调用系统调用则有所不同,我们必须先将x8寄存器加载对应于AARCH64架构的getpid()的编号。
通过查阅这个极好的网站,我们发现我们想要的编号是172。
我们想要的系统调用特定的常量被加载到x8寄存器中。回想一下,x0到x7是临时寄存器。
然后在第7行,带有参数0的svc指令启动了从EL0到EL1的提升,在那里内核实现了我们所需的功能并返回给我们。
回顾
系统调用是在操作系统内部实现的函数。
要到达那里,在某些时候,也许在C运行时库(CRT)中找到的包装函数后面,特定的发行版系统调用编号被放置在x8中,其他临时寄存器获取系统调用的文档化参数,然后执行带有参数0的svc指令。
偏好
我们建议尽可能使用CRT包装函数,因为:
- 它们更容易编码
- 它们可以在操作系统的发行版之间移植