11.3 系统调用
11.3.1 默认的调用规范
通常, FreeBSD 的内核使用 C 语言的调用规范。 此外, 虽然我们使用 int 80h
来访问内核, 但是我们常常通过调用一个函数来执行 int 80h
, 而不是直接访问。
这个规范是非常方便的, 比 Microsoft® 的 MS-DOS® 上使用的规范更加优越。 为什么呢? 因为 UNIX® 的规范允许任何语言所写的程序访问内核。
汇编语言也可以这样做, 比如, 我们可以编写一个文件:
kernel: int 80h ; Call kernel ret open: push dword mode push dword flags push dword path mov eax, 5 call kernel add esp, byte 12 ret
这是一种非常清晰而易于移植的编码方法。 如果你需要将代码移植到一个使用和 FreeBSD 完全不同中断或参数传递方式的 UNIX 系统, 那么你所需要做的仅仅是改变那一段内核程序。
但是汇编程序员喜欢使用一些技巧来削减程序执行所需的时钟周期数。 以上的例子需要一个 call/ret
组合, 我们可以通过压栈 push
一个额外的双字节来去除它。
open: push dword mode push dword flags push dword path mov eax, 5 push eax ; Or any other dword int 80h add esp, byte 16
在上一个 open
的例子中, 我们放在 EAX
中的数字 5
表示了一个内核函数。
11.3.2 另一种调用规范
FreeBSD 是一个非常灵活的系统。 它提供了访问内核的其他方式。 但是, 如果你要用到它, 你的系统必须安装 Linux emulation。
Linux 是一个类 UNIX 操作系统。 但是, 它的内核在传递参数的时候, 使用和 MS-DOS 相同系统调用规范。 比如在 UNIX 的规范中, 代表内核函数的数字存放在 EAX
中。 但是在 Linux 中, 参数不进行压栈而是存放在 EBX, ECX, EDX, ESI, EDI, EBP
:
open: mov eax, 5 mov ebx, path mov ecx, flags mov edx, mode int 80h
这种做法与 UNIX 的常规做法相比有一个严重的缺点, 至少现在汇编语言程序需要注意的: 每次进行系统调用, 你必须对寄存器的值进行压栈和出栈操作, 这让你的程序变得冗长而效率低下。 因此, FreeBSD 提供给你了一个其他的选择。
如果你确实选择了 Linux 的规范, 你必要让你的系统知道这一点。 在你的程序完成汇编和连接之后, 你需要对可执行文件进行标识:
% brandelf -t Linux filename
11.3.3 你该选择哪个规范?
如果你专门为 FreeBSD 写代码, 你必须使用 UNIX 的规范: 因为它快速, 你可以在寄存器中存取全局变量, 而且不需要进行可执行的标识, 更不需要在你的系统上安装 Linux emulation。
如果你想做一些可以在 Linux 上运行的程序, 并且你打算尽可能地给 FreeBSD 用户提供最有效率地程序。 我将在我讲解完汇编基础之后, 告诉你怎样完成这样地程序。
11.3.4 系统调用号
要通知内核你调用了什么系统服务, 将代表调用的数字放入 EAX
中。 当然, 你需要知道那些系统调用号代表了什么。
11.3.4.1 syscalls 文件
系统调用号列在了 syscalls 文件里面。 locate syscalls 去寻找这个文件的不同格式, 它们都是从 syscalls.master 中自动生成的。
你可以在 /usr/src/sys/kern/syscalls.master 下找到这个描述默认情况下 UNIX 系统调用规范的文件。 如果你需要使用在 Linux emulation 中使用的规范, 请参阅 /usr/src/sys/i386/linux/syscalls.master。
注意: FreeBSD 和 Linux 不仅仅是使用不同的调用规范, 有时候它们也使用不同的系统调用号来表示相同函数。
syscalls.master 描述了如何使用这些调用:
0 STD NOHIDE { int nosys(void); } syscall nosys_args int 1 STD NOHIDE { void exit(int rval); } exit rexit_args void 2 STD POSIX { int fork(void); } 3 STD POSIX { ssize_t read(int fd, void *buf, size_t nbyte); } 4 STD POSIX { ssize_t write(int fd, const void *buf, size_t nbyte); } 5 STD POSIX { int open(char *path, int flags, int mode); } 6 STD POSIX { int close(int fd); } etc...
最左边的一列告诉我们需要放入 EAX
的数字。
最右边的一列告诉我们那些需要被压栈 push
的参数, 它们的压栈 push
顺序是 从右到左。
例如,要打开 open
一个文件, 我们需要先首先压栈 push
模式字 mode
, 然后是压栈标志字 flags
, 然后是压栈保存路径 path
的地址。