当前位置:首页 > 嵌入式 > 嵌入式教程
[导读]内联汇编和嵌入型汇编是包含在C/C++编译器中的汇编器。使用它可以在C/C++程序中实现C/C++语言不能完成的一些工作。例如,在下面几种情况中必须使用内联汇编或嵌入型汇编。

12.1内联汇编和嵌入型汇编的使用

内联汇编和嵌入型汇编是包含在C' target='_blank' style='cursor:pointer;color:#D05C38;text-decoration:underline;'>C/C++编译器中的汇编器。使用它可以在C/C++程序中实现C/C++语言不能完成的一些工作。例如,在下面几种情况中必须使用内联汇编或嵌入型汇编。

·程序中使用饱和算术运算(Saturatingarithmetic),如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{ADDx,y,(f(),z)}。

⑤内联汇编语言中的寄存器名被编译器视为C或C++语言中的变量,所以内联汇编中出现的寄存器名不一定和同名的物理寄存器相对应。这些寄存器名在使用前必须声明,否则编译器将提示警告信息。

⑥内联汇编中的寄存器(除程序状态寄存器CPSR和SPSR外)在读取前必须先赋值,否则编译器将产生错误信息。下面的例子显示了内联汇编和真正汇编的区别。

错误的内联汇编函数如下所示。

intf(intx)

{

__asm

{

STMFDsp!,{r0} //保存r0不合法,因为在读之前没有对寄存器写操作

ADDr0,x,1

EORx,r0,x

LDMFDsp!,{r0} //不需要恢复寄存器

}

returnx;

}

将其进行改写,使它符合内联汇编的语法规则。

intf(intx)

{

intr0;

__asm

{

ADDr0,x,1

EORx,r0,x

}

returnx;

}

下面通过几个例子进一步了解内联汇编的语法。

①字符串拷贝

下面的例子使用一个循环完成了字符串的拷贝工作。

#include<stdio.h>

voidmy_strcpy(constchar*src,char*dst)

{

intch;

__asm

{

loop:

LDRBch,[src],#1

STRBch,[dst],#1

CMPch,#0

BNEloop

}

}

intmain(void)

{

constchar*a="Helloworld!";

charb[20];

my_strcpy(a,b);

printf("Originalstring:'%s'\n",a);

printf("Copiedstring:'%s'\n",b);

return0;

}

②中断使能

下面的例子通过读取程序状态寄存器CPSR并设置它的中断使能位bit[7]来禁止/打开中断。需要注意的是,该例只能运行在系统模式下,因为用户模式是无权修改程序状态寄存器的。

__inlinevoidenable_IRQ(void)

{

inttmp;

__asm

{

MRStmp,CPSR

BICtmp,tmp,#0x80

MSRCPSR_c,tmp

}

}

__inlinevoiddisable_IRQ(void)

{

inttmp;

__asm

{

MRStmp,CPSR

ORRtmp,tmp,#0x80

MSRCPSR_c,tmp

}

}

intmain(void)

{

disable_IRQ();

enable_IRQ();

}

③分隔符的计算

下面的例子计算两个整数数组中分隔符“,”的个数。该例子显示了如何在内联汇编中访问C或C++语言中的数据类型。该例中的内联汇编函数mlal()被编译器优化为一条SMLAL指令,可以使用-S–interleave编译选项使编译器输出汇编结果。

#include<stdio.h>

/*changewordorderifbig-endian*/

#definelo64(a)(((unsigned*)&a)[0]) /*longlong型的低32位*/

#definehi64(a)(((int*)&a)[1]) /*longlong型的高32位*/

__inline__int64mlal(__int64sum,inta,intb)

{

#if!defined(__thumb)&&defined(__TARGET_FEATURE_MULTIPLY)

__asm

{

SMLALlo64(sum),hi64(sum),a,b

}

#else

sum+=(__int64)a*(__int64)b;

#endif

returnsum;

}

__int64dotprod(int*a,int*b,unsignedn)

{

__int64sum=0;

do

sum=mlal(sum,*a++,*b++);

while(--n!=0);

returnsum;

}

inta[10]={1,2,3,4,5,6,7,8,9,10};

intb[10]={10,9,8,7,6,5,4,3,2,1};

intmain(void)

{

printf("Dotproduct%lld(shouldbe%d)\n",dotprod(a,b,10),220);

return0;

}

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指令可能被等效为ARMLDR或STR指令。

3.内联汇编中的虚拟寄存器

内联汇编程序提供对ARM处理器物理寄存器的非直接访问。如果在内联汇编程序指令中将某个ARM寄存器用作操作数,它就成为相同名称的虚拟寄存器的引用,而不是对实际物理ARM寄存器的引用。例如内联汇编指令中使用了寄存器r0,但对于C编译器,指令中出现的r0只是一个变量,并非实际的物理寄存器r0,当程序运行时,可能是由物理寄存器r1来存放r0所代表的值。

下面的例子显示了编译器如何对内联汇编指令的寄存器进行分配。

程序的源代码如下。

#include<stdio.h>

voidtest_inline_register(void)

{

inti;

intr5,r6,r7;

__asm

{

MOVi,#0

loop:

MOVr5,#0

MOVr6,#0

MOVr7,#0

ADDi,i,#1

CMPi,#3

BNEloop

}

}

intmain(void)

{

test_inline_register();

printf("testinlineregister\n");

return0;

}

由C编译器编译出的汇编码如下所示。

test_inline_register:

0000807CE3A00000MOVr0,#0

>>>TEST_INLINE_REGISTER\#12loop:

00008080E1A00000NOP

>>>TEST_INLINE_REGISTER\#13MOVr5,#0

00008084E3A01000MOVr1,#0

>>>TEST_INLINE_REGISTER\#14MOVr6,#0

00008088E3A02000MOVr2,#0

>>>TEST_INLINE_REGISTER\#15MOVr7,#0

0000808CE3A03000MOVr3,#0

>>>TEST_INLINE_REGISTER\#16ADDi,i,#1

00008090E2800001ADDr0,r0,#1

>>>TEST_INLINE_REGISTER\#17CMPi,#3

00008094E3500003CMPr0,#3

000080980A000000BEQ0x80a0<TEST_INLINE_REGISTER\#21>

>>>TEST_INLINE_REGISTER\#18BNEloop

0000809CEAFFFFF8B0x8084<TEST_INLINE_REGISTER\#13>

>>>TEST_INLINE_REGISTER\#21}

000080A0E12FFF1EBXr14

>>>TEST_INLINE_REGISTER\#25{

注意

下面的代码是由Realview2.2编译出的代码,使用其他编译器结果可能有差异。同一段内嵌汇编经过不同版本的编译器编译后,在指令里可能使用不一样的实际寄存器,但是只要遵循文档里的编码指导,执行的功能肯定相同。

例子中以“>>>”的开头的行是程序的源码部分,紧接其后的是由编译器编译出的汇编代码。从上例可以很清楚地看出,源程序中使用了r5、r6和r7,但由编译器编译后的代码使用了寄存器r1、r2和r3。

另外,需要特别指出的是在内联汇编中使用寄存器必须先声明其变量类型,如上例中的“intr5,r6,r7”。如果不在使用前进行声明,编译器将给出以下错误信息。

#1267-D:ImplicitphysicalregisterR3shouldbedefinedasavariable

编译程序定义的虚拟寄存器有函数局部作用范围,即在同一个C函数中,涉及相同虚拟寄存器名称的多个asm语句或声明,访问相同的虚拟寄存器。

内联汇编没有为pc(r15)、lr(r14)和sp(r13)寄存器创建虚拟寄存器,而且不能在内联汇编代码中读取或直接修改它们的值。如果内联汇编程序中出现了对这些寄存器的访问,编译器将给出以下错误消息。例如,如果指定r14:

#20:identifier"r14"isundefined

内联汇编可以直接使用CPSR和SPSR对程序状态字进行操作,因为内联汇编中不存在虚拟处理器状态寄存器(PSR)。任何对PSR的引用总是指向物理PSR。

4.内联汇编中的指令展开

内联汇编代码中的ARM指令可能会在编译过程中扩展为几条指令。扩展取决于指令、指令中指定的操作数个数以及每个操作数的类型和值。通常,被扩展的指令有以下两种情况:

·含有常数操作的指令;

·LDM、STM、LDRD和STRD指令;

·乘法指令MUL被扩展为一系列的加法和移位指令。

下面的例子说明了编译器如何对含有常数操作的指令进行扩展。

包含有常数操作的加法指令:

ADDr0,r0,#1023

被编译器编译为如下两条指令:

ADDr0,r0,#1024

SUBr0,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,(长整型longlong除外)或指针类型。当表达式作为内联汇编指令的操作数时,编译器在编译时自动增加一段代码计算表示式的值并将其加载到指定的寄存器中。

注意

数据类型中除char和short(默认为无符号类型)外,其他均为有符号类型。

下面的例子显示了编译器如何处理内联汇编中的表达式操作数。

程序源代码如下所示。

/*ExampleOperands*/

voidmy_operand(void)

{

inti,j,total;

__asm

{

movi,#0

movj,#1

addtotal,j,i+j

}

}

intmain(void)

{

my_operand();

}

由编译器编译出的汇编代码如下所示(其中只列出了内联汇编的一段代码)。

my_operand:

0000807CE3A01000MOVr1,#0

>>>OPERANDS\#12movj,#1

00008080E3A00001MOVr0,#1

00008084E0812000ADDr2,r1,r0

>>>OPERANDS\#13addtotal,j,i+j

00008088E0803002ADDr3,r0,r2

>>>OPERANDS\#15}

0000808CE12FFF1EBXr14

>>>OPERANDS\#19{

从编译的代码可以看出,编译器将“addtotal,j,i+j”分为两步来完成,用户在编写自己的内联汇编应用程序时要特别注意这一点。

包含多个表达式操作数的指令,没有指定表达式操作数求值的顺序。

将C或C++表达式用作内联汇编程序操作数,如果表达式的值不能满足ARM指令中所要求的指令操作数约束条件,一条指令将被扩展为多条指令。

如果用作操作数的表达式创建需要析构的临时函数,析构将发生在执行内联汇编指令之后,这与C++析构临时函数的规则相类似。

简单表达式操作数包含以下几种类型。

·变量值

·变量地址

·指针变量的反引用(thedereferencingofapointvarable)

·伪操作指定的程序常量

非简单表达式操作数包含以下几种类型。

·隐式函数调用,如除法,或显式函数调用

·C++临时函数的构造

·算术或逻辑操作

(3)寄存器列表

寄存器列表最多可包含16个操作数。这些操作数可以是虚拟寄存器或表达式操作数。在寄存器列表中指定虚拟寄存器和表达式操作数的顺序非常重要。寄存器列表中操作数的读写顺序是从左到右。第一个操作数使用最低地址,随后的操作数的地址依次在前一地址基础上增加4。这一点与LDM或STM指令的普通操作(编号最低的物理寄存器总是存入最低的存储器地址)是不同的。之所以存在这种区别是因为在内联汇编中使用的寄存器被编译器虚拟化了。

同一个表达式操作数或虚拟寄存器可以在寄存器列表中出现多次,重复使用。

如果表达式操作数或虚拟寄存器被指定为指令中的基址寄存器,表达式或虚拟寄存器的值按照ARM指令寻址方式进行更新。更新将覆盖原表达式或虚拟寄存器的值。

(4)中间操作数(Intermediateoperands)

在内联汇编指令中,可能将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指令。不能在任何输入、输出或“被破坏的寄存器列表(corruptedregisterlist)”中指定lr、sp或pc寄存器;任何SWI指令或函数调用不能更改sp寄存器。

下面分别详细介绍语法格式中各参数的使用。

(1)未指定任何列表

如果在SWI和BL指令后没指定任何列表,则有下面规则。

·r0~r3用作输入参数;

·r0用于输出值;

·r12和r14的值将会被修改。

(2)输入参数列表

指令中的输入参数列表{input_param_list}列出了传递给被调用函数function和SWI的参数。被传递的参数可以是表达式、变量或包含表达式或变量的物理寄存器。

内联汇编编译器在编译时增加一小段编译程序负责在函数和SWI调用前

将传递的参数载入特定的物理寄存器中。为确保与现有内联汇编代码的向后兼容性,程序中指定物理寄存器名称而并不对其赋值,使相同名称虚拟寄存器中的值出现在物理寄存器中。

例如,指令BLfoo{r0=expression1,r1=expression2,r2}生成以下伪代码:

MOV(physical)r0,expression1

MOV(physical)r1,expression2

MOV(physical)r2,(virtual)r2

BLfoo

(3)输出参数列表

输出参数列表{output_value_list}列出了用来存放功能函数和SWI调用返回值的寄存器或表达式。列表中的值可以是物理寄存器、可修改长值表达式或单个物理寄存器名称。

内联汇编程序从特定的物理寄存器中取值并赋值到特定的表达式中。指定物理寄存器名称而并不赋值,导致相同名称虚拟寄存器被物理寄存器中的值更新。

例如,BLfoo{},{result1=r0,r1}生成以下伪码:

BLfoo

MOVresult1,(physical)r0

MOV(virtual)r1,(physical)r1

(4)被破坏的寄存器列表(Corruptedregisterlist)

此列表指定被函数调用破坏的物理寄存器。如果条件标志被调用的函数修改,必须在被破坏的寄存器列表中指定PSR。

BL和SWI指令总是破坏lr。

如果指令中缺少此列表项,则r0~r3、ip、lr和PSR被破坏。

注意

指令BL和B的区别在于,跳转指令B只能使程序跳转到C或C++程序的一个地址标号,不能用于子程序调用。

9.内嵌汇编中的标号

内联汇编代码中定义的标号可被用作跳转或C和C++“goto”语句的目标。在内联汇编代码中,C和C++中定义的标号可被用作跳转指令的目标。

10.内嵌汇编器版本间的差异

不同版本的ARM编译器对内联汇编程序的语法要求有显著差异。在具体使用时请参见相关文档。

·如果使用的是ADSv1.2,请参阅ADS开发者指南;

·如果使用的是RVCTv1.2,请参阅RealView编译工具1.2版开发者指南。

12.1.2嵌入式汇编

利用ARM编译器可将汇编代码包括到一个或多个C或C++函数定义中去。嵌入式汇编器提供对目标处理器不受限制的低级别访问,利用它可以使用C和C++预处理程序伪操作(preprocessordirective)并可以方便的使用偏移量访问结构成员。

本小节将介绍以下内容:

·嵌入式汇编程序语法;

·嵌入式汇编语句的限制;

·嵌入式汇编程序表达式和C或C++表达式之间的差异;

·嵌入式汇编函数的生成;

·__cpp关键字;

·手动重复解决方案;

·相关基类的关键字;

·成员函数类的关键字;

·调用非静态成员函数。

有关为ARM处理器编写汇编语言的详细信息,请参阅ADS或RealView编译工具的汇编程序指南。

1.嵌入式汇编语言语法

嵌入式汇编函数定义由--asm(C和C++)或asm(C++)函数限定符标记,可用于:

·成员函数;

·非成员函数;

·模板函数;

·模板类成员函数。

用__asm或asm声明的函数可以有调用参数和返回类型。它们从C和C++中调用的方式与普通C和C++函数调用方式相同。嵌入式汇编函数语法是:

__asmreturn-typefunction-name(parameter-list)

{

//ARM/Thumb/Thumb-2assemblercode

instruction[;instruction]

...

[instruction]

}

嵌入式汇编的初始执行状态是在编译程序时由编译选项决定的。这些编译选项如下所示:

·如果初始状态为ARM状态,则内嵌汇编器使用--arm选项;

·如果初始状态为Thumb状态,则内嵌汇编器使用--thumb选项。

注意

嵌入式汇编的初始状态由编译器的编译选项确定,与程序中的#pragmaarm和#pragmathumb伪操作无关。

可以显示地使用ARM、THUMB和CODE16伪操作改变嵌入式汇编的执行状态。关于ARM伪操作的详细信息请参加指令伪操作一节。如果使用的处理器支持Thumb-2指令,则可以在Thumb状态下,在嵌入式汇编中使用Thumb-2指令。

参数名允许用在参数列表中,但不能用在嵌入式汇编函数体内。例如,以下函数在函数体内使用整数i,但在汇编中无效:

__asmintf(inti){

ADDi,i,#1//编译器报错

}

可以使用r0代替i。

下面通过嵌入式汇编的例子,来进一步熟悉嵌入式汇编的使用。

下面的例子实现了字符串的拷贝,注意和上一节中内联汇编中字符串拷贝的例子相比较,分析其中的区别。

#include<stdio.h>

__asmvoidmy_strcpy(constchar*src,constchar*dst){

loop

LDRBr3,[r0],#1

STRBr3,[r1],#1

CMPr3,#0

BNEloop

MOVpc,lr

}

voidmain()

{

constchar*a="Helloworld!";

charb[20];

my_strcpy(a,b);

printf("Originalstring:'%s'\n",a);

printf("Copiedstring:'%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++中有不同值。例如:

MOVr0,#(-33554432/2)//结果为0x7f000000

MOVr0,#__cpp(-33554432/2)//结果为0xff000000

②以0开头的汇编程序编码仍是十进制的。例如:

MOVr0,#0700//十进制700

MOVr0,#__cpp(0700)//八进制0700等于十进制448

③汇编程序运算符优先顺序与C和C++不同。例如:

MOVr0,#(0x23:AND:0xf+1)//((0x23&0xf)+1)=>4

MOVr0,#__cpp(0x23&0xf+1)//(0x23&(0xf+1))=>0

④汇编程序字符串不是以空字符为终止标志的:

DCB"notrailingnull"//16bytes

DCB__cpp("Ihaveatrailingnull!!")//25bytes

注意

在_cpp标识符作用范围之内使用C或C++语法规则。

4.嵌入式汇编函数的生成

由关键字__asm声明的嵌入式汇编程序,在编译时将作为整个文件体传递给ARM汇编器。传递过程中,__asm函数的顺序保持不变(用模板实例生成的函数除外)。正是由于嵌入式汇编的这个特性,使得由一个__asm标识的嵌入式汇编程序调用在同一文件中的另一个嵌入式汇编程序是可以实现的。

当使用编译器armcc时,局部链接器(PartialLink)将汇编程序产生的目标文件与编译C程序的目标文件相结合,产生单个目标文件。

编译程序为每个__asm函数生成AREA命令。例如,以下__asm函数:

#include<cstddef>

structX{intx,y;voidaddto_y(int);};

__asmvoidX::addto_y(int){

LDRr2,[r0,#__cpp(offsetof(X,y))]

ADDr1,r2,r1

STRr1,[r0,#__cpp(offsetof(X,y))]

BXlr

}

对于此函数,编译程序生成:

AREA||.emb_text||,CODE,READONLY

EXPORT|_ZN1X7addto_yEi|

#linenum"file"

|_ZN1X7addto_yEi|PROC

LDRr2,[r0,#4]

ADDr1,r2,r1

STRr1,[r0,#4]

BXlr

ENDP

END

由上面的例子可以看出,对于变量offsetof的使用必须加__cpp()标识符才能引用,因为该变量是在cstddef头文件中定义的。

由__asm声明的常规函数被放在名为.emb_text的段(Section)中。这一点也是嵌入式汇编和内联汇编最大的不同。相反,隐式实例模板函数(ImplicitlyInstantiatedTemplateFunction)和内联汇编函数放在与函数名同名的区域(Area)内,并为该区域增加公共属性。这就确保了这类函数的特殊语义得以保持。

由于内联和模板函数的区域的特殊命名,所以这些函数不按照文件中定义的顺序排列,而是任意排序。因此,不能以__asm函数在原文件中的排列顺序,来判断它们的执行顺序,也就是说,即使两个连续排列的__asm函数,也不一定能顺序执行。

5.关键字__cpp

可用__cpp关键字从汇编代码中访问C或C++的编译时常量表达式,其中包括含有外部链接的数据或函数地址。标识符__cpp内的表达式必须是适合用作C++静态初始化的常量表达式(请参阅ISO/IEC14882:1998中的3.6.2非本地对象初始化一节和本书的常量表达式一节)。

编译时,编译器将使用__cpp(expr)的地方用汇编程序可以使用的常量所取代。例如:

LDRr0,=__cpp(&some_variable)

LDRr1,=__cpp(some_function)

BL__cpp(some_function)

MOVr0,#__cpp(some_constant_expr)

__cpp表达式中的名称可在__asm函数的C++上下文中查阅。__cpp表达式结果中的任何名称按照要求被损毁并自动为其生成IMPORT语句。

6.手动重复解决方案

可以在嵌入式汇编中使用C++转换为非虚拟函数调用解决重复。例如:

voidg(int);

voidg(long);

structT{

intmf(int);

intmf(int,int);

};

__asmvoidf(T*,int,int){

BL__cpp(static_cast<int(T::*)(int,int)>(&T::mf))//callsT::mf(int,int)

BL__cpp(static_cast<void(*)(int)>(g))//callsg(int)

MOVpc,lr

}

7.相关基类的关键字

利用以下关键字可以确定从对象起始处到其相关基类的偏移量:

__offsetof_base(D,B)

其中,B必须是D的非虚拟基类。

该函数返回从D对象的起始处到其中B基子对象的起始处的偏移量。结果可能是零。必须将偏移量(以字节为单位)添加到D*p来执行。

static_cast<B*>(p)的等效功能,如下程序段所示:

__asmB*my_static_base_cast(D*/*p*/){

if__offsetof_base(D,B)<>0 //排除偏移量为0的情况

ADDr0,r0,#__offsetof_base(D,B)

endif

MOVpc,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)调用非虚拟成员函数

例如,在虚拟基(virtualbase)或非虚拟基(non-virtualbase)中,以下代码可用于调用虚拟函数:

//rp包含指向D的指针,该程序的功能是实现在使用rp时调用D的非虚成员函数f

//所有参数准备好

//假设并不返回一个结构类型

if__mcall_is_in_vbase(D,f)

ASSERT{FALSE}//can'taccessvirtualbase

else

MOVr0,rp //使用指向D的指针rp*

ADDr0,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

MOVr0,rp //使用指向D的指针rp

LDRr12,[rp] //加载vtable表结构指针

ADDr0,r0,#__mcall_this_offset(D,f) //地址调整

endif

MOVlr,pc //保存返回地址到lr

LDRpc,[r12,#__vcall_offsetof_vfunc(D,f)] //调用函数rp→f()

10.嵌入式汇编版本间的差异

不同版本的ARM编译器对嵌入式汇编程序的语法要求会有所差异。在具体使用时请参见相关文档。

值得注意的是,目前的嵌入式汇编器已经完全支持ARMv6指令集,也就是说可以在嵌入式汇编中使用ARMv6指令集中的指令。

12.1.3内联汇编中使用SP、LR和PC寄存器的遗留问题

虽然目前的编译器不支持在内联汇编中使用SP、LR和PC寄存器,但在RVCTv1.2及其以前的编译器版本中是允许的。下面的例子显示了使用早期编译器版本,在内联汇编中使用LR寄存器的例子。

voidfunc()

{

intvar;

__asm

{

movvar,lr/*得到func()函数的返回地址*/

}

}

如果使用RVCTv2.0编译器编译上面的代码,编译器将报告以下错误。

Error:#20:identifier"lr"isundefined

使用RVCTv2.0版本及其以后的编译器,要在C或C++代码中使用汇编访问SP、LR和PC寄存器可以使用下面几种方法。

①使用嵌入式汇编代码。嵌入式汇编支持所有的ARM指令,同时允许在代码中访问SP、LR和PC寄存器。

②在内联汇编中使用以下一些指令。

·__current_pc():访问PC寄存器。

·__current_sp():访问SP寄存器。

·__return_address():访问LR,返回地址寄存器。

下面给出了两个访问SP、LR和PC寄存器的典型实例程序。

①使用编译器给定的指令。

voidprintReg()

{

unsignedintspReg,lrReg,pcReg;

__asm{

MOVspReg,__current_sp()

MOVpcReg,__current_pc()

MOVlrReg,__return_address()

}

printf("SP=0x%X\n",spReg);

printf("PC=0x%X\n",pcReg);

printf("LR=0x%X\n",lrReg);

}

②使用嵌入式汇编。

__asmvoidfunc()

{

MOVr0,lr

...

BXlr

}

使用嵌入式汇编可以使用调试器捕获程序的返回地址。

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指令

不直接支持

不支持

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

Arm CPU正在从根本上推动AI变革,并造福地球。Arm架构是未来AI计算的基石。​

关键字: ARM AI

近日,Arm推出了Arm® Ethos™-U85神经网络处理器(NPU)和Arm Corstone™-320物联网参考设计平台,旨在满足海量的数据处理和大规模计算,加速推进边缘AI的发展进程。

关键字: ARM

为了赶超云计算市场上的竞争对手,谷歌正试图通过定制的Arm服务器芯片降低云计算服务成本。

关键字: 谷歌 ARM 定制芯片

嵌入式开发作为一个融合了计算机软硬件和系统工程的综合性领域,其成功与否往往取决于三个核心要素的有效整合与协调。这三个要素分别是:硬件平台的选择与设计、软件开发及其优化、以及系统级的设计与集成。深入理解并熟练掌握这三个方面...

关键字: 嵌入式开发 ARM

随着汽车软件数量爆发式的增长,整个行业都需要重新思考汽车产品的开发流程。为此,Arm推出了丰富的硬件IP、新的系统IP,以及全新的汽车计算与计算子系统产品路线图,旨在为各种汽车应用实现性能、功能安全、可扩展等方面的支持。

关键字: ARM 汽车电子

知名移动芯片设计公司ARM最近迈出重要一步,它正式推出汽车芯片设计。ARM推出的芯片设计方案名叫Neoverse,随同芯片一起推出的还有面向汽车制造商、汽车供应商的新系统。

关键字: ARM 汽车芯片 芯片

随着通用人工智能的发展,数据中心的计算需求逐步提高。针对多模态数据、大模型的推理和训练需要更高的算力支持,而随着算力提升与之而来的还需更关注在功耗方面的优化。对于头部云计算和服务厂商而言,针对专门用例提高每瓦性能变得至关...

关键字: ARM 服务器 AI Neoverse CSS

一直以来,riscv架构都是大家的关注焦点之一。因此针对大家的兴趣点所在,小编将为大家带来riscv架构的相关介绍,详细内容请看下文。

关键字: riscv ARM riscv架构

最新消息报道,知情人士透露Arm近日裁掉了中国70多名软件工程师,并会将部分职位转移到中国以外的地区。Arm通过“全球服务”部门已经将支持其中国客户的工作外包给安谋科技,该部门曾经拥有约200名员工。

关键字: ARM 裁员

凭借着在个人计算机领域的广泛应用打下的坚实基础,X86自始至终统治着整个服务器生态。而这并不是业界希望看到的,因此Arm服务器被给予厚望。业界期盼Arm能够带来新的服务器CPU替代:打破一个同质化的数据中心架构,实现更高...

关键字: ARM 服务器 AI
关闭
关闭