Linux 中的标准编程语言是 C。因此,系统调用的最佳描述将它们显示为要调用的 C 函数。鉴于它们作为 C 函数的描述以及如何将它们映射到汇编中的实际系统调用的知识,您将能够轻松地使用任何您想要的系统调用。
首先,您需要所有系统调用的引用,因为它们对 C 程序员来说是这样的。我所知道的最好的是Linux man-pages project,尤其是system calls 部分。
让我们以write 系统调用为例,因为它是您问题中的那个。可以看到,第一个参数是有符号整数,通常是open系统调用返回的文件描述符。这些文件描述符也可能是从您的父进程继承的,前三个文件描述符(0=stdin、1=stdout、2=stderr)通常会发生这种情况。第二个参数是指向缓冲区的指针,第三个参数是缓冲区的大小(作为无符号整数)。最后,该函数返回一个有符号整数,即写入的字节数,或者一个负数表示错误。
现在,如何将其映射到实际的系统调用?有很多方法可以在 32 位 x86 上进行系统调用(这可能是您正在使用的,基于您的寄存器名称);请注意,它在 64 位 x86 上完全不同(请确保您在 32 位模式下汇编并链接 32 位可执行文件;请参阅this question 以了解其他情况如何出错的示例)。在 32 位 x86 中最古老、最简单和最慢的方法是 int $0x80 方法。
对于int $0x80方法,将系统调用号放在%eax中,参数放在%ebx、%ecx、%edx、%esi、%edi和%ebp中,以该顺序。然后调用int $0x80,系统调用的返回值是%eax。请注意,此返回值与参考中所说的不同;该参考显示了 C 库将如何返回它,但系统调用在错误时返回 -errno(例如 -EINVAL)。在这种情况下,C 库会将其移动到 errno 并返回 -1。请参阅syscalls(2) 和intro(2) 了解更多详情。
因此,在write 示例中,您可以将write 系统调用号放入%eax,将第一个参数(文件描述符编号)放入%ebx,将第二个参数(指向字符串的指针)放入%ecx,以及%edx 中的第三个参数(字符串的长度)。系统调用将在%eax 中返回写入的字节数或取反的错误号(如果返回值介于 -1 和 -4095 之间,则为取反的错误号)。
最后,如何找到系统调用号?他们可以在/usr/include/linux/unistd.h 找到。在我的系统上,这只是包括/usr/include/asm/unistd.h,最后包括/usr/include/asm/unistd_32.h,所以数字在那里(对于write,你可以看到__NR_write是4)。来自/usr/include/linux/errno.h 的错误编号也是如此(在我的系统上,在追逐包含链后,我在/usr/include/asm-generic/errno-base.h 找到第一个错误编号,其余的在/usr/include/asm-generic/errno.h 找到)。对于使用其他常量或结构的系统调用,它们的文档会告诉您应该查看哪些标头以找到相应的定义。
现在,正如我所说,int $0x80 是最古老和最慢的方法。较新的处理器具有更快的特殊系统调用指令。为了使用它们,内核提供了一个虚拟动态共享对象(vDSO;它就像一个共享库,但仅在内存中),您可以调用一个函数来使用适用于您的硬件的最佳方法进行系统调用.它还提供了一些特殊功能来获取当前时间,甚至无需进行系统调用和其他一些事情。当然,如果不使用动态链接器,使用起来会有点困难。
还有另一种较旧的方法vsyscall,它类似于vDSO,但使用固定地址的单个页面。此方法已弃用,如果您使用的是最新内核,则会在系统日志中显示警告,甚至可以在更新的内核上在引导时禁用,并且将来可能会被删除。不要使用它。