如果您正在开发混合 C-汇编项目,这是首选方法。请查阅 GCC 文档和 Linux 内核中的示例.S文件,这些文件通过 gas (而不是通过 as86 的文件)。
32 位参数以相反的语法顺序压入堆栈(因此以正确的顺序访问/弹出),位于 32 位近返回地址之上。%ebp, %esi, %edi, %ebx是被调用者保存的寄存器,其他寄存器是调用者保存的;%eax用于保存结果,或者%edx:%eax用于 64 位结果。
FP 栈:我不确定,但我认为结果在st(0)中,整个栈由调用者保存。
请注意,GCC 具有通过保留寄存器、在寄存器中传递参数、不假定 FPU 等方式来修改调用约定的选项。请查阅 i386.info页面。
请注意,您必须为将遵循标准 GCC 调用约定的函数声明cdecl或regparm(0)属性。请参阅 GCC info 页面的C Extensions::Extended Asm:章节。另请参阅 Linux 如何定义其asmlinkage宏。
一些 C 编译器在每个符号前添加下划线,而另一些则不添加。
特别是,Linux a.out GCC 会进行这种前缀添加,而 Linux ELF GCC 则不会。
如果您需要同时处理这两种行为,请参阅现有软件包的做法。例如,获取旧的 Linux 源代码树,例如 Elk、qthreads 或 OCaml。
您还可以通过插入如下语句来覆盖隐式的 C->asm 重命名
void foo asm("bar") (void); |
请注意,binutils 包中的 objcopy 实用程序应该允许您将 a.out 对象转换为 ELF 对象,并且在某些情况下,可能也可以反向转换。更一般地说,它将进行大量文件格式转换。
通常您会被告知使用 C 库 (libc) 是唯一的方法,而直接系统调用是不好的。这在某种程度上是正确的。一般来说,您必须知道 libc 并非神圣不可侵犯,在大多数情况下,它只是进行一些检查,然后调用内核,然后设置 errno。您也可以在您的程序中轻松地做到这一点(如果需要),并且您的程序将小十几倍,并且这也将提高性能,仅仅因为您没有使用共享库(静态二进制文件更快)。在汇编编程中使用或不使用 libc 更多的是一个品味/信仰问题,而不是实际问题。请记住,Linux 的目标是符合 POSIX 标准,libc 也是如此。这意味着几乎所有 libc “系统调用”的语法都与实际内核系统调用的语法完全匹配(反之亦然)。此外,GNU libc(glibc) 从版本到版本变得越来越慢,并且消耗越来越多的内存;因此,使用直接系统调用的情况变得非常普遍。然而,抛弃 libc 的主要缺点是您可能需要自行实现一些 libc 特定的函数(这些函数不仅仅是系统调用包装器)(printf()和 Co.),您为此做好了准备,不是吗? :-)
以下是直接系统调用的优缺点总结。
优点
尽可能小的尺寸;从系统中挤出最后一个字节
尽可能高的速度;从您最喜欢的基准测试中挤出周期
完全控制:您可以使您的程序/库适应您的特定语言或内存需求或任何其他需求
不受 libc 垃圾的污染
不受 C 调用约定的污染(如果您正在开发自己的语言或环境)
静态二进制文件使您独立于 libc 升级或崩溃,或独立于悬空的#!解释器路径(并且速度更快)
仅仅是为了从中获得乐趣(汇编编程不会让您感到兴奋吗?)
缺点
如果计算机上的任何其他程序都使用 libc,那么复制 libc 代码实际上会浪费内存,而不是节省内存。
在许多静态二进制文件中冗余实现的服务会浪费内存。但是您可以使您的 libc 替代品成为共享库。
与用汇编编写所有内容相比,通过某种字节码、字码或结构解释器可以更好地节省大小。(解释器本身可以用 C 或汇编编写。)保持多个二进制文件小的最佳方法不是拥有多个二进制文件,而是拥有一个解释器来处理带有#!前缀的文件。这就是 OCaml 在字码模式(而不是优化的本机代码模式)下工作的方式,并且它与使用 libc 兼容。这也是 Tom Christiansen 的 Perl PowerTools 对 unix 实用程序的重新实现的工作方式。最后,保持事物小的最后一种方法,它不依赖于具有硬编码路径的外部文件(无论是库还是解释器),是只拥有一个二进制文件,并对其进行多个命名的硬链接或软链接:同一个二进制文件将以最佳空间提供您所需的一切,而不会冗余子例程或无用的二进制标头;它将根据其argv[0]来调度其特定行为;如果它没有以识别的名称调用,它可能会默认为 shell,因此也可用作解释器!
您无法从 libc 提供的除 linux 系统调用之外的许多功能中受益:也就是说,手册页第 3 节中描述的功能,而不是第 2 节中的功能,例如 malloc、线程、locale、密码、高级网络管理等。
因此,您可能必须重新实现 libc 的大部分内容,从printf()到malloc()和gethostbyname。这与 libc 的努力是冗余的,有时可能相当无聊。请注意,有些人已经重新实现了“轻量级”的 libc 部分替代品——请查看它们!(Redhat 的 minilibc,Rick Hohensee 的 libsys,Felix von Leitner 的 dietlibc,asmutils 项目正在开发纯汇编 libc)
静态库阻止您从 libc 升级以及 libc 附加组件(例如 zlibc 包)中受益,该软件包可以对 gzip 压缩文件进行即时透明解压缩。
与系统调用的成本相比,libc 添加的少量指令可能是非常小的速度开销。如果速度是一个问题,那么您主要的问题在于您对系统调用的使用,而不是其包装器的实现。
当在 Linux 的微内核版本(例如 L4Linux)中运行时,使用标准汇编 API 进行系统调用比使用 libc API 慢得多,L4Linux 有自己更快的调用约定,并且在使用标准调用约定(L4Linux 附带使用其系统调用 API 重新编译的 libc;当然,您也可以使用其 API 重新编译您的代码)。
有关一般速度优化问题,请参阅前面的讨论。
如果系统调用对您来说太慢,您可能需要破解内核源代码(用 C 语言),而不是停留在用户空间。
如果您仔细考虑了上述优缺点,并且仍然想使用直接系统调用,那么这里有一些建议。
您可以通过包含asm/unistd.h,并使用提供的宏,以可移植的方式在 C 中轻松定义您的系统调用函数(而不是不可移植地使用汇编)。
由于您正尝试替换它,请获取 libc 的源代码,并理解它们。(如果您认为您可以做得更好,请向作者发送反馈!)
作为纯汇编代码的一个示例,它可以完成您想要的一切,请检查 .
基本上,您发出一个int 0x80,其中__NR_syscallname 编号(来自asm/unistd.h)在eax中,参数(最多六个)在ebx, ecx, edx, esi, edi, ebp中,依次排列。
结果在eax中返回,负结果表示错误,其相反数是 libc 将放入errno中的内容。用户堆栈未被触及,因此在进行系统调用时,您不需要拥有有效的堆栈。
《Linux 内核内幕》,特别是“如何在 i386 架构上实现系统调用?”章节将为您提供更全面的概述。
至于启动时传递给进程的调用参数,一般原则是堆栈最初包含参数的数量argc,然后是指针列表,这些指针构成*argv,然后是以空字符结尾的以空字符结尾的variable=value字符串序列,用于environment。有关更多详细信息,请检查 ,阅读 libc 中的 C 启动代码的源代码(crt0.S或crt1.S),或 Linux 内核中的源代码(exec.c和binfmt_*.c在linux/fs/).
如果您想在 Linux 下执行直接端口 I/O,要么是一些非常简单的、不需要 OS 仲裁的东西,您应该查看IO-Port-Programming迷你 HOWTO;要么它需要内核设备驱动程序,您应该尝试了解更多关于内核黑客、设备驱动程序开发、内核模块等的信息,LDP 中有其他优秀的 HOWTO 和文档。
特别是,如果您想要进行图形编程,请加入 GGI 或 XFree86 项目之一。
有些人甚至做得更好,用解释型领域特定语言 GAL 编写小型而健壮的 XFree86 驱动程序,并通过部分求值实现了手写 C 驱动程序的效率(驱动程序不仅不是用汇编编写的,甚至不是用 C 编写的!)。问题是他们用来实现效率的部分求值器不是自由软件。有人愿意替换它吗?
无论如何,在所有这些情况下,当将 GCC 内联汇编与linux/asm/*.h中的宏一起使用时,您会比编写完整的汇编源文件更好。
这种事情在理论上是可能的(证明:请参阅 DOSEMU 如何有选择地向程序授予硬件端口访问权限),并且我听说有传言说有人在某个地方实际做到了(在 PCI 驱动程序中?一些 VESA 访问的东西?ISA PnP?不知道)。如果您有关于此的更准确信息,我们将非常欢迎。无论如何,查找更多信息的良好位置是 Linux 内核源代码、DOSEMU 源代码以及 Linux 下各种底层程序的源代码。(如果 GGI 支持 VESA,也许是 GGI)。
基本上,您必须使用 16 位保护模式或 vm86 模式。
前者设置起来更简单,但仅适用于行为良好的代码,这些代码不会进行任何类型的段算术或绝对段寻址(特别是寻址段 0),除非偶然所有使用的段都可以预先在 LDT 中设置好。
后者允许与原始 16 位环境具有更多的“兼容性”,但需要更复杂的处理。
在这两种情况下,在您可以跳转到 16 位代码之前,您必须
从/dev/mem到进程的地址空间,
mmap 16 位代码中使用的任何绝对地址(例如 ROM、视频缓冲区、DMA 目标和内存映射 I/O)
设置 LDT 和/或 vm86 模式监视器。
从内核获取正确的 I/O 权限(请参阅上一节)
再次强调,请仔细阅读为 DOSEMU 项目贡献的代码的源代码,特别是这些用于在 Linux/i386 下运行 ELKS 和/或简单.COM程序的迷你模拟器。