内联函数
大多数函数都包含在库中,但也有一些函数是在编译器中生成的(即内部函数)。 这些被称为内联函数或内部函数。
如果一个函数是内部函数,在通常会采用内联方式插入该函数的代码,从而避免函数调用的开销并可发出该函数的高效率计算机指令。 内部函数通常比等效的内联程序集速度更快,因为优化程序拥有众多内部函数行为方式的内置知识,因此可以优化使用内联程序集无法优化的内容。 此外,优化程序还可以采用不同的方式扩展内部函数、对齐缓冲区或根据上下文和调用自变量进行其他方面的调整。
使用内部函数会影响到代码的可移植性,因为在 Visual C++ 中可用的内部函数如果用其他编译器编译代码则可能不可用,并且对于某些目标体系结构可用的部分内部函数并非对所有体系结构都可用。 但是,内部函数通常比内联程序集可移植性更大。 64 位体系结构要求内部函数,但不支持内联程序集。
某些内部函数(例如 __assume 和 __ReadWriteBarrier)向编译器提供信息,但这会影响到优化程序的行为。
某些内部函数只能用作内部函数,某些内部函数可以同时用于函数和内部函数实现。 你可以指示编译器使用这两种方式中的一种来使用内部函数实现,具体取决于你是想仅启用特定函数还是想启用所有内部函数。 第一种方法是使用 #pragma intrinsic(intrinsic-function-name-list)。 杂注可用于指定单个内部函数或用逗号分隔的多个内部函数。 第二种方法是使用 /Oi(生成内部函数)编译器选项,让指定平台的所有内部函数可用。 在 /Oi 下,使用 #pragma function(intrinsic-function-name-list) 强制使用函数调用,而不是内部函数。 如果特定内部函数的文档规定例程只能用作内部函数,则不管是指定 /Oi 还是 #pragma intrinsic 都会使用内部函数实现。 在所有情况下,/Oi 或 #pragma intrinsic 允许但不是强制优化程序使用内部函数。 优化程序仍然可以调用函数。
一些标准的 C/C++ 库函数在某些体系结构上可用于内部函数实现。 调用 CRT 函数时,如果在命令行指定了 /Oi,则会使用内部函数实现。
头文件 <intrin.h> 可用,其用于声明常用内部函数的原型。 制造商指定的内部函数可用于 <immintrin.h> 和 <ammintrin.h> 头文件。 此外,某些 Windows 头文件还可声明在编译器内部函数上映射的函数。
内联汇编
由于内联汇编程序不需要单独的程序集和链接步骤,因此它比单独的汇编程序更方便。 内联程序集代码可以使用任何 C 变量或范围中的函数名,因此,将其与程序的 C 代码集成非常容易。 由于程序集代码可与 C 或 C++ 语句内联组合,因此它可以执行在 C 或 C++ 中难以完成或无法完成的任务。
内联程序集的用法包括:
- 使用汇编语言编写函数;
- 代码的点优化速度临界区;
- 通过硬件直接访问设备驱动程序;
- 为“naked”调用编写 prolog 和 epilog 代码;
内联程序集是一个具有特殊用途的工具。 如果你计划通过端口将应用程序传输到其他计算机,你可能需要在单独的模块中放置计算机特定的代码。 由于内联汇编程序不支持任何 Microsoft 宏汇编程序 (MASM) 的宏和数据指令,因此您可能会发现对此类模块使用 MASM 会更方便。
在内联汇编程序中使用和保留寄存器
通常,__asm 块开始时,不应假定寄存器将具有给定值。 不保证将在单独的 __asm 块中保留寄存器值。 如果结束一个内联代码块并开始另一个内联代码块,则不能依赖第二个块中的寄存器来保留其在第一个块中的值。 __asm 块从正常控制流继承任意寄存器值结果。
如果使用 __fastcall 调用约定,则编译器将传递寄存器中而不是堆栈上的函数参数。 这可能会导致带 __asm 块的函数出现问题,因为函数无法告知哪一参数位于哪一寄存器中。 如果函数碰巧在 EAX 中收到一个参数并立即在 EAX 中存储其他内容,则原始参数将丢失。 此外,您还必须在使用 __fastcall 声明的任何函数中保留 ECX 寄存器。
若要避免此类寄存器冲突,请勿对包含 __fastcall 块的函数使用 __asm 约定。 如果使用 /Gr 编译器选项全局指定 __fastcall 约定,请使用 __asm 或 __cdecl 来声明每个包含 __stdcall 块的函数。 (__cdecl 属性指示编译器使用该函数的 C 调用约定。)如果不使用 /Gr 进行编译,请避免使用 __fastcall 属性声明函数。
当使用 __asm 在 C/C++ 函数中编写汇编语言时,不需要保留 EAX、EBX、ECX、EDX、ESI 或 EDI 寄存器。 例如,在使用内联程序集编写函数的 POWER2.C 示例中,power2 函数没有在 EAX 寄存器中保留此值。 但是,使用这些寄存器将影响代码质量,因为寄存器分配器无法使用它们在 __asm 块中储存值。 此外,通过在内联程序集代码中使用 EBX、ESI 或 EDI,将强制编译器在函数序言和尾声中保存并还原这些寄存器。
您应保留用于 __asm 块的范围的其他寄存器(如 DS、SS、SP、BP 和标记寄存器)。 您应保留 ESP 和 EBP 寄存器,除非您出于某种原因要更改它们(例如,切换堆栈)。
某些 SSE 类型需要 8 字节堆栈对齐,以强制编译器发出动态堆栈对齐代码。 为了能在对齐之后访问局部变量和函数参数,编译器将保留两个帧指针。 如果编译器执行帧指针省略 (FPO),它将使用 EBP 和 ESP。 如果编译器不执行 FPO,它将使用 EBX 和 EBP。 为了确保代码正常运行,当函数需要动态堆栈对齐以便能修改帧指针时,请勿在 asm 代码中修改 EBX。 请将 8 字节对齐的类型移出函数,或者避免使用 EBX。
在内联汇编程序内跳转到标签
与普通的 C 或 C++ 标签一样,__asm 块的标签具有在整个已定义的函数中的范围(不只是在块中)。 程序集指令和 goto 语句可以跳转到 __asm 块内部或外部的标签。
__asm 块中定义的标签不区分大小写;goto 语句和程序集指令可以引用这些标签而无需考虑大小写。 C 和 C++ 标签仅在由 goto 语句使用时区分大小写。 程序集指令可以跳转到 C 或 C++ 标签而不考虑大小写。
以下代码显示了所有排列:
void func( void ) { goto C_Dest; /* Legal: correct case */ goto c_dest; /* Error: incorrect case */ goto A_Dest; /* Legal: correct case */ goto a_dest; /* Legal: incorrect case */ __asm { jmp C_Dest ; Legal: correct case jmp c_dest ; Legal: incorrect case jmp A_Dest ; Legal: correct case jmp a_dest ; Legal: incorrect case a_dest: ; __asm label } C_Dest: /* C label */ return; } int main() { }
不要使用 C 库函数名称作为 __asm
块中的标签。 例如,您可能想使用 exit
作为标签,如下所示:
; BAD TECHNIQUE: using library function name as label jne exit . . . exit: ; More __asm code follows
由于 exit 是 C 库函数的名称,因此该代码可能导致跳转到 exit 函数而非所需位置。
与在 MASM 程序中一样,美元符号 ($) 用作当前位置计数器。 它是当前组合的指令的标签。 在 __asm 块中,其主要用途是做长条件跳转:
jne $+5 ; next instruction is 5 bytes long jmp farlabel ; $+5 . . . farlabel: