ARM汇编

1.DSB和ISB命令

"dsb" 和 "isb" 是ARM处理器中的两个特殊指令,用于控制内存和指令的同步顺序。它们是内存栅栏(Memory Barrier)指令的一种形式。

  1. DSB(Data Synchronization Barrier)指令:
    DSB指令用于确保在指令执行流中的所有数据访问和存储指令都已经完成,然后再继续执行后续指令。它可以用于确保前面的数据操作对后续的指令是可见的。DSB指令的语法为:

    dsb [option]
    

    其中,option 是可选参数,用于指定栅栏的类型,如 sy(全系统内存栅栏)、ish(指令和数据栅栏,保证对应用程序的影响可见)等。DSB指令常用于多线程编程中,用于确保内存操作的有序性和一致性。

  2. ISB(Instruction Synchronization Barrier)指令:
    ISB指令用于刷新处理器流水线,确保在ISB指令之前的所有指令都已经执行完毕,然后再继续执行后续指令。它可以用于确保前面的指令对后续的指令是可见的。ISB指令的语法为:

    isb [option]
    

    其中,option 是可选参数,用于指定栅栏的类型。ISB指令常用于处理器状态切换、缓存同步等场景,以确保指令执行的正确性和一致性。

这两个指令的作用是确保内存和指令的同步顺序,以避免因处理器流水线、多核心或多线程等因素引起的数据不一致性和可见性问题。在并发编程和系统级编程中,合理地使用这些栅栏指令可以确保数据的正确性和一致性。

DSB(Data Synchronization Barrier)和ISB(Instruction Synchronization Barrier)是ARM处理器中的两个不同的栅栏指令,它们在功能和作用上有一些区别

  1. DSB(Data Synchronization Barrier)指令:
    DSB指令用于确保在指令执行流中的所有数据访问和存储指令都已经完成,然后再继续执行后续指令。它的作用是确保前面的数据操作对后续的指令是可见的。DSB指令主要用于实现内存屏障,保证内存操作的有序性和一致性。它可以用于多线程编程中,用于同步不同线程之间的内存访问顺序。

  2. ISB(Instruction Synchronization Barrier)指令:
    ISB指令用于刷新处理器流水线,确保在ISB指令之前的所有指令都已经执行完毕,然后再继续执行后续指令。它的作用是确保前面的指令对后续的指令是可见的。ISB指令主要用于处理器状态切换、缓存同步等场景,以确保指令执行的正确性和一致性。ISB指令可以用于刷新流水线,以便在切换上下文或执行特殊指令之前确保指令的一致性。

2.SUBS指令

subs指令是ARM 指令集中的减法指令,与SUB 指令类似,需要注意的是,在SUBS 指令中,如果发生了借位操作,CPSR 寄存器中的 C 标志位设置为 0;如果没有发生借位操作,CPSR 寄存器中的 C 标志位设置成 1 。这与 ADDS 指令中的进位指令正好相反。这主要是为了适应 SBC 等指令的操作需要。

3.C语言内联汇编

以MIPS架构的指令集为例介绍__asm__的语法

 内嵌汇编(Assembly)是可以直接插入在c/c++语言中汇编程序。它实现了汇编语言和高级语言的混合编程。当在高级语言中要实现一些高级语言没有的功能,或者提高程序局部代码的执行效率时,都可以考虑内嵌汇编的方式。

         内嵌汇编标识为asm()。asm是c/c++中的内嵌汇编关键字,或称模板。用于通知编译器,接下来的()内的代码是内嵌汇编程序, 需要特殊处理。()内部的有自己专门的语法格式。内嵌汇编实现了c/c++语言和汇编语言的混合编程。比如你可以在一个c语言文件中使用MIPS汇编指令MOVE完成两个数/地址的拷贝:

  asm("move %0,%1\n\t" :"=r"(ret) :"r"(src) );

       上面的内嵌汇编指令功能类似于c语言中的赋值操作:ret = src 。 这里src和ret都是c语言中的变量,src在内嵌汇编中作为输入操作数。ret在内嵌汇编中作为输出操作数。“=g”中的“=”符号表明这是个输出操作数。%0,%1称为占位符(顾名思义,占位符就是先占住一个固定的位置,等着你再往里面添加内容的符号),代表指令的操作数,分别代表c语言变量的ret和src。 内嵌汇编指令"move %0,%1\n\t"中的move还不是真正的MIPS汇编指令,MIPS中的move指令的2个操作数是寄存器,而此处move的操作数是c语言中的变量。C变量与寄存器的对应关系由GCC编译器自动处理,处理后的结果就是2条load指令加载变量到某2个寄存器,然后再执行move指令操作。扩展后的真实汇编指令大概是下面这样:

lw t1,src
lw t2,ret
move t2,t1

         可以看出使用内嵌汇编,我们就省去了加载变量到寄存器的过程,也不用考虑使用哪个寄存器的问题。方便我们更快捷的编写程序。

一、内嵌汇编基本格式
asm ( 汇编语句
    : 输出操作数		// 非必需
    : 输入操作数		// 非必需
    : 其他被污染的寄存器	// 非必需
    );

内嵌汇编以 asm(); 格式表示,里面分成4个部分,内嵌汇编指令、输出操作数、输入操作数、破坏描述。各部分之间使用“:”分割。其中内嵌汇编指令是必不可少的,但可以为空。其他3部分根据程序需要可选。如果只有内嵌汇编指令时,后面的“:”可以省略。例如:

asm("break" );

同时asm是__asm__的别名,所以上面语句也可以写成

__asm__(“break”);

再看下面的内嵌汇编:

asm("daddu %0,%1,%2\n\t" 
     :"=r"(ret) 
     :"r"(a),"r"(b)
);

其中"daddu %0,%1,%2\n\t" 就是内嵌汇编指令,指令由指令操作符和指令操作数组成。操作符就使用MIPS汇编指令中的助记符,操作数可以是%0,%1,%2形式的占位符,来表示c语言中变量ret、a和b。指令操作数也可以是寄存器。使用寄存器做指令操作数时,寄存器前面需要符号$。例如:

asm("move $31,%0\n\t" 
    :        /*此处的:不能省略*/
    :"r"(a)
);

上面这条指令实现了把c语言变量a的值存入通用寄存器ra($31)。

注意:内嵌汇编程序中如果没有输出部分,但是有输入部分,那么输出部分的“:”不能省略。同时asm模板里面可以使用/**/或者//添加注释。

②MIPS架构中使用$来代表寄存器,而ARM架构中使用%%/%来代表寄存器,当仅有指令时必须使用%代表寄存器,当有输入输出列表以及破坏描述时必须用%%来代表寄存器。

__asm__( " mov %eax, %ebx" ) //正确
__asm__( " mov %eax, %ebx" : : );//错误

__asm__( " mov %eax, %ebx" ) //正确
__asm__( " mov %%eax, %%ebx" )//错误

详见:__asm__ volatile 之 C语言嵌入式汇编-腾讯云开发者社区-腾讯云

③asm模板里可以有多条内嵌汇编指令。每条指令都以" "为单位。多条指令可以使用" ;"号、\n\t或者换行来分割。

asm("dadd %0,%1\n\t"
    "dsub %0,%2\n\t"
    :"=r"(ret)
    :"r"(a),"r"(b)
);
二、输入操作数和输出操作数

内嵌汇编中的操作数包括输出操作数和输入操作数,输出操作数和输入操作数里的每一个操作数都由一个约束字符串和一个带括号的c语言表达式或变量组成,比如“r”(src)。多个操作数之间使用“,”分割。内嵌汇编指令中使用%num的形式依次表示每一个操作数,num从0开始。比如:

asm("daddu %0,%1,%2\n\t"
    :"=r"(ret)         /* 输出操作数,也是第0个操作数%0 */
    :"r"(a),"r"(b)     /* 输入操作数,也是第1个操作数和第2个操作数 %1,%2 */
);

这里使用了daddu指令实现了c语言中ret=a+b的操作。两个输入操作数"r"(a)和"r"(b)之间使用“,”分割。%0代表操作数"=r"(ret)、%1代表操作数"r"(a)、%2代表操作数"r"(b)。

          每个操作数前面的约束字符串表示对后面c语言表达式或变量的限制条件。GCC会根据这个约束条件来决定处理方式。比如"=r"(ret)中的"=g"表示有两个约束条件,"="表明此操作数是输出操作数,"r"(b)中的"r"表示将变量b放入通用寄存器(relation相关联)。约束字符还有很多,有些还与特定体系结构相关,在下一节会详细列举。

输入操作数通常是c语言的变量,但是也可以是c语言表达式。比如:

asm("move %0,%1\n\t"
    :"=r"(ret)
    :"r"(&src+4)
);

这里输入操作数 &src+4 就是c语言表达式。执行的结果就是把&src+4的地址赋给ret。

         输出操作数必须是左值,GCC编译器会对此做检查。左值概念就是以赋值符号 = 为界,= 左边的就是左值,= 右边就是右值。输入操作数可以是左值也可以是右值。所以输出操作数必须使用"="标识自己。同时默认情况下输出操作数必须是只写(write-only)的,但是GCC不会对此做检查。这个特性有时会给我们带来麻烦。如果你要在内嵌汇编指令里把输出操作数当右值来操作,GCC编译时不会报错,但是程序运行后你可能无法得到你想要的结果。为此我们可以使用限制符"+"来把输出操作符的权限改为可读可写。例如:

asm("daddu %0,%0,%1\n\t"
    :"+r"(ret)
    :"r"(a)
);

这就实现了ret = ret+a的操作。 "+r"中的"+"就表示ret为可读可写。同时我们也可以使用数字限制符"0"达到修改输出操作符权限的目的。

asm("daddu %0,%1,%2\n\t"
     :"=r"(ret)
     :"0"(ret),"r"(a)
);

这里数字限制符"0"意思是第1个输入操作数ret和第0个输出操作数使用同样的地址空间。数字限制符只能用在输入操作数部分,而且必须指向某个输出操作数。

三、破坏描述

破坏描述部分就是声明内嵌汇编中有些寄存器被改变。通常内嵌汇编程序中会使用到一些寄存器,并对其做修改。如果在破坏描述部分不做说明,那么gcc编译内嵌汇编时不会做任何的检查和保护。这可能就会导致程序出错或致命异常。例如:

asm("dadd %0,%1,%2\n\t"
    "move $31,%0\n\t"
    :"=g"(ret)
    :"r"(a),"r"(b)
);

上面程序完成ret=a+b,然后ret的值写入寄存器ra($31)。我们知道寄存器ra被用来做函数返回的。但是ra被改变,将导致函数无法正常返回。这时就需要在破坏描述部分添加声明来告诉编译器此寄存器的值被改变。MIPS的内嵌汇编中寄存器的使用以$num形式,num代表寄存器编号。在破坏部分声明就使用"$num"形式,多个声明之间使用“,”分开。例如:

asm("dadd %0,%1,%2\n\t"
    "move $31,%0\n\t"
    :"=g"(ret)
    :"r"(a),"r"(b)
    :"$31"
);

“破坏描述”表示那些在汇编代码中修改了的、 又没有在输入/输出列表中列出的寄存器, 这样gcc 就不会擅自使用这些"危险的"寄存器,而是会把被修改的寄存器的原始值备份以确保程序不出问题。

破坏描述符除了寄存器还有“memory”。

asm volatile("": : :"memory")

是我们平时经常遇到的内嵌汇编格式。其中有一个关键字volatile和一个破坏描述“memory”。当然这两个关键字不是必须同时出现的,使用时要根据情况。

为解释清楚它,先介绍一下编译器的优化知识与C语言关键字volatile。最后去看该描述符。

内存访问速度远不及CPU处理速度,为提高机器整体性能,在硬件上引入硬件高速缓存Cache,加速对内存的访问。另外在现代CPU中指令的执行并不一定严格按照顺序执行,没有相关性的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度。以上是硬件级别的优化。再看软件一级的优化:一种是在编写代码时由程序员优化,另一种是由编译器进行优化。编译器优化常用的方法有:将内存变量缓存到寄存器;调整指令顺序充分利用CPU指令流水线,常见的是重新排序读写指令。对常规内存进行优化的时候,这些优化是透明的,而且效率很好。由编译器优化或者硬件重新排序引起的问题的解决办法是在从硬件(或者其他处理器)的角度看必须以特定顺序执行的操作之间设置内存屏障(memory barrier),linux 提供了一个宏解决编译器的执行顺序问题。
void Barrier(void)
这个函数通知编译器插入一个内存屏障,但对硬件无效,编译后的代码会把当前CPU寄存器中的所有修改过的数值存入内存,需要这些数据的时候再重新从内存中读出。

        编译器优化就会依赖这个特性在编译时调整指令顺序,让没有相关性的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度。如果内嵌汇编的指令中直接使用了某些寄存器或内存。GCC编译器在优化后很可能带来错误。这种情况我们可以使用volatile关键字来修饰。volatile用于告诉编译器,严禁将此处的asm汇编语句与它之前和之后的c语句在编译时重组合。

        还可以用 "memory" 表示在内联汇编中修改了内存,之前缓存在寄存器中的内存变量需要重新读取。如果你的内嵌汇编中使用了一段未知大小的内存,或者使用的内存用于在多线程。那么请务必使用约束字符“memory”。“memory”就是通知GCC编译器,此段内嵌汇编修改了memory中的内容,asm之前的c代码块和之后的c代码块看到的memory可能是不一样的,对memory的访问不能依赖之前的缓存,需要重新加载。

memory总结:

1)不要将该段内嵌汇编指令与前面的指令重新排序;也就是在执行内嵌汇编代码之前,它前面的指令都执行完毕
2)不要将变量缓存到寄存器,因为这段代码可能会用到内存变量,而这些内存变量会以不可预知的方式发生改变,因此GCC插入必要的代码先将缓存到寄存器的变量值写回内存,如果后面又访问这些变量,需要重新访问内存。

如果汇编指令修改了内存,但是GCC 本身却察觉不到,因为在输出部分没有描述,此时就需要在修改描述部分增加"memory",告诉GCC 内存已经被修改,GCC 得知这个信息后,就会在这段指令之前,插入必要的指令将前面因为优化Cache 到寄存器中的变量值先写回内存,如果以后又要使用这些变量再重新读取。

四、有名操作数和指定寄存器

从gcc的3.1版本之后,内嵌汇编支持有名操作数。就是可以在内嵌汇编中为输入操作数、输出操作数取名字,名字形式是[name],放在每个操作数的前面,然后汇编程序模板里面就可以使用%[name]的形式,而不是上面%num形式。例如: 

asm("daddu %[out],%[in1],%[in2]\n\t"
     :[out]"=g"(ret)
     :[in1]"r"(a),[in2]"r"(b)
);

这里给c语言变量ret取名为out、变量a和b取名为int1和in2。这里的别名要求可以是大小写字母、数字、下划线等,但是你必须确保同一汇编程序中没有两个操作数使用相同的别名。

当然,你可以仅输出或者输入中的部分操作数去名字。例如:

asm("daddu %[out],%1,%2\n\t"
    :[out]"=g"(ret)
    :"r"(a),"r"(b)
);

这里我只给第0个操作数"=g"(ret)取名字。那么后面的第1个操作数和第2个操作数在使用时还是序列号形式%1、%2。  

        有时候我们需要在指令中使用指定的寄存器;比如系统调用时需要将系统调用号放在v0寄存器,参数放入a0-a7寄存器。那么这是我们可以在c语言声明变量时使用指定寄存器功能。例如:

register int sys_id asm("$2") = 5001;

这里使用关键字register声明了一个寄存器变量 sys_id,并通知GCC编译器使用$2(v0)寄存器来加载sys_id变量。

五、操作数的修饰符:约束字符

约束字符就是输入操作数和输出操作数前面的修饰符。约束字符可以说明操作数是否可以在寄存器中,以及哪种寄存器;操作数是否可以是内存引用,以及哪种地址;操作数是否可以是立即常数,以及它可能具有的值。本节介绍常用的约束字符信息。

“r” 通知汇编器可以使用通用寄存器中的任意一个来加载操作数。最常用的一个约束。
“g” 允许使用任何通用寄存器、内存或立即整数操作数。
“i”通知汇编器这个操作数是个立即数(一个具有常量值)。例如:

#define DEFAULT 1
 
asm("li %0,%1\n\t" 
    :"=r"(ret) 
    :"i"(-TCP_MSS_DEFAULT)
);

此处的"i"也可以使用"g"代替。

“n”同约束字符“i”。
“m”内存操作数,用在访存指令的地址加载和存储。
“o”内存操作数,用在访存指令的地址加载和存储。在MIPS架构中功能同“m”。
“+”修改操作数的权限为可读可写,通常只修饰输出操作数。


六、内嵌汇编实例
/* C语言实现MCR指令 */

#define __STRINGIFY(x) #x

#define __MCR(coproc, opcode_1, src, CRn, CRm, opcode_2)                          \

    __ASM volatile ("MCR " __STRINGIFY(p##coproc) ", " __STRINGIFY(opcode_1) ", " \

                    "%0, " __STRINGIFY(c##CRn) ", " __STRINGIFY(c##CRm) ", "      \

                    __STRINGIFY(opcode_2)                                         \

                    : : "r" (src) )

“\’”表示一行未写完,启用下一行

#define __STRINGIFY(x) #x  使用#表示将参数转换为字符串表面量

如_STRINGIFY(43)  展开后为“43”

p##coproc  先将coproc的参数带入,再将p和带入后的值连接起来,如coproc为15,则最终结果为p15,且是字符串“p15”.

_ASM 的宏展开为 _asm  表示这条语句是汇编语句,其中汇编语句的每一部分都有引号,

在使用 __asm 关键字插入汇编语句时,通常需要用引号将汇编语句括起来。这是因为引号告诉编译器这部分代码应该被视为汇编语句,而不是源代码中的其他内容。

使用引号括起汇编代码的好处是,它使得编译器能够正确地区分汇编指令和其他源代码部分。这有助于避免可能产生的语法冲突或混淆,因为汇编语言和高级编程语言的语法规则往往不同。通过使用引号,编译器能够准确地解析并识别汇编语句,将其传递给相应的汇编器进行处理。

此外,引号也有助于与其他代码元素进行区分,比如约束(constraints)、寄存器或变量名称等。引号可以将汇编代码与其他部分隔离开来,确保编译器能够正确解析并处理它们。

因此,使用引号将内联汇编语句括起来是一种通用的做法,以确保编译器能够正确理解和处理嵌入的汇编代码。

注意:__asm__与__asm区别:

取决于编译器的不同,在GNU C中二者区别在于:

①用来避免命名空间冲突(有名为asm的用户定义函数)

②__asm可以直接使用寄存器名,__asm__需要用%? 

__asm("mov eax, %0" ::);

__asm__("movl %0, %%eax"::);

https://stackoverflow.com/questions/3323445/what-is-the-difference-between-asm-asm-and-asm#:~:text=As%20far%20as%20I%20can,(var)%20at%20the%20end.

在VC中二者有所区别:详见

simplelinux/inlineasm.md at master · 1184893257/simplelinux · GitHub

参考文献:

MIPS指令集:内嵌汇编asm语法介绍-CSDN博客

C语言内嵌汇编:__asm__ __volatile___asm volatile( "call $005eb654" );-CSDN博客

linux的内嵌汇编代码_#define mcr-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值