5.1. Linux

5.1.1. 链接到 GCC

如果您正在开发混合 C-汇编项目,这是首选方法。请查阅 GCC 文档和 Linux 内核中的示例.S文件,这些文件通过 gas (而不是通过 as86 的文件)。

32 位参数以相反的语法顺序压入堆栈(因此以正确的顺序访问/弹出),位于 32 位近返回地址之上。%ebp, %esi, %edi, %ebx是被调用者保存的寄存器,其他寄存器是调用者保存的;%eax用于保存结果,或者%edx:%eax用于 64 位结果。

FP 栈:我不确定,但我认为结果在st(0)中,整个栈由调用者保存。

请注意,GCC 具有通过保留寄存器、在寄存器中传递参数、不假定 FPU 等方式来修改调用约定的选项。请查阅 i386.info页面。

请注意,您必须为将遵循标准 GCC 调用约定的函数声明cdeclregparm(0)属性。请参阅 GCC info 页面的C Extensions::Extended Asm:章节。另请参阅 Linux 如何定义其asmlinkage宏。

5.1.2. ELF 与 a.out 问题

一些 C 编译器在每个符号前添加下划线,而另一些则不添加。

特别是,Linux a.out GCC 会进行这种前缀添加,而 Linux ELF GCC 则不会。

如果您需要同时处理这两种行为,请参阅现有软件包的做法。例如,获取旧的 Linux 源代码树,例如 Elk、qthreads 或 OCaml。

您还可以通过插入如下语句来覆盖隐式的 C->asm 重命名

	void foo asm("bar") (void);
以确保 C 函数foo()将被实际调用为bar在汇编中。

请注意,binutils 包中的 objcopy 实用程序应该允许您将 a.out 对象转换为 ELF 对象,并且在某些情况下,可能也可以反向转换。更一般地说,它将进行大量文件格式转换。

5.1.3. 直接 Linux 系统调用

通常您会被告知使用 C 库 (libc) 是唯一的方法,而直接系统调用是不好的。这在某种程度上是正确的。一般来说,您必须知道 libc 并非神圣不可侵犯,在大多数情况下,它只是进行一些检查,然后调用内核,然后设置 errno。您也可以在您的程序中轻松地做到这一点(如果需要),并且您的程序将小十几倍,并且这也将提高性能,仅仅因为您没有使用共享库(静态二进制文件更快)。在汇编编程中使用或不使用 libc 更多的是一个品味/信仰问题,而不是实际问题。请记住,Linux 的目标是符合 POSIX 标准,libc 也是如此。这意味着几乎所有 libc “系统调用”的语法都与实际内核系统调用的语法完全匹配(反之亦然)。此外,GNU libc(glibc) 从版本到版本变得越来越慢,并且消耗越来越多的内存;因此,使用直接系统调用的情况变得非常普遍。然而,抛弃 libc 的主要缺点是您可能需要自行实现一些 libc 特定的函数(这些函数不仅仅是系统调用包装器)(printf()和 Co.),您为此做好了准备,不是吗? :-)

以下是直接系统调用的优缺点总结。

优点

缺点

如果您仔细考虑了上述优缺点,并且仍然想使用直接系统调用,那么这里有一些建议。

基本上,您发出一个int 0x80,其中__NR_syscallname 编号(来自asm/unistd.h)在eax中,参数(最多六个)在ebx, ecx, edx, esi, edi, ebp中,依次排列。

结果在eax中返回,负结果表示错误,其相反数是 libc 将放入errno中的内容。用户堆栈未被触及,因此在进行系统调用时,您不需要拥有有效的堆栈。

Note

在 Linux 2.4 中出现了在ebp中传递第六个参数,之前的 Linux 版本仅理解寄存器中的 5 个参数。

《Linux 内核内幕》,特别是“如何在 i386 架构上实现系统调用?”章节将为您提供更全面的概述。

至于启动时传递给进程的调用参数,一般原则是堆栈最初包含参数的数量argc,然后是指针列表,这些指针构成*argv,然后是以空字符结尾的以空字符结尾的variable=value字符串序列,用于environment。有关更多详细信息,请检查 ,阅读 libc 中的 C 启动代码的源代码(crt0.Scrt1.S),或 Linux 内核中的源代码(exec.cbinfmt_*.clinux/fs/).

5.1.4. Linux 下的硬件 I/O

如果您想在 Linux 下执行直接端口 I/O,要么是一些非常简单的、不需要 OS 仲裁的东西,您应该查看IO-Port-Programming迷你 HOWTO;要么它需要内核设备驱动程序,您应该尝试了解更多关于内核黑客、设备驱动程序开发、内核模块等的信息,LDP 中有其他优秀的 HOWTO 和文档。

特别是,如果您想要进行图形编程,请加入 GGI 或 XFree86 项目之一。

有些人甚至做得更好,用解释型领域特定语言 GAL 编写小型而健壮的 XFree86 驱动程序,并通过部分求值实现了手写 C 驱动程序的效率(驱动程序不仅不是用汇编编写的,甚至不是用 C 编写的!)。问题是他们用来实现效率的部分求值器不是自由软件。有人愿意替换它吗?

无论如何,在所有这些情况下,当将 GCC 内联汇编与linux/asm/*.h中的宏一起使用时,您会比编写完整的汇编源文件更好。

5.1.5. 从 Linux/i386 访问 16 位驱动程序

这种事情在理论上是可能的(证明:请参阅 DOSEMU 如何有选择地向程序授予硬件端口访问权限),并且我听说有传言说有人在某个地方实际做到了(在 PCI 驱动程序中?一些 VESA 访问的东西?ISA PnP?不知道)。如果您有关于此的更准确信息,我们将非常欢迎。无论如何,查找更多信息的良好位置是 Linux 内核源代码、DOSEMU 源代码以及 Linux 下各种底层程序的源代码。(如果 GGI 支持 VESA,也许是 GGI)。

基本上,您必须使用 16 位保护模式或 vm86 模式。

前者设置起来更简单,但仅适用于行为良好的代码,这些代码不会进行任何类型的段算术或绝对段寻址(特别是寻址段 0),除非偶然所有使用的段都可以预先在 LDT 中设置好。

后者允许与原始 16 位环境具有更多的“兼容性”,但需要更复杂的处理。

在这两种情况下,在您可以跳转到 16 位代码之前,您必须

再次强调,请仔细阅读为 DOSEMU 项目贡献的代码的源代码,特别是这些用于在 Linux/i386 下运行 ELKS 和/或简单.COM程序的迷你模拟器。