好记性不如铅笔头

ARM, 操作系统

《RealView编译工具编译器用户指南》摘录:内联汇编器

备注:

1 本部分文档摘录自ARM网站【 http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0348bc/CJAHICFI.html 】,版权归属于ARM。

2 作者只摘录了部分内容,全面详细的内容清参考ARM网址。

7.1 内联汇编器

7.1. 内联汇编器

ARM 编译器提供了内联汇编器,利用内联汇编器可以编写优化的汇编语言例程,并可使用不能从 C 或 C++ 获得的目标处理器功能。

7.1.1. 内联汇编器支持

内联汇编器只支持 ARM 汇编语言,而不支持以下语言和指令:
    Thumb 汇编语言
    Thumb-2 汇编语言
    ARMv7 指令
    VFP 指令
    NEON 指令。
您可使用嵌入式汇编器来支持 Thumb 和 Thumb-2

PS:作者尝试了以下,发现cortexM3/M4也可以使用内联汇编,但是两个MCU都不支持ARM,仅供参考。

内联汇编器支持大多数 ARMv6 指令,包括完整的 ARMv6 SIMD 指令集。内联汇编器不支持的 ARMv6 指令为 SETEND 和一些系统扩展。
内联汇编器支持大部分 ARMv5 指令,包括通用协处理器指令。内联汇编器不支持的 ARMv5 指令为 BX、BLX 和 BXJ。

7.1.2. 内联汇编器语法

ARM 编译器支持 asm 关键字 (C++) 或 __asm 关键字(C 和 C++)引入的一种扩展内联汇编器语法。以下各节介绍了这些关键字的语法:
    含有 __asm 关键字的内联汇编
    含有 asm 关键字的内联汇编
    使用 __asm 和 asm 的规则
在任何语句位置,都可以使用 asm 或 __asm 语句。
含有 __asm 关键字的内联汇编
内联汇编器使用汇编器说明符进行调用,后面跟用大括号或括号括起来的汇编器指令列表。您可以指定使用以下格式的内联汇编器代码:
    在单行中,示例如下:
    __asm(“instruction[;instruction]”); // Must be a single string
    __asm{instruction[;instruction]}
    不能包括注释。
    在多行中,示例如下:
    __asm
    {
        …
        instruction
        …
    }
    在内联汇编语言块中的任何位置,都可以使用 C 或 C++ 注释。
另请参阅使用 __asm 和 asm 的规则。
含有 asm 关键字的内联汇编
编译 C++ 时,ARM 编译器支持 ISO C++ 标准中建议的 asm 语法。您可以指定使用以下格式的内联汇编器代码:
    在单行中,示例如下:
    asm(“instruction[;instruction]”); // Must be a single string
    asm{instruction[;instruction]}
    不能包括注释。
    在多行中,示例如下:
    asm
    {
        …
        instruction
        …
    }
    在内联汇编语言块中的任何位置,都可以使用 C 或 C++ 注释。
使用 __asm 和 asm 的规则
使用 __asm 和 asm 关键字时,请遵循以下规则:
    如果在同一行中包括多个指令,则必须用分号 (;) 进行分隔。如果使用双引号,则必须将所有指令包含在一对双引号 (“) 内。
    如果一条指令需要占用多行,则必须用反斜杠符号 (\) 指定后续行。
    对于多行格式,可以在内联汇编语言块中的任何位置使用 C 或 C++ 注释。但当一行中包含多个指令时,不能嵌入注释。
    在汇编语言中,逗号 (,) 用作分隔符,因此使用逗号运算符的 C 表达式必须括在括号中,以区分二者:
    __asm
    {
        ADD x, y, (f(), z)
    }
    asm 语句必须位于 C++ 函数内。可在任何应该出现 C++ 语句的地方使用 asm 语句。
    在内联汇编器中,寄存器名视为 C 或 C++ 变量。它们不一定与相同名称的物理寄存器相关(请参阅虚拟寄存器)。如果未将寄存器声明为 C 或 C++ 变量,编译器将生成一个警告。
  在内联汇编器中不要保存和恢复寄存器。编译器会为您完成此操作。此外,内联汇编器不提供对物理寄存器的直接访问。请参阅虚拟寄存器。
    如果未向寄存器写入内容就读取寄存器(CPSR 和 SPSR 除外),则会发出一条错误消息。例如:
    int f(int x)
    {
        __asm
        {
            STMFD sp!, {r0}    // save r0 – illegal: read before write
            ADD r0, x, 1
            EOR x, r0, x
            LDMFD sp!, {r0}    // restore r0 – not needed.
        }
        return x;
    }
    该函数必须按以下形式编写:
    int f(int x)
    {
        int r0;
        __asm
        {
            ADD r0, x, 1
            EOR x, r0, x
        }
        return x;
    }
请参阅内联汇编操作的限制。

7.1.3. 内联汇编操作的限制

可在内联汇编代码中执行的操作存在许多限制。这些限制提供了安全的方法,并确保在汇编代码中不违反已编译的 C 和 C++ 代码中的假设。
其他限制
内联汇编器具有以下限制:
    内联汇编器是一种高级汇编器,它生成的代码可能不总是与您编写的代码完全一致。不能用它来生成比编译器生成的代码更有效的代码。应当使用嵌入式汇编器或 ARM 汇编器 armasm 来实现此目的。
    不支持 ARM 汇编器 armasm 中提供的某些低级功能,如跳转和写入 PC。
    不支持标签表达式。
    不能使用点表示法 (.)或 {PC} 获取当前指令的地址。
    不能使用 & 运算符表示十六进制常数。应改为使用 0x 前缀。例如:
    __asm { AND x, y, 0xF00 }
    用于指定 8 位常数实际循环的表示法在内联汇编语言中不能使用。这意味着在使用 8 位移位常数时,如果更新了 NZCV 标记,C 标记必须视为已破坏。
    不得修改堆栈。这是没有必要的,因为编译器会根据需要自动堆叠和恢复任何工作寄存器。编译器不允许显式堆叠和恢复工作寄存器。
寄存器
必须小心使用寄存器(如 r0-r3、sp 和 lr)和 CPSR 中的 NZCV 标记。如果使用 C 或 C++ 表达式,这些寄存器可能被用作临时寄存器,并且 NZCV 标记可能在计算表达式时被编译器破坏。请参阅虚拟寄存器。
因为不能直接访问任何物理寄存器,所以无法用内联汇编代码显式读取或修改 pc、lr 和 sp。不过,您可以使用《编译器参考指南》 中介绍的以下内在函数访问这些寄存器:
    第 4-74 页的__current_pc
    第 4-74 页的__current_sp
    第 4-89 页的__return_address
处理器模式
可以更改处理器模式或修改协处理器状态,但编译器无法识别这些更改。如果更改处理器模式,则直到改回到原模式后才能使用 C 或 C++ 表达式,否则编译器将破坏新处理器模式的寄存器。
同样,如果通过执行浮点指令更改了浮点协处理器状态,则直到恢复原状态后才能使用浮点表达式。
Thumb 指令集
为 Thumb 状态编译 C 或 C++ 时,内联汇编器不可用且不汇编 Thumb 指令。相反,编译器会自动切换至 ARM 状态。
如果要在包含为 Thumb 编译的代码的源文件中包括内联汇编,则在 #pragma arm 和 #pragma thumb 语句之间包括含有内联汇编器代码的函数。例如:
…         // Thumb code
#pragma arm // ARM code. Switch code generation to the ARM instruction set so
            // that the inline assembler is available.
int add(int i, int j)
{
    int res;
    __asm
    {
        ADD   res, i, j   // add here
    }
    return res;
}
#pragma thumb   // Thumb code. Switch back to the Thumb instruction set.
                // The inline assembler is no longer available.
还必须使用 –apcs /interwork 编译器选项编译代码。
请参阅:
    交互操作限定符
    《编译器参考指南》 中第 4-55 页的编译指示。
VFP 协处理器
内联汇编器不提供对 VFP 指令的直接支持。不过,可以使用通用协处理器指令指定这些指令。
不得使用内联汇编代码更改 VFP 向量模式。内联汇编可能包含可使用编译器生成的 VFP 代码进行计算的浮点表达式操作数。因此,仅编译器可修改 VFP 状态是非常重要的。
不支持的指令
内联汇编器不支持以下指令:
    BKPT    、BX    、BXJ 和 BLX    指令
    Note
    可以使用 __breakpoint() 内在函数,在 C 和 C++ 代码中插入 BKPT 指令。
    LDR Rn, =expression 伪指令。应改用 MOV Rn, expression。(这可以生成从文字池进行的加载操作。)
    LDRT、LDRBT、STRT 和 STRBT 指令
    MUL、MLA、UMULL、UMLAL、SMULL 和 SMLAL 标记设置指令
    MOV 或 MVN 标记设置指令,其中第二个操作数为常数
    用户模式 LDM 指令
    ADR 和 ADRL 伪指令。  

7.1.4. 虚拟寄存器

内联汇编器不提供对 ARM 处理器物理寄存器的直接访问。如果在内联汇编器中将 ARM 寄存器名称用作操作数,它就成为对具有相同名称的虚拟寄存器的引用,而不是对物理 ARM 寄存器的引用。
在优化和代码生成过程中,编译器给每个虚拟寄存器分配相应的物理寄存器。不过,汇编代码中使用的物理寄存器可能与在指令中指定的不同。可将这些虚拟寄存器显式定义为标准 C 或 C++ 变量。如果这些虚拟寄存器未定义,编译器会为它们提供隐式定义。
编译器定义的虚拟寄存器具有函数局部范围,即在单个函数内,引用同一虚拟寄存器名称的多个 asm 语句或声明访问的是同一个虚拟寄存器。
没有为 sp (r13)、lr (r14) 和 pc (r15) 寄存器创建虚拟寄存器,而且不能在内联汇编代码中读取或直接修改它们。有关如何修改源代码的信息,请参阅访问 sp、lr 或 pc 的旧内联汇编器。
不存在虚拟处理器状态寄存器 (PSR)。任何对 PSR 的引用总是指向物理 PSR。
符合以前所述准则的现有内联汇编器代码继续执行与编译器先前版本相同的功能,但在每个指令中使用的实际寄存器可能不同。
每个虚拟寄存器中的初值是不可预测的。必须在读之前向虚拟寄存器写入初值。如果在写入之前试图读取虚拟寄存器(例如试图读取与变量 r1 相关联的虚拟寄存器),编译器将会生成错误。
您还必须在 C 或 C++ 代码中明确声明变量的名称。最好将 C 或 C++ 变量用作指令操作数。在第一次使用虚拟或物理寄存器名称时编译器会生成一个警告,每个转换单元仅生成一次。例如,如果指定寄存器 r3,则将显示一个警告。

7.1.5. 常数

常数表达式说明符 # 是可选的。如果使用了它,其后的表达式必须是常数。

7.1.6. 指令扩展

内联汇编代码中的 ARM 指令可能会在编译对象中扩展为几条指令。扩展取决于指令、指令中指定的操作数个数以及每个操作数的类型和值。
使用常数的指令
带有常数操作数的指令中的常数不仅仅局限于该指令所允许的值。相反,编译器会将指令转换为具有相同效果的指令序列。例如:
ADD r0,r0,#1023
可能解释为:
ADD r0,r0,#1024
SUB r0,r0,#1
除了协处理器指令之外,所有带有常数操作数的 ARM 指令都支持指令扩展。此外,当 MUL 指令的第三个操作数是常数时,该指令可以扩展为一系列加法和移位指令。
使用扩展指令更新 CPSR 的效果是:
    算法指令正确设置 NZCV 标记
    逻辑指令:
        正确设置 NZ 标记
        不改变 V 标记
        破坏 C 标记。
加载和存储指令
LDM、STM、LDRD 和 STRD 指令可替换为等效的 ARM 指令。在这种情况下,编译器输出一条警告消息,通知您它可能扩展指令。
编写内联汇编代码的方式必须是不依赖于期望的指令条数或每条指定指令期望的执行时间。
通常约束操作数寄存器对的指令,如 LDRD 和 STRD,被具有等效功能而并无约束的指令序列替换。不过,这些指令可重新组合成 LDRD 和 STRD 指令。
所有的 LDM 和 STM 指令被扩展为等效的 LDR 和 STR 指令序列。不过,在优化过程中,编译器可能因此将单独的指令重新组合为一条 LDM 或 STM 指令。

7.1.7. 条件标记

内联汇编指令可能显式或隐式地试图更新处理器条件标记。有些内联汇编指令仅包含虚拟寄存器操作数或简单表达式操作数(请参阅操作数),其行为可以预见。如果指定了隐式或显式更新,则由指令设置条件标记。如果未指定更新,则条件标记不会更改。指令操作数中的任何一个都不是简单操作数时,除非指令更新条件标记,否则条件标记可能会被破坏。一般情况下,编译器不易诊断出对条件标记的潜在破坏。不过,对于需要构造随后析构 C++ 临时函数的操作数,如果指令试图更新条件标记,编译器将显示警告。这是因为析构可能会破坏条件标记。

7.1.8. 操作数

操作数可以是多种类型之一:
虚拟寄存器
在内联汇编指令中指定的寄存器总表示虚拟寄存器而不是物理 ARM 整数寄存器。虚拟寄存器不需要声明,其大小与物理寄存器相同。不过,汇编代码中使用的物理寄存器可能与在指令中指定的不同。请参阅虚拟寄存器。
表达式操作数
在内联汇编指令中,可将函数参数、C 或 C++ 变量和其他 C 或 C++ 表达式指定为寄存器操作数。
代替 ARM 整数寄存器的表达式类型必须为除 long long 之外的整数类型(即 char、short、int 或 long)或指针类型。对 char 或 short 类型不执行符号扩展。您必须对这些类型执行显式符号扩展。编译器可能会添加代码,以计算这些表达式并将它们分配给寄存器。
当操作数用作目标时,如果在修改寄存器的位置将表达式用作操作数,则表达式必须为可修改的左值。例如,基址寄存器的目标寄存器或基址寄存器发生更新。
对于包含多个表达式操作数的指令,没有指定表达式操作数求值的顺序。
只有满足了指令的条件,才能对条件指令的表达式操作数求值。
将 C 或 C++ 表达式用作内联汇编器操作数,可能会导致一条指令被扩展为多条指令。如果表达式的值不能满足《ARM 体系结构参考手册》 中阐明的指令操作数约束,就会发生这种情况。
如果用作操作数的表达式创建需要析构的临时函数,则析构发生在执行内联汇编指令之后。这与 C++ 析构临时函数的规则相类似。
简单表达式操作数是以下类型之一:
    变量值
    变量地址
    指针变量的反引用
    编译时常数。
包含以下类型之一的任何表达式都不是简单表达式操作数:
    隐式函数调用,如除法,或显式函数调用
    C++ 临时函数的构造
    算术或逻辑运算。
寄存器列表
寄存器列表最多可包含 16 个操作数。这些操作数可以是虚拟寄存器或表达式寄存器操作数。
在寄存器列表中指定虚拟寄存器和表达式操作数的顺序很重要。寄存器列表中操作数的读写顺序是从左到右。第一个操作数使用最低地址,随后的操作数的地址依次在前一地址基础上增加 4。在 LDM 或 STM 指令的普通操作中,编号最低的物理寄存器总是存入最低的内存地址,此新行为与之相反。这个行为上的区别是寄存器虚拟化的结果。
表达式操作数或虚拟寄存器可以在寄存器列表中出现多次,并且在每次指定后使用。
如果指定基址寄存器,则进行更新。在内存加载操作过程中,更新操作将改写加载到基址寄存器的所有值。
在特权模式下,内联汇编器不支持通过在寄存器列表后指定 ^ 来对用户模式下的寄存器进行操作。
中间操作数
在内联汇编指令中,可能将 C 或 C++ 整型常数表达式用作立即值。
用于指定立即数移位的常数表达式的值必须位于《ARM 体系结构参考手册》 中定义的适用于移位操作的范围内。
用于为内存或协处理器数据传送指令指定直接偏移量的常数表达式,必须有一个适当对齐的值。

7.1.9. 函数调用和跳转

利用内联汇编器的 BL 和 SVC 指令,可在常规指令字段后指定 3 个可选列表。这些指令的格式如下:
SVC{cond} svc_num, {input_param_list}, {output_value_list}, {corrupt_reg_list}
BL{cond} function, {input_param_list}, {output_value_list}, {corrupt_reg_list}
Note
SVC 指令以前的名称是 SWI。内联汇编器仍然接受用 SWI 代替 SVC。
以下各节介绍这些列表:
    未指定任何列表
    输入参数列表
    输出值列表
    被破坏的寄存器的列表
Note
    内联汇编器不支持 BX、BLX 和 BXJ 指令。
    不能在任何输入、输出或被破坏的寄存器列表中指定 lr、sp 或 pc 寄存器。
    任何 SVC 指令或函数调用不能更改 sp 寄存器。
未指定任何列表
如果未指定任何列表,则:
    将 r0-r3 用作输入参数
    将 r0 用于输出值
    r12 和 r14 可能损坏。
输入参数列表
此列表指定作为函数调用或 SVC 指令的输入参数的表达式或变量,以及包含表达式或变量的物理寄存器。它们被指定为对物理寄存器的赋值或物理寄存器名称。单一列表中可包含输入寄存器规范的两种类型。
内联汇编器确保在输入 BL 或 SVC 指令之前,指定的物理寄存器中存在正确的值。指定物理寄存器名称而并不赋值,这样确保物理寄存器中可存在相同名称的虚拟寄存器中的值。这确保了与现有内联汇编器代码的向后兼容性。
例如,指令:
BL foo, { r0=expression1, r1=expression2, r2 }
生成以下伪代码:
MOV (physical) r0, expression1
MOV (physical) r1, expression2
MOV (physical) r2, (virtual) r2
BL foo
输出值列表
此列表指定的物理寄存器包含来自 BL 或 SVC 的输出值以及这些输出值必须存储到的目标位置。输出值被指定为从物理寄存器到可修改左值表达式的赋值,或被指定为单个物理寄存器名称。
内联汇编器从指定的物理寄存器中取值并赋值到指定的表达式中。指定物理寄存器名称而并不赋值,会导致相同名称的虚拟寄存器被物理寄存器中的值更新。
例如,指令:
BL foo, { }, { result1=r0, r1 }
生成以下伪代码:
BL foo
MOV result1, (physical) r0
MOV (virtual) r1, (physical) r1
被破坏的寄存器的列表
此列表指定被所调用函数破坏的物理寄存器。如果条件标记被调用的函数修改,则必须在被破坏的寄存器的列表中指定 PSR。
BL 和 SVC 指令总是破坏 lr。
如果此列表被忽略,则对于 BL 和 SVC,寄存器 r0-r3、pc、lr 和 PSR 被破坏。
跳转指令 B 只能用于跳转到单个 C 或 C++ 函数内的标签。

 7.1.10. 标签

内联汇编代码中定义的标签可用作跳转或 C 和 C++ goto 语句的目标。在内联汇编代码中,C 和 C++ 中定义的标签可通过以下形式用作跳转指令的目标:
BL{cond} label

7.1.11. 与先前版本 ARM C/C++ 编译器的差异

ARM 编译器与先前版本 ARM C 和 C++ 编译器中的内联汇编器有显著差异。本节着重介绍两者的主要差异。有关将现有汇编代码用于内联汇编器的详细信息,请参阅《RealView 编译工具开发指南》。
ARMv6 指令
在所有 ARMv6 指令中,内联汇编器只支持 ARMv6 媒体指令。
虚拟寄存器
编译器的内联汇编代码总是指定虚拟寄存器。编译器选择在代码生成过程中用于每条指令的物理寄存器,并让编译器完全优化汇编代码和周围的 C 或 C++ 代码。
pc (r15)、lr (r14) 和 sp (r13) 寄存器根本不能访问。访问这些寄存器时,会产生错误消息。
虚拟寄存器的初值未定义。因此,必须在读之前先写入虚拟寄存器。如果代码在写入之前读取虚拟寄存器,编译器将发出警告。如果内联汇编代码起始处出现依赖于物理寄存器中特定值的旧代码,编译器也会生成这些警告,例如:
int add(int i, int j)
{
    int res;
    __asm
    {
        ADD res, r0, r1   // relies on i passed in r0 and j passed in r1
    }
    return res;
}
此代码生成警告和错误消息。
这是由于它在写入虚拟寄存器 r0 和 r1 之前读取它们,因而生成了错误消息。生成警告消息是因为 r0 和 r1 必须定义为 C 或 C++ 变量。改正的代码是:
int add(int i, int j)
{
    int res;
    __asm
    {
            ADD res, i, j
    }
    return res;
}
指令扩展
编译器中的内联汇编器将指令 LDM、STM、LDRD 和 STRD 扩展为执行等效功能的单寄存器内存操作的序列。
编译器可能优化单寄存器内存操作指令序列,回到多寄存器内存操作。
寄存器列表
LDM 或 STM 指令的寄存器列表中操作数的顺序很重要。它们按给定的顺序依次使用,即从左到右,并且第一个操作数引用生成的最低内存地址。这与先前编译器的行为相反,在先前编译器行为中,编号最低的寄存器总是引用由指令生成的最低内存地址。
因为现在可以在寄存器列表中与虚拟寄存器一起使用表达式操作数,所以会发生这种变化。如果编译器遇到仅包含虚拟寄存器的寄存器列表,而在此列表中新的排序结果与先前版本的 ARM C 和 C++ 编译器有所不同,则会显示警告消息。
Thumb 指令
编译器中的内联汇编器不支持 Thumb® 指令集。除非使用 #pragma arm 和 #pragma thumb 编译指示,否则内联汇编器不汇编 Thumb 指令;而且在为 Thumb 状态编译 C 或 C++ 时,内联汇编器根本不能使用(请参阅Thumb 指令集)。

Leave a Reply

4 × 1 =

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据