12.1 内联汇编和嵌入型汇编的使用
内联汇编和嵌入型汇编是包含在C/C++编译器中的汇编器。使用它可以在C/C++程序中实现C/C++语言不能完成的一些工作。例如,在下面几种情况中必须使用内联汇编或嵌入型汇编。
· 程序中使用饱和算术运算(Saturating arithmetic),如SSAT16 和 USAT16指令。
· 程序中需要对协处理器进行操作。
· 在C或C++程序中完成对程序状态寄存器的操作。
使用内联汇编编写的程序代码效率也比较高。
12.1.1 内联汇编
1.内联汇编语法
内联汇编使用“_asm”(C++)和“asm”(C和C++)关键字声明,语法格式如下所示。
· __asm("instruction[;instruction]"); // 必须为单条指令
__asm{instruction[;instruction]}
· __asm{
...
instruction
...
}
· asm("instruction[;instruction]"); // 必须为单条指令
asm{instruction[;instruction]}
· asm{
...
instruction
...
}
内联汇编支持大部分的ARM指令,但不支持带状态转移的跳转指令,如BX和BLX指令,详见ARM相关文档。
由于内联汇编嵌入在C或C++程序中,所有在用法上有其自身的一些特点。
① 如果同一行中包含多条指令,则用分号隔开。
② 如果一条指令不能在一行中完成,使用反斜杠“/”将其连接。
③ 内联汇编中的注释语句可以使用C或C++风格的。
④ 汇编语言中使用逗号“,”作为指令操作数的分隔符,所以如果在C语言中使用逗号必须用圆括号括起来。如,__asm {ADD x, y, (f(), z)}。
⑤ 内联汇编语言中的寄存器名被编译器视为C或C++语言中的变量,所以内联汇编中出现的寄存器名不一定和同名的物理寄存器相对应。这些寄存器名在使用前必须声明,否则编译器将提示警告信息。
⑥ 内联汇编中的寄存器(除程序状态寄存器CPSR和SPSR外)在读取前必须先赋值,否则编译器将产生错误信息。下面的例子显示了内联汇编和真正汇编的区别。
错误的内联汇编函数如下所示。
int f(int x)
{
__asm
{
STMFD sp!, {r0} // 保存r0不合法,因为在读之前没有对寄存器写操作
ADD r0, x, 1
EOR x, r0, x
LDMFD sp!, {r0} // 不需要恢复寄存器
}
return x;
}
将其进行改写,使它符合内联汇编的语法规则。
int f(int x)
{
int r0;
__asm
{
ADD r0, x, 1
EOR x, r0, x
}
return x;
}
下面通过几个例子进一步了解内联汇编的语法。
① 字符串拷贝
下面的例子使用一个循环完成了字符串的拷贝工作。
#include <stdio.h>
void my_strcpy(const char *src, char *dst)
{
int ch;
__asm
{
loop:
LDRB ch, [src], #1
STRB ch, [dst], #1
CMP ch, #0
BNE loop
}
}
int main(void)
{
const char *a = "Hello world!";
char b[20];
my_strcpy (a, b);
printf("Original string: '%s'\n", a);
printf("Copied string: '%s'\n", b);
return 0;
}
② 中断使能
下面的例子通过读取程序状态寄存器CPSR并设置它的中断使能位bit[7]来禁止/打开中断。需要注意的是,该例只能运行在系统模式下,因为用户模式是无权修改程序状态寄存器的。
__inline void enable_IRQ(void)
{
int tmp;
__asm
{
MRS tmp, CPSR
BIC tmp, tmp, #0x80
MSR CPSR_c, tmp
}
}
__inline void disable_IRQ(void)
{
int tmp;
__asm
{
MRS tmp, CPSR
ORR tmp, tmp, #0x80
MSR CPSR_c, tmp
}
}
int main(void)
{
disable_IRQ();
enable_IRQ();
}
③ 分隔符的计算
下面的例子计算两个整数数组中分隔符“,”的个数。该例子显示了如何在内联汇编中访问C或C++语言中的数据类型。该例中的内联汇编函数mlal()被编译器优化为一条SMLAL指令,可以使用-S –interleave编译选项使编译器输出汇编结果。
#include <stdio.h>
/* change word order if big-endian */
#define lo64(a) (((unsigned*) &a)[0]) /* long long型的低32位 */
#define hi64(a) (((int*) &a)[1]) /* long long型的高32位 */
__inline __int64 mlal(__int64 sum, int a, int b)
{
#if !defined(__thumb) && defined(__TARGET_FEATURE_MULTIPLY)
__asm
{
SMLAL lo64(sum), hi64(sum), a, b
}
#else
sum += (__int64) a * (__int64) b;
#endif
return sum;
}
__int64 dotprod(int *a, int *b, unsigned n)
{
__int64 sum = 0;
do
sum = mlal(sum, *a++, *b++);
while (--n != 0);
return sum;
}
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int b[10] = { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
int main(void)
{
printf("Dotproduct %lld (should be %d)\n", dotprod(a, b, 10), 220);
return 0;
}
2.内联汇编中的限制
可以在内联汇编代码中执行的操作有许多限制。这些限制提供安全的方法,并确保在汇编代码中不违反 C 和 C++ 代码编译中的假设。
① 不能直接向程序计数器PC赋值。
② 内联汇编不支持标号变量。
③ 不能在程序中使用“.”或{PC}得到当前指令地址值。
④ 在16进制常量前加“0x”代替“&”。
⑤ 建议不要对堆栈进行操作。
⑥ 编译器可能会使用r12和r13寄存器存放编译的中间结果,在计算表达式值时可能会将寄存器r0~r3、r12及r14用于子程序调用。另外在内联汇编中设置程序状态寄存器CPSR中的标志位NZCV时,要特别小心,内联汇编中的设置很可能会和编译器计算的表达式的结果冲突。
⑦ 用内联汇编代码更改处理器模式是可能的。然而,更改处理器模式会禁止使用 C或 C++操作数或禁止对已编译C或C++代码的调用,直到将处理器模式更改回原设置之后之前的函数库才可正常使用。
⑧ 为Thumb状态编译C或C++时,内联汇编程序不可用且不汇编Thumb指令。
⑨ 尽管可以使用通用协处理器指令指定 VFP 或 FPA 指令,但内联汇编程序不为它们提供直接支持。
不能用内联汇编代码更改 VFP 向量模式。内联汇编可包含浮点表达式操作数,该操作数可使用编译程序生成的 VFP 代码求出操作数值。因此,仅由编译程序修改 VFP 状态很重要。
⑩ 内嵌汇编不支持的指令有BX、BLX、BXJ和BKPT指令。而LDM、STM、LDRD和STRD指令可能被等效为ARM LDR或STR指令。
3.内联汇编中的虚拟寄存器
内联汇编程序提供对 ARM 处理器物理寄存器的非直接访问。如果在内联汇编程序指令中将某个ARM寄存器用作操作数,它就成为相同名称的虚拟寄存器的引用,而不是对实际物理ARM寄存器的引用。例如内联汇编指令中使用了寄存器r0,但对于C编译器,指令中出现的r0只是一个变量,并非实际的物理寄存器r0,当程序运行时,可能是由物理寄存器r1来存放r0所代表的值。
下面的例子显示了编译器如何对内联汇编指令的寄存器进行分配。
程序的源代码如下。
#include <stdio.h>
void test_inline_register(void)
{
int i;
int r5,r6,r7;
__asm
{
MOV i,#0
loop:
MOV r5,#0
MOV r6,#0
MOV r7,#0
ADD i,i,#1
CMP i,#3
BNE loop
}
}
int main(void)
{
test_inline_register ();
printf("test inline register\n");
return 0;
}
由C编译器编译出的汇编码如下所示。
test_inline_register:
0000807C E3A00000 MOV r0,#0
>>> TEST_INLINE_REGISTER\#12 loop:
00008080 E1A00000 NOP
>>> TEST_INLINE_REGISTER\#13 MOV r5,#0
00008084 E3A01000 MOV r1,#0
>>> TEST_INLINE_REGISTER\#14 MOV r6,#0
00008088 E3A02000 MOV r2,#0
>>> TEST_INLINE_REGISTER\#15 MOV r7,#0
0000808C E3A03000 MOV r3,#0
>>> TEST_INLINE_REGISTER\#16 ADD i,i,#1
00008090 E2800001 ADD r0,r0,#1
>>> TEST_INLINE_REGISTER\#17 CMP i,#3
00008094 E3500003 CMP r0,#3
00008098 0A000000 BEQ 0x80a0 <TEST_INLINE_REGISTER\#21>
>>> TEST_INLINE_REGISTER\#18 BNE loop
0000809C EAFFFFF8 B 0x8084 <TEST_INLINE_REGISTER\#13>
>>> TEST_INLINE_REGISTER\#21 }
000080A0 E12FFF1E BX r14
>>> TEST_INLINE_REGISTER\#25 {
|
注意 |
下面的代码是由Realview2.2编译出的代码,使用其他编译器结果可能有差异。同一段内嵌汇编经过不同版本的编译器编译后,在指令里可能使用不一样的实际寄存器,但是只要遵循文档里的编码指导,执行的功能肯定相同。 |
例子中以“>>>”的开头的行是程序的源码部分,紧接其后的是由编译器编译出的汇编代码。从上例可以很清楚地看出,源程序中使用了r5、r6和r7,但由编译器编译后的代码使用了寄存器r1、r2和r3。
另外,需要特别指出的是在内联汇编中使用寄存器必须先声明其变量类型,如上例中的“int r5,r6,r7”。如果不在使用前进行声明,编译器将给出以下错误信息。
#1267-D: Implicit physical register R3 should be defined as a variable
编译程序定义的虚拟寄存器有函数局部作用范围,即在同一个C函数中,涉及相同虚拟寄存器名称的多个asm语句或声明,访问相同的虚拟寄存器。
内联汇编没有为pc(r15)、lr(r14)和sp(r13)寄存器创建虚拟寄存器,而且不能在内联汇编代码中读取或直接修改它们的值。如果内联汇编程序中出现了对这些寄存器的访问,编译器将给出以下错误消息。例如,如果指定r14:
#20: identifier "r14" is undefined
内联汇编可以直接使用CPSR和SPSR对程序状态字进行操作,因为内联汇编中不存在虚拟处理器状态寄存器(PSR)。任何对 PSR 的引用总是指向物理 PSR。
4.内联汇编中的指令展开
内联汇编代码中的ARM指令可能会在编译过程中扩展为几条指令。扩展取决于指令、指令中指定的操作数个数以及每个操作数的类型和值。通常,被扩展的指令有以下两种情况:
· 含有常数操作的指令;
· LDM、STM、LDRD 和 STRD指令;
· 乘法指令MUL被扩展为一系列的加法和移位指令。
下面的例子说明了编译器如何对含有常数操作的指令进行扩展。
包含有常数操作的加法指令:
ADD r0,r0,#1023
被编译器编译为如下两条指令:
ADD r0,r0,#1024
SUB r0,r0,#1
注意 |
扩展指令对程序状态寄存器CPSR的影响:算术指令影响相应的NZCV标准位;其他指令设置NZ标志位不影响V标志位。 |
所有的LDM和STM指令被扩展为等效的LDR和STR指令序列。然而,在优化过程中,编译程序可能因此将单独的指令重组为一条LDM或STM指令。
5.内联汇编中的常数
指令中的标志符“#”是可选的(前面的例子中,指令中常数前均加了标志符“#”)。如果在指令中使用了“#”,则其后的表达式必为常数。
6.内联汇编指令对标志位的影响
内联汇编指令可能显式或隐式地更新处理器程序状态寄存器的条件标志位。在仅包含虚拟寄存器操作数或简单表达式操作数的内联汇编中,其执行结果是可以预见。如果指令中指定了隐式或显式更新条件标志位,则条件标志位根据指令的执行进行设置。如果未指定更新,则条件标志不会更改。如果内嵌汇编指令的操作数都不是简单操作数时或指令不显式更新条件标志位,则条件标志位可能会被破坏。一般情况下,编译程序不易诊断出对条件标志的潜在破坏。然而,在构造析构C++临时函数的操作数时,如果指令试图更新条件标志,编译程序将给予警告,因为析构函数可能会破坏条件标志位。
7.内联汇编指令中的操作数
内联汇编指令中的操作数分为以下4种。
· 虚拟寄存器
· 表达式操作数
· 寄存器列表
· 中间操作数
(1)虚拟寄存器
在内联汇编指令中指定的寄存器表示虚拟寄存器而不是实际的物理寄存器。由编译器编译的汇编代码中使用的物理寄存器可能与在指令中指定的不同。每个虚拟寄存器的初值是不可预测的,必须在读取之前将初值写入虚拟寄存器。如果在写入之前试图读虚拟寄存器,编译程序会给予警告。
(2)表达式操作数
在内联汇编指令中,可将函数自变量、C或C++变量和其他C或C++表达式指定为寄存器操作数。用作操作数的表达式必须为整数类型,如char、short、int或long,(长整型long long除外)或指针类型。当表达式作为内联汇编指令的操作数时,编译器在编译时自动增加一段代码计算表示式的值并将其加载到指定的寄存器中。
注意 |
数据类型中除char和short(默认为无符号类型)外,其他均为有符号类型。 |
下面的例子显示了编译器如何处理内联汇编中的表达式操作数。
程序源代码如下所示。
/* Example Operands */
void my_operand(void)
{
int i,j,total;
__asm
{
mov i,#0
mov j,#1
add total,j,i+j
}
}
int main(void)
{
my_operand ();
}
由编译器编译出的汇编代码如下所示(其中只列出了内联汇编的一段代码)。
my_operand:
0000807C E3A01000 MOV r1,#0
>>> OPERANDS\#12 mov j,#1
00008080 E3A00001 MOV r0,#1
00008084 E0812000 ADD r2,r1,r0
>>> OPERANDS\#13 add total,j,i+j
00008088 E0803002 ADD r3,r0,r2
>>> OPERANDS\#15 }
0000808C E12FFF1E BX r14
>>> OPERANDS\#19 {
从编译的代码可以看出,编译器将“add total,j,i+j”分为两步来完成,用户在编写自己的内联汇编应用程序时要特别注意这一点。
包含多个表达式操作数的指令,没有指定表达式操作数求值的顺序。
将C或C++表达式用作内联汇编程序操作数,如果表达式的值不能满足 ARM指令中所要求的指令操作数约束条件,一条指令将被扩展为多条指令。
如果用作操作数的表达式创建需要析构的临时函数,析构将发生在执行内联汇编指令之后,这与C++析构临时函数的规则相类似。
简单表达式操作数包含以下几种类型。
· 变量值
· 变量地址
· 指针变量的反引用(the dereferencing of a point varable)
· 伪操作指定的程序常量
非简单表达式操作数包含以下几种类型。
· 隐式函数调用,如除法,或显式函数调用
· C++临时函数的构造
· 算术或逻辑操作
(3)寄存器列表
寄存器列表最多可包含 16 个操作数。这些操作数可以是虚拟寄存器或表达式操作数。在寄存器列表中指定虚拟寄存器和表达式操作数的顺序非常重要。寄存器列表中操作数的读写顺序是从左到右。第一个操作数使用最低地址,随后的操作数的地址依次在前一地址基础上增加 4。这一点与LDM 或 STM 指令的普通操作(编号最低的物理寄存器总是存入最低的存储器地址)是不同的。之所以存在这种区别是因为在内联汇编中使用的寄存器被编译器虚拟化了。
同一个表达式操作数或虚拟寄存器可以在寄存器列表中出现多次,重复使用。
如果表达式操作数或虚拟寄存器被指定为指令中的基址寄存器,表达式或虚拟寄存器的值按照ARM指令寻址方式进行更新。更新将覆盖原表达式或虚拟寄存器的值。
(4)中间操作数(Intermediate operands)
在内联汇编指令中,可能将C或C++整型常量表达式用作立即数处理。用于指定直接移位的常量表达式的值必须在ARM指令规定的移位操作数的范围内;用于为存储器或协处理器数据传送指令指定直接偏移量的常量表达式,必须符合ARM体系结构中的内存对齐标准。
8.函数调用和分支跳转
利用内联汇编程序的BL和SWI指令可在常规指令字段后指定3个可选列表。这些指令格式有以下几种。
SWI{cond} swi_num , { input_param_list }, { output_value_list }, { corrupt_reg_list }
BL{cond} function, { input_param_list }, { output_value_list }, { corrupt_reg_list }
其中,swi_num为SWI调用的中断号;function为被调用函数名;{input_param_list}为输入参数列表;{output_value_list}为输出参数列表;{corrupt_reg_list}为被破坏寄存器列表。
注意 |
内联汇编程序不支持BX、BLX和BXJ指令。不能在任何输入、输出或“被破坏的寄存器列表(corrupted register list)”中指定lr、sp或pc寄存器;任何SWI指令或函数调用不能更改sp寄存器。 |
下面分别详细介绍语法格式中各参数的使用。
(1)未指定任何列表
如果在SWI和BL指令后没指定任何列表,则有下面规则。
· r0~r3用作输入参数;
· r0 用于输出值;
· r12和r14的值将会被修改。
(2)输入参数列表
指令中的输入参数列表{ input_param_list }列出了传递给被调用函数function和SWI的参数。被传递的参数可以是表达式、变量或包含表达式或变量的物理寄存器。
内联汇编编译器在编译时增加一小段编译程序负责在函数和SWI调用前
将传递的参数载入特定的物理寄存器中。为确保与现有内联汇编代码的向后兼容性,程序中指定物理寄存器名称而并不对其赋值,使相同名称虚拟寄存器中的值出现在物理寄存器中。
例如,指令BL foo {r0=expression1, r1=expression2, r2}生成以下伪代码:
MOV (physical) r0, expression1
MOV (physical) r1, expression2
MOV (physical) r2, (virtual) r2
BL foo
(3)输出参数列表
输出参数列表{ output_value_list }列出了用来存放功能函数和SWI调用返回值的寄存器或表达式。列表中的值可以是物理寄存器、可修改长值表达式或单个物理寄存器名称。
内联汇编程序从特定的物理寄存器中取值并赋值到特定的表达式中。指定物理寄存器名称而并不赋值,导致相同名称虚拟寄存器被物理寄存器中的值更新。
例如,BL foo { }, {result1=r0, r1}生成以下伪码:
BL foo
MOV result1, (physical) r0
MOV (virtual) r1, (physical) r1
(4)被破坏的寄存器列表(Corrupted register list)
此列表指定被函数调用破坏的物理寄存器。如果条件标志被调用的函数修改,必须在被破坏的寄存器列表中指定PSR。
BL和SWI指令总是破坏lr。
如果指令中缺少此列表项,则r0~r3、ip、lr和PSR被破坏。
注意 |
指令BL和B的区别在于,跳转指令B只能使程序跳转到C或C++程序的一个地址标号,不能用于子程序调用。 |
9.内嵌汇编中的标号
内联汇编代码中定义的标号可被用作跳转或C和C++“goto”语句的目标。在内联汇编代码中,C和C++中定义的标号可被用作跳转指令的目标。
10.内嵌汇编器版本间的差异
不同版本的ARM编译器对内联汇编程序的语法要求有显著差异。在具体使用时请参见相关文档。
· 如果使用的是 ADS v1.2,请参阅 ADS 开发者指南;
· 如果使用的是 RVCT v1.2,请参阅 RealView 编译工具 1.2 版开发者指南。
12.1.2 嵌入式汇编
利用 ARM 编译器可将汇编代码包括到一个或多个C或C++函数定义中去。嵌入式汇编器提供对目标处理器不受限制的低级别访问,利用它可以使用C和C++预处理程序伪操作(preprocessor directive)并可以方便的使用偏移量访问结构成员。
本小节将介绍以下内容:
· 嵌入式汇编程序语法;
· 嵌入式汇编语句的限制;
· 嵌入式汇编程序表达式和C或C++表达式之间的差异;
· 嵌入式汇编函数的生成;
· __cpp 关键字;
· 手动重复解决方案;
· 相关基类的关键字;
· 成员函数类的关键字;
· 调用非静态成员函数。
有关为 ARM 处理器编写汇编语言的详细信息,请参阅ADS或RealView编译工具的汇编程序指南。
1.嵌入式汇编语言语法
嵌入式汇编函数定义由 --asm(C和C++)或asm(C++) 函数限定符标记,可用于:
· 成员函数;
· 非成员函数;
· 模板函数;
· 模板类成员函数。
用__asm或asm声明的函数可以有调用参数和返回类型。它们从C和C++中调用的方式与普通C和C++函数调用方式相同。嵌入式汇编函数语法是:
__asm return-type function-name(parameter-list)
{
// ARM/Thumb/Thumb-2 assembler code
instruction[;instruction]
...
[instruction]
}
嵌入式汇编的初始执行状态是在编译程序时由编译选项决定的。这些编译选项如下所示:
· 如果初始状态为ARM状态,则内嵌汇编器使用--arm选项;
· 如果初始状态为Thumb状态,则内嵌汇编器使用--thumb选项。
注意 |
嵌入式汇编的初始状态由编译器的编译选项确定,与程序中的#pragma arm 和 #pragma thumb伪操作无关。 |
可以显示地使用ARM、THUMB和CODE16伪操作改变嵌入式汇编的执行状态。关于ARM伪操作的详细信息请参加指令伪操作一节。如果使用的处理器支持Thumb-2指令,则可以在Thumb状态下,在嵌入式汇编中使用Thumb-2指令。
参数名允许用在参数列表中,但不能用在嵌入式汇编函数体内。例如,以下函数在函数体内使用整数i,但在汇编中无效:
__asm int f(int i) {
ADD i, i, #1 // 编译器报错
}
可以使用 r0 代替 i。
下面通过嵌入式汇编的例子,来进一步熟悉嵌入式汇编的使用。
下面的例子实现了字符串的拷贝,注意和上一节中内联汇编中字符串拷贝的例子相比较,分析其中的区别。
#include <stdio.h>
__asm void my_strcpy(const char *src, const char *dst) {
loop
LDRB r3, [r0], #1
STRB r3, [r1], #1
CMP r3, #0
BNE loop
MOV pc, lr
}
void main()
{
const char *a = "Hello world!";
char b[20];
my_strcpy (a, b);
printf("Original string: '%s'\n", a);
printf("Copied string: '%s'\n", b);
}
2.嵌入式汇编语言的使用限制
嵌入式汇编的使用有下面一些限制。
① 在预处理之后,__asm 函数只能包含汇编代码,但以下标识符除外:
· __cpp(expr);
· __offsetof_base(D, B);
· __mcall_is_virtual(D, f);
· __mcall_is_in_vbase(D, f);
· __mcall_this_offset(D, f);
· __vcall_offsetof_vfunc(D, f);
② 编译程序不为__asm 函数生成返回指令。如果要从 __asm 函数返回,必须将用汇编代码编写的返回指令包含到函数体内。由于嵌入式汇编执行__asm函数的顺序是在编译时定义好的,所有从一个内嵌汇编跳转到一个内嵌汇编程序是运行的,但在内联汇编中却不能实现。
③ __asm 函数调用遵循AAPCS规则。所以,即使在__asm 函数体内可用的汇编代码(例如,更改状态),在__asm函数和普通C或C++函数相互调用时,未必可用,因为此调用也必须遵循 AAPCS规则。
3.嵌入式汇编程序表达式和C或C++表达式之间的差异
嵌入式汇编表达式和C或C++表达式之间存在以下差异。
① 汇编程序表达式总是无符号的。相同的表达式在汇编程序和 C 或 C++ 中有不同值。例如:
MOV r0, #(-33554432 / 2) // 结果为 0x7f000000
MOV r0, #__cpp(-33554432 / 2) // 结果为 0xff000000
② 以0开头的汇编程序编码仍是十进制的。例如:
MOV r0, #0700 // 十进制 700
MOV r0, #__cpp(0700) // 八进制 0700 等于 十进制 448
③ 汇编程序运算符优先顺序与 C 和 C++ 不同。例如:
MOV r0, #(0x23 :AND: 0xf + 1) // ((0x23 & 0xf) + 1) => 4
MOV r0, #__cpp(0x23 & 0xf + 1) // (0x23 & (0xf + 1)) => 0
④ 汇编程序字符串不是以空字符为终止标志的:
DCB "no trailing null" // 16 bytes
DCB __cpp("I have a trailing null!!") // 25 bytes
注意 |
在_cpp标识符作用范围之内使用C或C++语法规则。 |
4.嵌入式汇编函数的生成
由关键字__asm声明的嵌入式汇编程序,在编译时将作为整个文件体传递给ARM汇编器。传递过程中,__asm函数的顺序保持不变(用模板实例生成的函数除外)。正是由于嵌入式汇编的这个特性,使得由一个__asm标识的嵌入式汇编程序调用在同一文件中的另一个嵌入式汇编程序是可以实现的。
当使用编译器 armcc 时,局部链接器(Partial Link)将汇编程序产生的目标文件与编译C程序的目标文件相结合,产生单个目标文件。
编译程序为每个 __asm 函数生成AREA命令。例如,以下__asm函数:
#include <cstddef>
struct X { int x,y; void addto_y(int); };
__asm void X::addto_y(int) {
LDR r2,[r0, #__cpp(offsetof(X, y))]
ADD r1,r2,r1
STR r1,[r0, #__cpp(offsetof(X, y))]
BX lr
}
对于此函数,编译程序生成:
AREA ||.emb_text||, CODE, READONLY
EXPORT |_ZN1X7addto_yEi|
#line num "file"
|_ZN1X7addto_yEi| PROC
LDR r2,[r0, #4]
ADD r1,r2,r1
STR r1,[r0, #4]
BX lr
ENDP
END
由上面的例子可以看出,对于变量offsetof的使用必须加__cpp()标识符才能引用,因为该变量是在cstddef头文件中定义的。
由__asm声明的常规函数被放在名为.emb_text的段(Section)中。这一点也是嵌入式汇编和内联汇编最大的不同。相反,隐式实例模板函数(Implicitly Instantiated Template Function)和内联汇编函数放在与函数名同名的区域(Area)内,并为该区域增加公共属性。这就确保了这类函数的特殊语义得以保持。
由于内联和模板函数的区域的特殊命名,所以这些函数不按照文件中定义的顺序排列,而是任意排序。因此,不能以__asm函数在原文件中的排列顺序,来判断它们的执行顺序,也就是说,即使两个连续排列的__asm函数,也不一定能顺序执行。
5.关键字__cpp
可用__cpp关键字从汇编代码中访问C或C++的编译时常量表达式,其中包括含有外部链接的数据或函数地址。标识符__cpp内的表达式必须是适合用作C++静态初始化的常量表达式(请参阅ISO/IEC 14882:1998中的3.6.2非本地对象初始化一节和本书的常量表达式一节)。
编译时,编译器将使用__cpp(expr) 的地方用汇编程序可以使用的常量所取代。例如:
LDR r0, =__cpp(&some_variable)
LDR r1, =__cpp(some_function)
BL __cpp(some_function)
MOV r0, #__cpp(some_constant_expr)
__cpp表达式中的名称可在__asm函数的C++上下文中查阅。__cpp表达式结果中的任何名称按照要求被损毁并自动为其生成IMPORT语句。
6.手动重复解决方案
可以在嵌入式汇编中使用C++转换为非虚拟函数调用解决重复。例如:
void g(int);
void g(long);
struct T {
int mf(int);
int mf(int,int);
};
__asm void f(T*, int, int) {
BL __cpp(static_cast<int (T::*)(int, int)>(&T::mf)) // calls T::mf(int, int)
BL __cpp(static_cast<void (*)(int)>(g)) // calls g(int)
MOV pc, lr
}
7.相关基类的关键字
利用以下关键字可以确定从对象起始处到其相关基类的偏移量:
__offsetof_base(D, B)
其中,B必须是D的非虚拟基类。
该函数返回从D对象的起始处到其中B基子对象的起始处的偏移量。结果可能是零。必须将偏移量(以字节为单位)添加到D* p来执行。
static_cast<B*>(p) 的等效功能,如下程序段所示:
__asm B* my_static_base_cast(D* /*p*/) {
if __offsetof_base(D, B) <> 0 //排除偏移量为0的情况
ADD r0, r0, #__offsetof_base(D, B)
endif
MOV pc, lr
}
在汇编程序源代码中,这些关键字被转换为整数或逻辑常量。只能将它们用于__asm函数,而不能用于__cpp表达式。
8.成员函数类的关键字
以下关键字方便了从__asm函数中调用虚拟或非虚拟成员函数。以__mcall开头的关键字可用于虚拟和非虚拟函数。以__vcall开头的关键字仅能用于虚拟函数。在调用静态成员函数的过程中,这些关键字没有特别的作用。
下面详细介绍这些关键字的使用。
① __mcall_is_virtual(D, f)
如果f是D中的虚拟成员函数或是D的基类,结果是{TRUE},否则结果是{FALSE}。如果返回{TRUE},可用虚拟调度进行调用,否则必须直接进行调用。
② __mcall_is_in_vbase(D, f)
如果f是D虚拟基类中的非静态成员函数,结果是{TRUE},否则结果是{FALSE}。如果返回{TRUE},必须用__mcall_offsetof_vbaseptr(D, f)进行this调整,否则必须用__mcall_this_
offset(D, f)进行调整。
③ __mcall_this_offset(D, f)
其中D是类,f是D中定义的非静态成员函数或是D的非虚拟基类。该函数返回从D对象的起始处到定义f的基的起始处的偏移量。在用指向D的指针调用f的过程中,这是必要的this调整。返回值在D中可找到f时为零,或者与__offsetof_base(D, B)相同,其中B为包含f的D非虚拟基类。在D的虚拟基类中找到f时,如果使用__mcall_this_offset(D, f),则返回任意值,在程序中使用该返回值,汇编器将报告__mcall_this_offset无效使用的错误。
④ __vcall_offsetof_vfunc(D, f)
其中D是类,f是D中定义的虚拟函数或是D的基类。将偏移量返回到虚拟函数表,在该表中可以找到从虚拟函数表到虚拟函数的偏移量。在f不是虚拟成员函数时,如果使用__vcall_offsetof_vfunc(D, f),则返回任意值,而在设计上使用该值时会导致汇编错误。
9.调用非静态成员函数
本小节列出了可以从 __asm 函数中调用虚拟或非虚拟函数的关键字。静态成员函数的参数不相同(没有 this),使得检测静态成员函数的关键字__mcall_is_static不可用,因此调用位置很可能已经专用于调用静态成员函数。
(1)调用非虚拟成员函数
例如,在虚拟基(virtual base)或非虚拟基(non-virtual base)中,以下代码可用于调用虚拟函数:
// rp包含指向D的指针,该程序的功能是实现在使用rp时调用D的非虚成员函数f
// 所有参数准备好
// 假设并不返回一个结构类型
if __mcall_is_in_vbase(D, f)
ASSERT {FALSE} // can't access virtual base
else
MOV r0, rp //使用指向D的指针rp*
ADD r0, r0, #__mcall_this_offset(D, f) //地址调整
endif
BL __cpp(&D::f)
(2)调用虚拟成员函数
例如,在虚拟或非虚拟基中,以下代码可用于调用虚拟函数:
// rp包含指向D的指针,该程序的功能是在使用rp时调用D的虚拟函数f
// 所有参数准备好
// 假如函数并不返回一个结构类型
if __mcall_is_in_vbase(D, f)
ASSERT {FALSE} // 不能调用虚拟基
else
MOV r0, rp // 使用指向D的指针rp
LDR r12, [rp] // 加载vtable表结构指针
ADD r0, r0, #__mcall_this_offset(D, f) // 地址调整
endif
MOV lr, pc // 保存返回地址到lr
LDR pc, [r12, #__vcall_offsetof_vfunc(D, f)] // 调用函数rp→f()
10.嵌入式汇编版本间的差异
不同版本的ARM编译器对嵌入式汇编程序的语法要求会有所差异。在具体使用时请参见相关文档。
值得注意的是,目前的嵌入式汇编器已经完全支持ARMv6指令集,也就是说可以在嵌入式汇编中使用ARMv6指令集中的指令。
12.1.3 内联汇编中使用SP、LR和PC寄存器的遗留问题
虽然目前的编译器不支持在内联汇编中使用SP、LR和PC寄存器,但在RVCT v1.2及其以前的编译器版本中是允许的。下面的例子显示了使用早期编译器版本,在内联汇编中使用LR寄存器的例子。
void func()
{
int var;
__asm
{
mov var, lr /* 得到func()函数的返回地址 */
}
}
如果使用RVCT v2.0编译器编译上面的代码,编译器将报告以下错误。
Error: #20: identifier "lr" is undefined
使用RVCT v2.0版本及其以后的编译器,要在C或C++代码中使用汇编访问SP、LR和PC寄存器可以使用下面几种方法。
① 使用嵌入式汇编代码。嵌入式汇编支持所有的ARM指令,同时允许在代码中访问SP、LR和PC寄存器。
② 在内联汇编中使用以下一些指令。
· __current_pc():访问PC寄存器。
· __current_sp():访问SP寄存器。
· __return_address():访问LR,返回地址寄存器。
下面给出了两个访问SP、LR和PC寄存器的典型实例程序。
① 使用编译器给定的指令。
void printReg()
{
unsigned int spReg, lrReg, pcReg;
__asm {
MOV spReg, __current_sp()
MOV pcReg, __current_pc()
MOV lrReg, __return_address()
}
printf("SP = 0x%X\n",spReg);
printf("PC = 0x%X\n",pcReg);
printf("LR = 0x%X\n",lrReg);
}
② 使用嵌入式汇编。
__asm void func()
{
MOV r0, lr
...
BX lr
}
使用嵌入式汇编可以使用调试器捕获程序的返回地址。
12.1.4 内联汇编代码与嵌入式汇编代码之间的差异
本节总结了内联汇编和嵌入式汇编在编译方法上存在的差异:
· 内联汇编代码使用高级处理器抽象,并在代码生成过程中与C和C++代码集成。因此,编译程序将C和C++代码与汇编代码一起进行优化。
· 与内联汇编代码不同,嵌入式汇编代码从C和C++代码中分离出来单独进行汇编,产生与C和C++源代码编译对象相结合的编译对象。
· 可通过编译程序来内联内联汇编代码,但无论是显式还是隐式,都无法内联嵌入式汇编代码。
表12.1总结了内联汇编程序与嵌入式汇编程序之间的主要差异。
表12.1 内联汇编程序与嵌入式汇编程序之间的主要差异
功 能 |
嵌入式汇编程序 |
内联汇编程序 |
指令集 |
ARM和Thumb |
仅支持ARM |
ARM汇编指令伪操作 |
支持 |
不支持 |
ARMv6指令集 |
支持 |
仅支持媒体指令 |
C/C++表达式 |
只支持常数表达式 |
完全支持 |
汇编代码是否优化 |
无优化 |
完全优化 |
能否被内联(Inling) |
不可能 |
有可能被内联 |
续表
功 能 |
嵌入式汇编程序 |
内联汇编程序 |
寄存器访问 |
使用指定的物理寄存器,还可以使用PC、LR和SP |
使用虚拟寄存器。不能使用PC、LR和SP寄存器 |
是否自动产生返回指令 |
手工添加返回指令 |
指定产生(但不支持BX、BXJ和BLX指令) |
是否支持BKPT指令 |
不直接支持 |
不支持 |