x86 汇编/与 Linux 交互
系统调用是用户程序和 Linux 内核之间的接口。它们用于让内核执行各种系统任务,例如文件访问、进程管理和网络。在 C 编程语言中,您通常会调用一个包装函数来执行所有必需的步骤,甚至使用高级功能,例如标准 IO 库。
在 Linux 上,有几种方法可以发出系统调用。此页面将重点介绍通过使用 int $0x80
或 syscall
调用软件中断来发出系统调用。这是一种在纯汇编程序中发出系统调用的简单直观的方法。
为了通过中断发出系统调用,您必须通过将所有必需的信息复制到 GPRs 中来传递它们给内核。
每个系统调用都有一个固定的编号。Linux 永久保证向后兼容性,因此一旦一个编号被分配给一个系统调用,它就不会再改变。永远。
int $0x80 和 syscall 的编号不同! |
您可以通过将编号写入 eax
/rax
寄存器来指定系统调用。
大多数系统调用都采用参数来执行其任务。这些参数通过在发出实际调用之前将它们写入相应的寄存器来传递。每个参数索引都有一个特定的寄存器。请查看小节中的表格,因为 int $0x80
和 syscall
之间的映射不同。参数按它们在相应 C 包装函数的函数签名中出现的顺序传递。您可以在每个 Linux ABI 文档中找到系统调用函数及其签名,例如参考手册(键入 man 2 open
查看 open
系统调用的签名)。
在一切设置正确后,您可以使用 int $0x80
或 syscall
调用中断,内核将执行任务。
系统调用的返回值/错误值将写入 eax
/rax
。
内核使用自己的堆栈来执行操作。用户堆栈不会以任何方式被触碰。
在 Linux x86 和 Linux x86_64 系统上,您可以通过使用 int
指令调用中断 $0x80
来发出系统调用。参数通过设置如下通用寄存器来传递
系统调用编号 | 第一个参数 | 第二个参数 | 第三个参数 | 第四个参数 | 第五个参数 | 第六个参数 | 结果 |
---|---|---|---|---|---|---|---|
eax
|
ebx
|
ecx
|
edx
|
esi
|
edi
|
ebp
|
eax
|
系统调用编号在 Linux 生成的文件 $build/arch/x86/include/generated/uapi/asm/unistd_32.h
或 $build/usr/include/asm/unistd_32.h
中描述。后者也可能存在于您的 Linux 系统中,只需省略 $build
。
在使用 int $0x80
发出系统调用期间,所有寄存器都会被保留,除了 eax
,返回值将存储在那里。
x86_64 架构引入了一条专用指令来发出系统调用。它不访问中断描述符表,并且速度更快。参数通过设置如下 GPRs 来传递
系统调用编号 | 第一个参数 | 第二个参数 | 第三个参数 | 第四个参数 | 第五个参数 | 第六个参数 | 结果 |
---|---|---|---|---|---|---|---|
rax
|
rdi
|
rsi
|
rdx
|
r10
|
r8
|
r9
|
rax
|
系统调用编号在 Linux 生成的文件 $build/usr/include/asm/unistd_64.h
中描述。此文件也可能存在于您的 Linux 系统中,只需省略 $build
。
在使用 syscall
发出系统调用期间,所有寄存器都会被保留,除了 rcx
和 r11
(以及返回值 rax
)。
为了实现最大兼容性,在 64 位平台上,Linux 会剪裁使用中断方法的系统调用的输入和输出。这意味着,例如,您不能在使用 int $0x80
方法的 x86-64 平台上传递或接收(完整的)64 位地址指针,因为所有参数和结果的高 32 位将被清零。这通常与 syscall
的一般偏好一致,因为它比中断更快。
在 x86-64 Linux 的 C 库函数调用中,参数 6 在 r9 上传递,后续参数在堆栈上传递(以相反顺序)。
第一个参数 | 第二个参数 | 第三个参数 | 第四个参数 | 第五个参数 | 第六个参数 |
---|---|---|---|---|---|
rdi
|
rsi
|
rdx
|
rcx
|
r8
|
r9
|
调用者可以预期在 rax
寄存器中找到子例程的返回值。
为了总结和澄清信息,让我们来看一个非常简单的例子:hello world 程序。它将使用 write
系统调用将文本 "Hello World" 写入标准输出,并使用 _exit
系统调用退出程序。
系统调用签名
ssize_t write(int fd, const void *buf, size_t count);
void _exit(int status);
这是在下面汇编中实现的 C 程序
#include <unistd.h>
int main(int argc, char *argv[])
{
write(1, "Hello World\n", 12); /* write "Hello World" to stdout */
_exit(0); /* exit with error code 0 (no error) */
}
两个示例的开头都一样:一个存储在数据段中的字符串和作为全局符号的 _start
。
.data
msg: .ascii "Hello World\n"
.text
.global _start
如 $build/usr/include/asm/unistd_32.h
中定义的那样,write
和 _exit
的系统调用号为
#define __NR_exit 1
#define __NR_write 4
参数的传递方式与在 C 程序中一样,使用正确的寄存器。设置好一切后,使用 int $0x80
进行系统调用。
_start:
movl $4, %eax ; use the `write` [interrupt-flavor] system call
movl $1, %ebx ; write to stdout
movl $msg, %ecx ; use string "Hello World"
movl $12, %edx ; write 12 characters
int $0x80 ; make system call
movl $1, %eax ; use the `_exit` [interrupt-flavor] system call
movl $0, %ebx ; error code 0
int $0x80 ; make system call
在 $build/usr/include/asm/unistd_64.h
中,系统调用号定义如下
#define __NR_write 1
#define __NR_exit 60
参数的传递方式与 int $0x80
示例中相同,只是寄存器的顺序不同。系统调用使用 syscall
完成。
_start:
movq $1, %rax ; use the `write` [fast] syscall
movq $1, %rdi ; write to stdout
movq $msg, %rsi ; use string "Hello World"
movq $12, %rdx ; write 12 characters
syscall ; make syscall
movq $60, %rax ; use the `_exit` [fast] syscall
movq $0, %rdi ; error code 0
syscall ; make syscall
这是一个示例库函数的 C 原型。
Window XCreateWindow(display, parent, x, y, width, height, border_width, depth,
class, visual, valuemask, attributes)
参数的传递方式与 int $0x80
示例中相同,只是寄存器的顺序不同。
库函数在源文件开头声明(库路径在编译链接时声明)。
extern XCreateWindow
mov rdi, [xserver_pdisplay]
mov rsi, [xwin_parent]
mov rdx, [xwin_x]
mov rcx, [xwin_y]
mov r8, [xwin_width]
mov r9, [xwin_height]
mov rax, attributes
push rax ; ARG 12
sub rax, rax
mov eax, [xwin_valuemask]
push rax ; ARG 11
mov rax, [xwin_visual]
push rax ; ARG 10
mov rax, [xwin_class]
push rax ; ARG 9
mov rax, [xwin_depth]
push rax ; ARG 8
mov rax, [xwin_border_width]
push rax ; ARG 7
call XCreateWindow
mov [xwin_window], rax
注意函数的最后几个参数,它们以相反的顺序被压入堆栈。